[
  {
    "path": ".dockerignore",
    "content": "ui\n.github\n.git"
  },
  {
    "path": ".editorconfig",
    "content": "root=true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = false\nmax_line_length = 120\ntab_width = 4\nij_continuation_indent_size = 8\nij_formatter_off_tag = @formatter:off\nij_formatter_on_tag = @formatter:on\nij_formatter_tags_enabled = true\nij_smart_tabs = false\nij_wrap_on_typing = false\n\n[*.java]\nmax_line_length = 100\nij_continuation_indent_size = 4\nij_java_align_consecutive_assignments = false\nij_java_align_consecutive_variable_declarations = false\nij_java_align_group_field_declarations = false\nij_java_align_multiline_annotation_parameters = false\nij_java_align_multiline_array_initializer_expression = false\nij_java_align_multiline_assignment = false\nij_java_align_multiline_binary_operation = false\nij_java_align_multiline_chained_methods = false\nij_java_align_multiline_extends_list = false\nij_java_align_multiline_for = true\nij_java_align_multiline_method_parentheses = false\nij_java_align_multiline_parameters = false\nij_java_align_multiline_parameters_in_calls = false\nij_java_align_multiline_parenthesized_expression = false\nij_java_align_multiline_records = true\nij_java_align_multiline_resources = true\nij_java_align_multiline_ternary_operation = false\nij_java_align_multiline_text_blocks = false\nij_java_align_multiline_throws_list = false\nij_java_align_subsequent_simple_methods = false\nij_java_align_throws_keyword = false\nij_java_annotation_parameter_wrap = off\nij_java_array_initializer_new_line_after_left_brace = false\nij_java_array_initializer_right_brace_on_new_line = false\nij_java_array_initializer_wrap = normal\nij_java_assert_statement_colon_on_next_line = false\nij_java_assert_statement_wrap = normal\nij_java_assignment_wrap = normal\nij_java_binary_operation_sign_on_next_line = true\nij_java_binary_operation_wrap = normal\nij_java_blank_lines_after_anonymous_class_header = 0\nij_java_blank_lines_after_class_header = 0\nij_java_blank_lines_after_imports = 1\nij_java_blank_lines_after_package = 1\nij_java_blank_lines_around_class = 1\nij_java_blank_lines_around_field = 0\nij_java_blank_lines_around_field_in_interface = 0\nij_java_blank_lines_around_initializer = 1\nij_java_blank_lines_around_method = 1\nij_java_blank_lines_around_method_in_interface = 1\nij_java_blank_lines_before_class_end = 0\nij_java_blank_lines_before_imports = 0\nij_java_blank_lines_before_method_body = 0\nij_java_blank_lines_before_package = 1\nij_java_block_brace_style = end_of_line\nij_java_block_comment_at_first_column = false\nij_java_call_parameters_new_line_after_left_paren = false\nij_java_call_parameters_right_paren_on_new_line = false\nij_java_call_parameters_wrap = normal\nij_java_case_statement_on_separate_line = true\nij_java_catch_on_new_line = false\nij_java_class_annotation_wrap = split_into_lines\nij_java_class_brace_style = end_of_line\nij_java_class_count_to_use_import_on_demand = 999\nij_java_class_names_in_javadoc = 1\nij_java_do_not_indent_top_level_class_members = false\nij_java_do_not_wrap_after_single_annotation = false\nij_java_do_while_brace_force = always\nij_java_doc_add_blank_line_after_description = true\nij_java_doc_add_blank_line_after_param_comments = false\nij_java_doc_add_blank_line_after_return = false\nij_java_doc_add_p_tag_on_empty_lines = true\nij_java_doc_align_exception_comments = true\nij_java_doc_align_param_comments = false\nij_java_doc_do_not_wrap_if_one_line = false\nij_java_doc_enable_formatting = true\nij_java_doc_enable_leading_asterisks = true\nij_java_doc_indent_on_continuation = false\nij_java_doc_keep_empty_lines = true\nij_java_doc_keep_empty_parameter_tag = true\nij_java_doc_keep_empty_return_tag = true\nij_java_doc_keep_empty_throws_tag = true\nij_java_doc_keep_invalid_tags = true\nij_java_doc_param_description_on_new_line = false\nij_java_doc_preserve_line_breaks = false\nij_java_doc_use_throws_not_exception_tag = true\nij_java_else_on_new_line = false\nij_java_enum_constants_wrap = normal\nij_java_extends_keyword_wrap = normal\nij_java_extends_list_wrap = normal\nij_java_field_annotation_wrap = split_into_lines\nij_java_finally_on_new_line = false\nij_java_for_brace_force = always\nij_java_for_statement_new_line_after_left_paren = false\nij_java_for_statement_right_paren_on_new_line = false\nij_java_for_statement_wrap = normal\nij_java_generate_final_locals = false\nij_java_generate_final_parameters = false\nij_java_if_brace_force = always\nij_java_imports_layout = $*, |, *, |, *\nij_java_indent_case_from_switch = true\nij_java_insert_inner_class_imports = false\nij_java_insert_override_annotation = true\nij_java_keep_blank_lines_before_right_brace = 2\nij_java_keep_blank_lines_between_package_declaration_and_header = 2\nij_java_keep_blank_lines_in_code = 2\nij_java_keep_blank_lines_in_declarations = 2\nij_java_keep_control_statement_in_one_line = true\nij_java_keep_first_column_comment = true\nij_java_keep_indents_on_empty_lines = false\nij_java_keep_line_breaks = true\nij_java_keep_multiple_expressions_in_one_line = false\nij_java_keep_simple_blocks_in_one_line = false\nij_java_keep_simple_classes_in_one_line = false\nij_java_keep_simple_lambdas_in_one_line = false\nij_java_keep_simple_methods_in_one_line = false\nij_java_label_indent_absolute = false\nij_java_label_indent_size = 0\nij_java_lambda_brace_style = end_of_line\nij_java_layout_static_imports_separately = true\nij_java_line_comment_add_space = true\nij_java_line_comment_at_first_column = false\nij_java_method_annotation_wrap = split_into_lines\nij_java_method_brace_style = end_of_line\nij_java_method_call_chain_wrap = normal\nij_java_method_parameters_new_line_after_left_paren = false\nij_java_method_parameters_right_paren_on_new_line = false\nij_java_method_parameters_wrap = normal\nij_java_modifier_list_wrap = false\nij_java_names_count_to_use_import_on_demand = 999\nij_java_new_line_after_lparen_in_record_header = false\nij_java_parameter_annotation_wrap = normal\nij_java_parentheses_expression_new_line_after_left_paren = false\nij_java_parentheses_expression_right_paren_on_new_line = false\nij_java_place_assignment_sign_on_next_line = false\nij_java_prefer_longer_names = true\nij_java_prefer_parameters_wrap = false\nij_java_record_components_wrap = normal\nij_java_repeat_synchronized = true\nij_java_replace_instanceof_and_cast = false\nij_java_replace_null_check = true\nij_java_replace_sum_lambda_with_method_ref = true\nij_java_resource_list_new_line_after_left_paren = false\nij_java_resource_list_right_paren_on_new_line = false\nij_java_resource_list_wrap = normal\nij_java_rparen_on_new_line_in_record_header = false\nij_java_space_after_closing_angle_bracket_in_type_argument = false\nij_java_space_after_colon = true\nij_java_space_after_comma = true\nij_java_space_after_comma_in_type_arguments = true\nij_java_space_after_for_semicolon = true\nij_java_space_after_quest = true\nij_java_space_after_type_cast = true\nij_java_space_before_annotation_array_initializer_left_brace = false\nij_java_space_before_annotation_parameter_list = false\nij_java_space_before_array_initializer_left_brace = true\nij_java_space_before_catch_keyword = true\nij_java_space_before_catch_left_brace = true\nij_java_space_before_catch_parentheses = true\nij_java_space_before_class_left_brace = true\nij_java_space_before_colon = true\nij_java_space_before_colon_in_foreach = true\nij_java_space_before_comma = false\nij_java_space_before_do_left_brace = true\nij_java_space_before_else_keyword = true\nij_java_space_before_else_left_brace = true\nij_java_space_before_finally_keyword = true\nij_java_space_before_finally_left_brace = true\nij_java_space_before_for_left_brace = true\nij_java_space_before_for_parentheses = true\nij_java_space_before_for_semicolon = false\nij_java_space_before_if_left_brace = true\nij_java_space_before_if_parentheses = true\nij_java_space_before_method_call_parentheses = false\nij_java_space_before_method_left_brace = true\nij_java_space_before_method_parentheses = false\nij_java_space_before_opening_angle_bracket_in_type_parameter = false\nij_java_space_before_quest = true\nij_java_space_before_switch_left_brace = true\nij_java_space_before_switch_parentheses = true\nij_java_space_before_synchronized_left_brace = true\nij_java_space_before_synchronized_parentheses = true\nij_java_space_before_try_left_brace = true\nij_java_space_before_try_parentheses = true\nij_java_space_before_type_parameter_list = false\nij_java_space_before_while_keyword = true\nij_java_space_before_while_left_brace = true\nij_java_space_before_while_parentheses = true\nij_java_space_inside_one_line_enum_braces = false\nij_java_space_within_empty_array_initializer_braces = false\nij_java_space_within_empty_method_call_parentheses = false\nij_java_space_within_empty_method_parentheses = false\nij_java_spaces_around_additive_operators = true\nij_java_spaces_around_assignment_operators = true\nij_java_spaces_around_bitwise_operators = true\nij_java_spaces_around_equality_operators = true\nij_java_spaces_around_lambda_arrow = true\nij_java_spaces_around_logical_operators = true\nij_java_spaces_around_method_ref_dbl_colon = false\nij_java_spaces_around_multiplicative_operators = true\nij_java_spaces_around_relational_operators = true\nij_java_spaces_around_shift_operators = true\nij_java_spaces_around_type_bounds_in_type_parameters = true\nij_java_spaces_around_unary_operator = false\nij_java_spaces_within_angle_brackets = false\nij_java_spaces_within_annotation_parentheses = false\nij_java_spaces_within_array_initializer_braces = false\nij_java_spaces_within_braces = false\nij_java_spaces_within_brackets = false\nij_java_spaces_within_cast_parentheses = false\nij_java_spaces_within_catch_parentheses = false\nij_java_spaces_within_for_parentheses = false\nij_java_spaces_within_if_parentheses = false\nij_java_spaces_within_method_call_parentheses = false\nij_java_spaces_within_method_parentheses = false\nij_java_spaces_within_parentheses = false\nij_java_spaces_within_switch_parentheses = false\nij_java_spaces_within_synchronized_parentheses = false\nij_java_spaces_within_try_parentheses = false\nij_java_spaces_within_while_parentheses = false\nij_java_special_else_if_treatment = true\nij_java_subclass_name_suffix = Impl\nij_java_ternary_operation_signs_on_next_line = true\nij_java_ternary_operation_wrap = normal\nij_java_test_name_suffix = Test\nij_java_throws_keyword_wrap = normal\nij_java_throws_list_wrap = normal\nij_java_use_external_annotations = false\nij_java_use_fq_class_names = false\nij_java_use_relative_indents = false\nij_java_use_single_class_imports = true\nij_java_variable_annotation_wrap = normal\nij_java_visibility = public\nij_java_while_brace_force = always\nij_java_while_on_new_line = false\nij_java_wrap_comments = false\nij_java_wrap_first_method_in_call_chain = false\nij_java_wrap_long_lines = true\n\n[*.properties]\nij_properties_align_group_field_declarations = false\nij_properties_keep_blank_lines = false\nij_properties_key_value_delimiter = equals\nij_properties_spaces_around_key_value_delimiter = false\n\n[.editorconfig]\nij_editorconfig_align_group_field_declarations = false\nij_editorconfig_space_after_colon = false\nij_editorconfig_space_after_comma = true\nij_editorconfig_space_before_colon = false\nij_editorconfig_space_before_comma = false\nij_editorconfig_spaces_around_assignment_operators = true\n\n[{*.ant, *.fxml, *.jhm, *.jnlp, *.jrxml, *.jspx, *.pom, *.rng, *.tagx, *.tld, *.wsdl, *.xml, *.xsd, *.xsl, *.xslt, *.xul}]\nij_xml_align_attributes = true\nij_xml_align_text = false\nij_xml_attribute_wrap = normal\nij_xml_block_comment_at_first_column = true\nij_xml_keep_blank_lines = 2\nij_xml_keep_indents_on_empty_lines = false\nij_xml_keep_line_breaks = true\nij_xml_keep_line_breaks_in_text = true\nij_xml_keep_whitespaces = false\nij_xml_keep_whitespaces_around_cdata = preserve\nij_xml_keep_whitespaces_inside_cdata = false\nij_xml_line_comment_at_first_column = true\nij_xml_space_after_tag_name = false\nij_xml_space_around_equals_in_attribute = false\nij_xml_space_inside_empty_tag = false\nij_xml_text_wrap = normal\n\n[{*.bash, *.sh, *.zsh}]\nindent_size = 2\ntab_width = 2\nij_shell_binary_ops_start_line = false\nij_shell_keep_column_alignment_padding = false\nij_shell_minify_program = false\nij_shell_redirect_followed_by_space = false\nij_shell_switch_cases_indented = false\n\n[{*.gant, *.gradle, *.groovy, *.gy}]\nij_groovy_align_group_field_declarations = false\nij_groovy_align_multiline_array_initializer_expression = false\nij_groovy_align_multiline_assignment = false\nij_groovy_align_multiline_binary_operation = false\nij_groovy_align_multiline_chained_methods = false\nij_groovy_align_multiline_extends_list = false\nij_groovy_align_multiline_for = true\nij_groovy_align_multiline_list_or_map = true\nij_groovy_align_multiline_method_parentheses = false\nij_groovy_align_multiline_parameters = true\nij_groovy_align_multiline_parameters_in_calls = false\nij_groovy_align_multiline_resources = true\nij_groovy_align_multiline_ternary_operation = false\nij_groovy_align_multiline_throws_list = false\nij_groovy_align_named_args_in_map = true\nij_groovy_align_throws_keyword = false\nij_groovy_array_initializer_new_line_after_left_brace = false\nij_groovy_array_initializer_right_brace_on_new_line = false\nij_groovy_array_initializer_wrap = off\nij_groovy_assert_statement_wrap = off\nij_groovy_assignment_wrap = off\nij_groovy_binary_operation_wrap = off\nij_groovy_blank_lines_after_class_header = 0\nij_groovy_blank_lines_after_imports = 1\nij_groovy_blank_lines_after_package = 1\nij_groovy_blank_lines_around_class = 1\nij_groovy_blank_lines_around_field = 0\nij_groovy_blank_lines_around_field_in_interface = 0\nij_groovy_blank_lines_around_method = 1\nij_groovy_blank_lines_around_method_in_interface = 1\nij_groovy_blank_lines_before_imports = 1\nij_groovy_blank_lines_before_method_body = 0\nij_groovy_blank_lines_before_package = 0\nij_groovy_block_brace_style = end_of_line\nij_groovy_block_comment_at_first_column = true\nij_groovy_call_parameters_new_line_after_left_paren = false\nij_groovy_call_parameters_right_paren_on_new_line = false\nij_groovy_call_parameters_wrap = off\nij_groovy_catch_on_new_line = false\nij_groovy_class_annotation_wrap = split_into_lines\nij_groovy_class_brace_style = end_of_line\nij_groovy_class_count_to_use_import_on_demand = 5\nij_groovy_do_while_brace_force = never\nij_groovy_else_on_new_line = false\nij_groovy_enum_constants_wrap = off\nij_groovy_extends_keyword_wrap = off\nij_groovy_extends_list_wrap = off\nij_groovy_field_annotation_wrap = split_into_lines\nij_groovy_finally_on_new_line = false\nij_groovy_for_brace_force = never\nij_groovy_for_statement_new_line_after_left_paren = false\nij_groovy_for_statement_right_paren_on_new_line = false\nij_groovy_for_statement_wrap = off\nij_groovy_if_brace_force = never\nij_groovy_import_annotation_wrap = 2\nij_groovy_indent_case_from_switch = true\nij_groovy_indent_label_blocks = true\nij_groovy_insert_inner_class_imports = false\nij_groovy_keep_blank_lines_before_right_brace = 2\nij_groovy_keep_blank_lines_in_code = 2\nij_groovy_keep_blank_lines_in_declarations = 2\nij_groovy_keep_control_statement_in_one_line = true\nij_groovy_keep_first_column_comment = true\nij_groovy_keep_indents_on_empty_lines = false\nij_groovy_keep_line_breaks = true\nij_groovy_keep_multiple_expressions_in_one_line = false\nij_groovy_keep_simple_blocks_in_one_line = false\nij_groovy_keep_simple_classes_in_one_line = true\nij_groovy_keep_simple_lambdas_in_one_line = true\nij_groovy_keep_simple_methods_in_one_line = true\nij_groovy_label_indent_absolute = false\nij_groovy_label_indent_size = 0\nij_groovy_lambda_brace_style = end_of_line\nij_groovy_layout_static_imports_separately = true\nij_groovy_line_comment_add_space = false\nij_groovy_line_comment_at_first_column = true\nij_groovy_method_annotation_wrap = split_into_lines\nij_groovy_method_brace_style = end_of_line\nij_groovy_method_call_chain_wrap = off\nij_groovy_method_parameters_new_line_after_left_paren = false\nij_groovy_method_parameters_right_paren_on_new_line = false\nij_groovy_method_parameters_wrap = off\nij_groovy_modifier_list_wrap = false\nij_groovy_names_count_to_use_import_on_demand = 3\nij_groovy_parameter_annotation_wrap = off\nij_groovy_parentheses_expression_new_line_after_left_paren = false\nij_groovy_parentheses_expression_right_paren_on_new_line = false\nij_groovy_prefer_parameters_wrap = false\nij_groovy_resource_list_new_line_after_left_paren = false\nij_groovy_resource_list_right_paren_on_new_line = false\nij_groovy_resource_list_wrap = off\nij_groovy_space_after_assert_separator = true\nij_groovy_space_after_colon = true\nij_groovy_space_after_comma = true\nij_groovy_space_after_comma_in_type_arguments = true\nij_groovy_space_after_for_semicolon = true\nij_groovy_space_after_quest = true\nij_groovy_space_after_type_cast = true\nij_groovy_space_before_annotation_parameter_list = false\nij_groovy_space_before_array_initializer_left_brace = false\nij_groovy_space_before_assert_separator = false\nij_groovy_space_before_catch_keyword = true\nij_groovy_space_before_catch_left_brace = true\nij_groovy_space_before_catch_parentheses = true\nij_groovy_space_before_class_left_brace = true\nij_groovy_space_before_closure_left_brace = true\nij_groovy_space_before_colon = true\nij_groovy_space_before_comma = false\nij_groovy_space_before_do_left_brace = true\nij_groovy_space_before_else_keyword = true\nij_groovy_space_before_else_left_brace = true\nij_groovy_space_before_finally_keyword = true\nij_groovy_space_before_finally_left_brace = true\nij_groovy_space_before_for_left_brace = true\nij_groovy_space_before_for_parentheses = true\nij_groovy_space_before_for_semicolon = false\nij_groovy_space_before_if_left_brace = true\nij_groovy_space_before_if_parentheses = true\nij_groovy_space_before_method_call_parentheses = false\nij_groovy_space_before_method_left_brace = true\nij_groovy_space_before_method_parentheses = false\nij_groovy_space_before_quest = true\nij_groovy_space_before_switch_left_brace = true\nij_groovy_space_before_switch_parentheses = true\nij_groovy_space_before_synchronized_left_brace = true\nij_groovy_space_before_synchronized_parentheses = true\nij_groovy_space_before_try_left_brace = true\nij_groovy_space_before_try_parentheses = true\nij_groovy_space_before_while_keyword = true\nij_groovy_space_before_while_left_brace = true\nij_groovy_space_before_while_parentheses = true\nij_groovy_space_in_named_argument = true\nij_groovy_space_in_named_argument_before_colon = false\nij_groovy_space_within_empty_array_initializer_braces = false\nij_groovy_space_within_empty_method_call_parentheses = false\nij_groovy_spaces_around_additive_operators = true\nij_groovy_spaces_around_assignment_operators = true\nij_groovy_spaces_around_bitwise_operators = true\nij_groovy_spaces_around_equality_operators = true\nij_groovy_spaces_around_lambda_arrow = true\nij_groovy_spaces_around_logical_operators = true\nij_groovy_spaces_around_multiplicative_operators = true\nij_groovy_spaces_around_regex_operators = true\nij_groovy_spaces_around_relational_operators = true\nij_groovy_spaces_around_shift_operators = true\nij_groovy_spaces_within_annotation_parentheses = false\nij_groovy_spaces_within_array_initializer_braces = false\nij_groovy_spaces_within_braces = true\nij_groovy_spaces_within_brackets = false\nij_groovy_spaces_within_cast_parentheses = false\nij_groovy_spaces_within_catch_parentheses = false\nij_groovy_spaces_within_for_parentheses = false\nij_groovy_spaces_within_gstring_injection_braces = false\nij_groovy_spaces_within_if_parentheses = false\nij_groovy_spaces_within_list_or_map = false\nij_groovy_spaces_within_method_call_parentheses = false\nij_groovy_spaces_within_method_parentheses = false\nij_groovy_spaces_within_parentheses = false\nij_groovy_spaces_within_switch_parentheses = false\nij_groovy_spaces_within_synchronized_parentheses = false\nij_groovy_spaces_within_try_parentheses = false\nij_groovy_spaces_within_tuple_expression = false\nij_groovy_spaces_within_while_parentheses = false\nij_groovy_special_else_if_treatment = true\nij_groovy_ternary_operation_wrap = off\nij_groovy_throws_keyword_wrap = off\nij_groovy_throws_list_wrap = off\nij_groovy_use_flying_geese_braces = false\nij_groovy_use_fq_class_names = false\nij_groovy_use_fq_class_names_in_javadoc = true\nij_groovy_use_relative_indents = false\nij_groovy_use_single_class_imports = true\nij_groovy_variable_annotation_wrap = off\nij_groovy_while_brace_force = never\nij_groovy_while_on_new_line = false\nij_groovy_wrap_long_lines = false\n\n[{*.har, *.json}]\nindent_size = 2\nij_json_keep_blank_lines_in_code = 0\nij_json_keep_indents_on_empty_lines = false\nij_json_keep_line_breaks = true\nij_json_space_after_colon = true\nij_json_space_after_comma = true\nij_json_space_before_colon = true\nij_json_space_before_comma = false\nij_json_spaces_within_braces = false\nij_json_spaces_within_brackets = false\nij_json_wrap_long_lines = false\n\n[{*.htm, *.html, *.sht, *.shtm, *.shtml}]\nij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3\nij_html_align_attributes = true\nij_html_align_text = false\nij_html_attribute_wrap = normal\nij_html_block_comment_at_first_column = true\nij_html_do_not_align_children_of_min_lines = 0\nij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p\nij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot\nij_html_enforce_quotes = false\nij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var\nij_html_keep_blank_lines = 2\nij_html_keep_indents_on_empty_lines = false\nij_html_keep_line_breaks = true\nij_html_keep_line_breaks_in_text = true\nij_html_keep_whitespaces = false\nij_html_keep_whitespaces_inside = span, pre, textarea\nij_html_line_comment_at_first_column = true\nij_html_new_line_after_last_attribute = never\nij_html_new_line_before_first_attribute = never\nij_html_quote_style = double\nij_html_remove_new_line_before_tags = br\nij_html_space_after_tag_name = false\nij_html_space_around_equality_in_attribute = false\nij_html_space_inside_empty_tag = false\nij_html_text_wrap = normal\n\n[{*.yaml, *.yml}]\nindent_size = 2\nij_yaml_keep_indents_on_empty_lines = false\nij_yaml_keep_line_breaks = true\n\n[*.md]\nindent_size = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.en.yml",
    "content": "name: Bug Report\ndescription: File a bug report\nlabels: [bug]\nbody:\n  - type: checkboxes\n    id: preface\n    attributes:\n      label: Prerequisites\n      description: Thank you for taking the time to fill out this issue report! Before we begin, we highly recommend reading through the [Open Source Guides](https://opensource.guide/), which will greatly improve our mutual efficiency.\n      options:\n        - label: I have searched for related issues in the [issues](https://github.com/halo-dev/halo/issues) list.\n          required: true\n        - label: \"This is an issue with the Halo project itself. If it is not an issue with the project itself(For example: Installation and deployment issues.), it is recommended to submit it in the [Discussions](https://github.com/halo-dev/halo/discussions).\"\n          required: true\n        - label: I have tried disabling all plugins to rule out plugins as the cause of the problem.\n          required: true\n        - label: If it is an issue with plugins and themes, please submit it in the respective plugin and theme repositories.\n          required: true\n  - type: markdown\n    id: environment\n    attributes:\n      value: \"## Environment\"\n  - type: textarea\n    id: system-information\n    attributes:\n      label: \"System information\"\n      description: \"Access the actuator page of the Console, click the copy button in the upper right corner, and paste the information here.\"\n      placeholder: |\n        - External url: https://demo.halo.run\n        - Start time: 2024-07-21 14:50\n        - Version: 2.x.x\n        - Build time: 2024-07-15 18:19\n        - Git Commit: 6d4bedd\n        - Java: IBM Semeru Runtime Open Edition / ...\n        - Database: PostgreSQL / 16.3 ...\n        - Operating system: Linux / 5.15.0-88 ...\n        - Activated theme: ...\n        - Enabled plugins:\n            - ...\n    validations:\n      required: true\n  - type: dropdown\n    id: operation-method\n    validations:\n      required: true\n    attributes:\n      label: \"What is the project operation method?\"\n      options:\n        - Docker\n        - Docker Compose\n        - Fat Jar\n        - Source Code\n  - type: markdown\n    id: details\n    attributes:\n      value: \"## Details\"\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: \"What happened?\"\n      description: \"For ease of management, please do not report multiple unrelated issues under the same issue.\"\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce-steps\n    attributes:\n      label: \"Reproduce Steps\"\n      description: \"If it can be consistently reproduced, please provide detailed steps.\"\n      placeholder: |\n        1. Open '...'\n        2. Click '...'\n  - type: textarea\n    id: logs\n    attributes:\n      label: \"Relevant log output\"\n      description: \"Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.\"\n      render: shell\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: \"Additional information\"\n      description: \"If you have other information to note, you can fill it in here (screenshots, videos, etc.).\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.zh.yml",
    "content": "name: Bug 反馈\ndescription: 提交 Bug 反馈\nlabels: [bug]\nbody:\n  - type: checkboxes\n    id: preface\n    attributes:\n      label: 前置条件\n      description: 感谢你花时间填写此错误报告！在开始之前，我们非常推荐阅读一遍[《开源软件指南》](https://opensource.guide/zh-hans/)，这会在很大程度上提高我们彼此的效率。\n      options:\n        - label: 已经在 [issues](https://github.com/halo-dev/halo/issues) 列表中搜索了相关问题。\n          required: true\n        - label: 这是 Halo 项目本身存在的问题，如果是非项目本身的问题（如：安装部署问题），建议在 [Discussions](https://github.com/halo-dev/halo/discussions) 提交。\n          required: true\n        - label: 已经尝试过停用所有的插件，排除是插件导致的问题。\n          required: true\n        - label: 如果是插件和主题的问题，请在对应的插件和主题仓库提交。\n          required: true\n  - type: markdown\n    id: environment\n    attributes:\n      value: \"## 环境信息\"\n  - type: textarea\n    id: system-information\n    attributes:\n      label: \"系统信息\"\n      description: \"访问 Console 的概览页面，点击右上角的复制按钮，将信息粘贴到此处。\"\n      placeholder: |\n        - 外部访问地址: https://demo.halo.run\n        - 启动时间: 2024-07-21 14:50\n        - 版本: 2.x.x\n        - 构建时间: 2024-07-15 18:19\n        - Git Commit: 6d4bedd\n        - Java: IBM Semeru Runtime Open Edition / ...\n        - 数据库: PostgreSQL / 16.3 ...\n        - 操作系统: Linux / 5.15.0-88 ...\n        - 已激活主题: ...\n        - 已启动插件:\n            - ...\n    validations:\n      required: true\n  - type: dropdown\n    id: operation-method\n    validations:\n      required: true\n    attributes:\n      label: \"使用的哪种方式运行？\"\n      options:\n        - Docker\n        - Docker Compose\n        - Fat Jar\n        - Source Code\n  - type: markdown\n    id: details\n    attributes:\n      value: \"## 详细信息\"\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: \"发生了什么？\"\n      description: \"为了方便我们管理，请不要在同一个 issue 下报告多个不相关的问题。\"\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce-steps\n    attributes:\n      label: \"复现步骤\"\n      description: \"如果可以稳定复现，请提供详细的步骤。\"\n      placeholder: |\n        1. 打开 '...'\n        2. 点击 '...'\n  - type: textarea\n    id: logs\n    attributes:\n      label: \"相关日志输出\"\n      description: \"请复制并粘贴任何相关的日志输出。这将自动格式化为代码，因此无需反引号。\"\n      render: shell\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: \"附加信息\"\n      description: \"如果你还有其他需要提供的信息，可以在这里填写（可以提供截图、视频等）。\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 商业产品反馈\n    url: https://github.com/orgs/lxware-dev/discussions\n    about: Halo 付费版以及应用市场商业应用的问题，建议优先在这里反馈。\n  - name: 对 Halo 有其他问题\n    url: https://bbs.halo.run\n    about: 如果你对 Halo 有其他想要提问的，欢迎到官方社区进行提问。"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.en.yml",
    "content": "name: Feature Request\ndescription: File a feature request\nbody:\n  - type: checkboxes\n    id: preface\n    attributes:\n      label: Prerequisites\n      description: Hello! Thank you for submitting a new feature suggestion for Halo. Before we begin, we highly recommend reading through the [Open Source Guides](https://opensource.guide/), which will greatly improve our mutual efficiency.\n      options:\n        - label: I have searched for related issues in the [Issues](https://github.com/halo-dev/halo/issues) list.\n          required: true\n        - label: This is a feature related to Halo. If it is not an issue with the project itself, it is recommended to submit it in the [Discussions](https://github.com/halo-dev/halo/discussions).\n          required: true\n        - label: If it is a feature suggestion for plugins and themes, please submit it in the respective plugin and theme repositories.\n          required: true\n  - type: markdown\n    id: environment\n    attributes:\n      value: \"## Environment\"\n  - type: input\n    id: version\n    attributes:\n      label: \"Your current Halo version\"\n  - type: markdown\n    id: details\n    attributes:\n      value: \"## Details\"\n  - type: textarea\n    id: description\n    attributes:\n      label: \"Describe this feature\"\n      description: \"For ease of management, please do not submit multiple unrelated features under the same issue.\"\n    validations:\n      required: true\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: \"Additional information\"\n      description: \"If you have other information to note, you can fill it in here (screenshots, videos, etc.).\""
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.zh.yml",
    "content": "name: 新特性建议\ndescription: 提交新特性建议\nbody:\n  - type: checkboxes\n    id: preface\n    attributes:\n      label: 前置条件\n      description: 你好！感谢你为 Halo 提交新特性建议。在开始之前，我们非常推荐阅读一遍[《开源软件指南》](https://opensource.guide/zh-hans/)，这会在很大程度上提高我们彼此的效率。\n      options:\n        - label: 已经在 [Issues](https://github.com/halo-dev/halo/issues) 列表中搜索了相关问题。\n          required: true\n        - label: 这是和 Halo 相关的特性，如果是非项目本身的问题，建议在 [Discussions](https://github.com/halo-dev/halo/discussions) 提交。\n          required: true\n        - label: 如果是插件和主题特性建议，请在对应的插件和主题仓库提交。\n          required: true\n  - type: markdown\n    id: environment\n    attributes:\n      value: \"## 环境信息\"\n  - type: input\n    id: version\n    attributes:\n      label: \"你当前使用的版本\"\n  - type: markdown\n    id: details\n    attributes:\n      value: \"## 详细信息\"\n  - type: textarea\n    id: description\n    attributes:\n      label: \"描述一下此特性\"\n      description: \"为了方便我们管理，请不要在同一个 issue 下提交多个没有相关性的特性。\"\n    validations:\n      required: true\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: \"附加信息\"\n      description: \"如果你还有其他需要提供的信息，可以在这里填写（可以提供截图、视频等）。\""
  },
  {
    "path": ".github/actions/docker-buildx-push/action.yaml",
    "content": "name: \"Docker buildx and push\"\ndescription: \"Buildx and push the Docker image.\"\n\ninputs:\n  ghcr-token:\n    description: Token of current GitHub account in GitHub container registry.\n    required: false\n    default: \"\"\n  dockerhub-user:\n    description: \"User name for the DockerHub account\"\n    required: false\n    default: \"\"\n  dockerhub-token:\n    description: Token for the DockerHub account\n    required: false\n    default: \"\"\n  f2c-registry-user:\n    description: \"User name of Fit2Cloud Docker Registry.\"\n    required: false\n    default: \"\"\n  f2c-registry-token:\n    description: \"Token of Fit2Cloud Docker Registry.\"\n    required: false\n    default: \"\"\n  push:\n    description: Should push the docker image or not.\n    required: false\n    default: \"false\"\n  platforms:\n    description: Target platforms for building image\n    required: false\n    default: \"linux/amd64,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x\"\n  image-name:\n    description: The basic name of docker.\n    required: false\n    default: \"halo\"\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Docker meta for Halo\n      id: meta\n      uses: docker/metadata-action@v5\n      with:\n        images: |\n          ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}\n          halohub/${{ inputs.image-name }}\n          registry.fit2cloud.com/halo/${{ inputs.image-name }}\n        tags: |\n          type=schedule,pattern=nightly-{{date 'YYYYMMDD'}},enabled=${{ github.event_name == 'schedule' }}\n          type=ref,event=branch,enabled=${{ github.event_name == 'push' }}\n          type=ref,event=pr,enabled=${{ github.event_name == 'pull_request' }}\n          type=semver,pattern={{major}}\n          type=semver,pattern={{major}}.{{minor}}\n          type=semver,pattern={{ version }}\n          type=sha,enabled=${{ github.event_name == 'push' }}\n        flavor: |\n          latest=false\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@v3\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v3\n    - name: Login to GHCR\n      uses: docker/login-action@v3\n      if: inputs.ghcr-token != '' && github.event_name != 'pull_request'\n      with:\n        registry: ghcr.io\n        username: ${{ github.repository_owner }}\n        password: ${{ inputs.ghcr-token }}\n    - name: Login to DockerHub\n      if: inputs.dockerhub-token != '' && github.event_name != 'pull_request'\n      uses: docker/login-action@v3\n      with:\n        username: ${{ inputs.dockerhub-user }}\n        password: ${{ inputs.dockerhub-token }}\n    - name: Login to Fit2Cloud Docker Registry\n      if: inputs.f2c-registry-token != '' && github.event_name != 'pull_request'\n      uses: docker/login-action@v3\n      with:\n        registry: registry.fit2cloud.com\n        username: ${{ inputs.f2c-registry-user }}\n        password: ${{ inputs.f2c-registry-token }}\n    - name: Build and push\n      uses: docker/build-push-action@v5\n      with:\n        context: .\n        file: ./Dockerfile\n        platforms: ${{ inputs.platforms }}\n        labels: ${{ steps.meta.outputs.labels }}\n        tags: ${{ steps.meta.outputs.tags }}\n        push: ${{ (inputs.ghcr-token != '' || inputs.dockerhub-token != '') && inputs.push == 'true' }}\n"
  },
  {
    "path": ".github/actions/setup-env/action.yaml",
    "content": "name: Setup Environment\ndescription: Setup environment to check and build Halo, including console and core projects.\n\ninputs:\n  node-version:\n    description: Node.js version.\n    required: false\n    default: \"24\"\n\n  pnpm-version:\n    description: pnpm version.\n    required: false\n    default: \"10\"\n\n  java-version:\n    description: Java version.\n    required: false\n    default: \"21\"\n\nruns:\n  using: \"composite\"\n  steps:\n    - uses: pnpm/action-setup@v3\n      name: Setup pnpm\n      with:\n        version: ${{ inputs.pnpm-version }}\n\n    - name: Set up Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ inputs.node-version }}\n        cache: \"pnpm\"\n        cache-dependency-path: \"ui/pnpm-lock.yaml\"\n\n    - name: Setup JDK\n      uses: actions/setup-java@v4\n      with:\n        distribution: \"temurin\"\n        cache: \"gradle\"\n        java-version: ${{ inputs.java-version }}\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--  Thanks for sending a pull request!  Here are some tips for you:\n1. 如果这是你的第一次，请阅读我们的贡献指南：<https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md>。\n1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md>.\n2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。\n2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request.\n3. 请确保你已经添加并运行了适当的测试。\n3. Ensure you have added or ran the appropriate tests for your PR.\n4. 如果你的 PR 使用了 LLM 生成代码，请在 PR 中添加相应的说明，我们不反对使用 LLM 辅助开发，但希望你能够先对生成的代码进行审查。\n5. If your PR uses LLM generated code, please add a corresponding description in the PR, we do not oppose using LLM to assist development, but we hope you can review the generated code first.\n-->\n\n#### What type of PR is this?\n\n<!--\n添加其中一个类别：\nAdd one of the following kinds:\n\n/kind bug\n/kind cleanup\n/kind documentation\n/kind feature\n/kind improvement\n\n适当添加其中一个或多个类别（可选）：\nOptionally add one or more of the following kinds if applicable:\n\n/kind api-change\n/kind deprecation\n/kind failing-test\n/kind flake\n/kind regression\n-->\n\n#### What this PR does / why we need it:\n\n#### Which issue(s) this PR fixes:\n\n<!--\nPR 合并时自动关闭 issue。\nAutomatically closes linked issue when PR is merged.\n\n用法：`Fixes #<issue 号>`，或者 `Fixes (粘贴 issue 完整链接)`\nUsage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.\n-->\nFixes #\n\n#### Special notes for your reviewer:\n\n#### Does this PR introduce a user-facing change?\n\n<!--\n如果当前 Pull Request 的修改不会造成用户侧的任何变更，在 `release-note` 代码块儿中填写 `NONE`。\n否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新（Break Change），\nRelease Note 需要以 `action required` 开头。\nIf no, just write \"NONE\" in the release-note block below.\nIf yes, a release note is required:\nEnter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string \"action required\".\n-->\n\n```release-note\n```\n"
  },
  {
    "path": ".github/workflows/halo.yaml",
    "content": "name: Halo Workflow\n\non:\n  pull_request:\n    branches:\n      - main\n      - release-*\n    paths:\n      - \"**\"\n      - \"!**.md\"\n  push:\n    branches:\n      - main\n      - release-*\n    paths:\n      - \"**\"\n      - \"!**.md\"\n  release:\n    types:\n      - published\n\nconcurrency: \n  group: ${{github.workflow}} - ${{github.ref}}\n  cancel-in-progress: true\n\njobs:\n  test:\n    if: github.event_name == 'pull_request' || github.event_name == 'push'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Environment\n        uses: ./.github/actions/setup-env\n      - name: Check Halo\n        run: ./gradlew clean check --configuration-cache --configuration-cache-problems=warn\n      - name: Upload coverage reports to Codecov\n        if: github.repository == 'halo-dev/halo'\n        uses: codecov/codecov-action@v4\n\n  build:\n    runs-on: ubuntu-latest\n    if: always() && (needs.test.result == 'skipped' || needs.test.result == 'success')\n    needs: test\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Environment\n        uses: ./.github/actions/setup-env\n      - name: Reset version of Halo\n        if: github.event_name == 'release'\n        shell: bash\n        run: |\n          # Set the version with tag name when releasing\n          version=${{ github.event.release.tag_name }}\n          version=${version#v}\n          sed -i \"s/version=.*-SNAPSHOT$/version=$version/1\" gradle.properties\n      - name: Build Halo\n        run: ./gradlew clean downloadPluginPresets build -x check --configuration-cache --configuration-cache-problems=warn\n      - name: Upload Artifacts\n        if: github.repository == 'halo-dev/halo'\n        uses: actions/upload-artifact@v4\n        with:\n          name: halo-artifacts\n          path: application/build/libs\n          retention-days: 1\n\n  github-release:\n    runs-on: ubuntu-latest\n    if: always() && needs.build.result == 'success' && github.event_name == 'release'\n    needs: build\n    steps:\n      - uses: actions/checkout@v4\n      - name: Download Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: halo-artifacts\n          path: application/build/libs\n      - name: Upload Artifacts\n        if: github.repository == 'halo-dev/halo'\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: gh release upload ${{ github.event.release.tag_name }} application/build/libs/*\n\n  build-and-publish-container-image-with-buildpacks:\n    needs: build\n    if: always() && needs.build.result == 'success' && (github.event_name == 'push' || github.event_name == 'release')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Environment\n        uses: ./.github/actions/setup-env\n      - name: Reset version of Halo\n        if: github.event_name == 'release'\n        shell: bash\n        run: |\n          # Set the version with tag name when releasing\n          version=${{ github.event.release.tag_name }}\n          version=${version#v}\n          sed -i \"s/version=.*-SNAPSHOT$/version=$version/1\" gradle.properties\n      - name: Publish To Container Registries\n        env:\n          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}\n          DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}\n          F2C_USERNAME: ${{ secrets.F2C_REGISTRY_USER }}\n          F2C_TOKEN: ${{ secrets.F2C_REGISTRY_TOKEN }}\n          GHCR_USERNAME: ${{ github.repository_owner }}\n          GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run:\n          ./gradlew publishToAllRegistries -Prelease=${{ github.event_name == 'release' && 'true' || 'false' }}\n\n  docker-build-and-push:\n    if: always() && needs.build.result == 'success' && (github.event_name == 'push' || github.event_name == 'release')\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - uses: actions/checkout@v4\n      - name: Download Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: halo-artifacts\n          path: application/build/libs\n      - name: Docker Buildx and Push\n        uses: ./.github/actions/docker-buildx-push\n        with:\n          image-name: ${{ github.event_name == 'release' && 'halo' || 'halo-dev' }}\n          ghcr-token: ${{ secrets.GITHUB_TOKEN }}\n          dockerhub-user: ${{ secrets.DOCKER_USERNAME }}\n          dockerhub-token: ${{ secrets.DOCKER_TOKEN }}\n          f2c-registry-user: ${{ secrets.F2C_REGISTRY_USER }}\n          f2c-registry-token: ${{ secrets.F2C_REGISTRY_TOKEN }}\n          push: true\n          platforms: linux/amd64,linux/arm64/v8,linux/ppc64le,linux/s390x\n\n  e2e-test:\n    if: always() && needs.build.result == 'success' && (github.event_name == 'pull_request' || github.event_name == 'push')\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - uses: actions/checkout@v4\n      - name: Download Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: halo-artifacts\n          path: application/build/libs\n      - name: Docker Build\n        uses: docker/build-push-action@v5\n        with:\n          tags: ghcr.io/halo-dev/halo-dev:main\n          push: false\n          context: .\n      - name: E2E Testing\n        continue-on-error: true\n        run: |\n          sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose\n          sudo chmod u+x /usr/local/bin/docker-compose\n          cd e2e && make all\n"
  },
  {
    "path": ".github/workflows/openapi-check.yaml",
    "content": "name: OpenAPI Check\n\non:\n  pull_request:\n    branches:\n      - main\n      - release-*\n    paths:\n      - 'application/src/**'\n      - 'api/src/**'\n      - 'api-docs/openapi/**'\n      - 'ui/packages/api-client/**'\n  push:\n    branches:\n      - main\n      - release-*\n    paths:\n      - 'application/src/**'\n      - 'api/src/**'\n      - 'api-docs/openapi/**'\n      - 'ui/packages/api-client/**'\n\nconcurrency:\n  group: ${{github.workflow}} - ${{github.ref}}\n  cancel-in-progress: true\n\njobs:\n  openapi-check:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Environment\n        uses: ./.github/actions/setup-env\n      - name: Install UI dependencies\n        run: ./gradlew pnpmInstall\n      - name: Regenerate OpenAPI docs\n        run: ./gradlew generateOpenApiDocs --no-configuration-cache\n      - name: Regenerate api-client\n        run: cd ui && pnpm run api-client:gen\n      - name: Verify OpenAPI docs and api-client are in sync\n        run: |\n          if ! git diff --exit-code -- api-docs/openapi ui/packages/api-client/src; then\n            echo \"::error::OpenAPI docs or api-client generated code is out of sync with the current API. Run './gradlew generateOpenApiDocs --no-configuration-cache' and 'cd ui && pnpm run api-client:gen', then commit the changes under api-docs/openapi and ui/packages/api-client/src.\"\n            git diff --stat api-docs/openapi ui/packages/api-client/src\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/packages-preview-release.yaml",
    "content": "name: \"Packages preview release\"\non:\n  push:\n    paths:\n      - \"ui/packages/**\"\n    branches:\n      - main\n  pull_request:\n    paths:\n      - \"ui/packages/**\"\n    branches:\n      - main\njobs:\n  packages-preview-release:\n    runs-on: ubuntu-latest\n    if: github.repository == 'halo-dev/halo'\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Environment\n        uses: ./.github/actions/setup-env\n\n      - name: Install Dependencies\n        run: ./gradlew pnpmInstall\n\n      - name: Build Packages\n        run: cd ui && pnpm build:packages\n\n      - name: Release\n        run: cd ui && pnpx pkg-pr-new publish --compact --pnpm './packages/*'\n"
  },
  {
    "path": ".github/workflows/release-ui-packages.yaml",
    "content": "name: Release UI Packages\n\non:\n  release:\n    types:\n      - published\n\npermissions:\n  contents: write\n  id-token: write\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    if: github.repository == 'halo-dev/halo' && github.event.release.prerelease == false\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Setup Environment\n        uses: ./.github/actions/setup-env\n      - run: ./gradlew pnpmInstall\n      - name: Publish to NPM\n        run: cd ui && pnpm run publish:packages\n"
  },
  {
    "path": ".github/workflows/stale-issues.yaml",
    "content": "name: Close Stale Issues\n\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n  workflow_dispatch:\n\npermissions:\n  issues: write\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v10\n        with:\n          stale-issue-message: |\n            This issue has been automatically marked as stale because it has not had recent activity.\n            It will be closed if no further activity occurs within the next 2 days.\n            If you believe this issue is still relevant, please provide the requested information.\n          close-issue-message: |\n            This issue has been automatically closed due to inactivity.\n            If you have the requested information, feel free to reopen it or create a new issue.\n          days-before-issue-stale: 60\n          days-before-issue-close: 2\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n          stale-issue-label: \"lifecycle/stale\"\n          any-of-issue-labels: \"triage/needs-information,priority/awaiting-more-evidence,help wanted\"\n          operations-per-run: 100\n          ascending: true\n"
  },
  {
    "path": ".gitignore",
    "content": "### Maven\ntarget/\nlogs/\n!.mvn/wrapper/maven-wrapper.jar\n\n### Gradle\n.gradle\nbuild/\nout/\n!gradle/wrapper/gradle-wrapper.jar\nbin/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\nlog/\n\n### NetBeans ###\nnbproject/private/\nbuild/\nnbbuild/\ndist/\nnbdist/\n.nb-gradle/\n\n### Mac\n.DS_Store\n*/.DS_Store\n\n### VS Code ###\n*.project\n*.factorypath\n\n### Compiled class file\n*.class\n\n### Log file\n*.log\n\n### BlueJ files\n*.ctxt\n\n### Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n### Package Files\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n### VSCode\n.vscode\n!.vscode/settings.json\n!.vscode/extensions.json\n\n### Local file\napplication-local.yml\napplication-local.yaml\napplication-local.properties\n\n### Zip file for test\n!application/src/test/resources/themes/*.zip\n!application/src/main/resources/themes/*.zip\napplication/src/main/resources/console/\napplication/src/main/resources/uc/\napplication/src/main/resources/presets/\n\n### Node\nnode_modules\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\n### Frontend\ndist\ncoverage\n*.local\n\n### Cypress\n/cypress/videos/\n/cypress/screenshots/\n\n### Frontend build\n!src/build\nstorybook-static\ntsconfig.tsbuildinfo\n.tgz"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nhi@halo.run.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guide\n\nThank you for your interest in contributing to Halo.\n\nThis document explains the recommended workflow for submitting high-quality contributions, including code, tests, and documentation updates.\n\n## Before You Start\n\n- For new features or major behavior changes, please open an issue first so we can align on scope and design.\n- For clear bug fixes, you can submit a pull request directly.\n- If your report is not about the core project itself (for example, deployment questions), please use Discussions instead of Issues.\n\n## Development Environment\n\nThis repository mainly contains:\n\n- Backend and platform modules built with Gradle.\n- Frontend code in `ui`, managed with `pnpm` workspaces.\n\n### Prerequisites\n\n- Git\n- JDK (version compatible with the project build)\n- Node.js and `pnpm` (see `ui/package.json` for the current package manager)\n- Docker / Docker Compose (required for e2e scenarios)\n\n## Contribution Workflow\n\n### 1. Fork and Clone\n\nFork this repository, then clone your fork:\n\n```bash\ngit clone https://github.com/{YOUR_USERNAME}/{REPOSITORY}.git\ncd {REPOSITORY}\n```\n\n### 2. Add Upstream Remote\n\n```bash\ngit remote add upstream https://github.com/halo-dev/halo.git\ngit fetch upstream\n```\n\n### 3. Create a Branch\n\nUse a focused branch name that reflects your change:\n\n```bash\ngit checkout -b feat/short-description\n```\n\n### 4. Implement and Validate\n\nRun relevant checks before opening a PR.\n\nBackend and general checks:\n\n```bash\n./gradlew clean check\n```\n\nFrontend checks (in `ui`):\n\n```bash\ncd ui\npnpm install\npnpm build:packages\npnpm lint\npnpm typecheck\npnpm test:unit\n```\n\n### 5. Commit and Push\n\n```bash\ngit push origin <your-branch>\n```\n\n### 6. Open a Pull Request\n\nOpen a PR from your branch to `main` and fill out the PR template carefully:\n\n- Describe what changed and why.\n- Link related issues (for example, `Fixes #123`).\n- Add release note content or `NONE` when no user-facing change is introduced.\n- Add proper `/kind` labels as requested in the template.\n\n## AI-Assisted Contribution Policy\n\nAI-assisted development is not prohibited, including code generation and refactoring support.\n\nHowever, you are fully responsible for any code in your PR.\n\nIf you used AI tools, please follow these rules:\n\n- Review all AI-generated content before submission.\n- Verify correctness, security, performance, and maintainability.\n- Ensure generated code follows project conventions and architecture.\n- Remove low-quality or redundant generated code.\n- Mention AI assistance in your PR description when AI materially contributed to the final changes.\n\nIn short: AI assistance is allowed, but unreviewed AI output is not acceptable.\n\n## Testing Expectations\n\n- Add or update tests whenever you change behavior.\n- If you add or modify APIs, please include corresponding e2e test cases.\n- See `e2e/README.md` for e2e workflow and local execution details.\n\n## Coding Standards\n\n- Follow the project coding style guide: <https://docs.halo.run/developer-guide/core/code-style>\n- Keep changes focused and avoid unrelated refactors in the same PR.\n- Run formatters and linters before pushing.\n\n## Keep Your Fork Updated\n\nBefore starting new work, sync your branch with upstream:\n\n```bash\ngit fetch upstream\ngit checkout main\ngit merge upstream/main\ngit push origin main\n```\n\n## Need Help?\n\n- Open an issue for confirmed bugs and feature proposals.\n- Use Discussions for general questions and usage/deployment topics.\n\nThanks again for helping improve Halo.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM eclipse-temurin:21-jre as builder\n\nWORKDIR application\nARG JAR_FILE=application/build/libs/*.jar\nCOPY ${JAR_FILE} application.jar\nRUN java -Djarmode=layertools -jar application.jar extract\n\n################################\n\nFROM ibm-semeru-runtimes:open-21-jre\nLABEL maintainer=\"johnniang <johnniang@foxmail.com>\"\nWORKDIR application\nCOPY --from=builder application/dependencies/ ./\nCOPY --from=builder application/spring-boot-loader/ ./\nCOPY --from=builder application/snapshot-dependencies/ ./\nCOPY --from=builder application/application/ ./\n\nENV JVM_OPTS=\"\" \\\n    HALO_WORK_DIR=\"/root/.halo2\" \\\n    SPRING_CONFIG_LOCATION=\"optional:classpath:/;optional:file:/root/.halo2/\" \\\n    TZ=Asia/Shanghai\n\nRUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime \\\n    && echo $TZ > /etc/timezone\n\nExpose 8090\n\nENTRYPOINT [\"sh\", \"-c\", \"java ${JVM_OPTS} org.springframework.boot.loader.launch.JarLauncher ${0} ${@}\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "OWNERS",
    "content": "approvers:\n- ruibaby\n- guqing\n- JohnNiang\n- LIlGG\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <a href=\"https://www.halo.run\" target=\"_blank\" rel=\"noopener noreferrer\">\n        <img width=\"100\" src=\"https://www.halo.run/logo\" alt=\"Halo logo\" />\n    </a>\n</p>\n\n<p align=\"center\"><b>Halo</b> [ˈheɪloʊ]，强大易用的开源建站工具。</p>\n<p align=\"center\">\n<a href=\"https://github.com/halo-dev/halo/releases\"><img alt=\"GitHub release\" src=\"https://img.shields.io/github/release/halo-dev/halo.svg?style=flat-square&include_prereleases\" /></a>\n<a href=\"https://hub.docker.com/r/halohub/halo\"><img alt=\"Docker pulls\" src=\"https://img.shields.io/docker/pulls/halohub/halo?style=flat-square\" /></a>\n<a href=\"https://github.com/halo-dev/halo/commits\"><img alt=\"GitHub last commit\" src=\"https://img.shields.io/github/last-commit/halo-dev/halo.svg?style=flat-square\" /></a>\n<a href=\"https://github.com/halo-dev/halo/actions\"><img alt=\"GitHub Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/halo-dev/halo/halo.yaml?branch=main&style=flat-square\" /></a>\n<a href=\"https://codecov.io/gh/halo-dev/halo\"><img alt=\"Codecov percentage\" src=\"https://img.shields.io/codecov/c/github/halo-dev/halo/main?style=flat-square&token=YsRUg9fall\"/></a>\n<a href=\"https://gitcode.com/feizhiyun/Halo\"><img src=\"https://gitcode.com/feizhiyun/Halo/star/badge.svg\" alt=\"GitCode Stars\"></a>\n<a href=\"https://www.producthunt.com/posts/halo-6b401e75-bb58-4dff-9fe9-2ada3323c874?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-halo&#0045;6b401e75&#0045;bb58&#0045;4dff&#0045;9fe9&#0045;2ada3323c874\" target=\"_blank\"><img src=\"https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=407442&theme=light\" alt=\"Halo - Powerful&#0032;and&#0032;easy&#0045;to&#0045;use&#0032;Open&#0045;Source&#0032;website&#0032;building&#0032;tool | Product Hunt\" style=\"height: 20px;\" height=\"20px\" /></a>\n<br />\n<a href=\"https://www.halo.run\">官网</a>\n<a href=\"https://docs.halo.run\">文档</a>\n<a href=\"https://bbs.halo.run\">社区</a>\n<a href=\"https://gitee.com/halo-dev\">Gitee</a>\n<a href=\"https://t.me/halo_dev\">Telegram 频道</a>\n</p>\n\n[![Watch the video](https://www.halo.run/upload/halo-github-screenshot.png)](https://www.bilibili.com/video/BV15x4y1U7RU/?share_source=copy_web&vd_source=0ab6cf86ca512a363f04f18b86f55b86)\n\n------------------------------\n\n## 快速开始\n\n如果你的设备有 Docker 环境，可以使用以下命令快速启动一个 Halo 的体验环境：\n\n```bash\ndocker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.22\n```\n\n或者点击下方按钮使用 [Gitpod](https://gitpod.io/) 或 [ClawCloud Run](https://template.us-west-1.run.claw.cloud/deploy?templateName=halo) 启动一个体验环境：\n\n[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/halo-sigs/gitpod-demo)\n\n[![Run on ClawCloud](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.us-west-1.run.claw.cloud/deploy?templateName=halo)\n\n**以上方式仅作为体验使用，推荐使用开源 Linux 服务器运维管理面板 [1Panel](https://github.com/1Panel-dev/1Panel) 进行部署（[查看文档](https://docs.halo.run/getting-started/install/1panel)），轻松搞定反向代理、SSL 证书及升级备份任务。更多部署方式，请[查看文档](https://docs.halo.run/category/%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97)。**\n\n## 在线体验\n\n- 环境地址：<https://demo.halocms.site>\n- 后台地址：<https://demo.halocms.site/console>\n- 用户名：`demo`\n- 密码：`P@ssw0rd123..`\n\n## 付费版\n\n相比于社区版，Halo 付费版为用户提供了大量增强功能及技术支持服务，增强功能包括商城、短信验证码注册登录、全站私有化、LDAP 登录、三方账号登录及自定义 Logo 等。 [点击查看付费版详细介绍](https://www.lxware.cn/halo)。\n\n## 生态\n\n可访问 [官方应用市场](https://www.halo.run/store/apps) 或 [awesome-halo 仓库](https://github.com/halo-sigs/awesome-halo) 查看适用于 Halo 2.x 的主题和插件。\n\n## 许可证\n\n[![license](https://img.shields.io/github/license/halo-dev/halo.svg?style=flat-square)](https://github.com/halo-dev/halo/blob/master/LICENSE)\n\nHalo 使用 GPL-v3.0 协议开源，请遵守开源协议。\n\n## 贡献\n\n参考 [CONTRIBUTING](https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md)。\n\n<a href=\"https://github.com/halo-dev/halo/graphs/contributors\"><img src=\"https://opencollective.com/halo/contributors.svg?width=890&button=false\" /></a>\n\n## 状态\n\n![Repobeats analytics](https://repobeats.axiom.co/api/embed/ad008b2151c22e7cf734d2688befaa795d593b95.svg \"Repobeats analytics image\")\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nHalo currently supports the versions listed below, where as:\n\n- :white_check_mark: indicates an active development roadmap, is therefore maintaining, and **will** receive Security\n  Vulnerability Report.\n- :x: indicates such version has already deprecated and **will not** be receiving Security Vulnerability Report.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 0.x     | :x:                |\n| 1.x     | :x:                |\n| 2.x     | :white_check_mark: |\n\n## Reporting a Vulnerability\n\nWe first appreciate and are very thankful that you've found a vulnerability issue in Halo! By disclosing such issue to\nHalo development team you are helping Halo to become a much more safer project than before! ;)\n\nTo protect the existing users of Halo, we kindly ask you to not disclose the vulnerability to anyone except the Halo\ndevelopment team before a fix has been rolled out.\n\nTo Report a Vulnerability, please complete the form below, and send such report by email to `hi@halo.run`.\n\n```\nVulnerability has been observed in...\n  - Docker? [n/y]: \n    if yes for the question above,\n    - `docker -v`: \n    - `docker images halohub/halo`: \n  \n  - by `java -jar halo.jar`? [n/y]: \n    if yes for the question above,\n    - `uname -a`: \n    - `java -version`: \n \n- Affected by Halo version(s) [e.g. v2.4.0]: \n- Vulnerability self-scoring [1-10]: \n- Would you like to be attributed? (Whether you agree us to appreciate you by putting your name in the CHANGELOG of the next fix release) [n/y]: \n```\n"
  },
  {
    "path": "api/build.gradle",
    "content": "plugins {\n    id 'checkstyle'\n    id 'java-library'\n    id 'halo.publish'\n    id 'jacoco'\n    alias(libs.plugins.lombok)\n    alias(libs.plugins.versions)\n}\n\ngroup = 'run.halo.app'\ndescription = 'API of halo project, connecting by other projects.'\n\ntasks.withType(JavaCompile).configureEach {\n    options.release = 21\n    options.encoding = 'UTF-8'\n}\n\ntasks.withType(Javadoc).configureEach {\n    options.encoding = 'UTF-8'\n}\n\njava {\n    toolchain {\n        languageVersion = JavaLanguageVersion.of(21)\n    }\n    withJavadocJar()\n    withSourcesJar()\n}\n\ncheckstyle {\n    toolVersion = libs.versions.checkstyle.get()\n    showViolations = false\n    ignoreFailures = false\n}\n\njar {\n    manifest {\n        attributes(\n                'Implementation-Title': project.name,\n                'Implementation-Version': project.version,\n                'Implementation-Vendor': 'Halo Project',\n        )\n    }\n}\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    api platform(project(':platform:application'))\n    annotationProcessor platform(project(':platform:application'))\n\n    api 'org.springframework.boot:spring-boot-starter-actuator'\n    api 'org.springframework.boot:spring-boot-starter-mail'\n    api 'org.springframework.boot:spring-boot-starter-thymeleaf'\n    api 'org.springframework.boot:spring-boot-starter-webflux'\n    api 'org.springframework.boot:spring-boot-starter-validation'\n    api 'org.springframework.boot:spring-boot-starter-data-r2dbc'\n    api 'org.springframework.session:spring-session-core'\n    api 'org.springframework.boot:spring-boot-jackson2'\n    api 'org.springframework.boot:spring-boot-integration'\n    api 'org.springframework.boot:spring-boot-session'\n\n    // Spring Security\n    api 'org.springframework.boot:spring-boot-starter-security'\n    api 'org.springframework.security:spring-security-oauth2-jose'\n    api 'org.springframework.security:spring-security-oauth2-client'\n    api 'org.springframework.security:spring-security-oauth2-resource-server'\n\n    api 'io.micrometer:context-propagation'\n\n    // Cache\n    api \"org.springframework.boot:spring-boot-starter-cache\"\n    api \"com.github.ben-manes.caffeine:caffeine\"\n\n    api \"org.springdoc:springdoc-openapi-starter-webflux-ui\"\n    api 'org.openapi4j:openapi-schema-validator'\n    api \"net.bytebuddy:byte-buddy\"\n    api \"org.bouncycastle:bcpkix-jdk18on\"\n\n    // Apache Lucene\n    api \"org.apache.lucene:lucene-core\"\n    api \"org.apache.lucene:lucene-queryparser\"\n    api \"org.apache.lucene:lucene-highlighter\"\n    api \"org.apache.lucene:lucene-backward-codecs\"\n    api 'org.apache.lucene:lucene-analysis-common'\n\n    api \"org.apache.commons:commons-lang3\"\n    api \"io.seruco.encoding:base62\"\n    api \"org.pf4j:pf4j\"\n    api \"com.google.guava:guava\"\n    api \"org.jsoup:jsoup\"\n    api \"io.github.java-diff-utils:java-diff-utils\"\n    api \"org.springframework.integration:spring-integration-core\"\n    api \"com.github.java-json-tools:json-patch\"\n    api \"org.thymeleaf.extras:thymeleaf-extras-springsecurity6\"\n    api 'org.apache.tika:tika-core'\n    api 'net.coobird:thumbnailator'\n\n    api \"io.github.resilience4j:resilience4j-spring-boot3\"\n    api \"io.github.resilience4j:resilience4j-reactor\"\n\n    api \"com.j256.two-factor-auth:two-factor-auth\"\n\n    runtimeOnly 'io.r2dbc:r2dbc-h2'\n    runtimeOnly 'org.postgresql:postgresql'\n    runtimeOnly 'org.postgresql:r2dbc-postgresql'\n    runtimeOnly 'org.mariadb:r2dbc-mariadb'\n    runtimeOnly 'io.asyncer:r2dbc-mysql'\n    runtimeOnly 'com.github.therapi:therapi-runtime-javadoc'\n\n    annotationProcessor \"com.github.therapi:therapi-runtime-javadoc-scribe\"\n\n    testImplementation 'org.springframework.boot:spring-boot-starter-test'\n    testImplementation 'org.springframework.security:spring-security-test'\n    testImplementation 'io.projectreactor:reactor-test'\n\n    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'\n}\n\npublishing {\n    publications.named('mavenJava', MavenPublication) {\n        from components.java\n        pom {\n            name = 'API library'\n            description = \"$project.description\"\n        }\n    }\n}\n\ntasks.named('test') {\n    useJUnitPlatform()\n    finalizedBy jacocoTestReport\n}\n\ntasks.named('jacocoTestReport') {\n    reports {\n        xml.required = true\n        html.required = false\n    }\n}\n\ntasks.named('uploadBundle') {\n    mustRunAfter project(':platform:application').tasks.named('uploadBundle')\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/content/ContentWrapper.java",
    "content": "package run.halo.app.content;\n\nimport lombok.Builder;\nimport lombok.Data;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.Snapshot;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Builder\npublic class ContentWrapper {\n    private String snapshotName;\n    private String raw;\n    private String content;\n    private String rawType;\n\n    public static ContentWrapper patchSnapshot(Snapshot patchSnapshot, Snapshot baseSnapshot) {\n        Assert.notNull(baseSnapshot, \"The baseSnapshot must not be null.\");\n        String baseSnapshotName = baseSnapshot.getMetadata().getName();\n        if (StringUtils.equals(patchSnapshot.getMetadata().getName(), baseSnapshotName)) {\n            return ContentWrapper.builder()\n                .snapshotName(patchSnapshot.getMetadata().getName())\n                .raw(patchSnapshot.getSpec().getRawPatch())\n                .content(patchSnapshot.getSpec().getContentPatch())\n                .rawType(patchSnapshot.getSpec().getRawType())\n                .build();\n        }\n        String patchedContent = PatchUtils.applyPatch(baseSnapshot.getSpec().getContentPatch(),\n            patchSnapshot.getSpec().getContentPatch());\n        String patchedRaw = PatchUtils.applyPatch(baseSnapshot.getSpec().getRawPatch(),\n            patchSnapshot.getSpec().getRawPatch());\n        return ContentWrapper.builder()\n            .snapshotName(patchSnapshot.getMetadata().getName())\n            .raw(patchedRaw)\n            .content(patchedContent)\n            .rawType(patchSnapshot.getSpec().getRawType())\n            .build();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/content/ExcerptGenerator.java",
    "content": "package run.halo.app.content;\n\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Mono;\n\npublic interface ExcerptGenerator extends ExtensionPoint {\n\n    Mono<String> generate(ExcerptGenerator.Context context);\n\n    @Data\n    @Accessors(chain = true)\n    class Context {\n        private String raw;\n        /**\n         * html content.\n         */\n        private String content;\n\n        private String rawType;\n        /**\n         * keywords in the content to help the excerpt generation more accurate.\n         */\n        private Set<String> keywords;\n        /**\n         * Max length of the generated excerpt.\n         */\n        private int maxLength;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/content/PatchUtils.java",
    "content": "package run.halo.app.content;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.github.difflib.DiffUtils;\nimport com.github.difflib.patch.AbstractDelta;\nimport com.github.difflib.patch.ChangeDelta;\nimport com.github.difflib.patch.Chunk;\nimport com.github.difflib.patch.DeleteDelta;\nimport com.github.difflib.patch.DeltaType;\nimport com.github.difflib.patch.InsertDelta;\nimport com.github.difflib.patch.Patch;\nimport com.github.difflib.patch.PatchFailedException;\nimport com.google.common.base.Splitter;\nimport java.util.Collections;\nimport java.util.List;\nimport lombok.Data;\nimport org.apache.commons.lang3.StringUtils;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class PatchUtils {\n    private static final String DELIMITER = \"\\n\";\n    private static final Splitter lineSplitter = Splitter.on(DELIMITER);\n\n    public static Patch<String> create(String deltasJson) {\n        List<Delta> deltas = JsonUtils.jsonToObject(deltasJson, new TypeReference<>() {\n        });\n        Patch<String> patch = new Patch<>();\n        for (Delta delta : deltas) {\n            StringChunk sourceChunk = delta.getSource();\n            StringChunk targetChunk = delta.getTarget();\n            Chunk<String> orgChunk = new Chunk<>(sourceChunk.getPosition(), sourceChunk.getLines(),\n                sourceChunk.getChangePosition());\n            Chunk<String> revChunk = new Chunk<>(targetChunk.getPosition(), targetChunk.getLines(),\n                targetChunk.getChangePosition());\n            switch (delta.getType()) {\n                case DELETE -> patch.addDelta(new DeleteDelta<>(orgChunk, revChunk));\n                case INSERT -> patch.addDelta(new InsertDelta<>(orgChunk, revChunk));\n                case CHANGE -> patch.addDelta(new ChangeDelta<>(orgChunk, revChunk));\n                default -> throw new IllegalArgumentException(\"Unsupported delta type.\");\n            }\n        }\n        return patch;\n    }\n\n    public static String patchToJson(Patch<String> patch) {\n        List<AbstractDelta<String>> deltas = patch.getDeltas();\n        return JsonUtils.objectToJson(deltas);\n    }\n\n    public static String applyPatch(String original, String patchJson) {\n        Patch<String> patch = PatchUtils.create(patchJson);\n        try {\n            return String.join(DELIMITER, patch.applyTo(breakLine(original)));\n        } catch (PatchFailedException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String diffToJsonPatch(String original, String revised) {\n        Patch<String> patch = DiffUtils.diff(breakLine(original), breakLine(revised));\n        return PatchUtils.patchToJson(patch);\n    }\n\n    public static List<String> breakLine(String content) {\n        if (StringUtils.isBlank(content)) {\n            return Collections.emptyList();\n        }\n        return lineSplitter.splitToList(content);\n    }\n\n    @Data\n    public static class Delta {\n        private StringChunk source;\n        private StringChunk target;\n        private DeltaType type;\n    }\n\n    @Data\n    public static class StringChunk {\n        private int position;\n        private List<String> lines;\n        private List<Integer> changePosition;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/content/PostContentService.java",
    "content": "package run.halo.app.content;\n\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\npublic interface PostContentService {\n\n    Mono<ContentWrapper> getHeadContent(String postName);\n\n    Mono<ContentWrapper> getReleaseContent(String postName);\n\n    Mono<ContentWrapper> getSpecifiedContent(String postName, String snapshotName);\n\n    Flux<String> listSnapshots(String postName);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/content/comment/CommentSubject.java",
    "content": "package run.halo.app.content.comment;\n\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.Ref;\n\n/**\n * Comment subject.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface CommentSubject<T extends Extension> extends ExtensionPoint {\n\n    Mono<T> get(String name);\n\n    default Mono<SubjectDisplay> getSubjectDisplay(String name) {\n        return Mono.empty();\n    }\n\n    boolean supports(Ref ref);\n\n    record SubjectDisplay(String title, String url, String kindName) {\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/attachment/ThumbnailProvider.java",
    "content": "package run.halo.app.core.attachment;\n\nimport java.net.URI;\nimport java.net.URL;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;\n\n/**\n * Thumbnail provider extension.\n *\n * @since 2.22.0\n * @deprecated Use {@link AttachmentHandler} instead. We are planing to remove this extension\n * point in future release.\n */\n@Deprecated(forRemoval = true, since = \"2.22.0\")\npublic interface ThumbnailProvider extends ExtensionPoint {\n\n    /**\n     * Generate thumbnail URI for given image URL and size.\n     *\n     * @param context Thumbnail context including image URI and size\n     * @return Generated thumbnail URI\n     */\n    Mono<URI> generate(ThumbnailContext context);\n\n    /**\n     * Delete thumbnail file for given image URL.\n     *\n     * @param imageUrl original image URL\n     */\n    Mono<Void> delete(URL imageUrl);\n\n    /**\n     * Whether the provider supports the given image URI.\n     *\n     * @return {@code true} if supports, {@code false} otherwise\n     */\n    Mono<Boolean> supports(ThumbnailContext context);\n\n    @Data\n    @Builder\n    class ThumbnailContext {\n        private final URL imageUrl;\n        private final ThumbnailSize size;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/attachment/ThumbnailSize.java",
    "content": "package run.halo.app.core.attachment;\n\nimport java.util.Arrays;\nimport java.util.Optional;\nimport lombok.Getter;\n\n@Getter\npublic enum ThumbnailSize {\n    S(400),\n    M(800),\n    L(1200),\n    XL(1600);\n\n    private final int width;\n\n    ThumbnailSize(int width) {\n        this.width = width;\n    }\n\n    /**\n     * Convert width string to {@link ThumbnailSize}.\n     *\n     * @param width width string\n     */\n    public static ThumbnailSize fromWidth(String width) {\n        for (ThumbnailSize value : values()) {\n            if (String.valueOf(value.getWidth()).equals(width)) {\n                return value;\n            }\n        }\n        return ThumbnailSize.M;\n    }\n\n    /**\n     * Convert name to {@link ThumbnailSize}.\n     */\n    public static ThumbnailSize fromName(String name) {\n        for (ThumbnailSize value : values()) {\n            if (value.name().equalsIgnoreCase(name)) {\n                return value;\n            }\n        }\n        throw new IllegalArgumentException(\"No such thumbnail size: \" + name);\n    }\n\n    public static Optional<ThumbnailSize> optionalValueOf(String name) {\n        for (ThumbnailSize value : values()) {\n            if (value.name().equalsIgnoreCase(name)) {\n                return Optional.of(value);\n            }\n        }\n        return Optional.empty();\n    }\n\n    public static Integer[] allowedWidths() {\n        return Arrays.stream(ThumbnailSize.values())\n            .map(ThumbnailSize::getWidth)\n            .toArray(Integer[]::new);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/endpoint/WebSocketEndpoint.java",
    "content": "package run.halo.app.core.endpoint;\n\nimport org.springframework.web.reactive.socket.WebSocketHandler;\nimport run.halo.app.extension.GroupVersion;\n\n/**\n * Endpoint for WebSocket.\n *\n * @author johnniang\n */\npublic interface WebSocketEndpoint {\n\n    /**\n     * Path of the URL after group version.\n     *\n     * @return path of the URL.\n     */\n    String urlPath();\n\n    /**\n     * Group and version parts of the endpoint.\n     *\n     * @return GroupVersion.\n     */\n    GroupVersion groupVersion();\n\n    /**\n     * Real WebSocket handler for the endpoint.\n     *\n     * @return WebSocket handler.\n     */\n    WebSocketHandler handler();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/AnnotationSetting.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.core.extension.AnnotationSetting.KIND;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.GroupKind;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"\", version = \"v1alpha1\", kind = KIND,\n    plural = \"annotationsettings\", singular = \"annotationsetting\")\npublic class AnnotationSetting extends AbstractExtension {\n    public static final String TARGET_REF_LABEL = \"halo.run/target-ref\";\n\n    public static final String KIND = \"AnnotationSetting\";\n\n    @Schema(requiredMode = REQUIRED)\n    private AnnotationSettingSpec spec;\n\n    @Data\n    public static class AnnotationSettingSpec {\n        @Schema(requiredMode = REQUIRED)\n        private GroupKind targetRef;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private List<Object> formSchema;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/AuthProvider.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * Auth provider extension.\n *\n * @author guqing\n * @since 2.4.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\n@GVK(group = \"auth.halo.run\", version = \"v1alpha1\", kind = \"AuthProvider\",\n    singular = \"authprovider\", plural = \"authproviders\")\npublic class AuthProvider extends AbstractExtension {\n\n    public static final String AUTH_BINDING_LABEL = \"auth.halo.run/auth-binding\";\n\n    public static final String PRIVILEGED_LABEL = \"auth.halo.run/privileged\";\n\n    @Schema(requiredMode = REQUIRED)\n    private AuthProviderSpec spec;\n\n    @Data\n    @ToString\n    public static class AuthProviderSpec {\n\n        @Schema(requiredMode = REQUIRED, description = \"Display name of the auth provider\")\n        private String displayName;\n\n        private String description;\n\n        private String logo;\n\n        private String website;\n\n        private String helpPage;\n\n        @Schema(requiredMode = REQUIRED, description = \"Authentication url of the auth provider\")\n        private String authenticationUrl;\n\n        private String method = \"GET\";\n\n        private boolean rememberMeSupport = false;\n\n        /**\n         * Auth type: form or oauth2.\n         */\n        @Getter(onMethod_ = @NonNull)\n        @Schema(requiredMode = REQUIRED)\n        private AuthType authType = AuthType.OAUTH2;\n\n        private String bindingUrl;\n\n        private String unbindUrl;\n\n        @Schema(requiredMode = NOT_REQUIRED)\n        private SettingRef settingRef;\n\n        @Schema(requiredMode = NOT_REQUIRED)\n        private ConfigMapRef configMapRef;\n\n        public void setAuthType(AuthType authType) {\n            this.authType = (authType == null ? AuthType.OAUTH2 : authType);\n        }\n    }\n\n    @Data\n    @ToString\n    public static class SettingRef {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String name;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String group;\n    }\n\n    @Data\n    @ToString\n    public static class ConfigMapRef {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String name;\n    }\n\n    public enum AuthType {\n        FORM,\n        OAUTH2\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/Counter.java",
    "content": "package run.halo.app.core.extension;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Metadata;\n\n/**\n * A counter for number of requests by extension resource name.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@GVK(group = \"metrics.halo.run\", version = \"v1alpha1\", kind = \"Counter\", plural = \"counters\",\n    singular = \"counter\")\n@EqualsAndHashCode(callSuper = true)\npublic class Counter extends AbstractExtension {\n\n    private Integer visit;\n\n    private Integer upvote;\n\n    private Integer downvote;\n\n    private Integer totalComment;\n\n    private Integer approvedComment;\n\n    public static Counter emptyCounter(String name) {\n        Counter counter = new Counter();\n        counter.setMetadata(new Metadata());\n        counter.getMetadata().setName(name);\n        counter.setUpvote(0);\n        counter.setTotalComment(0);\n        counter.setApprovedComment(0);\n        counter.setVisit(0);\n        return counter;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/Device.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.experimental.Accessors;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = Device.GROUP, version = Device.VERSION, kind = Device.KIND, plural = \"devices\",\n    singular = \"device\")\npublic class Device extends AbstractExtension {\n    public static final String GROUP = \"security.halo.run\";\n    public static final String VERSION = \"v1alpha1\";\n    public static final String KIND = \"Device\";\n\n    @Schema(requiredMode = REQUIRED)\n    private Spec spec;\n\n    @Getter(onMethod_ = @NonNull)\n    private Status status = new Status();\n\n    public void setStatus(Status status) {\n        this.status = (status == null ? new Status() : status);\n    }\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"DeviceSpec\")\n    public static class Spec {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String sessionId;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String principalName;\n\n        @Schema(requiredMode = REQUIRED, maxLength = 129)\n        private String ipAddress;\n\n        @Schema(maxLength = 500)\n        private String userAgent;\n\n        private String rememberMeSeriesId;\n\n        private Instant lastAccessedTime;\n\n        private Instant lastAuthenticatedTime;\n    }\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"DeviceStatus\")\n    public static class Status {\n        private String browser;\n        private String os;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/Menu.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.ArraySchema;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.LinkedHashSet;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"\", version = \"v1alpha1\", kind = \"Menu\", plural = \"menus\", singular = \"menu\")\npublic class Menu extends AbstractExtension {\n\n    @Schema(description = \"The spec of menu.\", requiredMode = REQUIRED)\n    private Spec spec;\n\n    @Data\n    @Schema(name = \"MenuSpec\")\n    public static class Spec {\n\n        @Schema(description = \"The display name of the menu.\", requiredMode = REQUIRED)\n        private String displayName;\n\n        @ArraySchema(\n            uniqueItems = true,\n            arraySchema = @Schema(description = \"Menu items of this menu.\"),\n            schema = @Schema(description = \"Name of menu item.\")\n        )\n        private LinkedHashSet<String> menuItems;\n\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/MenuItem.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonValue;\nimport io.swagger.v3.oas.annotations.media.ArraySchema;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.LinkedHashSet;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Ref;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"\", version = \"v1alpha1\", kind = \"MenuItem\",\n    plural = \"menuitems\", singular = \"menuitem\")\npublic class MenuItem extends AbstractExtension {\n\n    @Schema(description = \"The spec of menu item.\", requiredMode = REQUIRED)\n    private MenuItemSpec spec;\n\n    @Schema(description = \"The status of menu item.\")\n    private MenuItemStatus status;\n\n    public enum Target {\n        BLANK(\"_blank\"),\n        SELF(\"_self\"),\n        PARENT(\"_parent\"),\n        TOP(\"_top\");\n\n        private final String value;\n\n        @JsonCreator\n        Target(String value) {\n            this.value = value;\n        }\n\n        @JsonValue\n        public String getValue() {\n            return value;\n        }\n    }\n\n    @Data\n    public static class MenuItemSpec {\n\n        @Schema(description = \"The display name of menu item.\")\n        private String displayName;\n\n        @Schema(description = \"The href of this menu item.\")\n        private String href;\n\n        @Schema(description = \"The <a> target attribute of this menu item.\")\n        private Target target;\n\n        @Schema(description = \"The priority is for ordering.\")\n        private Integer priority;\n\n        @ArraySchema(\n            uniqueItems = true,\n            arraySchema = @Schema(description = \"Children of this menu item\"),\n            schema = @Schema(description = \"The name of menu item child\"))\n        private LinkedHashSet<String> children;\n\n        @Schema(description = \"Target reference. Like Category, Tag, Post or SinglePage\")\n        private Ref targetRef;\n\n    }\n\n    @Data\n    public static class MenuItemStatus {\n\n        @Schema(description = \"Calculated Display name of menu item.\")\n        private String displayName;\n\n        @Schema(description = \"Calculated href of manu item.\")\n        private String href;\n\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/Plugin.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.net.URI;\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport org.pf4j.PluginState;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.infra.ConditionList;\n\n/**\n * A custom resource for Plugin.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = \"plugin.halo.run\", version = \"v1alpha1\", kind = \"Plugin\", plural = \"plugins\",\n    singular = \"plugin\")\n@EqualsAndHashCode(callSuper = true)\npublic class Plugin extends AbstractExtension {\n\n    public static final String SYSTEM_RESERVED_LABEL_KEY = \"plugin.halo.run/system-reserved\";\n\n    public static final String BUILT_IN_KEEPER_FINALIZER = \"plugin.halo.run/built-in-keeper\";\n\n    @Schema(requiredMode = REQUIRED)\n    private PluginSpec spec;\n\n    private PluginStatus status;\n\n    /**\n     * Gets plugin status.\n     *\n     * @return empty object if status is null.\n     */\n    @NonNull\n    @JsonIgnore\n    public PluginStatus statusNonNull() {\n        if (this.status == null) {\n            this.status = new PluginStatus();\n        }\n        return status;\n    }\n\n    @Data\n    public static class PluginSpec {\n\n        private String displayName;\n\n        /**\n         * plugin version.\n         *\n         * @see <a href=\"semver.org\">semantic version</a>\n         */\n        @Schema(requiredMode = REQUIRED,\n            pattern = \"^(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)(?:-(\"\n                + \"(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.\"\n                + \"(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+([0-9a-zA-Z-]+(?:\\\\\"\n                + \".[0-9a-zA-Z-]+)*))?$\")\n        private String version;\n\n        private PluginAuthor author;\n\n        private String logo;\n\n        private Map<String, String> pluginDependencies = new HashMap<>(4);\n\n        private String homepage;\n\n        private String repo;\n\n        private String issues;\n\n        private String description;\n\n        private List<License> license;\n\n        /**\n         * SemVer format.\n         */\n        private String requires = \"*\";\n\n        private Boolean enabled = false;\n\n        private String settingName;\n\n        private String configMapName;\n    }\n\n    /**\n     * In the future, we may consider using {@link run.halo.app.infra.model.License} instead of it.\n     * But now, replace it will lead to incompatibility with downstream.\n     */\n    @Data\n    public static class License {\n        private String name;\n        private String url;\n    }\n\n    @Data\n    public static class PluginStatus {\n\n        private Phase phase;\n\n        private ConditionList conditions;\n\n        private Instant lastStartTime;\n\n        private PluginState lastProbeState;\n\n        private String entry;\n\n        private String stylesheet;\n\n        private String logo;\n\n        @Schema(description = \"Load location of the plugin, often a path.\")\n        private URI loadLocation;\n\n        public static ConditionList nullSafeConditions(@NonNull PluginStatus status) {\n            Assert.notNull(status, \"The status must not be null.\");\n            if (status.getConditions() == null) {\n                status.setConditions(new ConditionList());\n            }\n            return status.getConditions();\n        }\n    }\n\n    public enum Phase {\n        PENDING,\n        STARTING,\n        CREATED,\n        DISABLING,\n        DISABLED,\n        RESOLVED,\n        STARTED,\n        STOPPED,\n        FAILED,\n        UNKNOWN,\n        ;\n    }\n\n    @Data\n    @ToString\n    public static class PluginAuthor {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String name;\n\n        private String website;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/RememberMeToken.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"security.halo.run\", version = \"v1alpha1\", kind = \"RememberMeToken\", plural =\n    \"remembermetokens\", singular = \"remembermetoken\")\npublic class RememberMeToken extends AbstractExtension {\n\n    @Schema(requiredMode = REQUIRED)\n    private Spec spec;\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"RememberMeTokenSpec\")\n    public static class Spec {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String username;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String series;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String tokenValue;\n\n        private Instant lastUsed;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/ReverseProxy.java",
    "content": "package run.halo.app.core.extension;\n\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * <p>The reverse proxy custom resource is used to configure a path to proxy it to a directory or\n * file.</p>\n * <p>HTTP proxy may be added in the future.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"plugin.halo.run\", kind = \"ReverseProxy\", version = \"v1alpha1\",\n    plural = \"reverseproxies\", singular = \"reverseproxy\")\npublic class ReverseProxy extends AbstractExtension {\n    private List<ReverseProxyRule> rules;\n\n    public record ReverseProxyRule(String path, FileReverseProxyProvider file) {\n    }\n\n    public record FileReverseProxyProvider(String directory, String filename) {\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/Role.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static java.util.Arrays.compare;\nimport static run.halo.app.core.extension.Role.GROUP;\nimport static run.halo.app.core.extension.Role.KIND;\nimport static run.halo.app.core.extension.Role.VERSION;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\n@GVK(group = GROUP,\n    version = VERSION,\n    kind = KIND,\n    plural = \"roles\",\n    singular = \"role\")\npublic class Role extends AbstractExtension {\n    public static final String ROLE_DEPENDENCY_RULES =\n        \"rbac.authorization.halo.run/dependency-rules\";\n    public static final String ROLE_AGGREGATE_LABEL_PREFIX =\n        \"rbac.authorization.halo.run/aggregate-to-\";\n    public static final String ROLE_DEPENDENCIES_ANNO = \"rbac.authorization.halo.run/dependencies\";\n    public static final String UI_PERMISSIONS_ANNO = \"rbac.authorization.halo.run/ui-permissions\";\n\n    public static final String SYSTEM_RESERVED_LABELS =\n        \"rbac.authorization.halo.run/system-reserved\";\n    public static final String HIDDEN_LABEL_NAME = \"halo.run/hidden\";\n    public static final String TEMPLATE_LABEL_NAME = \"halo.run/role-template\";\n    public static final String UI_PERMISSIONS_AGGREGATED_ANNO =\n        \"rbac.authorization.halo.run/ui-permissions-aggregated\";\n\n    public static final String GROUP = \"\";\n    public static final String VERSION = \"v1alpha1\";\n    public static final String KIND = \"Role\";\n\n    @Schema(requiredMode = REQUIRED)\n    List<PolicyRule> rules;\n\n    /**\n     * PolicyRule holds information that describes a policy rule, but does not contain information\n     * about whom the rule applies to or which namespace the rule applies to.\n     *\n     * @author guqing\n     * @since 2.0.0\n     */\n    @Getter\n    @EqualsAndHashCode\n    public static class PolicyRule implements Comparable<PolicyRule> {\n        /**\n         * APIGroups is the name of the APIGroup that contains the resources.\n         * If multiple API groups are specified, any action requested against one of the enumerated\n         * resources in any API group will be allowed.\n         */\n        final String[] apiGroups;\n\n        /**\n         * Resources is a list of resources this rule applies to.  '*' represents all resources in\n         * the specified apiGroups.\n         * '*&#47;foo' represents the subresource 'foo' for all resources in the specified\n         * apiGroups.\n         */\n        final String[] resources;\n\n        /**\n         * ResourceNames is an optional white list of names that the rule applies to.  An empty set\n         * means that everything is allowed.\n         */\n        final String[] resourceNames;\n\n        /**\n         * NonResourceURLs is a set of partial urls that a user should have access to.\n         * *s are allowed, but only as the full, final step in the path\n         * If an action is not a resource API request, then the URL is split on '/' and is checked\n         * against the NonResourceURLs to look for a match.\n         * Since non-resource URLs are not namespaced, this field is only applicable for\n         * ClusterRoles referenced from a ClusterRoleBinding.\n         * Rules can either apply to API resources (such as \"pods\" or \"secrets\") or non-resource\n         * URL paths (such as \"/api\"),  but not both.\n         */\n        final String[] nonResourceURLs;\n\n        /**\n         * about who the rule applies to or which namespace the rule applies to.\n         */\n        final String[] verbs;\n\n        public PolicyRule() {\n            this(null, null, null, null, null);\n        }\n\n        public PolicyRule(String[] apiGroups, String[] resources,\n            String[] resourceNames,\n            String[] nonResourceURLs, String[] verbs) {\n            this.apiGroups = nullElseEmpty(apiGroups);\n            this.resources = nullElseEmpty(resources);\n            this.resourceNames = nullElseEmpty(resourceNames);\n            this.nonResourceURLs = nullElseEmpty(nonResourceURLs);\n            this.verbs = nullElseEmpty(verbs);\n        }\n\n        String[] nullElseEmpty(String... items) {\n            if (items == null) {\n                return new String[] {};\n            }\n            return items;\n        }\n\n        @Override\n        public int compareTo(@NonNull PolicyRule other) {\n            int result = compare(apiGroups, other.apiGroups);\n            if (result != 0) {\n                return result;\n            }\n            result = compare(resources, other.resources);\n            if (result != 0) {\n                return result;\n            }\n            result = compare(resourceNames, other.resourceNames);\n            if (result != 0) {\n                return result;\n            }\n            result = compare(nonResourceURLs, other.nonResourceURLs);\n            if (result != 0) {\n                return result;\n            }\n            result = compare(verbs, other.verbs);\n            return result;\n        }\n\n        public static class Builder {\n            String[] apiGroups;\n\n            String[] resources;\n\n            String[] resourceNames;\n\n            String[] nonResourceURLs;\n\n            String[] verbs;\n\n            public Builder apiGroups(String... apiGroups) {\n                this.apiGroups = apiGroups;\n                return this;\n            }\n\n            public Builder resources(String... resources) {\n                this.resources = resources;\n                return this;\n            }\n\n            public Builder resourceNames(String... resourceNames) {\n                this.resourceNames = resourceNames;\n                return this;\n            }\n\n            public Builder nonResourceURLs(String... nonResourceURLs) {\n                this.nonResourceURLs = nonResourceURLs;\n                return this;\n            }\n\n            public Builder verbs(String... verbs) {\n                this.verbs = verbs;\n                return this;\n            }\n\n            public PolicyRule build() {\n                return new PolicyRule(apiGroups, resources, resourceNames,\n                    nonResourceURLs,\n                    verbs);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/RoleBinding.java",
    "content": "package run.halo.app.core.extension;\n\nimport static run.halo.app.core.extension.RoleBinding.GROUP;\nimport static run.halo.app.core.extension.RoleBinding.KIND;\nimport static run.halo.app.core.extension.RoleBinding.VERSION;\n\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.NoArgsConstructor;\nimport lombok.ToString;\nimport org.springframework.util.StringUtils;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.ExtensionOperator;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Metadata;\n\n/**\n * RoleBinding references a role, but does not contain it.\n * It can reference a Role in the global.\n * It adds who information via Subjects.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\n@GVK(group = GROUP,\n    version = VERSION,\n    kind = KIND,\n    plural = \"rolebindings\",\n    singular = \"rolebinding\")\npublic class RoleBinding extends AbstractExtension {\n\n    public static final String GROUP = \"\";\n\n    public static final String VERSION = \"v1alpha1\";\n\n    public static final String KIND = \"RoleBinding\";\n\n    /**\n     * Subjects holds references to the objects the role applies to.\n     */\n    List<Subject> subjects;\n\n    /**\n     * RoleRef can reference a Role in the current namespace or a ClusterRole in the global\n     * namespace.\n     * If the RoleRef cannot be resolved, the Authorizer must return an error.\n     */\n    RoleRef roleRef;\n\n    /**\n     * RoleRef contains information that points to the role being used.\n     *\n     * @author guqing\n     * @since 2.0.0\n     */\n    @Data\n    public static class RoleRef {\n\n        /**\n         * Kind is the type of resource being referenced.\n         */\n        String kind;\n\n        /**\n         * Name is the name of resource being referenced.\n         */\n        String name;\n\n        /**\n         * APIGroup is the group for the resource being referenced.\n         */\n        String apiGroup;\n    }\n\n    /**\n     * @author guqing\n     * @since 2.0.0\n     */\n    @Data\n    @NoArgsConstructor\n    @AllArgsConstructor\n    public static class Subject {\n        /**\n         * Kind of object being referenced. Values defined by this API group are \"User\", \"Group\",\n         * and \"ServiceAccount\".\n         * If the Authorizer does not recognize the kind value, the Authorizer should report\n         * an error.\n         */\n        String kind;\n\n        /**\n         * Name of the object being referenced.\n         */\n        String name;\n\n        /**\n         * APIGroup holds the API group of the referenced subject.\n         * Defaults to \"\" for ServiceAccount subjects.\n         * Defaults to \"rbac.authorization.halo.run\" for User and Group subjects.\n         */\n        String apiGroup;\n\n        public static Predicate<Subject> isUser(String username) {\n            return subject -> User.KIND.equals(subject.getKind())\n                && User.GROUP.equals(subject.getApiGroup())\n                && username.equals(subject.getName());\n        }\n\n        public static Predicate<Subject> containsUser(Set<String> usernames) {\n            return subject -> User.KIND.equals(subject.getKind())\n                && User.GROUP.equals(subject.apiGroup)\n                && usernames.contains(subject.getName());\n        }\n\n        @Override\n        public String toString() {\n            if (StringUtils.hasText(apiGroup)) {\n                return apiGroup + \"/\" + kind + \"/\" + name;\n            }\n            return kind + \"/\" + name;\n        }\n    }\n\n    public static RoleBinding create(String username, String roleName) {\n        var metadata = new Metadata();\n        metadata.setName(String.join(\"-\", username, roleName, \"binding\"));\n\n        var roleRef = new RoleRef();\n        roleRef.setKind(Role.KIND);\n        roleRef.setName(roleName);\n        roleRef.setApiGroup(Role.GROUP);\n\n        var subject = new Subject();\n        subject.setKind(User.KIND);\n        subject.setName(username);\n        subject.setApiGroup(User.GROUP);\n\n        var binding = new RoleBinding();\n        binding.setMetadata(metadata);\n        binding.setRoleRef(roleRef);\n\n        // keep the subjects mutable\n        var subjects = new LinkedList<Subject>();\n        subjects.add(subject);\n\n        binding.setSubjects(subjects);\n        return binding;\n    }\n\n    public static Predicate<RoleBinding> containsUser(String username) {\n        return ExtensionOperator.<RoleBinding>isNotDeleted().and(\n            binding -> binding.getSubjects().stream()\n                .anyMatch(Subject.isUser(username)));\n    }\n\n    public static Predicate<RoleBinding> containsUser(Set<String> usernames) {\n        return ExtensionOperator.<RoleBinding>isNotDeleted()\n            .and(binding -> binding.getSubjects().stream()\n                .anyMatch(Subject.containsUser(usernames)));\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/Setting.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.extension.GroupVersionKind.fromExtension;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.GroupVersionKind;\n\n/**\n * {@link Setting} is a custom extension to generate forms based on configuration.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"\", version = \"v1alpha1\", kind = Setting.KIND,\n    plural = \"settings\", singular = \"setting\")\npublic class Setting extends AbstractExtension {\n\n    public static final String KIND = \"Setting\";\n\n    public static final GroupVersionKind GVK = fromExtension(Setting.class);\n\n    @Schema(requiredMode = REQUIRED)\n    private SettingSpec spec;\n\n    @Data\n    public static class SettingSpec {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private List<SettingForm> forms;\n    }\n\n    @Data\n    public static class SettingForm {\n\n        @Schema(requiredMode = REQUIRED)\n        private String group;\n\n        private String label;\n\n        @Schema(requiredMode = REQUIRED)\n        private List<Object> formSchema;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/Theme.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport java.util.Objects;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.infra.ConditionList;\nimport run.halo.app.infra.model.License;\n\n/**\n * <p>Theme extension.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"theme.halo.run\", version = \"v1alpha1\", kind = Theme.KIND,\n    plural = \"themes\", singular = \"theme\")\npublic class Theme extends AbstractExtension {\n\n    public static final String KIND = \"Theme\";\n\n    public static final String THEME_NAME_LABEL = \"theme.halo.run/theme-name\";\n\n    @Schema(requiredMode = REQUIRED)\n    private ThemeSpec spec;\n\n    private ThemeStatus status;\n\n    @Data\n    @ToString\n    public static class ThemeSpec {\n        private static final String WILDCARD = \"*\";\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String displayName;\n\n        @Schema(requiredMode = REQUIRED)\n        private Author author;\n\n        private String description;\n\n        private String logo;\n\n        private String homepage;\n\n        private String repo;\n\n        private String issues;\n\n        private String version = WILDCARD;\n\n        @Schema(requiredMode = NOT_REQUIRED)\n        private String requires = WILDCARD;\n\n        private String settingName;\n\n        private String configMapName;\n\n        private List<License> license;\n\n        @Schema\n        private CustomTemplates customTemplates;\n\n    }\n\n    @Data\n    public static class ThemeStatus {\n        private ThemePhase phase;\n        private ConditionList conditions;\n        private String location;\n    }\n\n    /**\n     * Null-safe get {@link ConditionList} from theme status.\n     *\n     * @param theme theme must not be null\n     * @return condition list\n     */\n    public static ConditionList nullSafeConditionList(Theme theme) {\n        Assert.notNull(theme, \"The theme must not be null\");\n        var status = Objects.requireNonNullElseGet(theme.getStatus(), ThemeStatus::new);\n        theme.setStatus(status);\n\n        var conditions = Objects.requireNonNullElseGet(status.getConditions(), ConditionList::new);\n        status.setConditions(conditions);\n        return conditions;\n    }\n\n    public enum ThemePhase {\n        READY,\n        FAILED,\n        UNKNOWN,\n    }\n\n    @Data\n    @ToString\n    public static class Author {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String name;\n\n        private String website;\n    }\n\n    @Data\n    public static class CustomTemplates {\n        private List<TemplateDescriptor> post;\n        private List<TemplateDescriptor> category;\n        private List<TemplateDescriptor> page;\n    }\n\n    /**\n     * Type used to describe custom template page.\n     *\n     * @author guqing\n     * @since 2.0.0\n     */\n    @Data\n    public static class TemplateDescriptor {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String name;\n\n        private String description;\n\n        private String screenshot;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String file;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/User.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.core.extension.User.GROUP;\nimport static run.halo.app.core.extension.User.KIND;\nimport static run.halo.app.core.extension.User.VERSION;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * The extension represents user details of Halo.\n *\n * @author johnniang\n */\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = GROUP,\n    version = VERSION,\n    kind = KIND,\n    singular = \"user\",\n    plural = \"users\")\npublic class User extends AbstractExtension {\n\n    public static final String GROUP = \"\";\n    public static final String VERSION = \"v1alpha1\";\n    public static final String KIND = \"User\";\n\n    public static final String USER_RELATED_ROLES_INDEX = \"roles\";\n\n    public static final String ROLE_NAMES_ANNO = \"rbac.authorization.halo.run/role-names\";\n\n    public static final String EMAIL_TO_VERIFY = \"halo.run/email-to-verify\";\n\n    public static final String LAST_AVATAR_ATTACHMENT_NAME_ANNO =\n        \"halo.run/last-avatar-attachment-name\";\n\n    public static final String AVATAR_ATTACHMENT_NAME_ANNO = \"halo.run/avatar-attachment-name\";\n\n    public static final String HIDDEN_USER_LABEL = \"halo.run/hidden-user\";\n\n    public static final String REQUEST_TO_UPDATE = \"halo.run/request-to-update\";\n\n    @Schema(requiredMode = REQUIRED)\n    private UserSpec spec = new UserSpec();\n\n    private UserStatus status = new UserStatus();\n\n    @Data\n    public static class UserSpec {\n\n        @Schema(requiredMode = REQUIRED)\n        private String displayName;\n\n        private String avatar;\n\n        @Schema(requiredMode = REQUIRED)\n        private String email;\n\n        private boolean emailVerified;\n\n        private String phone;\n\n        private String password;\n\n        private String bio;\n\n        private Instant registeredAt;\n\n        private Boolean twoFactorAuthEnabled;\n\n        private String totpEncryptedSecret;\n\n        private Boolean disabled;\n\n        private Integer loginHistoryLimit;\n\n    }\n\n    @Data\n    public static class UserStatus {\n\n        private String permalink;\n\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/UserConnection.java",
    "content": "package run.halo.app.core.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Metadata;\n\n/**\n * User connection extension.\n *\n * @author guqing\n * @since 2.4.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"auth.halo.run\", version = \"v1alpha1\", kind = \"UserConnection\",\n    singular = \"userconnection\", plural = \"userconnections\")\npublic class UserConnection extends AbstractExtension {\n\n    @Schema(requiredMode = REQUIRED)\n    private UserConnectionSpec spec;\n\n    @Data\n    public static class UserConnectionSpec {\n\n        /**\n         * The name of the OAuth provider (e.g. Google, Facebook, Twitter).\n         */\n        @Schema(requiredMode = REQUIRED)\n        private String registrationId;\n\n        /**\n         * The {@link Metadata#getName()} of the user associated with the OAuth connection.\n         */\n        @Schema(requiredMode = REQUIRED)\n        private String username;\n\n        /**\n         * The unique identifier for the user's connection to the OAuth provider.\n         * for example, the user's GitHub id.\n         */\n        @Schema(requiredMode = REQUIRED)\n        private String providerUserId;\n\n        /**\n         * The time when the user connection was last updated.\n         */\n        private Instant updatedAt;\n\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/Attachment.java",
    "content": "package run.halo.app.core.extension.attachment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.core.extension.attachment.Attachment.KIND;\n\nimport io.swagger.v3.oas.annotations.media.ArraySchema;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport org.jspecify.annotations.Nullable;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,\n    plural = \"attachments\", singular = \"attachment\")\npublic class Attachment extends AbstractExtension {\n\n    public static final String KIND = \"Attachment\";\n\n    @Schema(requiredMode = REQUIRED)\n    private AttachmentSpec spec;\n\n    private AttachmentStatus status;\n\n    @Data\n    public static class AttachmentSpec {\n\n        @Schema(description = \"Display name of attachment\")\n        private String displayName;\n\n        @Schema(description = \"Group name\")\n        private String groupName;\n\n        @Schema(description = \"Policy name\")\n        private String policyName;\n\n        @Schema(description = \"Name of User who uploads the attachment\")\n        private String ownerName;\n\n        @Schema(description = \"Media type of attachment\")\n        @Nullable\n        private String mediaType;\n\n        @Schema(description = \"Size of attachment. Unit is Byte\", minimum = \"0\")\n        private Long size;\n\n        @ArraySchema(\n            arraySchema = @Schema(description = \"Tags of attachment\"),\n            schema = @Schema(description = \"Tag name\"))\n        private Set<String> tags;\n\n    }\n\n    @Data\n    public static class AttachmentStatus {\n\n        @Schema(description = \"\"\"\n            Permalink of attachment.\n            If it is in local storage, the public URL will be set.\n            If it is in s3 storage, the Object URL will be set.\n            \"\"\")\n        private String permalink;\n\n        private Map<String, String> thumbnails;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/Constant.java",
    "content": "package run.halo.app.core.extension.attachment;\n\nimport run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;\n\npublic enum Constant {\n    ;\n\n    public static final String GROUP = \"storage.halo.run\";\n    public static final String VERSION = \"v1alpha1\";\n    /**\n     * The relative path starting from attachments folder is for deletion.\n     */\n    public static final String LOCAL_REL_PATH_ANNO_KEY = GROUP + \"/local-relative-path\";\n    /**\n     * The encoded URI is for building external url.\n     */\n    public static final String URI_ANNO_KEY = GROUP + \"/uri\";\n\n    /**\n     * Do not use this key to set external link. You could implement\n     * {@link AttachmentHandler#getPermalink} by your self.\n     * <p>\n     */\n    public static final String EXTERNAL_LINK_ANNO_KEY = GROUP + \"/external-link\";\n\n    public static final String FINALIZER_NAME = \"attachment-manager\";\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/Group.java",
    "content": "package run.halo.app.core.extension.attachment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.core.extension.attachment.Group.KIND;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,\n    plural = \"groups\", singular = \"group\")\npublic class Group extends AbstractExtension {\n\n    public static final String KIND = \"Group\";\n    public static final String HIDDEN_LABEL = \"halo.run/hidden\";\n\n    @Schema(requiredMode = REQUIRED)\n    private GroupSpec spec;\n\n    private GroupStatus status;\n\n    @Data\n    public static class GroupSpec {\n\n        @Schema(requiredMode = REQUIRED, description = \"Display name of group\")\n        private String displayName;\n\n    }\n\n    @Data\n    public static class GroupStatus {\n\n        @Schema(description = \"Update timestamp of the group\")\n        private Instant updateTimestamp;\n\n        @Schema(description = \"Total of attachments under the current group\", minimum = \"0\")\n        private Long totalAttachments;\n\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/Policy.java",
    "content": "package run.halo.app.core.extension.attachment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.core.extension.attachment.Policy.KIND;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,\n    plural = \"policies\", singular = \"policy\")\npublic class Policy extends AbstractExtension {\n    public static final String POLICY_OWNER_LABEL = \"storage.halo.run/policy-owner\";\n\n    public static final String KIND = \"Policy\";\n\n    @Schema(requiredMode = REQUIRED)\n    private PolicySpec spec;\n\n    @Data\n    public static class PolicySpec {\n\n        @Schema(requiredMode = REQUIRED, description = \"Display name of policy\")\n        private String displayName;\n\n        @Schema(requiredMode = REQUIRED, description = \"Reference name of PolicyTemplate\")\n        private String templateName;\n\n        @Schema(description = \"Reference name of ConfigMap extension\")\n        private String configMapName;\n\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/PolicyTemplate.java",
    "content": "package run.halo.app.core.extension.attachment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.core.extension.attachment.PolicyTemplate.KIND;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = KIND,\n    plural = \"policytemplates\", singular = \"policytemplate\")\npublic class PolicyTemplate extends AbstractExtension {\n\n    public static final String KIND = \"PolicyTemplate\";\n\n    private PolicyTemplateSpec spec;\n\n    @Data\n    public static class PolicyTemplateSpec {\n\n        private String displayName;\n\n        @Schema(requiredMode = REQUIRED)\n        private String settingName;\n\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentHandler.java",
    "content": "package run.halo.app.core.extension.attachment.endpoint;\n\nimport java.net.URI;\nimport java.time.Duration;\nimport java.util.Map;\nimport org.jspecify.annotations.Nullable;\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.http.codec.multipart.FilePart;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Group;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.extension.ConfigMap;\n\npublic interface AttachmentHandler extends ExtensionPoint {\n\n    Mono<Attachment> upload(UploadContext context);\n\n    Mono<Attachment> delete(DeleteContext context);\n\n    /**\n     * Gets a shared URL which could be accessed publicly.\n     * 1. If the attachment is in local storage, the permalink will be returned.\n     * 2. If the attachment is in s3 storage, the Presigned URL will be returned.\n     * <p>\n     * Please note that the default implementation is only for back compatibility.\n     *\n     * @param attachment contains detail of attachment.\n     * @param policy is storage policy.\n     * @param configMap contains configuration needed by handler.\n     * @param ttl indicates how long the URL is alive.\n     * @return shared URL which could be accessed publicly. Might be relative URL.\n     */\n    default Mono<URI> getSharedURL(Attachment attachment,\n        Policy policy,\n        ConfigMap configMap,\n        Duration ttl) {\n        return Mono.empty();\n    }\n\n    /**\n     * Gets a permalink representing a unique attachment.\n     * If the attachment is in local storage, the permalink will be returned.\n     * If the attachment is in s3 storage, the Object URL will be returned.\n     * <p>\n     * Please note that the default implementation is only for back compatibility.\n     *\n     * @param attachment contains detail of attachment.\n     * @param policy is storage policy.\n     * @param configMap contains configuration needed by handler.\n     * @return permalink representing a unique attachment. Might be relative URL.\n     */\n    default Mono<URI> getPermalink(Attachment attachment,\n        Policy policy,\n        ConfigMap configMap) {\n        return Mono.empty();\n    }\n\n    /**\n     * Gets thumbnail links for given attachment.\n     *\n     * @param attachment the attachment\n     * @param policy the policy\n     * @param configMap the config map\n     * @return a map of thumbnail sizes to their respective URIs\n     */\n    default Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment,\n        Policy policy,\n        ConfigMap configMap) {\n        return Mono.empty();\n    }\n\n    interface UploadContext {\n\n        FilePart file();\n\n        Policy policy();\n\n        ConfigMap configMap();\n\n        /**\n         * Gets the group info if available.\n         *\n         * @return the group info, or null if not available\n         */\n        @Nullable\n        default Group group() {\n            return null;\n        }\n\n    }\n\n    interface DeleteContext {\n        Attachment attachment();\n\n        Policy policy();\n\n        ConfigMap configMap();\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/endpoint/DeleteOption.java",
    "content": "package run.halo.app.core.extension.attachment.endpoint;\n\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.extension.ConfigMap;\n\npublic record DeleteOption(Attachment attachment, Policy policy, ConfigMap configMap)\n    implements AttachmentHandler.DeleteContext {\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/endpoint/SimpleFilePart.java",
    "content": "package run.halo.app.core.extension.attachment.endpoint;\n\nimport java.nio.file.Path;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n/**\n * SimpleFilePart is an adapter of simple data for uploading.\n *\n * @param filename is name of the attachment file.\n * @param content is binary data of the attachment file.\n * @param mediaType is media type of the attachment file.\n */\npublic record SimpleFilePart(\n    String filename,\n    Flux<DataBuffer> content,\n    MediaType mediaType\n) implements FilePart {\n    @Override\n    public Mono<Void> transferTo(Path dest) {\n        return DataBufferUtils.write(content(), dest);\n    }\n\n    @Override\n    public String name() {\n        return filename();\n    }\n\n    @Override\n    public HttpHeaders headers() {\n        var headers = new HttpHeaders();\n        headers.setContentType(mediaType);\n        return HttpHeaders.readOnlyHttpHeaders(headers);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/attachment/endpoint/UploadOption.java",
    "content": "package run.halo.app.core.extension.attachment.endpoint;\n\nimport lombok.Builder;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.core.extension.attachment.Group;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.extension.ConfigMap;\n\n@Builder\npublic record UploadOption(FilePart file,\n                           Policy policy,\n                           ConfigMap configMap,\n                           @Nullable Group group) implements AttachmentHandler.UploadContext {\n\n    public static UploadOption from(String filename,\n        Flux<DataBuffer> content,\n        MediaType mediaType,\n        Policy policy,\n        ConfigMap configMap) {\n        var filePart = new SimpleFilePart(filename, content, mediaType);\n        return new UploadOption(filePart, policy, configMap, null);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/Category.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static run.halo.app.core.extension.content.Category.KIND;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.GroupVersionKind;\n\n/**\n * @author guqing\n * @see <a href=\"https://github.com/halo-dev/halo/issues/2322\">issue#2322</a>\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION,\n    kind = KIND, plural = \"categories\", singular = \"category\")\n@EqualsAndHashCode(callSuper = true)\npublic class Category extends AbstractExtension {\n\n    public static final String KIND = \"Category\";\n    public static final String LAST_HIDDEN_STATE_ANNO = \"content.halo.run/last-hidden-state\";\n\n    public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Category.class);\n\n    @Schema(requiredMode = REQUIRED)\n    private CategorySpec spec;\n\n    @Schema\n    private CategoryStatus status;\n\n    @JsonIgnore\n    public boolean isDeleted() {\n        return getMetadata().getDeletionTimestamp() != null;\n    }\n\n    @Data\n    public static class CategorySpec {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String displayName;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String slug;\n\n        private String description;\n\n        private String cover;\n\n        @Schema(requiredMode = NOT_REQUIRED, maxLength = 255)\n        private String template;\n\n        /**\n         * <p>Used to specify the template for the posts associated with the category.</p>\n         * <p>The priority is not as high as that of the post.</p>\n         * <p>If the post also specifies a template, the post's template will prevail.</p>\n         */\n        @Schema(requiredMode = NOT_REQUIRED, maxLength = 255)\n        private String postTemplate;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"0\")\n        private Integer priority;\n\n        private List<String> children;\n\n        /**\n         * <p>if a category is queried for related posts, the default behavior is to\n         * query all posts under the category including its subcategories, but if this field is\n         * set to true, cascade query behavior will be terminated here.</p>\n         * <p>For example, if a category has subcategories A and B, and A has subcategories C and\n         * D and C marked this field as true, when querying posts under A category,all posts under A\n         * and B will be queried, but C and D will not be queried.</p>\n         */\n        private boolean preventParentPostCascadeQuery;\n\n        /**\n         * <p>Whether to hide the category from the category list.</p>\n         * <p>When set to true, the category including its subcategories and related posts will\n         * not be displayed in the category list, but it can still be accessed by permalink.</p>\n         * <p>Limitation: It only takes effect on the theme-side categorized list and it only\n         * allows to be set to true on the first level(root node) of categories.</p>\n         */\n        private boolean hideFromList;\n    }\n\n    @JsonIgnore\n    public CategoryStatus getStatusOrDefault() {\n        if (this.status == null) {\n            this.status = new CategoryStatus();\n        }\n        return this.status;\n    }\n\n    @Data\n    public static class CategoryStatus {\n\n        private String permalink;\n\n        /**\n         * 包括当前和其下所有层级的文章数量 (depth=max).\n         */\n        public Integer postCount;\n\n        /**\n         * 包括当前和其下所有层级的已发布且公开的文章数量 (depth=max).\n         */\n        public Integer visiblePostCount;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/Comment.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Ref;\n\n/**\n * @author guqing\n * @see <a href=\"https://github.com/halo-dev/halo/issues/2322\">issue#2322</a>\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Comment.KIND,\n    plural = \"comments\", singular = \"comment\")\n@EqualsAndHashCode(callSuper = true)\npublic class Comment extends AbstractExtension {\n\n    public static final String KIND = \"Comment\";\n\n    public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = \"requireSyncOnStartup\";\n\n    @Schema(requiredMode = REQUIRED)\n    private CommentSpec spec;\n\n    @Schema\n    private CommentStatus status;\n\n    @JsonIgnore\n    public CommentStatus getStatusOrDefault() {\n        if (this.status == null) {\n            this.status = new CommentStatus();\n        }\n        return this.status;\n    }\n\n    @Data\n    @ToString(callSuper = true)\n    @EqualsAndHashCode(callSuper = true)\n    public static class CommentSpec extends BaseCommentSpec {\n\n        @Schema(requiredMode = REQUIRED)\n        private Ref subjectRef;\n\n        private Instant lastReadTime;\n    }\n\n    @Data\n    public static class BaseCommentSpec {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String raw;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String content;\n\n        @Schema(requiredMode = REQUIRED)\n        private CommentOwner owner;\n\n        private String userAgent;\n\n        private String ipAddress;\n\n        private Instant approvedTime;\n\n        /**\n         * The user-defined creation time default is <code>metadata.creationTimestamp</code>.\n         */\n        private Instant creationTime;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"0\")\n        private Integer priority;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"false\")\n        private Boolean top;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"true\")\n        private Boolean allowNotification;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"false\")\n        private Boolean approved;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"false\")\n        private Boolean hidden;\n    }\n\n    @Data\n    public static class CommentOwner {\n        public static final String KIND_EMAIL = \"Email\";\n        public static final String AVATAR_ANNO = \"avatar\";\n        public static final String WEBSITE_ANNO = \"website\";\n        public static final String EMAIL_HASH_ANNO = \"email-hash\";\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String kind;\n\n        @Schema(requiredMode = REQUIRED, maxLength = 64)\n        private String name;\n\n        private String displayName;\n\n        private Map<String, String> annotations;\n\n        @Nullable\n        @JsonIgnore\n        public String getAnnotation(String key) {\n            return annotations == null ? null : annotations.get(key);\n        }\n\n        public static String ownerIdentity(String kind, String name) {\n            return kind + \"#\" + name;\n        }\n    }\n\n    @Data\n    public static class CommentStatus {\n\n        private Instant lastReplyTime;\n\n        private Integer replyCount;\n\n        private Integer visibleReplyCount;\n\n        private Integer unreadReplyCount;\n\n        private Boolean hasNewReply;\n\n        private Long observedVersion;\n    }\n\n    public static String toSubjectRefKey(Ref subjectRef) {\n        return subjectRef.getGroup() + \"/\" + subjectRef.getKind() + \"/\" + subjectRef.getName();\n    }\n\n    public static int getUnreadReplyCount(List<Reply> replies, Instant lastReadTime) {\n        if (CollectionUtils.isEmpty(replies)) {\n            return 0;\n        }\n        long unreadReplyCount = replies.stream()\n            .filter(existingReply -> {\n                if (lastReadTime == null) {\n                    return true;\n                }\n                Instant creationTime = defaultIfNull(existingReply.getSpec().getCreationTime(),\n                    existingReply.getMetadata().getCreationTimestamp());\n                return creationTime.isAfter(lastReadTime);\n            })\n            .count();\n        return (int) unreadReplyCount;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/Constant.java",
    "content": "package run.halo.app.core.extension.content;\n\npublic enum Constant {\n    ;\n\n    public static final String GROUP = \"content.halo.run\";\n    public static final String VERSION = \"v1alpha1\";\n\n    public static final String LAST_READ_TIME_ANNO = \"content.halo.run/last-read-time\";\n    public static final String PERMALINK_PATTERN_ANNO = \"content.halo.run/permalink-pattern\";\n\n    public static final String CHECKSUM_CONFIG_ANNO = \"checksum/config\";\n\n    public static final String CONTENT_CHECKSUM_ANNO = \"checksum/content\";\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/Post.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static java.lang.Boolean.parseBoolean;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.media.Schema.RequiredMode;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.MetadataOperator;\nimport run.halo.app.infra.ConditionList;\n\n/**\n * <p>Post extension.</p>\n *\n * @author guqing\n * @see <a href=\"https://github.com/halo-dev/halo/issues/2322\">issue#2322</a>\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Post.KIND,\n    plural = \"posts\", singular = \"post\")\n@EqualsAndHashCode(callSuper = true)\npublic class Post extends AbstractExtension {\n\n    public static final String KIND = \"Post\";\n\n    public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = \"requireSyncOnStartup\";\n\n    public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Post.class);\n\n    public static final String CATEGORIES_ANNO = \"content.halo.run/categories\";\n    public static final String LAST_RELEASED_SNAPSHOT_ANNO =\n        \"content.halo.run/last-released-snapshot\";\n    public static final String LAST_ASSOCIATED_TAGS_ANNO = \"content.halo.run/last-associated-tags\";\n    public static final String LAST_ASSOCIATED_CATEGORIES_ANNO =\n        \"content.halo.run/last-associated-categories\";\n\n    public static final String STATS_ANNO = \"content.halo.run/stats\";\n\n    /**\n     * <p>The key of the label that indicates that the post is scheduled to be published.</p>\n     * <p>Can be used to query posts that are scheduled to be published.</p>\n     */\n    public static final String SCHEDULING_PUBLISH_LABEL = \"content.halo.run/scheduling-publish\";\n\n    public static final String DELETED_LABEL = \"content.halo.run/deleted\";\n    public static final String PUBLISHED_LABEL = \"content.halo.run/published\";\n    public static final String OWNER_LABEL = \"content.halo.run/owner\";\n    public static final String VISIBLE_LABEL = \"content.halo.run/visible\";\n\n    public static final String ARCHIVE_YEAR_LABEL = \"content.halo.run/archive-year\";\n\n    public static final String ARCHIVE_MONTH_LABEL = \"content.halo.run/archive-month\";\n    public static final String ARCHIVE_DAY_LABEL = \"content.halo.run/archive-day\";\n\n    @Schema(requiredMode = RequiredMode.REQUIRED)\n    private PostSpec spec;\n\n    @Schema\n    private PostStatus status;\n\n    @JsonIgnore\n    public PostStatus getStatusOrDefault() {\n        if (this.status == null) {\n            this.status = new PostStatus();\n        }\n        return status;\n    }\n\n    @JsonIgnore\n    public boolean isDeleted() {\n        return Objects.equals(true, spec.getDeleted())\n            || getMetadata().getDeletionTimestamp() != null;\n    }\n\n    @JsonIgnore\n    public boolean isPublished() {\n        return isPublished(this.getMetadata());\n    }\n\n    public static boolean isPublished(MetadataOperator metadata) {\n        var labels = metadata.getLabels();\n        return labels != null && parseBoolean(labels.getOrDefault(PUBLISHED_LABEL, \"false\"));\n    }\n\n    public static boolean isRecycled(MetadataOperator metadata) {\n        var labels = metadata.getLabels();\n        return labels != null && parseBoolean(labels.getOrDefault(DELETED_LABEL, \"false\"));\n    }\n\n    public static boolean isPublic(PostSpec spec) {\n        return spec.getVisible() == null || VisibleEnum.PUBLIC.equals(spec.getVisible());\n    }\n\n    @Data\n    public static class PostSpec {\n        @Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1)\n        private String title;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1)\n        private String slug;\n\n        /**\n         * 文章引用到的已发布的内容，用于主题端显示.\n         */\n        private String releaseSnapshot;\n\n        private String headSnapshot;\n\n        private String baseSnapshot;\n\n        private String owner;\n\n        private String template;\n\n        private String cover;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = \"false\")\n        private Boolean deleted;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = \"false\")\n        private Boolean publish;\n\n        private Instant publishTime;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = \"false\")\n        private Boolean pinned;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = \"true\")\n        private Boolean allowComment;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = \"PUBLIC\")\n        private VisibleEnum visible;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = \"0\")\n        private Integer priority;\n\n        @Schema(requiredMode = RequiredMode.REQUIRED)\n        private Excerpt excerpt;\n\n        private List<String> categories;\n\n        private List<String> tags;\n\n        private List<Map<String, String>> htmlMetas;\n    }\n\n    @Data\n    public static class PostStatus {\n        private String phase;\n\n        @Schema\n        private ConditionList conditions;\n\n        private String permalink;\n\n        private String excerpt;\n\n        private Boolean inProgress;\n\n        private Integer commentsCount;\n\n        private List<String> contributors;\n\n        /**\n         * see {@link Category.CategorySpec#isHideFromList()}.\n         */\n        private Boolean hideFromList;\n\n        private Instant lastModifyTime;\n\n        private Long observedVersion;\n\n        @JsonIgnore\n        public ConditionList getConditionsOrDefault() {\n            if (this.conditions == null) {\n                this.conditions = new ConditionList();\n            }\n            return conditions;\n        }\n    }\n\n    @Data\n    public static class Excerpt {\n\n        @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = \"true\")\n        private Boolean autoGenerate;\n\n        private String raw;\n    }\n\n    public enum PostPhase {\n        DRAFT,\n        PENDING_APPROVAL,\n        PUBLISHED,\n        FAILED;\n\n        /**\n         * Convert string value to {@link PostPhase}.\n         *\n         * @param value enum value string\n         * @return {@link PostPhase} if found, otherwise null\n         */\n        public static PostPhase from(String value) {\n            for (PostPhase phase : PostPhase.values()) {\n                if (phase.name().equalsIgnoreCase(value)) {\n                    return phase;\n                }\n            }\n            return null;\n        }\n    }\n\n    public enum VisibleEnum {\n        PUBLIC,\n        INTERNAL,\n        PRIVATE;\n\n        /**\n         * Convert value string to {@link VisibleEnum}.\n         *\n         * @param value enum value string\n         * @return {@link VisibleEnum} if found, otherwise null\n         */\n        public static VisibleEnum from(String value) {\n            for (VisibleEnum visible : VisibleEnum.values()) {\n                if (visible.name().equalsIgnoreCase(value)) {\n                    return visible;\n                }\n            }\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/Reply.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.ToString;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * @author guqing\n * @see <a href=\"https://github.com/halo-dev/halo/issues/2322\">issue#2322</a>\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Reply.KIND,\n    plural = \"replies\", singular = \"reply\")\n@EqualsAndHashCode(callSuper = true)\npublic class Reply extends AbstractExtension {\n\n    public static final String KIND = \"Reply\";\n\n    public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = \"requireSyncOnStartup\";\n\n    @Schema(requiredMode = REQUIRED)\n    private ReplySpec spec;\n\n    @Schema\n    @Getter(onMethod_ = @NonNull)\n    private Status status = new Status();\n\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    public static class ReplySpec extends Comment.BaseCommentSpec {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String commentName;\n\n        private String quoteReply;\n    }\n\n    @Data\n    @Schema(name = \"ReplyStatus\")\n    public static class Status {\n        private Long observedVersion;\n    }\n\n    public void setStatus(Status status) {\n        this.status = status == null ? new Status() : status;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/SinglePage.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.MetadataUtil;\n\n/**\n * <p>Single page extension.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = SinglePage.KIND,\n    plural = \"singlepages\", singular = \"singlepage\")\n@EqualsAndHashCode(callSuper = true)\npublic class SinglePage extends AbstractExtension {\n\n    public static final String KIND = \"SinglePage\";\n\n    public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(SinglePage.class);\n    public static final String DELETED_LABEL = \"content.halo.run/deleted\";\n    public static final String PUBLISHED_LABEL = \"content.halo.run/published\";\n    public static final String LAST_RELEASED_SNAPSHOT_ANNO =\n        \"content.halo.run/last-released-snapshot\";\n    public static final String OWNER_LABEL = \"content.halo.run/owner\";\n    public static final String VISIBLE_LABEL = \"content.halo.run/visible\";\n\n    @Schema(requiredMode = REQUIRED)\n    private SinglePageSpec spec;\n\n    @Schema\n    private SinglePageStatus status;\n\n    @JsonIgnore\n    public SinglePageStatus getStatusOrDefault() {\n        if (this.status == null) {\n            this.status = new SinglePageStatus();\n        }\n        return this.status;\n    }\n\n    @JsonIgnore\n    public boolean isPublished() {\n        Map<String, String> labels = getMetadata().getLabels();\n        return labels != null && labels.getOrDefault(PUBLISHED_LABEL, \"false\").equals(\"true\");\n    }\n\n    @Data\n    public static class SinglePageSpec {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String title;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String slug;\n\n        /**\n         * 引用到的已发布的内容，用于主题端显示.\n         */\n        private String releaseSnapshot;\n\n        private String headSnapshot;\n\n        private String baseSnapshot;\n\n        private String owner;\n\n        private String template;\n\n        private String cover;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"false\")\n        private Boolean deleted;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"false\")\n        private Boolean publish;\n\n        private Instant publishTime;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"false\")\n        private Boolean pinned;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"true\")\n        private Boolean allowComment;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"PUBLIC\")\n        private Post.VisibleEnum visible;\n\n        @Schema(requiredMode = REQUIRED, defaultValue = \"0\")\n        private Integer priority;\n\n        @Schema(requiredMode = REQUIRED)\n        private Post.Excerpt excerpt;\n\n        private List<Map<String, String>> htmlMetas;\n    }\n\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    public static class SinglePageStatus extends Post.PostStatus {\n\n    }\n\n    public static void changePublishedState(SinglePage page, boolean value) {\n        Map<String, String> labels = MetadataUtil.nullSafeLabels(page);\n        labels.put(PUBLISHED_LABEL, String.valueOf(value));\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/Snapshot.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport java.util.LinkedHashSet;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Ref;\n\n/**\n * @author guqing\n * @see <a href=\"https://github.com/halo-dev/halo/issues/2322\">issue#2322</a>\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION, kind = Snapshot.KIND,\n    plural = \"snapshots\", singular = \"snapshot\")\n@EqualsAndHashCode(callSuper = true)\npublic class Snapshot extends AbstractExtension {\n    public static final String KIND = \"Snapshot\";\n    public static final String KEEP_RAW_ANNO = \"content.halo.run/keep-raw\";\n    public static final String PATCHED_CONTENT_ANNO = \"content.halo.run/patched-content\";\n    public static final String PATCHED_RAW_ANNO = \"content.halo.run/patched-raw\";\n\n    @Schema(requiredMode = REQUIRED)\n    private SnapShotSpec spec;\n\n    @Data\n    public static class SnapShotSpec {\n\n        @Schema(requiredMode = REQUIRED)\n        private Ref subjectRef;\n\n        /**\n         * such as: markdown | html | json | asciidoc | latex.\n         */\n        @Schema(requiredMode = REQUIRED, minLength = 1, maxLength = 50)\n        private String rawType;\n\n        private String rawPatch;\n\n        private String contentPatch;\n\n        private String parentSnapshotName;\n\n        private Instant lastModifyTime;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String owner;\n\n        private Set<String> contributors;\n    }\n\n    public static void addContributor(Snapshot snapshot, String name) {\n        Assert.notNull(name, \"The username must not be null.\");\n        Set<String> contributors = snapshot.getSpec().getContributors();\n        if (contributors == null) {\n            contributors = new LinkedHashSet<>();\n            snapshot.getSpec().setContributors(contributors);\n        }\n        contributors.add(name);\n    }\n\n    /**\n     * Check if the given snapshot is a base snapshot.\n     *\n     * @param snapshot must not be null.\n     * @return true if the given snapshot is a base snapshot; false otherwise.\n     */\n    public static boolean isBaseSnapshot(@NonNull Snapshot snapshot) {\n        var annotations = snapshot.getMetadata().getAnnotations();\n        if (annotations == null) {\n            return false;\n        }\n        return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO));\n    }\n\n    public static String toSubjectRefKey(Ref subjectRef) {\n        return subjectRef.getGroup() + \"/\" + subjectRef.getKind() + \"/\" + subjectRef.getName();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/content/Tag.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.GroupVersionKind;\n\n/**\n * @author guqing\n * @see <a href=\"https://github.com/halo-dev/halo/issues/2322\">issue#2322</a>\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@GVK(group = Constant.GROUP, version = Constant.VERSION,\n    kind = Tag.KIND, plural = \"tags\", singular = \"tag\")\n@EqualsAndHashCode(callSuper = true)\npublic class Tag extends AbstractExtension {\n\n    public static final String KIND = \"Tag\";\n\n    public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Tag.class);\n\n    public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = \"requireSyncOnStartup\";\n\n    @Schema(requiredMode = REQUIRED)\n    private TagSpec spec;\n\n    @Schema\n    private TagStatus status;\n\n    @Data\n    public static class TagSpec {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String displayName;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String slug;\n\n        private String description;\n\n        /**\n         * Color regex explanation.\n         * <pre>\n         * ^                 # start of the line\n         * #                 # start with a number sign `#`\n         * (                 # start of (group 1)\n         *   [a-fA-F0-9]{6}  # support z-f, A-F and 0-9, with a length of 6\n         *   |               # or\n         *   [a-fA-F0-9]{3}  # support z-f, A-F and 0-9, with a length of 3\n         * )                 # end of (group 1)\n         * $                 # end of the line\n         * </pre>\n         */\n        @Schema(pattern = \"^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$\")\n        private String color;\n\n        private String cover;\n    }\n\n    @JsonIgnore\n    public TagStatus getStatusOrDefault() {\n        if (this.status == null) {\n            this.status = new TagStatus();\n        }\n        return this.status;\n    }\n\n    @Data\n    public static class TagStatus {\n\n        private String permalink;\n\n        public Integer visiblePostCount;\n\n        public Integer postCount;\n\n        private Long observedVersion;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/endpoint/CustomEndpoint.java",
    "content": "package run.halo.app.core.extension.endpoint;\n\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.extension.GroupVersion;\n\n/**\n * RouterFunction provider for custom endpoints.\n *\n * @author johnniang\n */\npublic interface CustomEndpoint {\n\n    RouterFunction<ServerResponse> endpoint();\n\n    default GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"api.console.halo.run/v1alpha1\");\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/endpoint/SortResolver.java",
    "content": "package run.halo.app.core.extension.endpoint;\n\nimport org.springframework.core.MethodParameter;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.data.web.ReactiveSortHandlerMethodArgumentResolver;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ServerWebExchange;\n\npublic interface SortResolver {\n\n    SortResolver defaultInstance = new DefaultSortResolver();\n\n    @NonNull\n    Sort resolve(@NonNull ServerWebExchange exchange);\n\n    class DefaultSortResolver extends ReactiveSortHandlerMethodArgumentResolver\n        implements SortResolver {\n\n        @Override\n        @NonNull\n        protected Sort getDefaultFromAnnotationOrFallback(@Nullable MethodParameter parameter) {\n            return Sort.unsorted();\n        }\n\n        @Override\n        public Sort resolve(ServerWebExchange exchange) {\n            return resolveArgumentValue(null, null, exchange);\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/notification/Notification.java",
    "content": "package run.halo.app.core.extension.notification;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * <p>{@link Notification} is a custom extension that used to store notification information for\n * inner use, it's on-site notification.</p>\n *\n * <p>Supports the following operations:</p>\n * <ul>\n *    <li>Marked as read: {@link NotificationSpec#setUnread(boolean)}</li>\n *    <li>Get the last read time: {@link NotificationSpec#getLastReadAt()}</li>\n *    <li>Filter by recipient: {@link NotificationSpec#getRecipient()}</li>\n * </ul>\n *\n * @author guqing\n * @see Reason\n * @see ReasonType\n * @since 2.10.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"notification.halo.run\", version = \"v1alpha1\", kind = \"Notification\", plural =\n    \"notifications\", singular = \"notification\")\npublic class Notification extends AbstractExtension {\n\n    @Schema\n    private NotificationSpec spec;\n\n    @Data\n    public static class NotificationSpec {\n        @Schema(requiredMode = REQUIRED, minLength = 1, description = \"The name of user\")\n        private String recipient;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1, description = \"The name of reason\")\n        private String reason;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String title;\n\n        @Schema(requiredMode = REQUIRED)\n        private String rawContent;\n\n        @Schema(requiredMode = REQUIRED)\n        private String htmlContent;\n\n        private boolean unread;\n\n        private Instant lastReadAt;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/notification/NotificationTemplate.java",
    "content": "package run.halo.app.core.extension.notification;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * <p>{@link NotificationTemplate} is a custom extension that defines a notification template.</p>\n * <p>It describes the notification template's name, description, and the template content.</p>\n * <p>{@link Spec#getReasonSelector()} is used to select the template by reasonType and language,\n * if multiple templates are matched, the best match will be selected. This is useful when you\n * want to override the default template.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"notification.halo.run\", version = \"v1alpha1\", kind = \"NotificationTemplate\",\n    plural = \"notificationtemplates\", singular = \"notificationtemplate\")\npublic class NotificationTemplate extends AbstractExtension {\n\n    @Schema\n    private Spec spec;\n\n    @Data\n    @Schema(name = \"NotificationTemplateSpec\")\n    public static class Spec {\n        @Schema\n        private ReasonSelector reasonSelector;\n\n        @Schema\n        private Template template;\n    }\n\n    @Data\n    @Schema(name = \"TemplateContent\")\n    public static class Template {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String title;\n\n        private String htmlBody;\n\n        private String rawBody;\n    }\n\n    @Data\n    public static class ReasonSelector {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String reasonType;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1, defaultValue = \"default\")\n        private String language;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/notification/NotifierDescriptor.java",
    "content": "package run.halo.app.core.extension.notification;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * <p>{@link NotifierDescriptor} is a custom extension that defines a notifier.</p>\n * <p>It describes the notifier's name, description, and the extension name of the notifier to\n * let the user know what the notifier is and what it can do in the UI and also let the\n * {@code NotificationCenter} know how to load the notifier and prepare the notifier's settings.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"notification.halo.run\", version = \"v1alpha1\", kind = \"NotifierDescriptor\",\n    plural = \"notifierDescriptors\", singular = \"notifierDescriptor\")\npublic class NotifierDescriptor extends AbstractExtension {\n\n    @Schema\n    private Spec spec;\n\n    @Data\n    @Schema(name = \"NotifierDescriptorSpec\")\n    public static class Spec {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String displayName;\n\n        private String description;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String notifierExtName;\n\n        private SettingRef senderSettingRef;\n\n        private SettingRef receiverSettingRef;\n    }\n\n    @Data\n    @Schema(name = \"NotifierSettingRef\")\n    public static class SettingRef {\n        @Schema(requiredMode = REQUIRED)\n        private String name;\n\n        @Schema(requiredMode = REQUIRED)\n        private String group;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/notification/Reason.java",
    "content": "package run.halo.app.core.extension.notification;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.NoArgsConstructor;\nimport lombok.experimental.Accessors;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.notification.ReasonAttributes;\n\n/**\n * <p>{@link Reason} is a custom extension that defines a reason for a notification, It represents\n * an instance of a {@link ReasonType}.</p>\n * <p>It can be understood as an event that triggers a notification.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"notification.halo.run\", version = \"v1alpha1\", kind = \"Reason\", plural =\n    \"reasons\", singular = \"reason\")\npublic class Reason extends AbstractExtension {\n\n    @Schema\n    private Spec spec;\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"ReasonSpec\")\n    public static class Spec {\n        @Schema(requiredMode = REQUIRED)\n        private String reasonType;\n\n        @Schema(requiredMode = REQUIRED)\n        private Subject subject;\n\n        @Schema(requiredMode = REQUIRED)\n        private String author;\n\n        @Schema(implementation = ReasonAttributes.class, requiredMode = NOT_REQUIRED,\n            description = \"Attributes used to transfer data\")\n        private ReasonAttributes attributes;\n    }\n\n    @Data\n    @Builder\n    @NoArgsConstructor\n    @AllArgsConstructor\n    @Schema(name = \"ReasonSubject\")\n    public static class Subject {\n        @Schema(requiredMode = REQUIRED)\n        private String apiVersion;\n\n        @Schema(requiredMode = REQUIRED)\n        private String kind;\n\n        @Schema(requiredMode = REQUIRED)\n        private String name;\n\n        @Schema(requiredMode = REQUIRED)\n        private String title;\n\n        private String url;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/notification/ReasonType.java",
    "content": "package run.halo.app.core.extension.notification;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * <p>{@link ReasonType} is a custom extension that defines a type of reason.</p>\n * <p>One {@link ReasonType} can have multiple {@link Reason}s to notify.</p>\n *\n * @author guqing\n * @see NotificationTemplate\n * @see Reason\n * @since 2.10.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"notification.halo.run\", version = \"v1alpha1\", kind = \"ReasonType\",\n    plural = \"reasontypes\", singular = \"reasontype\")\npublic class ReasonType extends AbstractExtension {\n    public static final String LOCALIZED_RESOURCE_NAME_ANNO =\n        \"notification.halo.run/localized-resource-name\";\n\n    @Schema\n    private Spec spec;\n\n    @Data\n    @Schema(name = \"ReasonTypeSpec\")\n    public static class Spec {\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String displayName;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String description;\n\n        private List<ReasonProperty> properties;\n    }\n\n    @Data\n    public static class ReasonProperty {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String name;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String type;\n\n        private String description;\n\n        @Schema(defaultValue = \"false\")\n        private boolean optional;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/notification/Subscription.java",
    "content": "package run.halo.app.core.extension.notification;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.apache.commons.lang3.StringUtils.defaultString;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.UUID;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.NoArgsConstructor;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * <p>{@link Subscription} is a custom extension that defines a subscriber to be notified when a\n * certain {@link Reason} is triggered.</p>\n * <p>It holds a {@link Subscriber} to the user to be notified, a {@link InterestReason} to\n * subscribe to.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"notification.halo.run\", version = \"v1alpha1\", kind = \"Subscription\",\n    plural = \"subscriptions\", singular = \"subscription\")\npublic class Subscription extends AbstractExtension {\n\n    @Schema\n    private Spec spec;\n\n    @Data\n    @Schema(name = \"SubscriptionSpec\")\n    public static class Spec {\n        @Schema(requiredMode = REQUIRED, description = \"The subscriber to be notified\")\n        private Subscriber subscriber;\n\n        @Schema(requiredMode = REQUIRED, description = \"The token to unsubscribe\")\n        private String unsubscribeToken;\n\n        @Schema(requiredMode = REQUIRED, description = \"The reason to be interested in\")\n        private InterestReason reason;\n\n        @Schema(description = \"Perhaps users need to unsubscribe and \"\n            + \"interact without receiving notifications again\")\n        private boolean disabled;\n    }\n\n    @Data\n    public static class InterestReason {\n        @Schema(requiredMode = REQUIRED, description = \"The name of the reason definition to be \"\n            + \"interested in\")\n        private String reasonType;\n\n        @Schema(requiredMode = REQUIRED, description = \"The subject name of reason type to be\"\n            + \" interested in\")\n        private ReasonSubject subject;\n\n        @Schema(requiredMode = NOT_REQUIRED, description = \"The expression to be interested in\")\n        private String expression;\n\n        /**\n         * <p>Since 2.15.0, we have added a new field <code>expression</code> to the\n         * <code>InterestReason</code> object, so <code>subject</code> can be null.</p>\n         * <p>In this particular scenario, when the <code>subject</code> is null, we assign it a\n         * default <code>ReasonSubject</code> object. The properties of this object are set to\n         * specific values that do not occur in actual applications, thus we can consider this as\n         * <code>nonexistent data</code>.\n         * The purpose of this approach is to maintain backward compatibility, even if the\n         * <code>subject</code> can be null in the new version of the code.</p>\n         */\n        public static void ensureSubjectHasValue(InterestReason interestReason) {\n            if (interestReason.getSubject() == null) {\n                interestReason.setSubject(createFallbackSubject());\n            }\n        }\n\n        /**\n         * Check if the given reason subject is a fallback subject.\n         */\n        public static boolean isFallbackSubject(ReasonSubject reasonSubject) {\n            if (reasonSubject == null) {\n                return true;\n            }\n            var fallback = createFallbackSubject();\n            return fallback.getKind().equals(reasonSubject.getKind())\n                && fallback.getApiVersion().equals(reasonSubject.getApiVersion());\n        }\n\n        static ReasonSubject createFallbackSubject() {\n            return ReasonSubject.builder()\n                .apiVersion(\"notification.halo.run/v1alpha1\")\n                .kind(\"NonexistentKind\")\n                .build();\n        }\n    }\n\n    @Data\n    @Builder\n    @AllArgsConstructor\n    @NoArgsConstructor\n    @Schema(name = \"InterestReasonSubject\")\n    public static class ReasonSubject {\n\n        @Schema(requiredMode = NOT_REQUIRED, description = \"if name is not specified, it presents \"\n            + \"all subjects of the specified reason type and custom resources\")\n        private String name;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String apiVersion;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String kind;\n\n        @Override\n        public String toString() {\n            return kind + \"#\" + apiVersion + \"/\" + defaultString(name);\n        }\n    }\n\n    @Data\n    @Schema(name = \"SubscriptionSubscriber\")\n    public static class Subscriber {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String name;\n\n        @Override\n        public String toString() {\n            return name;\n        }\n    }\n\n    /**\n     * Generate unsubscribe token for unsubscribe.\n     *\n     * @return unsubscribe token\n     */\n    public static String generateUnsubscribeToken() {\n        return UUID.randomUUID().toString();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/extension/service/AttachmentService.java",
    "content": "package run.halo.app.core.extension.service;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.extension.attachment.Attachment;\n\n/**\n * AttachmentService.\n *\n * @author johnniang\n * @since 2.5.0\n */\npublic interface AttachmentService {\n\n    /**\n     * Uploads the given attachment to specific storage using handlers in plugins.\n     * <p>\n     * If no handler can be found to upload the given attachment, ServerError exception will be\n     * thrown.\n     *\n     * @param policyName is attachment policy name.\n     * @param groupName is group name the attachment belongs.\n     * @param filePart contains filename, content and media type.\n     * @param beforeCreating is an attachment modifier before creating.\n     * @return attachment.\n     */\n    Mono<Attachment> upload(\n        @NonNull String username,\n        @NonNull String policyName,\n        @Nullable String groupName,\n        @NonNull FilePart filePart,\n        @Nullable Consumer<Attachment> beforeCreating);\n\n    /**\n     * Uploads the given attachment to specific storage using handlers in plugins. Please note\n     * that we will make sure the request is authenticated, or an unauthorized exception throws.\n     * <p>\n     * If no handler can be found to upload the given attachment, ServerError exception will be\n     * thrown.\n     *\n     * @param policyName is attachment policy name.\n     * @param groupName is group name the attachment belongs.\n     * @param filename is filename of the attachment.\n     * @param content is binary data of the attachment.\n     * @param mediaType is media type of the attachment.\n     * @return attachment.\n     */\n    Mono<Attachment> upload(@NonNull String policyName,\n        @Nullable String groupName,\n        @NonNull String filename,\n        @NonNull Flux<DataBuffer> content,\n        @Nullable MediaType mediaType);\n\n    /**\n     * Deletes an attachment using handlers in plugins.\n     * <p>\n     * If no handler can be found to delete the given attachment, Mono.empty() will return.\n     *\n     * @param attachment is to be deleted.\n     * @return deleted attachment.\n     */\n    Mono<Attachment> delete(Attachment attachment);\n\n    /**\n     * Gets permalink using handlers in plugins.\n     * <p>\n     * If no handler can be found to delete the given attachment, Mono.empty() will return.\n     *\n     * @param attachment is created attachment.\n     * @return permalink\n     */\n    Mono<URI> getPermalink(Attachment attachment);\n\n    /**\n     * Gets shared URL using handlers in plugins.\n     * <p>\n     * If no handler can be found to delete the given attachment, Mono.empty() will return.\n     *\n     * @param attachment is created attachment.\n     * @param ttl is time to live of the shared URL.\n     * @return time-to-live shared URL. Please note that, if the attachment is stored in local, the\n     * shared URL is equal to permalink.\n     */\n    Mono<URI> getSharedURL(Attachment attachment, Duration ttl);\n\n    /**\n     * Gets thumbnail links using handlers in plugins. Please make sure the attachment has\n     * permalink.\n     *\n     * @param attachment is created attachment.\n     * @return thumbnail links\n     */\n    Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment);\n\n    /**\n     * Transfer external links to attachments.\n     *\n     * @param url external url\n     * @param policyName policy name\n     * @param groupName group name\n     * @param filename filename\n     * @return attachment\n     */\n    Mono<Attachment> uploadFromUrl(@NonNull URL url, @NonNull String policyName,\n        String groupName, String filename);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/user/service/RoleService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.Set;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.RoleBinding;\nimport run.halo.app.core.extension.RoleBinding.Subject;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic interface RoleService {\n\n    Flux<RoleBinding> listRoleBindings(Subject subject);\n\n    Flux<String> getRolesByUsername(String username);\n\n    Mono<Map<String, Collection<String>>> getRolesByUsernames(Collection<String> usernames);\n\n    Mono<Boolean> contains(Collection<String> source, Collection<String> candidates);\n\n    /**\n     * This method lists all role templates as permissions recursively according to given role\n     * name set.\n     *\n     * @param names is role name set.\n     * @return an array of permissions.\n     */\n    Flux<Role> listPermissions(Set<String> names);\n\n    Flux<Role> listDependenciesFlux(Set<String> names);\n\n    /**\n     * List roles by role names.\n     *\n     * @param roleNames role names\n     * @return roles\n     */\n    Flux<Role> list(Set<String> roleNames);\n\n    /**\n     * List roles by role names.\n     *\n     * @param roleNames role names\n     * @param excludeHidden should exclude hidden roles\n     * @return roles\n     */\n    Flux<Role> list(Set<String> roleNames, boolean excludeHidden);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/user/service/SignUpData.java",
    "content": "package run.halo.app.core.user.service;\n\nimport jakarta.validation.Constraint;\nimport jakarta.validation.ConstraintValidator;\nimport jakarta.validation.ConstraintValidatorContext;\nimport jakarta.validation.Payload;\nimport jakarta.validation.constraints.Email;\nimport jakarta.validation.constraints.NotBlank;\nimport jakarta.validation.constraints.Pattern;\nimport jakarta.validation.constraints.Size;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.util.Objects;\nimport lombok.Data;\nimport run.halo.app.infra.ValidationUtils;\n\n/**\n * Sign up data.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Data\n@SignUpData.SignUpDataConstraint\npublic class SignUpData {\n\n    @NotBlank\n    @Size(min = 4, max = 63)\n    @Pattern(regexp = ValidationUtils.NAME_REGEX,\n        message = \"{validation.error.username.pattern}\")\n    private String username;\n\n    @NotBlank\n    private String displayName;\n\n    @Email\n    private String email;\n\n    private String emailCode;\n\n    @NotBlank\n    @Size(min = 5, max = 257)\n    @Pattern(regexp = ValidationUtils.PASSWORD_REGEX,\n        message = \"{validation.error.password.pattern}\")\n    private String password;\n\n    @NotBlank\n    private String confirmPassword;\n\n    @Target({ElementType.TYPE})\n    @Retention(RetentionPolicy.RUNTIME)\n    @Constraint(validatedBy = {SignUpDataConstraintValidator.class})\n    public @interface SignUpDataConstraint {\n\n        String message() default \"\";\n\n        Class<?>[] groups() default {};\n\n        Class<? extends Payload>[] payload() default {};\n\n    }\n\n    private static class SignUpDataConstraintValidator\n        implements ConstraintValidator<SignUpDataConstraint, SignUpData> {\n\n        @Override\n        public boolean isValid(SignUpData signUpData, ConstraintValidatorContext context) {\n            var isValid = Objects.equals(signUpData.getPassword(), signUpData.getConfirmPassword());\n            if (!isValid) {\n                context.disableDefaultConstraintViolation();\n                context.buildConstraintViolationWithTemplate(\n                        \"{signup.error.confirm-password-not-match}\"\n                    )\n                    .addPropertyNode(\"confirmPassword\")\n                    .addConstraintViolation();\n            }\n            return isValid;\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/user/service/UserPostCreatingHandler.java",
    "content": "package run.halo.app.core.user.service;\n\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\n\n/**\n * User post-creating handler.\n *\n * @author johnniang\n * @since 2.20.8\n */\npublic interface UserPostCreatingHandler extends ExtensionPoint {\n\n    /**\n     * Do something after creating user.\n     *\n     * @param user create user.\n     * @return {@code Mono.empty()} if handling successfully.\n     */\n    Mono<Void> postCreating(User user);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/user/service/UserPreCreatingHandler.java",
    "content": "package run.halo.app.core.user.service;\n\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\n\n/**\n * User pre-creating handler.\n *\n * @author johnniang\n * @since 2.20.8\n */\npublic interface UserPreCreatingHandler extends ExtensionPoint {\n\n    /**\n     * Do something before user creating.\n     *\n     * @param user modifiable user detail\n     * @return {@code Mono.empty()} if handling successfully.\n     */\n    Mono<Void> preCreating(User user);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/core/user/service/UserService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport java.util.Collection;\nimport java.util.Set;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\n\npublic interface UserService {\n\n    String GHOST_USER_NAME = \"ghost\";\n\n    /**\n     * Get user by username.\n     *\n     * @param username username\n     * @return the mono user or user not found error\n     */\n    Mono<User> getUser(String username);\n\n    /**\n     * Get user by verified email.\n     *\n     * @param email the verified email\n     * @return the mono user or empty if not found\n     */\n    Mono<User> findUserByVerifiedEmail(String email);\n\n    Mono<User> getUserOrGhost(String username);\n\n    Flux<User> getUsersOrGhosts(Collection<String> names);\n\n    Mono<User> updatePassword(String username, String newPassword);\n\n    Mono<User> updateWithRawPassword(String username, String rawPassword);\n\n    Mono<User> grantRoles(String username, Set<String> roles);\n\n    /**\n     * Check if the user has sufficient roles.\n     *\n     * @param roles roles to check\n     * @return a Mono that emits true if the user has all the roles, false otherwise\n     */\n    Mono<Boolean> hasSufficientRoles(Collection<String> roles);\n\n    Mono<User> signUp(SignUpData signUpData);\n\n    Mono<User> createUser(User user, Set<String> roles);\n\n    Mono<Boolean> confirmPassword(String username, String rawPassword);\n\n    Flux<User> listByEmail(String email);\n\n    Mono<Boolean> checkEmailAlreadyVerified(String email);\n\n    String encryptPassword(String rawPassword);\n\n    Mono<User> disable(String username);\n\n    Mono<User> enable(String username);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/post/PostDeletedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.plugin.SharedEvent;\n\n@SharedEvent\npublic class PostDeletedEvent extends PostEvent {\n\n    private final Post post;\n\n    public PostDeletedEvent(Object source, Post post) {\n        super(source, post.getMetadata().getName());\n        this.post = post;\n    }\n\n    /**\n     * Get original post.\n     *\n     * @return original post.\n     */\n    public Post getPost() {\n        return post;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/post/PostEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * An abstract class for post events.\n *\n * @author johnniang\n */\npublic abstract class PostEvent extends ApplicationEvent {\n\n    private final String name;\n\n    public PostEvent(Object source, String name) {\n        super(source);\n        this.name = name;\n    }\n\n    /**\n     * Gets post metadata name.\n     *\n     * @return post metadata name\n     */\n    public String getName() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/post/PostPublishedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport run.halo.app.plugin.SharedEvent;\n\n@SharedEvent\npublic class PostPublishedEvent extends PostEvent {\n\n    public PostPublishedEvent(Object source, String postName) {\n        super(source, postName);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/post/PostUnpublishedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport run.halo.app.plugin.SharedEvent;\n\n@SharedEvent\npublic class PostUnpublishedEvent extends PostEvent {\n\n    public PostUnpublishedEvent(Object source, String postName) {\n        super(source, postName);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/post/PostUpdatedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport run.halo.app.plugin.SharedEvent;\n\n@SharedEvent\npublic class PostUpdatedEvent extends PostEvent {\n\n    public PostUpdatedEvent(Object source, String postName) {\n        super(source, postName);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/post/PostVisibleChangedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport org.springframework.lang.Nullable;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.plugin.SharedEvent;\n\n@SharedEvent\npublic class PostVisibleChangedEvent extends PostEvent {\n\n    @Nullable\n    private final Post.VisibleEnum oldVisible;\n\n    private final Post.VisibleEnum newVisible;\n\n    public PostVisibleChangedEvent(Object source, String postName,\n        @Nullable Post.VisibleEnum oldVisible, Post.VisibleEnum newVisible) {\n        super(source, postName);\n        this.oldVisible = oldVisible;\n        this.newVisible = newVisible;\n    }\n\n    @Nullable\n    public Post.VisibleEnum getOldVisible() {\n        return oldVisible;\n    }\n\n    public Post.VisibleEnum getNewVisible() {\n        return newVisible;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/user/UserConnectionDisconnectedEvent.java",
    "content": "package run.halo.app.event.user;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.UserConnection;\nimport run.halo.app.plugin.SharedEvent;\n\n/**\n * An event that will be triggered after a user connection is disconnected.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@SharedEvent\npublic class UserConnectionDisconnectedEvent extends ApplicationEvent {\n\n    @Getter\n    private final UserConnection userConnection;\n\n    public UserConnectionDisconnectedEvent(Object source, UserConnection userConnection) {\n        super(source);\n        this.userConnection = userConnection;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/user/UserLoginEvent.java",
    "content": "package run.halo.app.event.user;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.plugin.SharedEvent;\n\n/**\n * User login event.\n *\n * @author lywq\n **/\n@SharedEvent\npublic class UserLoginEvent extends ApplicationEvent {\n\n    @Getter\n    private final User user;\n\n    public UserLoginEvent(Object source, User user) {\n        super(source);\n        this.user = user;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/event/user/UserLogoutEvent.java",
    "content": "package run.halo.app.event.user;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.plugin.SharedEvent;\n\n/**\n * User logout event.\n *\n * @author lywq\n **/\n@SharedEvent\npublic class UserLogoutEvent extends ApplicationEvent {\n\n    @Getter\n    private final User user;\n\n    public UserLogoutEvent(Object source, User user) {\n        super(source);\n        this.user = user;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/AbstractExtension.java",
    "content": "package run.halo.app.extension;\n\nimport lombok.Data;\n\n/**\n * AbstractExtension contains basic structure of Extension and implements the Extension interface.\n *\n * @author johnniang\n */\n@Data\npublic abstract class AbstractExtension implements Extension {\n\n    private String apiVersion;\n\n    private String kind;\n\n    private MetadataOperator metadata;\n\n    @Override\n    public String getApiVersion() {\n        var apiVersionFromGvk = Extension.super.getApiVersion();\n        return apiVersionFromGvk != null ? apiVersionFromGvk : this.apiVersion;\n    }\n\n    @Override\n    public String getKind() {\n        var kindFromGvk = Extension.super.getKind();\n        return kindFromGvk != null ? kindFromGvk : this.kind;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Comparators.java",
    "content": "package run.halo.app.extension;\n\nimport java.time.Instant;\nimport java.util.Comparator;\n\npublic enum Comparators {\n    ;\n\n    public static <E extends Extension> Comparator<E> compareCreationTimestamp(boolean asc) {\n        var comparator =\n            Comparator.<E, Instant>comparing(e -> e.getMetadata().getCreationTimestamp());\n        return asc ? comparator : comparator.reversed();\n    }\n\n    public static <E extends Extension> Comparator<E> compareName(boolean asc) {\n        var comparator = Comparator.<E, String>comparing(e -> e.getMetadata().getName());\n        return asc ? comparator : comparator.reversed();\n    }\n\n    public static <T extends Extension> Comparator<T> defaultComparator() {\n        Comparator<T> comparator = compareCreationTimestamp(false);\n        comparator = comparator.thenComparing(compareName(true));\n        return comparator;\n    }\n\n    /**\n     * Get a nulls comparator.\n     *\n     * @param isAscending is ascending\n     * @return if ascending, return nulls high, else return nulls low\n     */\n    public static Comparator<Object> nullsComparator(boolean isAscending) {\n        return isAscending\n            ? org.springframework.util.comparator.Comparators.nullsHigh()\n            : org.springframework.util.comparator.Comparators.nullsLow();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ConfigMap.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport org.jspecify.annotations.Nullable;\n\n/**\n * <p>ConfigMap holds configuration data to consume.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"\", version = \"v1alpha1\", kind = ConfigMap.KIND, plural = \"configmaps\",\n    singular = \"configmap\")\npublic class ConfigMap extends AbstractExtension {\n\n    public static final String KIND = \"ConfigMap\";\n\n    @Nullable\n    private Map<String, String> data;\n\n    public ConfigMap putDataItem(String key, String dataItem) {\n        if (this.data == null) {\n            this.data = new LinkedHashMap<>();\n        }\n        this.data.put(key, dataItem);\n        return this;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/DefaultExtensionMatcher.java",
    "content": "package run.halo.app.extension;\n\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\n\n@Getter\n@RequiredArgsConstructor\n@Builder(builderMethodName = \"internalBuilder\")\npublic class DefaultExtensionMatcher implements ExtensionMatcher {\n    private final ExtensionClient client;\n    private final GroupVersionKind gvk;\n    private final LabelSelector labelSelector;\n    private final FieldSelector fieldSelector;\n\n    public static DefaultExtensionMatcherBuilder builder(ExtensionClient client,\n        GroupVersionKind gvk) {\n        return internalBuilder().client(client).gvk(gvk);\n    }\n\n    /**\n     * Match the given extension with the current matcher.\n     *\n     * @param extension extension to match\n     * @return true if the extension matches the current matcher\n     */\n    @Override\n    public boolean match(Extension extension) {\n        if (!gvk.equals(extension.groupVersionKind())) {\n            return false;\n        }\n        if (!hasFieldSelector() && !hasLabelSelector()) {\n            return true;\n        }\n        var listOptions = new ListOptions();\n        listOptions.setLabelSelector(labelSelector);\n        var fieldQuery = Queries.equal(\"metadata.name\", extension.getMetadata().getName());\n        if (hasFieldSelector()) {\n            fieldQuery = fieldQuery.and((Condition) fieldSelector.query());\n        }\n        listOptions.setFieldSelector(new FieldSelector(fieldQuery));\n        return client.countBy(extension.getClass(), listOptions) > 0;\n    }\n\n    boolean hasFieldSelector() {\n        return fieldSelector != null && fieldSelector.query() != null;\n    }\n\n    boolean hasLabelSelector() {\n        return labelSelector != null && !CollectionUtils.isEmpty(labelSelector.getConditions());\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Extension.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.Comparator;\nimport java.util.Objects;\n\n/**\n * Extension is an interface which represents an Extension. It contains setters and getters of\n * GroupVersionKind and Metadata.\n */\npublic interface Extension extends ExtensionOperator, Comparable<Extension> {\n\n    @Override\n    default int compareTo(Extension another) {\n        if (another == null || another.getMetadata() == null) {\n            return 1;\n        }\n        if (getMetadata() == null) {\n            return -1;\n        }\n        return Objects.compare(getMetadata().getName(), another.getMetadata().getName(),\n            Comparator.naturalOrder());\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ExtensionClient.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.extension.index.IndexedQueryEngine;\n\n/**\n * ExtensionClient is an interface which contains some operations on Extension instead of\n * ExtensionStore.\n * <br/><br/>\n * Please note that this client can only use in non-reactive environment. If you want to\n * use Extension client in reactive environment, please use {@link ReactiveExtensionClient} instead.\n *\n * @author johnniang\n */\npublic interface ExtensionClient {\n\n    /**\n     * Lists Extensions by Extension type, filter and sorter.\n     *\n     * @param type is the class type of Extension.\n     * @param predicate filters the reEnqueue.\n     * @param comparator sorts the reEnqueue.\n     * @param <E> is Extension type.\n     * @return all filtered and sorted Extensions.\n     */\n    <E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator);\n\n    /**\n     * Lists Extensions by Extension type, filter, sorter and page info.\n     *\n     * @param type is the class type of Extension.\n     * @param predicate filters the reEnqueue.\n     * @param comparator sorts the reEnqueue.\n     * @param page is page number which starts from 0.\n     * @param size is page size.\n     * @param <E> is Extension type.\n     * @return a list of Extensions.\n     * @deprecated use {@link #listAll(Class, ListOptions, Sort)} instead.\n     */\n    @Deprecated\n    <E extends Extension> ListResult<E> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator, int page, int size);\n\n    <E extends Extension> List<E> listAll(Class<E> type, ListOptions options, Sort sort);\n\n    <E extends Extension> List<String> listAllNames(Class<E> type, ListOptions options, Sort sort);\n\n    <E extends Extension> List<String> listTopNames(Class<E> type, ListOptions options, Sort sort,\n        int topN);\n\n    <E extends Extension> ListResult<E> listBy(Class<E> type, ListOptions options,\n        PageRequest page);\n\n    <E extends Extension> ListResult<String> listNamesBy(Class<E> type, ListOptions options,\n        PageRequest page);\n\n    <E extends Extension> long countBy(Class<E> type, ListOptions options);\n\n    /**\n     * Fetches Extension by its type and name.\n     *\n     * @param type is Extension type.\n     * @param name is Extension name.\n     * @param <E> is Extension type.\n     * @return an optional Extension.\n     */\n    <E extends Extension> Optional<E> fetch(Class<E> type, String name);\n\n    Optional<Unstructured> fetch(GroupVersionKind gvk, String name);\n\n\n    /**\n     * Creates an Extension.\n     *\n     * @param extension is fresh Extension to be created. Please make sure the Extension name does\n     * not exist.\n     * @param <E> is Extension type.\n     */\n    <E extends Extension> void create(E extension);\n\n    /**\n     * Updates an Extension.\n     *\n     * @param extension is an Extension to be updated. Please make sure the resource version is\n     * latest.\n     * @param <E> is Extension type.\n     */\n    <E extends Extension> void update(E extension);\n\n    /**\n     * Deletes an Extension.\n     *\n     * @param extension is an Extension to be deleted. Please make sure the resource version is\n     * latest.\n     * @param <E> is Extension type.\n     */\n    <E extends Extension> void delete(E extension);\n\n    @Deprecated(forRemoval = true, since = \"2.22.0\")\n    IndexedQueryEngine indexedQueryEngine();\n\n    void watch(Watcher watcher);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ExtensionMatcher.java",
    "content": "package run.halo.app.extension;\n\n@FunctionalInterface\npublic interface ExtensionMatcher {\n\n    boolean match(Extension extension);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ExtensionOperator.java",
    "content": "package run.halo.app.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.function.Predicate;\nimport org.springframework.util.StringUtils;\n\n/**\n * ExtensionOperator contains some getters and setters for required fields of Extension.\n *\n * @author johnniang\n */\npublic interface ExtensionOperator {\n\n    @Schema(requiredMode = REQUIRED)\n    @JsonProperty(\"apiVersion\")\n    default String getApiVersion() {\n        final var gvk = getClass().getAnnotation(GVK.class);\n        if (gvk == null) {\n            // return null if having no GVK annotation\n            return null;\n        }\n        if (StringUtils.hasText(gvk.group())) {\n            return gvk.group() + \"/\" + gvk.version();\n        }\n        return gvk.version();\n    }\n\n    @Schema(requiredMode = REQUIRED)\n    @JsonProperty(\"kind\")\n    default String getKind() {\n        final var gvk = getClass().getAnnotation(GVK.class);\n        if (gvk == null) {\n            // return null if having no GVK annotation\n            return null;\n        }\n        return gvk.kind();\n    }\n\n    @Schema(requiredMode = REQUIRED, implementation = Metadata.class)\n    @JsonProperty(\"metadata\")\n    MetadataOperator getMetadata();\n\n    void setApiVersion(String apiVersion);\n\n    void setKind(String kind);\n\n    void setMetadata(MetadataOperator metadata);\n\n    /**\n     * Sets GroupVersionKind of the Extension.\n     *\n     * @param gvk is GroupVersionKind data.\n     */\n    default void groupVersionKind(GroupVersionKind gvk) {\n        setApiVersion(gvk.groupVersion().toString());\n        setKind(gvk.kind());\n    }\n\n    /**\n     * Gets GroupVersionKind of the Extension.\n     *\n     * @return GroupVersionKind of the Extension.\n     */\n    @JsonIgnore\n    default GroupVersionKind groupVersionKind() {\n        return GroupVersionKind.fromAPIVersionAndKind(getApiVersion(), getKind());\n    }\n\n    static <T extends ExtensionOperator> Predicate<T> isNotDeleted() {\n        return ext -> ext.getMetadata().getDeletionTimestamp() == null;\n    }\n\n    static boolean isDeleted(ExtensionOperator extension) {\n        return ExtensionUtil.isDeleted(extension);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ExtensionUtil.java",
    "content": "package run.halo.app.extension;\n\nimport static org.springframework.data.domain.Sort.Order.asc;\nimport static org.springframework.data.domain.Sort.Order.desc;\n\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.index.query.Query;\n\npublic enum ExtensionUtil {\n    ;\n\n    /**\n     * Label to mark an extension resource should not be overwritten during initialization.\n     */\n    public static final String DO_NOT_OVERWRITE_LABEL = \"halo.run/do-not-overwrite\";\n\n    /**\n     * Check if the extension has the do-not-overwrite label. If the label is present and set to\n     * true, it indicates that the extension should not be overwritten during initialization.\n     *\n     * @param extension the extension\n     * @return true if it has the label, false otherwise\n     */\n    public static boolean hasDoNotOverwriteLabel(ExtensionOperator extension) {\n        if (extension.getMetadata() == null) {\n            return false;\n        }\n        var labels = extension.getMetadata().getLabels();\n        return labels != null && Boolean.parseBoolean(labels.get(DO_NOT_OVERWRITE_LABEL));\n    }\n\n    public static boolean isDeleted(ExtensionOperator extension) {\n        return extension.getMetadata() != null\n            && extension.getMetadata().getDeletionTimestamp() != null;\n    }\n\n    public static boolean addFinalizers(MetadataOperator metadata, Set<String> finalizers) {\n        var modifiableFinalizers = new HashSet<>(\n            metadata.getFinalizers() == null ? Collections.emptySet() : metadata.getFinalizers());\n        var added = modifiableFinalizers.addAll(finalizers);\n        if (added) {\n            metadata.setFinalizers(modifiableFinalizers);\n        }\n        return added;\n    }\n\n    public static boolean removeFinalizers(MetadataOperator metadata, Set<String> finalizers) {\n        if (metadata.getFinalizers() == null) {\n            return false;\n        }\n        var existingFinalizers = new HashSet<>(metadata.getFinalizers());\n        var removed = existingFinalizers.removeAll(finalizers);\n        if (removed) {\n            metadata.setFinalizers(existingFinalizers);\n        }\n        return removed;\n    }\n\n    /**\n     * Query for not deleting.\n     *\n     * @return Query\n     */\n    public static Query notDeleting() {\n        return Queries.isNull(\"metadata.deletionTimestamp\");\n    }\n\n    /**\n     * Default sort by creation timestamp desc and name asc.\n     *\n     * @return Sort\n     */\n    public static Sort defaultSort() {\n        return Sort.by(\n            desc(\"metadata.creationTimestamp\"),\n            asc(\"metadata.name\")\n        );\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/GVK.java",
    "content": "package run.halo.app.extension;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * GVK is an annotation to specific metadata of Extension.\n *\n * @author johnniang\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface GVK {\n\n    /**\n     * @return group name of Extension.\n     */\n    String group();\n\n    /**\n     * @return version name of Extension.\n     */\n    String version();\n\n    /**\n     * @return kind name of Extension.\n     */\n    String kind();\n\n    /**\n     * @return plural name of Extension.\n     */\n    String plural();\n\n    /**\n     * @return singular name of Extension.\n     */\n    String singular();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/GroupKind.java",
    "content": "package run.halo.app.extension;\n\n/**\n * GroupKind contains group and kind data only.\n *\n * @param group is group name of Extension.\n * @param kind is kind name of Extension.\n * @author johnniang\n */\npublic record GroupKind(String group, String kind) {\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/GroupVersion.java",
    "content": "package run.halo.app.extension;\n\nimport org.springframework.util.Assert;\nimport org.springframework.util.StringUtils;\n\n/**\n * GroupVersion contains group and version name of an Extension only.\n *\n * @param group is group name of Extension.\n * @param version is version name of Extension.\n * @author johnniang\n */\npublic record GroupVersion(String group, String version) {\n\n    @Override\n    public String toString() {\n        return StringUtils.hasText(group) ? group + \"/\" + version : version;\n    }\n\n    /**\n     * Parses APIVersion into GroupVersion record.\n     *\n     * @param apiVersion must not be blank.\n     * 1. If the given apiVersion does not contain any \"/\", we treat the group is empty.\n     * 2. If the given apiVersion contains more than 1 \"/\", we will throw an\n     * IllegalArgumentException.\n     * @return record contains group and version.\n     */\n    public static GroupVersion parseAPIVersion(String apiVersion) {\n        Assert.hasText(apiVersion, \"API version must not be blank\");\n\n        var groupVersion = apiVersion.split(\"/\");\n        return switch (groupVersion.length) {\n            case 1 -> new GroupVersion(\"\", apiVersion);\n            case 2 -> new GroupVersion(groupVersion[0], groupVersion[1]);\n            default ->\n                throw new IllegalArgumentException(\"Unexpected APIVersion string: \" + apiVersion);\n        };\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/GroupVersionKind.java",
    "content": "package run.halo.app.extension;\n\nimport org.springframework.util.Assert;\nimport org.springframework.util.StringUtils;\n\n/**\n * GroupVersionKind contains group, version and kind name of an Extension.\n *\n * @param group is group name of Extension.\n * @param version is version name of Extension.\n * @param kind is kind name of Extension.\n * @author johnniang\n */\npublic record GroupVersionKind(String group, String version, String kind) {\n\n    public GroupVersionKind {\n        Assert.hasText(version, \"Version must not be blank\");\n        Assert.hasText(kind, \"Kind must not be blank\");\n    }\n\n    /**\n     * Gets group and version name of Extension.\n     *\n     * @return group and version name of Extension.\n     */\n    public GroupVersion groupVersion() {\n        return new GroupVersion(group, version);\n    }\n\n    public GroupKind groupKind() {\n        return new GroupKind(group, kind);\n    }\n\n    public boolean hasGroup() {\n        return StringUtils.hasText(group);\n    }\n\n    /**\n     * Composes GroupVersionKind from API version and kind name.\n     *\n     * @param apiVersion is API version. Like \"core.halo.run/v1alpha1\"\n     * @param kind is kind name of Extension.\n     * @return GroupVersionKind of an Extension.\n     */\n    public static GroupVersionKind fromAPIVersionAndKind(String apiVersion, String kind) {\n        Assert.hasText(kind, \"Kind must not be blank\");\n\n        var gv = GroupVersion.parseAPIVersion(apiVersion);\n        return new GroupVersionKind(gv.group(), gv.version(), kind);\n    }\n\n    public static <T extends Extension> GroupVersionKind fromExtension(Class<T> extension) {\n        GVK gvk = extension.getAnnotation(GVK.class);\n        return new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind());\n    }\n\n    @Override\n    public String toString() {\n        if (hasGroup()) {\n            return group + \"/\" + version + \"/\" + kind;\n        }\n        return version + \"/\" + kind;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/JsonExtension.java",
    "content": "package run.halo.app.extension;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.fasterxml.jackson.databind.node.LongNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\n/**\n * JsonExtension is representation an extension using ObjectNode. This extension is preparing for\n * patching in the future.\n *\n * @author johnniang\n */\n@JsonSerialize(using = JsonExtension.ObjectNodeExtensionSerializer.class)\n@JsonDeserialize(using = JsonExtension.ObjectNodeExtensionDeSerializer.class)\n@Deprecated(forRemoval = true, since = \"2.23.0\")\npublic class JsonExtension implements Extension {\n\n    private final ObjectMapper objectMapper;\n\n    private final ObjectNode objectNode;\n\n    public JsonExtension(ObjectMapper objectMapper) {\n        this(objectMapper, objectMapper.createObjectNode());\n    }\n\n    public JsonExtension(ObjectMapper objectMapper, ObjectNode objectNode) {\n        this.objectMapper = objectMapper;\n        this.objectNode = objectNode;\n    }\n\n    public JsonExtension(ObjectMapper objectMapper, Extension e) {\n        this(objectMapper, (ObjectNode) objectMapper.valueToTree(e));\n    }\n\n    @Override\n    public MetadataOperator getMetadata() {\n        var metadataNode = objectNode.get(\"metadata\");\n        if (metadataNode == null) {\n            return null;\n        }\n        return new ObjectNodeMetadata((ObjectNode) metadataNode);\n    }\n\n    @Override\n    public String getApiVersion() {\n        var apiVersionNode = objectNode.get(\"apiVersion\");\n        return apiVersionNode == null ? null : apiVersionNode.asText();\n    }\n\n    @Override\n    public String getKind() {\n        return objectNode.get(\"kind\").asText();\n    }\n\n    @Override\n    public void setApiVersion(String apiVersion) {\n        objectNode.set(\"apiVersion\", new TextNode(apiVersion));\n    }\n\n    @Override\n    public void setKind(String kind) {\n        objectNode.set(\"kind\", new TextNode(kind));\n    }\n\n    @Override\n    public void setMetadata(MetadataOperator metadata) {\n        objectNode.set(\"metadata\", objectMapper.valueToTree(metadata));\n    }\n\n    public static class ObjectNodeExtensionSerializer extends JsonSerializer<JsonExtension> {\n\n        @Override\n        public void serialize(JsonExtension value, JsonGenerator gen,\n            SerializerProvider serializers) throws IOException {\n            gen.writeTree(value.objectNode);\n        }\n    }\n\n    public static class ObjectNodeExtensionDeSerializer\n        extends JsonDeserializer<JsonExtension> {\n\n        @Override\n        public JsonExtension deserialize(JsonParser p, DeserializationContext ctxt)\n            throws IOException {\n            var mapper = (ObjectMapper) p.getCodec();\n            var treeNode = mapper.readTree(p);\n            return new JsonExtension(mapper, (ObjectNode) treeNode);\n        }\n    }\n\n    /**\n     * Get internal representation.\n     *\n     * @return internal representation\n     */\n    public ObjectNode getInternal() {\n        return objectNode;\n    }\n\n    /**\n     * Get object mapper.\n     *\n     * @return object mapper\n     */\n    public ObjectMapper getObjectMapper() {\n        return objectMapper;\n    }\n\n    public MetadataOperator getMetadataOrCreate() {\n        var metadataNode = objectMapper.createObjectNode();\n        objectNode.set(\"metadata\", metadataNode);\n        return new ObjectNodeMetadata(metadataNode);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        JsonExtension that = (JsonExtension) o;\n        return Objects.equals(objectNode, that.objectNode);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(objectNode);\n    }\n\n    class ObjectNodeMetadata implements MetadataOperator {\n\n        private final ObjectNode objectNode;\n\n        public ObjectNodeMetadata(ObjectNode objectNode) {\n            this.objectNode = objectNode;\n        }\n\n        @Override\n        public String getName() {\n            var nameNode = objectNode.get(\"name\");\n            return objectMapper.convertValue(nameNode, String.class);\n        }\n\n        @Override\n        public String getGenerateName() {\n            var generateNameNode = objectNode.get(\"generateName\");\n            return objectMapper.convertValue(generateNameNode, String.class);\n        }\n\n        @Override\n        public Map<String, String> getLabels() {\n            var labelsNode = objectNode.get(\"labels\");\n            return objectMapper.convertValue(labelsNode, new TypeReference<>() {\n            });\n        }\n\n        @Override\n        public Map<String, String> getAnnotations() {\n            var annotationsNode = objectNode.get(\"annotations\");\n            return objectMapper.convertValue(annotationsNode, new TypeReference<>() {\n            });\n        }\n\n        @Override\n        public Long getVersion() {\n            JsonNode versionNode = objectNode.get(\"version\");\n            return objectMapper.convertValue(versionNode, Long.class);\n        }\n\n        @Override\n        public Instant getCreationTimestamp() {\n            return objectMapper.convertValue(objectNode.get(\"creationTimestamp\"), Instant.class);\n        }\n\n        @Override\n        public Instant getDeletionTimestamp() {\n            return objectMapper.convertValue(objectNode.get(\"deletionTimestamp\"), Instant.class);\n        }\n\n        @Override\n        public Set<String> getFinalizers() {\n            return objectMapper.convertValue(objectNode.get(\"finalizers\"), new TypeReference<>() {\n            });\n        }\n\n        @Override\n        public void setName(String name) {\n            if (name != null) {\n                objectNode.set(\"name\", TextNode.valueOf(name));\n            }\n        }\n\n        @Override\n        public void setGenerateName(String generateName) {\n            if (generateName != null) {\n                objectNode.set(\"generateName\", TextNode.valueOf(generateName));\n            }\n        }\n\n        @Override\n        public void setLabels(Map<String, String> labels) {\n            if (labels != null) {\n                objectNode.set(\"labels\", objectMapper.valueToTree(labels));\n            }\n        }\n\n        @Override\n        public void setAnnotations(Map<String, String> annotations) {\n            if (annotations != null) {\n                objectNode.set(\"annotations\", objectMapper.valueToTree(annotations));\n            }\n        }\n\n        @Override\n        public void setVersion(Long version) {\n            if (version != null) {\n                objectNode.set(\"version\", LongNode.valueOf(version));\n            }\n        }\n\n        @Override\n        public void setCreationTimestamp(Instant creationTimestamp) {\n            if (creationTimestamp != null) {\n                objectNode.set(\"creationTimestamp\", objectMapper.valueToTree(creationTimestamp));\n            }\n        }\n\n        @Override\n        public void setDeletionTimestamp(Instant deletionTimestamp) {\n            if (deletionTimestamp != null) {\n                objectNode.set(\"deletionTimestamp\", objectMapper.valueToTree(deletionTimestamp));\n            }\n        }\n\n        @Override\n        public void setFinalizers(Set<String> finalizers) {\n            if (finalizers != null) {\n                objectNode.set(\"finalizers\", objectMapper.valueToTree(finalizers));\n            }\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (o == null || getClass() != o.getClass()) {\n                return false;\n            }\n\n            ObjectNodeMetadata that = (ObjectNodeMetadata) o;\n            return Objects.equals(objectNode, that.objectNode);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hashCode(objectNode);\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ListOptions.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.LabelCondition;\nimport run.halo.app.extension.index.query.Query;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\n\n@Data\n@Accessors(chain = true)\npublic class ListOptions {\n\n    private LabelSelector labelSelector;\n\n    private FieldSelector fieldSelector;\n\n    @Override\n    public String toString() {\n        return toCondition().toString();\n    }\n\n    /**\n     * Convert to a single condition.\n     *\n     * @return the condition, never null\n     */\n    @NonNull\n    public Condition toCondition() {\n        Condition condition = null;\n        var fieldSelector = getFieldSelector();\n        if (fieldSelector != null && fieldSelector.query() != null) {\n            var query = fieldSelector.query();\n            if (!(query instanceof Condition fieldCondition)) {\n                throw new IllegalArgumentException(\"Only support condition query\");\n            }\n            condition = fieldCondition;\n        }\n        var labelSelector = getLabelSelector();\n        if (labelSelector != null) {\n            var labelCondition = labelSelector.getConditions().stream()\n                .map(Function.<Condition>identity())\n                .reduce(Condition::and)\n                .orElse(null);\n            if (labelCondition != null) {\n                if (condition == null) {\n                    condition = labelCondition;\n                } else {\n                    condition = condition.and(labelCondition);\n                }\n            }\n        }\n        return condition == null ? Condition.empty() : condition;\n    }\n\n    public static ListOptionsBuilder builder() {\n        return new ListOptionsBuilder();\n    }\n\n    public static ListOptionsBuilder builder(ListOptions listOptions) {\n        return new ListOptionsBuilder(listOptions);\n    }\n\n    public static class ListOptionsBuilder {\n        private LabelSelectorBuilder labelSelectorBuilder;\n        private Query query;\n\n        public ListOptionsBuilder() {\n        }\n\n        /**\n         * Create a new list options builder with the given list options.\n         */\n        public ListOptionsBuilder(ListOptions listOptions) {\n            if (listOptions.getLabelSelector() != null) {\n                this.labelSelectorBuilder = new LabelSelectorBuilder(\n                    listOptions.getLabelSelector().getConditions(), this);\n            }\n            if (listOptions.getFieldSelector() != null) {\n                this.query = listOptions.getFieldSelector().query();\n            }\n        }\n\n        /**\n         * Create a new label selector builder.\n         */\n        public LabelSelectorBuilder labelSelector() {\n            if (labelSelectorBuilder == null) {\n                labelSelectorBuilder = new LabelSelectorBuilder(this);\n            }\n            return labelSelectorBuilder;\n        }\n\n        public ListOptionsBuilder fieldQuery(Query query) {\n            this.query = query;\n            return this;\n        }\n\n        /**\n         * And the given query to the current query.\n         */\n        public ListOptionsBuilder andQuery(Query query) {\n            if (!(query instanceof Condition condition)) {\n                throw new IllegalArgumentException(\"Given query must be an instance of Condition\");\n            }\n            if (this.query == null) {\n                this.query = condition;\n            } else {\n                if (!(this.query instanceof Condition currentCondition)) {\n                    throw new IllegalArgumentException(\n                        \"Current query must be an instance of Condition\"\n                    );\n                }\n                this.query = currentCondition.and(condition);\n            }\n            return this;\n        }\n\n        /**\n         * Or the given query to the current query.\n         */\n        public ListOptionsBuilder orQuery(Query query) {\n            if (!(query instanceof Condition condition)) {\n                throw new IllegalArgumentException(\"Given query must be an instance of Condition\");\n            }\n            if (this.query == null) {\n                this.query = condition;\n            } else {\n                if (!(this.query instanceof Condition currentCondition)) {\n                    throw new IllegalArgumentException(\n                        \"Current query must be an instance of Condition\"\n                    );\n                }\n                this.query = currentCondition.or(condition);\n            }\n            return this;\n        }\n\n        /**\n         * Build the list options.\n         */\n        public ListOptions build() {\n            var listOptions = new ListOptions();\n            if (labelSelectorBuilder != null) {\n                listOptions.setLabelSelector(labelSelectorBuilder.build());\n            }\n            if (query != null) {\n                listOptions.setFieldSelector(FieldSelector.of(query));\n            }\n            return listOptions;\n        }\n    }\n\n    public static class LabelSelectorBuilder\n        extends LabelSelector.LabelSelectorBuilder<LabelSelectorBuilder> {\n        private final ListOptionsBuilder listOptionsBuilder;\n\n        public LabelSelectorBuilder(List<LabelCondition> conditions,\n            ListOptionsBuilder listOptionsBuilder) {\n            super(conditions);\n            this.listOptionsBuilder = listOptionsBuilder;\n        }\n\n        public LabelSelectorBuilder(ListOptionsBuilder listOptionsBuilder) {\n            this.listOptionsBuilder = listOptionsBuilder;\n        }\n\n        public ListOptionsBuilder end() {\n            return this.listOptionsBuilder;\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ListResult.java",
    "content": "package run.halo.app.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\nimport lombok.Data;\nimport org.springframework.util.Assert;\nimport run.halo.app.infra.utils.GenericClassUtils;\n\n@Data\npublic class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {\n\n    @Schema(description = \"Page number, starts from 1. If not set or equal to 0, it means no \"\n        + \"pagination.\", requiredMode = REQUIRED)\n    private final int page;\n\n    @Schema(description = \"Size of each page. If not set or equal to 0, it means no pagination.\",\n        requiredMode = REQUIRED)\n    private final int size;\n\n    @Schema(description = \"Total elements.\", requiredMode = REQUIRED)\n    private final long total;\n\n    @Schema(description = \"A chunk of items.\", requiredMode = REQUIRED)\n    private final List<T> items;\n\n    @JsonCreator\n    public ListResult(\n        @JsonProperty(\"page\") int page,\n        @JsonProperty(\"size\") int size,\n        @JsonProperty(\"total\") long total,\n        @JsonProperty(\"items\") List<T> items) {\n        Assert.isTrue(total >= 0, \"Total elements must be greater than or equal to 0\");\n        if (page < 0) {\n            page = 0;\n        }\n        if (size < 0) {\n            size = 0;\n        }\n        if (items == null) {\n            items = Collections.emptyList();\n        }\n        this.page = page;\n        this.size = size;\n        this.total = total;\n        this.items = items;\n    }\n\n    public ListResult(List<T> items) {\n        this(0, 0, items.size(), items);\n    }\n\n    @Schema(description = \"Indicates whether current page is the first page.\",\n        requiredMode = REQUIRED)\n    public boolean isFirst() {\n        return !hasPrevious();\n    }\n\n    @Schema(description = \"Indicates whether current page is the last page.\",\n        requiredMode = REQUIRED)\n    public boolean isLast() {\n        return !hasNext();\n    }\n\n    @Schema(description = \"Indicates whether current page has previous page.\",\n        requiredMode = REQUIRED)\n    @JsonProperty(\"hasNext\")\n    public boolean hasNext() {\n        if (page <= 0) {\n            return false;\n        }\n        return page < getTotalPages();\n    }\n\n    @Schema(description = \"Indicates whether current page has previous page.\",\n        requiredMode = REQUIRED)\n    @JsonProperty(\"hasPrevious\")\n    public boolean hasPrevious() {\n        return page > 1;\n    }\n\n    @Override\n    public Iterator<T> iterator() {\n        return items.iterator();\n    }\n\n    @Schema(description = \"Indicates total pages.\", requiredMode = REQUIRED)\n    @JsonProperty(\"totalPages\")\n    public long getTotalPages() {\n        return size == 0 ? 1 : (total + size - 1) / size;\n    }\n\n    /**\n     * Generate generic ListResult class. Like {@code ListResult<User>}, {@code ListResult<Post>},\n     * etc.\n     *\n     * @param scheme scheme of the generic type.\n     * @return generic ListResult class.\n     */\n    public static Class<?> generateGenericClass(Scheme scheme) {\n        return GenericClassUtils.generateConcreteClass(ListResult.class,\n            scheme.type(),\n            () -> {\n                var pkgName = scheme.type().getPackageName();\n                return pkgName + '.' + scheme.groupVersionKind().kind() + \"List\";\n            });\n    }\n\n    /**\n     * Generate generic ListResult class. Like {@code ListResult<User>}, {@code ListResult<Post>},\n     * etc.\n     *\n     * @param type the generic type of {@link ListResult}.\n     * @return generic ListResult class.\n     */\n    public static <T> Class<?> generateGenericClass(Class<T> type) {\n        return GenericClassUtils.generateConcreteClass(ListResult.class, type,\n            () -> type.getName() + \"List\");\n    }\n\n    public static <T> ListResult<T> emptyResult() {\n        return new ListResult<>(List.of());\n    }\n\n    /**\n     * Manually paginate the List collection.\n     */\n    public static <T> List<T> subList(List<T> list, int page, int size) {\n        if (page < 1) {\n            page = 1;\n        }\n        if (size < 1) {\n            return list;\n        }\n        List<T> listSort = new ArrayList<>();\n        int total = list.size();\n        int pageStart = page == 1 ? 0 : (page - 1) * size;\n        int pageEnd = Math.min(total, page * size);\n        if (total > pageStart) {\n            listSort = list.subList(pageStart, pageEnd);\n        }\n        return listSort;\n    }\n\n    /**\n     * Gets the first element of the list result.\n     */\n    public static <T> Optional<T> first(ListResult<T> listResult) {\n        return Optional.ofNullable(listResult)\n            .map(ListResult::getItems)\n            .map(list -> list.isEmpty() ? null : list.get(0));\n    }\n\n    @Override\n    public Stream<T> get() {\n        return items.stream();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Metadata.java",
    "content": "package run.halo.app.extension;\n\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\n/**\n * Metadata of Extension.\n *\n * @author johnniang\n */\n@Data\n@EqualsAndHashCode(exclude = \"version\")\npublic class Metadata implements MetadataOperator {\n\n    /**\n     * Metadata name. The name is unique globally.\n     */\n    private String name;\n\n    /**\n     * Generate name is for generating metadata name automatically.\n     */\n    private String generateName;\n\n    /**\n     * Labels are like key-value format.\n     */\n    private Map<String, String> labels;\n\n    /**\n     * Annotations are like key-value format.\n     */\n    private Map<String, String> annotations;\n\n    /**\n     * Current version of the Extension. It will be bumped up every update.\n     */\n    private Long version;\n\n    /**\n     * Creation timestamp of the Extension.\n     */\n    private Instant creationTimestamp;\n\n    /**\n     * Deletion timestamp of the Extension.\n     */\n    private Instant deletionTimestamp;\n\n    private Set<String> finalizers;\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/MetadataOperator.java",
    "content": "package run.halo.app.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\n/**\n * MetadataOperator contains some getters and setters for required fields of metadata.\n *\n * @author johnniang\n */\n@JsonDeserialize(as = Metadata.class)\n@tools.jackson.databind.annotation.JsonDeserialize(as = Metadata.class)\n@Schema(implementation = Metadata.class)\npublic interface MetadataOperator {\n\n    @Schema(name = \"name\", description = \"Metadata name\", requiredMode = REQUIRED)\n    @JsonProperty(\"name\")\n    String getName();\n\n    @Schema(name = \"generateName\", description = \"The name field will be generated automatically \"\n        + \"according to the given generateName field\")\n    String getGenerateName();\n\n    @Schema(name = \"labels\")\n    @JsonProperty(\"labels\")\n    Map<String, String> getLabels();\n\n    @Schema(name = \"annotations\")\n    @JsonProperty(\"annotations\")\n    Map<String, String> getAnnotations();\n\n    @Schema(name = \"version\", nullable = true)\n    @JsonProperty(\"version\")\n    Long getVersion();\n\n    @Schema(name = \"creationTimestamp\", nullable = true)\n    @JsonProperty(\"creationTimestamp\")\n    Instant getCreationTimestamp();\n\n    @Schema(name = \"deletionTimestamp\", nullable = true)\n    @JsonProperty(\"deletionTimestamp\")\n    Instant getDeletionTimestamp();\n\n    @Schema(name = \"finalizers\", nullable = true)\n    Set<String> getFinalizers();\n\n    void setName(String name);\n\n    void setGenerateName(String generateName);\n\n    void setLabels(Map<String, String> labels);\n\n    void setAnnotations(Map<String, String> annotations);\n\n    void setVersion(Long version);\n\n    void setCreationTimestamp(Instant creationTimestamp);\n\n    void setDeletionTimestamp(Instant deletionTimestamp);\n\n    void setFinalizers(Set<String> finalizers);\n\n    /**\n     * Equals method for metadata.\n     *\n     * @param left metadata\n     * @param right metadata\n     * @return true if equals, false otherwise\n     */\n    static boolean equals(MetadataOperator left, MetadataOperator right) {\n        if (left == null && right == null) {\n            return true;\n        }\n        if (left == null || right == null) {\n            return false;\n        }\n        return Objects.equals(left.getName(), right.getName())\n            && Objects.equals(left.getGenerateName(), right.getGenerateName())\n            && Objects.equals(left.getLabels(), right.getLabels())\n            && Objects.equals(left.getAnnotations(), right.getAnnotations())\n            && Objects.equals(left.getCreationTimestamp(), right.getCreationTimestamp())\n            && Objects.equals(left.getDeletionTimestamp(), right.getDeletionTimestamp())\n            && Objects.equals(left.getVersion(), right.getVersion())\n            && Objects.equals(left.getFinalizers(), right.getFinalizers());\n    }\n\n    /**\n     * Hash code for metadata.\n     *\n     * @param metadata metadata\n     * @return hash code\n     */\n    static int hashCode(MetadataOperator metadata) {\n        return Objects.hash(\n            metadata.getName(),\n            metadata.getGenerateName(),\n            metadata.getLabels(),\n            metadata.getAnnotations(),\n            metadata.getCreationTimestamp(),\n            metadata.getDeletionTimestamp(),\n            metadata.getVersion(),\n            metadata.getFinalizers()\n        );\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/MetadataUtil.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.springframework.util.Assert;\n\npublic enum MetadataUtil {\n    ;\n\n    public static final String SYSTEM_FINALIZER = \"system-protection\";\n    public static final String HIDDEN_LABEL = \"halo.run/hidden\";\n\n    /**\n     * Gets extension metadata labels null safe.\n     *\n     * @param extension extension must not be null\n     * @return extension metadata labels\n     */\n    public static Map<String, String> nullSafeLabels(AbstractExtension extension) {\n        Assert.notNull(extension, \"The extension must not be null.\");\n        Assert.notNull(extension.getMetadata(), \"The extension metadata must not be null.\");\n        Map<String, String> labels = extension.getMetadata().getLabels();\n        if (labels == null) {\n            labels = new HashMap<>();\n            extension.getMetadata().setLabels(labels);\n        }\n        return labels;\n    }\n\n    /**\n     * Gets extension metadata annotations null safe.\n     *\n     * @param extension extension must not be null\n     * @return extension metadata annotations\n     */\n    public static Map<String, String> nullSafeAnnotations(AbstractExtension extension) {\n        Assert.notNull(extension, \"The extension must not be null.\");\n        Assert.notNull(extension.getMetadata(), \"The extension metadata must not be null.\");\n        Map<String, String> annotations = extension.getMetadata().getAnnotations();\n        if (annotations == null) {\n            annotations = new HashMap<>();\n            extension.getMetadata().setAnnotations(annotations);\n        }\n        return annotations;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/PageRequest.java",
    "content": "package run.halo.app.extension;\n\nimport org.springframework.data.domain.Sort;\nimport org.springframework.util.Assert;\n\n/**\n * <p>{@link PageRequest} is an interface for pagination information.</p>\n * <p>Page number starts from 1.</p>\n * <p>if page size is 0, it means no pagination and all results will be returned.</p>\n *\n * @author guqing\n * @see PageRequestImpl\n * @since 2.12.0\n */\npublic interface PageRequest {\n    int getPageNumber();\n\n    int getPageSize();\n\n    PageRequest previous();\n\n    PageRequest next();\n\n    /**\n     * Returns the previous {@link PageRequest} or the first {@link PageRequest} if the current one\n     * already is the first one.\n     *\n     * @return a new {@link org.springframework.data.domain.PageRequest} with\n     * {@link #getPageNumber()} - 1 as {@link #getPageNumber()}\n     */\n    PageRequest previousOrFirst();\n\n    /**\n     * Returns the {@link PageRequest} requesting the first page.\n     *\n     * @return a new {@link org.springframework.data.domain.PageRequest} with\n     * {@link #getPageNumber()} = 1 as {@link #getPageNumber()}\n     */\n    PageRequest first();\n\n    /**\n     * Creates a new {@link PageRequest} with {@code pageNumber} applied.\n     *\n     * @param pageNumber 1-based page index.\n     * @return a new {@link org.springframework.data.domain.PageRequest}\n     */\n    PageRequest withPage(int pageNumber);\n\n    PageRequestImpl withSort(Sort sort);\n\n    boolean hasPrevious();\n\n    Sort getSort();\n\n    /**\n     * Returns the current {@link Sort} or the given one if the current one is unsorted.\n     *\n     * @param sort must not be {@literal null}.\n     * @return the current {@link Sort} or the given one if the current one is unsorted.\n     */\n    default Sort getSortOr(Sort sort) {\n        Assert.notNull(sort, \"Fallback Sort must not be null\");\n        return getSort().isSorted() ? getSort() : sort;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/PageRequestImpl.java",
    "content": "package run.halo.app.extension;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.util.Assert;\n\n@Slf4j\npublic class PageRequestImpl implements PageRequest {\n\n    public static final int MAX_SIZE = 1_000;\n\n    private final int pageNumber;\n    private final int pageSize;\n    private final Sort sort;\n\n    public PageRequestImpl(int pageNumber, int pageSize, Sort sort) {\n        Assert.notNull(sort, \"Sort must not be null\");\n        if (pageNumber < 1) {\n            pageNumber = 1;\n        }\n        if (pageSize <= 0) {\n            log.warn(\"Page size must be greater than 0, reset to default {}\", MAX_SIZE);\n            pageSize = MAX_SIZE;\n        }\n        if (pageSize > 1000) {\n            log.warn(\"Page size must not be greater than {}, reset to {}\", MAX_SIZE, MAX_SIZE);\n            pageSize = MAX_SIZE;\n        }\n        this.pageNumber = pageNumber;\n        this.pageSize = pageSize;\n        this.sort = sort;\n    }\n\n    public static PageRequestImpl of(int pageNumber, int pageSize) {\n        return of(pageNumber, pageSize, Sort.unsorted());\n    }\n\n    public static PageRequestImpl of(int pageNumber, int pageSize, Sort sort) {\n        return new PageRequestImpl(pageNumber, pageSize, sort);\n    }\n\n    public static PageRequestImpl ofSize(int pageSize) {\n        return PageRequestImpl.of(1, pageSize);\n    }\n\n    @Override\n    public int getPageNumber() {\n        return pageNumber;\n    }\n\n    @Override\n    public int getPageSize() {\n        return pageSize;\n    }\n\n    @Override\n    public PageRequest previous() {\n        return getPageNumber() == 0 ? this\n            : new PageRequestImpl(getPageNumber() - 1, getPageSize(), getSort());\n    }\n\n    @Override\n    public Sort getSort() {\n        return sort;\n    }\n\n    @Override\n    public PageRequest next() {\n        return new PageRequestImpl(getPageNumber() + 1, getPageSize(), getSort());\n    }\n\n    @Override\n    public PageRequest previousOrFirst() {\n        return hasPrevious() ? previous() : first();\n    }\n\n    @Override\n    public PageRequest first() {\n        return new PageRequestImpl(1, getPageSize(), getSort());\n    }\n\n    @Override\n    public PageRequest withPage(int pageNumber) {\n        return new PageRequestImpl(pageNumber, getPageSize(), getSort());\n    }\n\n    @Override\n    public PageRequestImpl withSort(Sort sort) {\n        return new PageRequestImpl(getPageNumber(), getPageSize(),\n            defaultIfNull(sort, Sort.unsorted()));\n    }\n\n    @Override\n    public boolean hasPrevious() {\n        return pageNumber > 1;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/ReactiveExtensionClient.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.Comparator;\nimport java.util.function.Predicate;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.index.IndexedQueryEngine;\n\n/**\n * ExtensionClient is an interface which contains some operations on Extension instead of\n * ExtensionStore.\n *\n * @author johnniang\n */\npublic interface ReactiveExtensionClient {\n\n    /**\n     * Lists Extensions by Extension type, filter and sorter.\n     *\n     * @param type is the class type of Extension.\n     * @param predicate filters the reEnqueue.\n     * @param comparator sorts the reEnqueue.\n     * @param <E> is Extension type.\n     * @return all filtered and sorted Extensions.\n     */\n    <E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator);\n\n    /**\n     * Lists Extensions by Extension type, filter, sorter and page info.\n     *\n     * @param type is the class type of Extension.\n     * @param predicate filters the reEnqueue.\n     * @param comparator sorts the reEnqueue.\n     * @param page is page number which starts from 0.\n     * @param size is page size.\n     * @param <E> is Extension type.\n     * @return a list of Extensions.\n     */\n    @Deprecated\n    <E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator, int page, int size);\n\n    <E extends Extension> Flux<E> listAll(Class<E> type, ListOptions options, Sort sort);\n\n    <E extends Extension> Flux<String> listAllNames(Class<E> type, ListOptions options, Sort sort);\n\n    <E extends Extension> Flux<String> listTopNames(Class<E> type, ListOptions options, Sort sort,\n        int topN);\n\n    <E extends Extension> Mono<ListResult<E>> listBy(Class<E> type, ListOptions options,\n        PageRequest pageable);\n\n    <E extends Extension> Mono<ListResult<String>> listNamesBy(Class<E> type, ListOptions options,\n        PageRequest pageable);\n\n    <E extends Extension> Mono<Long> countBy(Class<E> type, ListOptions options);\n\n    /**\n     * Fetches Extension by its type and name.\n     *\n     * @param type is Extension type.\n     * @param name is Extension name.\n     * @param <E> is Extension type.\n     * @return an optional Extension.\n     */\n    <E extends Extension> Mono<E> fetch(Class<E> type, String name);\n\n    Mono<Unstructured> fetch(GroupVersionKind gvk, String name);\n\n    <E extends Extension> Mono<E> get(Class<E> type, String name);\n\n    @Deprecated(forRemoval = true, since = \"2.23.0\")\n    Mono<JsonExtension> getJsonExtension(GroupVersionKind gvk, String name);\n\n    /**\n     * Creates an Extension.\n     *\n     * @param extension is fresh Extension to be created. Please make sure the Extension name does\n     * not exist.\n     * @param <E> is Extension type.\n     */\n    <E extends Extension> Mono<E> create(E extension);\n\n    /**\n     * Updates an Extension.\n     *\n     * @param extension is an Extension to be updated. Please make sure the resource version is\n     * latest.\n     * @param <E> is Extension type.\n     */\n    <E extends Extension> Mono<E> update(E extension);\n\n    /**\n     * Deletes an Extension.\n     *\n     * @param extension is an Extension to be deleted. Please make sure the resource version is\n     * latest.\n     * @param <E> is Extension type.\n     */\n    <E extends Extension> Mono<E> delete(E extension);\n\n    @Deprecated(forRemoval = true, since = \"2.22.0\")\n    IndexedQueryEngine indexedQueryEngine();\n\n    void watch(Watcher watcher);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Ref.java",
    "content": "package run.halo.app.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.Objects;\nimport lombok.Data;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.lang.NonNull;\n\n@Data\n@Schema(description = \"Extension reference object. The name is mandatory\")\npublic class Ref {\n\n    @Schema(description = \"Extension group\", requiredMode = REQUIRED)\n    private String group;\n\n    @Schema(description = \"Extension version\")\n    @Nullable\n    private String version;\n\n    @Schema(description = \"Extension kind\", requiredMode = REQUIRED)\n    private String kind;\n\n    @Schema(requiredMode = REQUIRED, description = \"Extension name. This field is mandatory\")\n    private String name;\n\n    public static Ref of(String name) {\n        Ref ref = new Ref();\n        ref.setName(name);\n        return ref;\n    }\n\n    public static Ref of(String name, GroupVersionKind gvk) {\n        Ref ref = new Ref();\n        ref.setName(name);\n        ref.setGroup(gvk.group());\n        ref.setVersion(gvk.version());\n        ref.setKind(gvk.kind());\n        return ref;\n    }\n\n    public static Ref of(Extension extension) {\n        var metadata = extension.getMetadata();\n        var gvk = extension.groupVersionKind();\n        var ref = new Ref();\n        ref.setName(metadata.getName());\n        ref.setGroup(gvk.group());\n        ref.setVersion(gvk.version());\n        ref.setKind(gvk.kind());\n        return ref;\n    }\n\n    /**\n     * Check the ref has the same group and kind.\n     *\n     * @param ref is target reference\n     * @param gvk is group version kind\n     * @return true if they have the same group and kind.\n     */\n    public static boolean groupKindEquals(Ref ref, GroupVersionKind gvk) {\n        return Objects.equals(ref.getGroup(), gvk.group())\n            && Objects.equals(ref.getKind(), gvk.kind());\n    }\n\n    /**\n     * Check if the extension is equal to the ref.\n     *\n     * @param ref must not be null.\n     * @param extension must not be null.\n     * @return true if they are equal; false otherwise.\n     */\n    public static boolean equals(@NonNull Ref ref, @NonNull ExtensionOperator extension) {\n        var gvk = extension.groupVersionKind();\n        var name = extension.getMetadata().getName();\n        return groupKindEquals(ref, gvk) && Objects.equals(ref.getName(), name);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Scheme.java",
    "content": "package run.halo.app.extension;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport io.swagger.v3.core.converter.ModelConverters;\nimport io.swagger.v3.core.util.Json;\nimport java.util.Map;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.exception.ExtensionException;\n\n/**\n * This class represents scheme of an Extension.\n *\n * @param type is Extension type.\n * @param groupVersionKind is GroupVersionKind of Extension.\n * @param plural is plural name of Extension.\n * @param singular is singular name of Extension.\n * @param openApiSchema is JSON schema of Extension.\n * @author johnniang\n */\npublic record Scheme(Class<? extends Extension> type,\n                     GroupVersionKind groupVersionKind,\n                     String plural,\n                     String singular,\n                     ObjectNode openApiSchema) {\n    public Scheme {\n        Assert.notNull(type, \"Type of Extension must not be null\");\n        Assert.notNull(groupVersionKind, \"GroupVersionKind of Extension must not be null\");\n        Assert.hasText(plural, \"Plural name of Extension must not be blank\");\n        Assert.hasText(singular, \"Singular name of Extension must not be blank\");\n        Assert.notNull(openApiSchema, \"Json Schema must not be null\");\n    }\n\n    /**\n     * Builds Scheme from type with @GVK annotation.\n     *\n     * @param type is Extension type with GVK annotation.\n     * @return Scheme definition.\n     * @throws ExtensionException when the type has not annotated @GVK.\n     */\n    public static Scheme buildFromType(Class<? extends Extension> type) {\n        // concrete scheme from annotation\n        var gvk = getGvkFromType(type);\n\n        // TODO Move the generation logic outside.\n        // generate OpenAPI schema\n        var resolvedSchema = ModelConverters.getInstance().readAllAsResolvedSchema(type);\n        var mapper = Json.mapper();\n        var schema = (ObjectNode) mapper.valueToTree(resolvedSchema.schema);\n        // for schema validation.\n        schema.set(\"components\",\n            mapper.valueToTree(Map.of(\"schemas\", resolvedSchema.referencedSchemas)));\n\n        return new Scheme(type,\n            new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind()),\n            gvk.plural(),\n            gvk.singular(),\n            // Deep copy for schema to avoid mutation by others.\n            schema.deepCopy());\n    }\n\n    /**\n     * Gets GVK annotation from Extension type.\n     *\n     * @param type is Extension type with GVK annotation.\n     * @return GVK annotation.\n     * @throws ExtensionException when the type has not annotated @GVK.\n     */\n    @NonNull\n    public static GVK getGvkFromType(@NonNull Class<? extends Extension> type) {\n        var gvk = type.getAnnotation(GVK.class);\n        Assert.notNull(gvk,\n            \"Missing annotation \" + GVK.class.getName() + \" on type \" + type.getName());\n        return gvk;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        Scheme scheme = (Scheme) o;\n        return groupVersionKind.equals(scheme.groupVersionKind);\n    }\n\n    @Override\n    public int hashCode() {\n        return groupVersionKind.hashCode();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/SchemeManager.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.function.Consumer;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport run.halo.app.extension.exception.SchemeNotFoundException;\nimport run.halo.app.extension.index.IndexSpecs;\n\npublic interface SchemeManager {\n\n    /**\n     * Registers an Extension using its type.\n     *\n     * @param type is Extension type.\n     * @param <E> Extension class.\n     */\n    default <E extends Extension> void register(Class<E> type) {\n        register(type, null);\n    }\n\n    default void register(Scheme scheme) {\n        register(scheme.type(), null);\n    }\n\n    <E extends Extension> void register(\n        Class<E> type, @Nullable Consumer<IndexSpecs<E>> specsConsumer\n    );\n\n    void unregister(@NonNull Scheme scheme);\n\n    default int size() {\n        return schemes().size();\n    }\n\n    @NonNull\n    List<Scheme> schemes();\n\n    @NonNull\n    default Optional<Scheme> fetch(@NonNull GroupVersionKind gvk) {\n        return schemes().stream()\n            .filter(scheme -> Objects.equals(scheme.groupVersionKind(), gvk))\n            .findFirst();\n    }\n\n    @NonNull\n    default Scheme get(@NonNull GroupVersionKind gvk) {\n        return fetch(gvk).orElseThrow(\n            () -> new SchemeNotFoundException(gvk));\n    }\n\n    @NonNull\n    default Scheme get(Class<? extends Extension> type) {\n        var gvk = Scheme.getGvkFromType(type);\n        return get(new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind()));\n    }\n\n    @NonNull\n    default Scheme get(Extension ext) {\n        var gvk = ext.groupVersionKind();\n        return get(gvk);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Secret.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.Map;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\n/**\n * Secret is a small piece of sensitive data which should be kept secret, such as a password,\n * a token, or a key.\n *\n * @author guqing\n * @see\n * <a href=\"https://github.com/kubernetes/kubernetes/blob/f33498a8256b455b677ad4d30440869318b84204/staging/src/k8s.io/api/core/v1/types.go\">kebernetes Secret</a>\n * @since 2.0.0\n */\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"\", version = \"v1alpha1\", kind = Secret.KIND, plural = \"secrets\", singular = \"secret\")\npublic class Secret extends AbstractExtension {\n    public static final String KIND = \"Secret\";\n\n    public static final String SECRET_TYPE_OPAQUE = \"Opaque\";\n\n    public static final int MAX_SECRET_SIZE = 1024 * 1024;\n\n    /**\n     * Used to facilitate programmatic handling of secret data.\n     * More info:\n     * <a href=\"https://kubernetes.io/docs/concepts/configuration/secret/#secret-types\">secret-types</a>\n     */\n    private String type;\n\n    /**\n     * <p>The total bytes of the values in\n     * the Data field must be less than {@link #MAX_SECRET_SIZE} bytes.</p>\n     * <p>{@code data} contains the secret data. Each key must consist of alphanumeric\n     * characters, '-', '_' or '.'. The serialized form of the secret data is a\n     * base64 encoded string, representing the arbitrary (possibly non-string)\n     * data value here. Described in\n     * <a href=\"https://tools.ietf.org/html/rfc4648#section-4\">rfc4648#section-4</a>\n     * </p>\n     */\n    private Map<String, byte[]> data;\n\n    /**\n     * {@code stringData} allows specifying non-binary secret data in string form.\n     * It is provided as a write-only input field for convenience.\n     * All keys and values are merged into the data field on write, overwriting any existing\n     * values.\n     * The stringData field is never output when reading from the API.\n     */\n    private Map<String, String> stringData;\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Unstructured.java",
    "content": "package run.halo.app.extension;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.MapperFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport io.swagger.v3.core.util.Json;\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.springframework.lang.NonNull;\nimport tools.jackson.core.JacksonException;\nimport tools.jackson.databind.SerializationContext;\nimport tools.jackson.databind.ValueDeserializer;\nimport tools.jackson.databind.ValueSerializer;\n\n/**\n * Unstructured is a generic Extension, which wraps ObjectNode to maintain the Extension data, like\n * apiVersion, kind, metadata and others.\n *\n * @author johnniang\n */\n@JsonSerialize(using = Unstructured.UnstructuredSerializer.class)\n@JsonDeserialize(using = Unstructured.UnstructuredDeserializer.class)\n@tools.jackson.databind.annotation.JsonSerialize(\n    using = Unstructured.UnstructuredValueSerializer.class\n)\n@tools.jackson.databind.annotation.JsonDeserialize(\n    using = Unstructured.UnstructuredValueDeserializer.class\n)\n@SuppressWarnings(\"rawtypes\")\npublic class Unstructured implements Extension {\n\n    @SuppressWarnings(\"deprecation\")\n    public static final ObjectMapper OBJECT_MAPPER = Json.mapper()\n        // We don't want to change the default mapper\n        // so we copy a new one and configure it\n        .copy()\n        .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);\n\n    private final Map data;\n\n    public Unstructured() {\n        this(new HashMap());\n    }\n\n    public Unstructured(Map data) {\n        this.data = data;\n    }\n\n    public Map getData() {\n        return data;\n    }\n\n    @Override\n    public String getApiVersion() {\n        return (String) data.get(\"apiVersion\");\n    }\n\n    @Override\n    public String getKind() {\n        return (String) data.get(\"kind\");\n    }\n\n    @Override\n    public MetadataOperator getMetadata() {\n        return getNestedMap(data, \"metadata\")\n            .map(UnstructuredMetadata::new)\n            .orElse(null);\n    }\n\n    static class UnstructuredMetadata implements MetadataOperator {\n\n        @NonNull\n        private final Map<String, Object> metadata;\n\n        UnstructuredMetadata(@NonNull Map<String, Object> metadata) {\n            this.metadata = metadata;\n        }\n\n        @Override\n        public String getName() {\n            return (String) getNestedValue(metadata, \"name\").orElse(null);\n        }\n\n        @Override\n        public String getGenerateName() {\n            return (String) getNestedValue(metadata, \"generateName\").orElse(null);\n        }\n\n        @Override\n        public Map<String, String> getLabels() {\n            return getNestedStringStringMap(metadata, \"labels\").orElse(null);\n        }\n\n        @Override\n        public Map<String, String> getAnnotations() {\n            return getNestedStringStringMap(metadata, \"annotations\").orElse(null);\n        }\n\n        @Override\n        public Long getVersion() {\n            return getNestedLong(metadata, \"version\").orElse(null);\n        }\n\n        @Override\n        public Instant getCreationTimestamp() {\n            return getNestedInstant(metadata, \"creationTimestamp\").orElse(null);\n        }\n\n        @Override\n        public Instant getDeletionTimestamp() {\n            return getNestedInstant(metadata, \"deletionTimestamp\").orElse(null);\n        }\n\n        @Override\n        public Set<String> getFinalizers() {\n            return getNestedStringSet(metadata, \"finalizers\").orElse(null);\n        }\n\n        @Override\n        public void setName(String name) {\n            setNestedValue(metadata, name, \"name\");\n        }\n\n        @Override\n        public void setGenerateName(String generateName) {\n            setNestedValue(metadata, generateName, \"generateName\");\n        }\n\n        @Override\n        public void setLabels(Map<String, String> labels) {\n            setNestedValue(metadata, labels, \"labels\");\n        }\n\n        @Override\n        public void setAnnotations(Map<String, String> annotations) {\n            setNestedValue(metadata, annotations, \"annotations\");\n        }\n\n        @Override\n        public void setVersion(Long version) {\n            setNestedValue(metadata, version, \"version\");\n        }\n\n        @Override\n        public void setCreationTimestamp(Instant creationTimestamp) {\n            setNestedValue(metadata, creationTimestamp, \"creationTimestamp\");\n        }\n\n        @Override\n        public void setDeletionTimestamp(Instant deletionTimestamp) {\n            setNestedValue(metadata, deletionTimestamp, \"deletionTimestamp\");\n        }\n\n        @Override\n        public void setFinalizers(Set<String> finalizers) {\n            setNestedValue(metadata, finalizers, \"finalizers\");\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (o == null || getClass() != o.getClass()) {\n                return false;\n            }\n            var that = (UnstructuredMetadata) o;\n            return Objects.equals(metadata, that.metadata);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hashCode(metadata);\n        }\n\n    }\n\n    @Override\n    public void setApiVersion(String apiVersion) {\n        setNestedValue(data, apiVersion, \"apiVersion\");\n    }\n\n    @Override\n    public void setKind(String kind) {\n        setNestedValue(data, kind, \"kind\");\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void setMetadata(MetadataOperator metadata) {\n        Map metadataMap = OBJECT_MAPPER.convertValue(metadata, Map.class);\n        data.put(\"metadata\", metadataMap);\n    }\n\n    public static Optional<Object> getNestedValue(Map map, String... fields) {\n        if (fields == null || fields.length == 0) {\n            return Optional.of(map);\n        }\n        Map tempMap = map;\n        for (int i = 0; i < fields.length - 1; i++) {\n            Object value = tempMap.get(fields[i]);\n            if (!(value instanceof Map)) {\n                return Optional.empty();\n            }\n            tempMap = (Map<?, ?>) value;\n        }\n        return Optional.ofNullable(tempMap.get(fields[fields.length - 1]));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Optional<List<String>> getNestedStringList(Map map, String... fields) {\n        return getNestedValue(map, fields).map(value -> (List<String>) value);\n    }\n\n    public static Optional<Set<String>> getNestedStringSet(Map map, String... fields) {\n        return getNestedValue(map, fields).map(value -> {\n            if (value instanceof Collection collection) {\n                return new LinkedHashSet<>(collection);\n            }\n            throw new IllegalArgumentException(\n                \"Incorrect value type: \" + value.getClass() + \", expected: \" + Set.class);\n        });\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static void setNestedValue(Map map, Object value, String... fields) {\n        if (fields == null || fields.length == 0) {\n            // do nothing when no fields provided\n            return;\n        }\n        var prevFields = Arrays.stream(fields, 0, fields.length - 1)\n            .toArray(String[]::new);\n        getNestedMap(map, prevFields).ifPresent(m -> {\n            var lastField = fields[fields.length - 1];\n            m.put(lastField, value);\n        });\n    }\n\n    public static Optional<Map> getNestedMap(Map map, String... fields) {\n        return getNestedValue(map, fields).map(value -> (Map) value);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Optional<Map<String, String>> getNestedStringStringMap(Map map,\n        String... fields) {\n        return getNestedValue(map, fields)\n            .map(labelsObj -> (Map<String, String>) labelsObj);\n    }\n\n    public static Optional<Instant> getNestedInstant(Map map, String... fields) {\n        return getNestedValue(map, fields)\n            .map(instantValue -> {\n                if (instantValue instanceof Instant instant) {\n                    return instant;\n                }\n                return Instant.parse(instantValue.toString());\n            });\n\n    }\n\n    public static Optional<Long> getNestedLong(Map map, String... fields) {\n        return getNestedValue(map, fields)\n            .map(longObj -> {\n                if (longObj instanceof Long l) {\n                    return l;\n                }\n                return Long.valueOf(longObj.toString());\n            });\n    }\n\n    public static class UnstructuredSerializer extends JsonSerializer<Unstructured> {\n\n        @Override\n        public void serialize(Unstructured value, JsonGenerator gen, SerializerProvider serializers)\n            throws IOException {\n            gen.writeObject(value.data);\n        }\n\n    }\n\n    static class UnstructuredValueSerializer extends ValueSerializer<Unstructured> {\n\n        @Override\n        public void serialize(\n            Unstructured value, tools.jackson.core.JsonGenerator gen, SerializationContext ctxt)\n            throws JacksonException {\n            gen.writePOJO(value.data);\n        }\n\n        @Override\n        public Class<?> handledType() {\n            return Unstructured.class;\n        }\n    }\n\n    static class UnstructuredValueDeserializer extends ValueDeserializer<Unstructured> {\n\n        @Override\n        public Unstructured deserialize(tools.jackson.core.JsonParser p,\n            tools.jackson.databind.DeserializationContext ctxt) throws JacksonException {\n            var map = p.readValueAs(Map.class);\n            return new Unstructured(map);\n        }\n    }\n\n    public static class UnstructuredDeserializer extends JsonDeserializer<Unstructured> {\n\n        @Override\n        public Unstructured deserialize(JsonParser p, DeserializationContext ctxt)\n            throws IOException {\n            Map data = p.getCodec().readValue(p, Map.class);\n            return new Unstructured(data);\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        Unstructured that = (Unstructured) o;\n        return Objects.equals(data, that.data);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(data);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/Watcher.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport reactor.core.Disposable;\nimport run.halo.app.extension.controller.Reconciler;\n\npublic interface Watcher extends Disposable {\n\n    default void onAdd(Reconciler.Request request) {\n        // Do nothing here, just for sync all on start.\n    }\n\n    default void onAdd(Extension extension) {\n        // Do nothing here\n    }\n\n    default void onUpdate(Extension oldExtension, Extension newExtension) {\n        // Do nothing here\n    }\n\n    default void onDelete(Extension extension) {\n        // Do nothing here\n    }\n\n    default void registerDisposeHook(Runnable dispose) {\n    }\n\n    class WatcherComposite implements Watcher {\n\n        private final List<Watcher> watchers;\n\n        private volatile boolean disposed = false;\n\n        private Runnable disposeHook;\n\n        public WatcherComposite() {\n            watchers = new CopyOnWriteArrayList<>();\n        }\n\n        @Override\n        public void onAdd(Extension extension) {\n            // TODO Deep copy extension and execute onAdd asynchronously\n            watchers.forEach(watcher -> watcher.onAdd(extension));\n        }\n\n        @Override\n        public void onUpdate(Extension oldExtension, Extension newExtension) {\n            // TODO Deep copy extension and execute onUpdate asynchronously\n            watchers.forEach(watcher -> watcher.onUpdate(oldExtension, newExtension));\n        }\n\n        @Override\n        public void onDelete(Extension extension) {\n            // TODO Deep copy extension and execute onDelete asynchronously\n            watchers.forEach(watcher -> watcher.onDelete(extension));\n        }\n\n        public void addWatcher(Watcher watcher) {\n            if (!watcher.isDisposed() && !watchers.contains(watcher)) {\n                watchers.add(watcher);\n                watcher.registerDisposeHook(() -> removeWatcher(watcher));\n            }\n        }\n\n        public void removeWatcher(Watcher watcher) {\n            watchers.remove(watcher);\n        }\n\n        @Override\n        public void registerDisposeHook(Runnable dispose) {\n            this.disposeHook = dispose;\n        }\n\n        @Override\n        public void dispose() {\n            this.disposed = true;\n            this.watchers.clear();\n            if (this.disposeHook != null) {\n                this.disposeHook.run();\n            }\n        }\n\n        @Override\n        public boolean isDisposed() {\n            return this.disposed;\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/WatcherExtensionMatchers.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.Objects;\nimport lombok.Builder;\nimport lombok.Getter;\nimport org.springframework.util.Assert;\n\npublic class WatcherExtensionMatchers {\n    @Getter\n    private final ExtensionClient client;\n    private final GroupVersionKind gvk;\n    private final ExtensionMatcher onAddMatcher;\n    private final ExtensionMatcher onUpdateMatcher;\n    private final ExtensionMatcher onDeleteMatcher;\n\n    /**\n     * Constructs a new {@link WatcherExtensionMatchers} with the given\n     * {@link DefaultExtensionMatcher}.\n     */\n    @Builder(builderMethodName = \"internalBuilder\")\n    public WatcherExtensionMatchers(ExtensionClient client,\n        GroupVersionKind gvk, ExtensionMatcher onAddMatcher,\n        ExtensionMatcher onUpdateMatcher, ExtensionMatcher onDeleteMatcher) {\n        Assert.notNull(client, \"The client must not be null.\");\n        Assert.notNull(gvk, \"The gvk must not be null.\");\n        this.client = client;\n        this.gvk = gvk;\n        this.onAddMatcher =\n            Objects.requireNonNullElseGet(onAddMatcher, () -> emptyMatcher(client, gvk));\n        this.onUpdateMatcher =\n            Objects.requireNonNullElseGet(onUpdateMatcher, () -> emptyMatcher(client, gvk));\n        this.onDeleteMatcher =\n            Objects.requireNonNullElseGet(onDeleteMatcher, () -> emptyMatcher(client, gvk));\n    }\n\n    public GroupVersionKind getGroupVersionKind() {\n        return this.gvk;\n    }\n\n    public ExtensionMatcher onAddMatcher() {\n        return delegateExtensionMatcher(this.onAddMatcher);\n    }\n\n    public ExtensionMatcher onUpdateMatcher() {\n        return delegateExtensionMatcher(this.onUpdateMatcher);\n    }\n\n    public ExtensionMatcher onDeleteMatcher() {\n        return delegateExtensionMatcher(this.onDeleteMatcher);\n    }\n\n    public static WatcherExtensionMatchersBuilder builder(ExtensionClient client,\n        GroupVersionKind gvk) {\n        return internalBuilder().gvk(gvk).client(client);\n    }\n\n    static ExtensionMatcher emptyMatcher(ExtensionClient client,\n        GroupVersionKind gvk) {\n        return DefaultExtensionMatcher.builder(client, gvk).build();\n    }\n\n    ExtensionMatcher delegateExtensionMatcher(ExtensionMatcher matcher) {\n        return extension -> extension.groupVersionKind().equals(gvk) && matcher.match(extension);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/WatcherPredicates.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.function.BiPredicate;\nimport java.util.function.Predicate;\n\npublic class WatcherPredicates {\n\n    static final Predicate<Extension> EMPTY_PREDICATE = (e) -> true;\n\n    static final BiPredicate<Extension, Extension> EMPTY_BI_PREDICATE = (oldExt, newExt) -> true;\n    private final Predicate<Extension> onAddPredicate;\n    private final BiPredicate<Extension, Extension> onUpdatePredicate;\n    private final Predicate<Extension> onDeletePredicate;\n\n    public WatcherPredicates(Predicate<Extension> onAddPredicate,\n        BiPredicate<Extension, Extension> onUpdatePredicate,\n        Predicate<Extension> onDeletePredicate) {\n        this.onAddPredicate = onAddPredicate;\n        this.onUpdatePredicate = onUpdatePredicate;\n        this.onDeletePredicate = onDeletePredicate;\n    }\n\n    public Predicate<Extension> onAddPredicate() {\n        if (onAddPredicate == null) {\n            return EMPTY_PREDICATE;\n        }\n        return onAddPredicate;\n    }\n\n    public BiPredicate<Extension, Extension> onUpdatePredicate() {\n        if (onUpdatePredicate == null) {\n            return EMPTY_BI_PREDICATE;\n        }\n        return onUpdatePredicate;\n    }\n\n    public Predicate<Extension> onDeletePredicate() {\n        if (onDeletePredicate == null) {\n            return EMPTY_PREDICATE;\n        }\n        return onDeletePredicate;\n    }\n\n    public static final class Builder {\n\n        private Predicate<Extension> onAddPredicate;\n        private BiPredicate<Extension, Extension> onUpdatePredicate;\n        private Predicate<Extension> onDeletePredicate;\n\n        private GroupVersionKind gvk;\n\n        public Builder withGroupVersionKind(GroupVersionKind gvk) {\n            this.gvk = gvk;\n            return this;\n        }\n\n        public Builder onAddPredicate(Predicate<Extension> onAddPredicate) {\n            this.onAddPredicate = onAddPredicate;\n            return this;\n        }\n\n        public Builder onUpdatePredicate(\n            BiPredicate<Extension, Extension> onUpdatePredicate) {\n            this.onUpdatePredicate = onUpdatePredicate;\n            return this;\n        }\n\n        public Builder onDeletePredicate(Predicate<Extension> onDeletePredicate) {\n            this.onDeletePredicate = onDeletePredicate;\n            return this;\n        }\n\n        public WatcherPredicates build() {\n            Predicate<Extension> gvkPredicate = EMPTY_PREDICATE;\n            BiPredicate<Extension, Extension> gvkBiPredicate = EMPTY_BI_PREDICATE;\n            if (gvk != null) {\n                gvkPredicate = e -> gvk.equals(e.groupVersionKind());\n                gvkBiPredicate = (oldE, newE) -> oldE.groupVersionKind().equals(gvk)\n                    && newE.groupVersionKind().equals(gvk);\n            }\n            if (onAddPredicate == null) {\n                onAddPredicate = EMPTY_PREDICATE;\n            }\n            if (onUpdatePredicate == null) {\n                onUpdatePredicate = EMPTY_BI_PREDICATE;\n            }\n            if (onDeletePredicate == null) {\n                onDeletePredicate = EMPTY_PREDICATE;\n            }\n\n            onAddPredicate = gvkPredicate.and(onAddPredicate);\n            onUpdatePredicate = gvkBiPredicate.and(onUpdatePredicate);\n            onDeletePredicate = gvkPredicate.and(onDeletePredicate);\n\n            return new WatcherPredicates(onAddPredicate, onUpdatePredicate, onDeletePredicate);\n        }\n\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/Controller.java",
    "content": "package run.halo.app.extension.controller;\n\nimport reactor.core.Disposable;\n\npublic interface Controller extends Disposable {\n\n    String getName();\n\n    void start();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/ControllerBuilder.java",
    "content": "package run.halo.app.extension.controller;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.function.Supplier;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionMatcher;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.WatcherExtensionMatchers;\nimport run.halo.app.extension.controller.Reconciler.Request;\n\npublic class ControllerBuilder {\n\n    private final String name;\n\n    private Duration minDelay;\n\n    private Duration maxDelay;\n\n    private final Reconciler<Request> reconciler;\n\n    private Supplier<Instant> nowSupplier;\n\n    private Extension extension;\n\n    private ExtensionMatcher onAddMatcher;\n\n    private ExtensionMatcher onDeleteMatcher;\n\n    private ExtensionMatcher onUpdateMatcher;\n\n    private ListOptions syncAllListOptions;\n\n    private final ExtensionClient client;\n\n    private boolean syncAllOnStart = true;\n\n    private int workerCount = 1;\n\n    public ControllerBuilder(Reconciler<Request> reconciler, ExtensionClient client) {\n        Assert.notNull(reconciler, \"Reconciler must not be null\");\n        Assert.notNull(client, \"Extension client must not be null\");\n        this.name = reconciler.getClass().getName();\n        this.reconciler = reconciler;\n        this.client = client;\n    }\n\n    public ControllerBuilder minDelay(Duration minDelay) {\n        this.minDelay = minDelay;\n        return this;\n    }\n\n    public ControllerBuilder maxDelay(Duration maxDelay) {\n        this.maxDelay = maxDelay;\n        return this;\n    }\n\n    public ControllerBuilder nowSupplier(Supplier<Instant> nowSupplier) {\n        this.nowSupplier = nowSupplier;\n        return this;\n    }\n\n    public ControllerBuilder extension(Extension extension) {\n        this.extension = extension;\n        return this;\n    }\n\n    public ControllerBuilder onAddMatcher(ExtensionMatcher onAddMatcher) {\n        this.onAddMatcher = onAddMatcher;\n        return this;\n    }\n\n    public ControllerBuilder onDeleteMatcher(ExtensionMatcher onDeleteMatcher) {\n        this.onDeleteMatcher = onDeleteMatcher;\n        return this;\n    }\n\n    public ControllerBuilder onUpdateMatcher(ExtensionMatcher extensionMatcher) {\n        this.onUpdateMatcher = extensionMatcher;\n        return this;\n    }\n\n    public ControllerBuilder syncAllOnStart(boolean syncAllAtStart) {\n        this.syncAllOnStart = syncAllAtStart;\n        return this;\n    }\n\n    public ControllerBuilder syncAllListOptions(ListOptions syncAllListOptions) {\n        this.syncAllListOptions = syncAllListOptions;\n        return this;\n    }\n\n    public ControllerBuilder workerCount(int workerCount) {\n        this.workerCount = workerCount;\n        return this;\n    }\n\n    public Controller build() {\n        if (nowSupplier == null) {\n            nowSupplier = Instant::now;\n        }\n        if (minDelay == null || minDelay.isNegative() || minDelay.isZero()) {\n            minDelay = Duration.ofMillis(5);\n        }\n        if (maxDelay == null || maxDelay.isNegative() || maxDelay.isZero()) {\n            maxDelay = Duration.ofSeconds(1000);\n        }\n        Assert.isTrue(minDelay.compareTo(maxDelay) <= 0,\n            \"Min delay must be less than or equal to max delay\");\n        Assert.notNull(extension, \"Extension must not be null\");\n        Assert.notNull(reconciler, \"Reconciler must not be null\");\n\n        var queue = new DefaultQueue<Request>(nowSupplier, minDelay);\n        var extensionMatchers = WatcherExtensionMatchers.builder(client,\n                extension.groupVersionKind())\n            .onAddMatcher(onAddMatcher)\n            .onUpdateMatcher(onUpdateMatcher)\n            .onDeleteMatcher(onDeleteMatcher)\n            .build();\n        var watcher = new ExtensionWatcher(queue, extensionMatchers);\n        var synchronizer = new RequestSynchronizer(syncAllOnStart,\n            client,\n            extension,\n            watcher,\n            determineSyncAllListOptions());\n        return new DefaultController<>(name, reconciler, queue, synchronizer, minDelay, maxDelay,\n            workerCount);\n    }\n\n    ListOptions determineSyncAllListOptions() {\n        if (syncAllListOptions != null) {\n            return syncAllListOptions;\n        }\n        return new ListOptions();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/DefaultController.java",
    "content": "package run.halo.app.extension.controller;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.function.Supplier;\nimport java.util.stream.IntStream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.Assert;\nimport org.springframework.util.StopWatch;\nimport run.halo.app.extension.controller.RequestQueue.DelayedEntry;\n\n@Slf4j\npublic class DefaultController<R> implements Controller {\n\n    private final String name;\n\n    private final Reconciler<R> reconciler;\n\n    private final Supplier<Instant> nowSupplier;\n\n    private final RequestQueue<R> queue;\n\n    private volatile boolean disposed = false;\n\n    private volatile boolean started = false;\n\n    private final Executor executor;\n\n    @Nullable\n    private final Synchronizer<R> synchronizer;\n\n    private final Duration minDelay;\n\n    private final Duration maxDelay;\n\n    private final int workerCount;\n\n    private final AtomicLong workerCounter;\n\n    public DefaultController(String name,\n        Reconciler<R> reconciler,\n        RequestQueue<R> queue,\n        Synchronizer<R> synchronizer,\n        Supplier<Instant> nowSupplier,\n        Duration minDelay,\n        Duration maxDelay,\n        ExecutorService executor, int workerCount) {\n        this(name, reconciler, queue, synchronizer, nowSupplier, minDelay, maxDelay,\n            (Executor) executor, workerCount);\n    }\n\n    public DefaultController(String name,\n        Reconciler<R> reconciler,\n        RequestQueue<R> queue,\n        Synchronizer<R> synchronizer,\n        Supplier<Instant> nowSupplier,\n        Duration minDelay,\n        Duration maxDelay,\n        Executor executor, int workerCount) {\n        Assert.isTrue(workerCount > 0, \"Worker count must not be less than 1\");\n        this.name = name;\n        this.reconciler = reconciler;\n        this.nowSupplier = nowSupplier;\n        this.queue = queue;\n        this.synchronizer = synchronizer;\n        this.minDelay = minDelay;\n        this.maxDelay = maxDelay;\n        this.executor = executor;\n        this.workerCount = workerCount;\n        this.workerCounter = new AtomicLong();\n    }\n\n    public DefaultController(String name,\n        Reconciler<R> reconciler,\n        RequestQueue<R> queue,\n        Synchronizer<R> synchronizer,\n        Duration minDelay,\n        Duration maxDelay) {\n        this(name, reconciler, queue, synchronizer, Instant::now, minDelay, maxDelay, 1);\n    }\n\n    public DefaultController(String name,\n        Reconciler<R> reconciler,\n        RequestQueue<R> queue,\n        Synchronizer<R> synchronizer,\n        Duration minDelay,\n        Duration maxDelay, int workerCount) {\n        this(name, reconciler, queue, synchronizer, Instant::now, minDelay, maxDelay, workerCount);\n    }\n\n    public DefaultController(String name,\n        Reconciler<R> reconciler,\n        RequestQueue<R> queue,\n        Synchronizer<R> synchronizer,\n        Supplier<Instant> nowSupplier,\n        Duration minDelay,\n        Duration maxDelay, int workerCount) {\n        this(name, reconciler, queue, synchronizer, nowSupplier, minDelay, maxDelay,\n            executor(name), workerCount);\n    }\n\n    private static Executor executor(String name) {\n        return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()\n            .name(name, 1)\n            .uncaughtExceptionHandler(\n                (t, e) -> log.error(\"Uncaught exception in thread: {}\", t.getName(), e)\n            )\n            .factory()\n        );\n    }\n\n    @Override\n    public String getName() {\n        return name;\n    }\n\n    public int getWorkerCount() {\n        return workerCount;\n    }\n\n    @Override\n    public void start() {\n        if (isStarted() || isDisposed()) {\n            log.warn(\"Controller {} is already started or disposed.\", getName());\n            return;\n        }\n        this.started = true;\n        if (synchronizer != null) {\n            executor.execute(synchronizer::start);\n        }\n        log.info(\"Starting controller {}\", name);\n        IntStream.range(0, getWorkerCount())\n            .mapToObj(i -> new Worker())\n            .forEach(executor::execute);\n    }\n\n    /**\n     * Worker for controller.\n     *\n     * @author johnniang\n     */\n    class Worker implements Runnable {\n\n        private final String name;\n\n        Worker() {\n            this.name =\n                DefaultController.this.getName() + \"-worker-\" + workerCounter.incrementAndGet();\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        @Override\n        public void run() {\n            log.info(\"Controller worker {} started\", this.name);\n            while (!isDisposed() && !Thread.currentThread().isInterrupted()) {\n                try {\n                    var entry = queue.take();\n                    Reconciler.Result result;\n                    try {\n                        log.debug(\"{} >>> Reconciling request {} at {}\", this.name,\n                            entry.getEntry(),\n                            nowSupplier.get());\n                        var watch = new StopWatch(this.name + \":reconcile: \" + entry.getEntry());\n                        watch.start(\"reconciliation\");\n                        result = reconciler.reconcile(entry.getEntry());\n                        watch.stop();\n                        log.debug(\"{} >>> Reconciled request: {} with result: {}, usage: {}\",\n                            this.name, entry.getEntry(), result, watch.getTotalTimeMillis());\n                    } catch (Throwable t) {\n                        result = new Reconciler.Result(true, null);\n                        if (t instanceof OptimisticLockingFailureException) {\n                            log.warn(\"Optimistic locking failure when reconciling request: {}/{}\",\n                                this.name, entry.getEntry());\n                        } else if (t instanceof RequeueException re) {\n                            log.warn(\"{}: Requeue {} due to {}\",\n                                this.name, entry.getEntry(), re.getMessage());\n                            result = re.getResult();\n                        } else {\n                            log.error(\"Reconciler in \" + this.name\n                                    + \" aborted with an error, re-enqueuing...\",\n                                t);\n                        }\n                    } finally {\n                        queue.done(entry.getEntry());\n                    }\n                    if (result == null) {\n                        result = new Reconciler.Result(false, null);\n                    }\n                    if (!result.reEnqueue()) {\n                        continue;\n                    }\n                    var retryAfter = result.retryAfter();\n                    if (retryAfter == null) {\n                        retryAfter = entry.getRetryAfter();\n                        if (retryAfter == null\n                            || retryAfter.isNegative()\n                            || retryAfter.isZero()\n                            || retryAfter.compareTo(minDelay) < 0) {\n                            // set min retry after\n                            retryAfter = minDelay;\n                        } else {\n                            try {\n                                // TODO Refactor the retryAfter with ratelimiter\n                                retryAfter = retryAfter.multipliedBy(2);\n                            } catch (ArithmeticException e) {\n                                retryAfter = maxDelay;\n                            }\n                        }\n                        if (retryAfter.compareTo(maxDelay) > 0) {\n                            retryAfter = maxDelay;\n                        }\n                    }\n                    queue.add(\n                        new DelayedEntry<>(entry.getEntry(), retryAfter, nowSupplier));\n                } catch (InterruptedException e) {\n                    Thread.currentThread().interrupt();\n                    log.info(\"Controller worker {} interrupted\", name);\n                }\n            }\n            log.info(\"Controller worker {} is stopped\", name);\n        }\n    }\n\n    @Override\n    public void dispose() {\n        disposed = true;\n        log.info(\"Disposing controller {}\", name);\n\n        if (synchronizer != null) {\n            synchronizer.dispose();\n        }\n\n        try {\n            // we have to check if the executor is an instance of ExecutorService at first.\n            // Because ExecutorService extends AutoCloseable interface in Java 21\n            if (executor instanceof ExecutorService executorService) {\n                closeExecutorService(executorService);\n            } else if (executor instanceof AutoCloseable closeable) {\n                closeable.close();\n                if (Thread.currentThread().isInterrupted()) {\n                    log.warn(\"Wait timeout for controller {} shutdown\", name);\n                } else {\n                    log.info(\"Controller {} is disposed\", name);\n                }\n            }\n        } catch (Exception e) {\n            log.warn(\"Interrupted while waiting for controller {} shutdown\", name);\n        } finally {\n            queue.dispose();\n        }\n    }\n\n    @Override\n    public boolean isDisposed() {\n        return disposed;\n    }\n\n    public boolean isStarted() {\n        return started;\n    }\n\n    /**\n     * Close executor service.\n     *\n     * @param executorService executor service to be closed\n     */\n    private static void closeExecutorService(ExecutorService executorService) {\n        boolean terminated = executorService.isTerminated();\n        if (!terminated) {\n            // Interrupt all running tasks first because of while loop waiting\n            executorService.shutdownNow();\n            var interrupted = false;\n            while (!terminated) {\n                try {\n                    terminated = executorService.awaitTermination(1L, TimeUnit.SECONDS);\n                } catch (InterruptedException ignored) {\n                    interrupted = true;\n                }\n                if (!terminated) {\n                    executorService.shutdown();\n                }\n            }\n            if (interrupted) {\n                Thread.currentThread().interrupt();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/DefaultQueue.java",
    "content": "package run.halo.app.extension.controller;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.DelayQueue;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.function.Supplier;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class DefaultQueue<R> implements RequestQueue<R> {\n\n    private final Lock lock;\n\n    private final DelayQueue<DelayedEntry<R>> queue;\n\n    private final Supplier<Instant> nowSupplier;\n\n    private volatile boolean disposed = false;\n\n    private final Duration minDelay;\n\n    private final Set<R> processing;\n\n    private final Set<R> dirty;\n\n    public DefaultQueue(Supplier<Instant> nowSupplier) {\n        this(nowSupplier, Duration.ZERO);\n    }\n\n    public DefaultQueue(Supplier<Instant> nowSupplier, Duration minDelay) {\n        this.lock = new ReentrantLock();\n        this.nowSupplier = nowSupplier;\n        this.minDelay = minDelay;\n        this.processing = new HashSet<>();\n        this.dirty = new HashSet<>();\n        this.queue = new DelayQueue<>();\n    }\n\n    @Override\n    public boolean addImmediately(R request) {\n        log.debug(\"Adding request {} immediately\", request);\n        var delayedEntry = new DelayedEntry<>(request, minDelay, nowSupplier);\n        return add(delayedEntry);\n    }\n\n    @Override\n    public boolean add(DelayedEntry<R> entry) {\n        lock.lock();\n        try {\n            if (isDisposed()) {\n                return false;\n            }\n            log.debug(\"Adding request {} after {}\", entry.getEntry(), entry.getRetryAfter());\n            if (entry.getRetryAfter().compareTo(minDelay) < 0) {\n                log.warn(\"Request {} will be retried after {} ms, but minimum delay is {} ms\",\n                    entry.getEntry(), entry.getRetryAfter().toMillis(), minDelay.toMillis());\n                entry = new DelayedEntry<>(entry.getEntry(), minDelay, nowSupplier);\n            }\n            if (dirty.contains(entry.getEntry())) {\n                var oldEntry = findOldEntry(entry);\n                if (oldEntry.isEmpty()) {\n                    return false;\n                }\n                var oldReadyAt = oldEntry.get().getReadyAt();\n                var readyAt = entry.getReadyAt();\n                if (!readyAt.isBefore(oldReadyAt)) {\n                    return false;\n                }\n            }\n            dirty.add(entry.getEntry());\n            if (processing.contains(entry.getEntry())) {\n                return false;\n            }\n\n            boolean added = queue.add(entry);\n            log.debug(\"Added request {} after {}\", entry.getEntry(), entry.getRetryAfter());\n            return added;\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public DelayedEntry<R> take() throws InterruptedException {\n        var entry = queue.take();\n        log.debug(\"Take request {} at {}\", entry.getEntry(), Instant.now());\n        lock.lockInterruptibly();\n        try {\n            if (isDisposed()) {\n                throw new InterruptedException(\n                    \"Queue has been disposed. Cannot take any elements now\");\n            }\n            processing.add(entry.getEntry());\n            dirty.remove(entry.getEntry());\n            return entry;\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public void done(R request) {\n        lock.lock();\n        try {\n            if (isDisposed()) {\n                return;\n            }\n            processing.remove(request);\n            if (dirty.contains(request)) {\n                queue.add(new DelayedEntry<>(request, minDelay, nowSupplier));\n            }\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public long size() {\n        return queue.size();\n    }\n\n    @Override\n    public DelayedEntry<R> peek() {\n        return queue.peek();\n    }\n\n    @Override\n    public void dispose() {\n        lock.lock();\n        try {\n            disposed = true;\n            queue.clear();\n            processing.clear();\n            dirty.clear();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public boolean isDisposed() {\n        return this.disposed;\n    }\n\n    private Optional<DelayedEntry<R>> findOldEntry(DelayedEntry<R> entry) {\n        for (DelayedEntry<R> element : queue) {\n            if (element.equals(entry)) {\n                return Optional.of(element);\n            }\n        }\n        return Optional.empty();\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/ExtensionWatcher.java",
    "content": "package run.halo.app.extension.controller;\n\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.Watcher;\nimport run.halo.app.extension.WatcherExtensionMatchers;\nimport run.halo.app.extension.controller.Reconciler.Request;\n\npublic class ExtensionWatcher implements Watcher {\n\n    private final RequestQueue<Request> queue;\n\n    private volatile boolean disposed = false;\n\n    private Runnable disposeHook;\n\n    private final WatcherExtensionMatchers matchers;\n\n    public ExtensionWatcher(RequestQueue<Request> queue, WatcherExtensionMatchers matchers) {\n        this.queue = queue;\n        this.matchers = matchers;\n    }\n\n    @Override\n    public void onAdd(Request request) {\n        if (isDisposed()) {\n            return;\n        }\n        queue.addImmediately(request);\n    }\n\n    @Override\n    public void onAdd(Extension extension) {\n        if (isDisposed() || !matchers.onAddMatcher().match(extension)) {\n            return;\n        }\n        // TODO filter the event\n        queue.addImmediately(new Request(extension.getMetadata().getName()));\n    }\n\n    @Override\n    public void onUpdate(Extension oldExtension, Extension newExtension) {\n        if (isDisposed() || !matchers.onUpdateMatcher().match(newExtension)) {\n            return;\n        }\n        // TODO filter the event\n        queue.addImmediately(new Request(newExtension.getMetadata().getName()));\n    }\n\n    @Override\n    public void onDelete(Extension extension) {\n        if (isDisposed() || !matchers.onDeleteMatcher().match(extension)) {\n            return;\n        }\n        // TODO filter the event\n        queue.addImmediately(new Request(extension.getMetadata().getName()));\n    }\n\n    @Override\n    public void registerDisposeHook(Runnable dispose) {\n        this.disposeHook = dispose;\n    }\n\n    @Override\n    public void dispose() {\n        disposed = true;\n        if (this.disposeHook != null) {\n            this.disposeHook.run();\n        }\n    }\n\n    @Override\n    public boolean isDisposed() {\n        return this.disposed;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/Reconciler.java",
    "content": "package run.halo.app.extension.controller;\n\nimport java.time.Duration;\n\npublic interface Reconciler<R> {\n\n    Result reconcile(R request);\n\n    Controller setupWith(ControllerBuilder builder);\n\n    record Request(String name) {\n    }\n\n    record Result(boolean reEnqueue, Duration retryAfter) {\n\n        public static Result doNotRetry() {\n            return new Result(false, null);\n        }\n\n        public static Result requeue(Duration retryAfter) {\n            return new Result(true, retryAfter);\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/RequestQueue.java",
    "content": "package run.halo.app.extension.controller;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Objects;\nimport java.util.concurrent.Delayed;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Supplier;\nimport reactor.core.Disposable;\n\npublic interface RequestQueue<E> extends Disposable {\n\n    boolean addImmediately(E request);\n\n    boolean add(DelayedEntry<E> entry);\n\n    DelayedEntry<E> take() throws InterruptedException;\n\n    void done(E request);\n\n    long size();\n\n    DelayedEntry<E> peek();\n\n    class DelayedEntry<E> implements Delayed {\n\n        private final E entry;\n\n        private final Instant readyAt;\n\n        private final Supplier<Instant> nowSupplier;\n\n        private final Duration retryAfter;\n\n        DelayedEntry(E entry, Duration retryAfter, Supplier<Instant> nowSupplier) {\n            this.entry = entry;\n            this.readyAt = nowSupplier.get().plusMillis(retryAfter.toMillis());\n            this.nowSupplier = nowSupplier;\n            this.retryAfter = retryAfter;\n        }\n\n        public DelayedEntry(E entry, Instant readyAt, Supplier<Instant> nowSupplier) {\n            this.entry = entry;\n            this.readyAt = readyAt;\n            this.nowSupplier = nowSupplier;\n            this.retryAfter = Duration.between(nowSupplier.get(), readyAt);\n        }\n\n        @Override\n        public long getDelay(TimeUnit unit) {\n            Duration diff = Duration.between(nowSupplier.get(), readyAt);\n            return unit.convert(diff);\n        }\n\n        public Duration getRetryAfter() {\n            return retryAfter;\n        }\n\n        public Instant getReadyAt() {\n            return readyAt;\n        }\n\n        @Override\n        public int compareTo(Delayed o) {\n            return Long.compare(getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));\n        }\n\n        public E getEntry() {\n            return entry;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) {\n                return true;\n            }\n            if (o == null || getClass() != o.getClass()) {\n                return false;\n            }\n            DelayedEntry<?> that = (DelayedEntry<?>) o;\n            return Objects.equals(entry, that.entry);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(entry);\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/RequestSynchronizer.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.springframework.data.domain.Sort.Direction.ASC;\n\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Watcher;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.extension.index.query.Queries;\n\n@Slf4j\npublic class RequestSynchronizer implements Synchronizer<Request> {\n\n    /**\n     * Default batch size for listing Extension names.\n     */\n    private static final Integer DEFAULT_BATCH_SIZE = 100;\n\n    private final ExtensionClient client;\n\n    private final Class<? extends Extension> type;\n\n    private final boolean syncAllOnStart;\n\n    private volatile boolean disposed = false;\n\n    private final Watcher watcher;\n\n    private final ListOptions listOptions;\n\n    @Getter\n    private volatile boolean started = false;\n\n    public RequestSynchronizer(boolean syncAllOnStart,\n        ExtensionClient client,\n        Extension extension,\n        Watcher watcher,\n        ListOptions listOptions) {\n        this.syncAllOnStart = syncAllOnStart;\n        this.client = client;\n        this.type = extension.getClass();\n        this.watcher = watcher;\n        this.listOptions = listOptions;\n    }\n\n    @Override\n    public void start() {\n        if (isDisposed() || started) {\n            return;\n        }\n        log.info(\"Starting request({}) synchronizer...\", type);\n        started = true;\n\n        if (syncAllOnStart) {\n            // list all in batch\n            int batchSize = DEFAULT_BATCH_SIZE;\n            var sort = Sort.by(ASC, \"metadata.name\");\n            // get the first batch to determine the current name\n            var names = client.listTopNames(type, listOptions, sort, batchSize);\n            names.forEach(name -> watcher.onAdd(new Request(name)));\n            while (names.size() == batchSize) {\n                var lastName = names.getLast();\n                var augmentedOptions = ListOptions.builder(listOptions)\n                    .andQuery(Queries.greaterThan(\"metadata.name\", lastName))\n                    .build();\n                names = client.listTopNames(type, augmentedOptions, sort, batchSize);\n                names.forEach(name -> watcher.onAdd(new Request(name)));\n            }\n        }\n        client.watch(this.watcher);\n        log.info(\"Started request({}) synchronizer.\", type);\n    }\n\n    @Override\n    public void dispose() {\n        disposed = true;\n        watcher.dispose();\n    }\n\n    @Override\n    public boolean isDisposed() {\n        return this.disposed;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/RequeueException.java",
    "content": "package run.halo.app.extension.controller;\n\nimport run.halo.app.extension.controller.Reconciler.Result;\n\n\n/**\n * Requeue with result data after throwing this exception.\n *\n * @author johnniang\n */\npublic class RequeueException extends RuntimeException {\n\n    private final Result result;\n\n    public RequeueException(Result result) {\n        this(result, null);\n    }\n\n    public RequeueException(Result result, String reason) {\n        this(result, reason, null);\n    }\n\n    public RequeueException(Result result, String reason, Throwable t) {\n        super(reason, t);\n        this.result = result;\n    }\n\n    public Result getResult() {\n        return result;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/controller/Synchronizer.java",
    "content": "package run.halo.app.extension.controller;\n\nimport reactor.core.Disposable;\n\npublic interface Synchronizer<R> extends Disposable {\n\n    void start();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/exception/ExtensionException.java",
    "content": "package run.halo.app.extension.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.HttpStatusCode;\nimport org.springframework.web.server.ResponseStatusException;\n\n/**\n * ExtensionException is the superclass of those exceptions that can be thrown by Extension module.\n *\n * @author johnniang\n */\npublic class ExtensionException extends ResponseStatusException {\n\n    public ExtensionException(String reason) {\n        this(reason, null);\n    }\n\n    public ExtensionException(String reason, Throwable cause) {\n        this(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause, null, new Object[] {reason});\n    }\n\n    protected ExtensionException(HttpStatusCode status, String reason, Throwable cause,\n        String messageDetailCode, Object[] messageDetailArguments) {\n        super(status, reason, cause, messageDetailCode, messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/exception/NotImplementedException.java",
    "content": "package run.halo.app.extension.exception;\n\n/**\n * Exception thrown to indicate that the requested operation is not implemented.\n *\n * @author johnniang\n */\npublic class NotImplementedException extends UnsupportedOperationException {\n\n    public NotImplementedException() {\n        this(\"Not implemented\");\n    }\n\n    public NotImplementedException(String message) {\n        super(message);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/exception/SchemeNotFoundException.java",
    "content": "package run.halo.app.extension.exception;\n\nimport org.springframework.http.HttpStatus;\nimport run.halo.app.extension.GroupVersionKind;\n\n/**\n * SchemeNotFoundException is thrown while we try to get a scheme but not found.\n *\n * @author johnniang\n */\npublic class SchemeNotFoundException extends ExtensionException {\n\n    public SchemeNotFoundException(GroupVersionKind gvk) {\n        super(HttpStatus.INTERNAL_SERVER_ERROR, \"Scheme not found for \" + gvk, null, null,\n            new Object[] {gvk});\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/AbstractValueIndexSpecBuilder.java",
    "content": "package run.halo.app.extension.index;\n\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.Extension;\n\n/**\n * Abstract base implementation of {@link IndexSpecBuilder} for value indexes.\n *\n * @param <E> the type of extension\n * @param <K> the type of key\n * @param <B> the type of builder\n * @author johnniang\n * @since 2.22.0\n */\nabstract class AbstractValueIndexSpecBuilder<\n    E extends Extension,\n    K extends Comparable<K>,\n    B extends IndexSpecBuilder<E, K, B>\n    > implements IndexSpecBuilder<E, K, B> {\n\n    protected final String name;\n\n    protected final Class<K> keyType;\n\n    protected boolean unique = false;\n\n    protected boolean nullable = true;\n\n    protected AbstractValueIndexSpecBuilder(String name, Class<K> keyType) {\n        Assert.hasText(name, \"Index name must not be blank\");\n        Assert.notNull(keyType, \"Key type must not be null\");\n        this.name = name;\n        this.keyType = keyType;\n    }\n\n    public B unique(boolean unique) {\n        this.unique = unique;\n        return (B) this;\n    }\n\n    public B nullable(boolean nullable) {\n        this.nullable = nullable;\n        return (B) this;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/DefaultIndexAttribute.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.Extension;\n\n@Deprecated(forRemoval = true, since = \"2.22.0\")\nclass DefaultIndexAttribute<E extends Extension, K extends Comparable<K>>\n    implements IndexAttribute<E, K> {\n\n    private final Class<E> objectType;\n\n    private final Class<K> keyType;\n\n    private final Function<E, Set<K>> valuesFunc;\n\n    private final boolean singleValue;\n\n    public DefaultIndexAttribute(\n        Function<E, Set<K>> valuesFunc,\n        Class<E> objectType,\n        Class<K> keyType,\n        boolean singleValue\n    ) {\n        this.singleValue = singleValue;\n        Assert.notNull(valuesFunc, \"Values function must not be null\");\n        Assert.notNull(objectType, \"Cannot resolve object type\");\n        Assert.notNull(keyType, \"Cannot resolve key type\");\n        this.valuesFunc = valuesFunc;\n        this.objectType = objectType;\n        this.keyType = keyType;\n    }\n\n    @Override\n    public Class<E> getObjectType() {\n        return objectType;\n    }\n\n    @Override\n    public Class<K> getKeyType() {\n        return keyType;\n    }\n\n    @Override\n    public Set<K> getValues(E e) {\n        if (!checkType(e)) {\n            throw new IllegalArgumentException(\"Object type does not match\");\n        }\n        return Optional.ofNullable(this.valuesFunc.apply(e)).orElse(Set.of());\n    }\n\n    private boolean checkType(Extension object) {\n        return getObjectType().isInstance(object);\n    }\n\n    @Override\n    public boolean singleValue() {\n        return this.singleValue;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/IndexAttribute.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Set;\nimport run.halo.app.extension.Extension;\n\n/**\n *\n * An attribute used for indexing extensions.\n *\n * @param <E> the type of the extension\n * @param <K> the type of the key\n * @deprecated Use {@link ValueIndexSpec} instead.\n */\n@Deprecated(forRemoval = true, since = \"2.22.0\")\npublic interface IndexAttribute<E extends Extension, K extends Comparable<K>> {\n\n    /**\n     * Specify this class is belonged to which extension.\n     *\n     * @return the extension class.\n     */\n    Class<E> getObjectType();\n\n    /**\n     * Gets the value type of the attribute.\n     *\n     * @return the value type of the attribute.\n     */\n    Class<K> getKeyType();\n\n    /**\n     * Gets the values of the attribute from the given extension.\n     *\n     * @param e the extension\n     * @return the values of the attribute\n     * @throws IllegalArgumentException if the given extension is not of the expected type\n     */\n    Set<K> getValues(E e);\n\n    /**\n     * Indicates whether this attribute is single-valued.\n     *\n     * @return true if single-valued, false otherwise\n     */\n    boolean singleValue();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/IndexAttributeFactory.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport lombok.experimental.UtilityClass;\nimport run.halo.app.extension.Extension;\n\n/**\n * Factory for creating index attributes.\n *\n * @deprecated Use {@link SingleValueIndexSpec} and {@link MultiValueIndexSpec} instead.\n */\n@Deprecated(forRemoval = true, since = \"2.22.0\")\n@UtilityClass\npublic class IndexAttributeFactory {\n\n    public static <E extends Extension> IndexAttribute<E, UnknownKey> simpleAttribute(Class<E> type,\n        Function<E, String> valueFunc) {\n        return attribute(type, UnknownKey.class, (E e) -> Optional.ofNullable(valueFunc.apply(e))\n            .map(UnknownKey::new)\n            .orElse(null));\n    }\n\n    public static <E extends Extension> IndexAttribute<E, UnknownKey> multiValueAttribute(\n        Class<E> type,\n        Function<E, Set<String>> valuesFunc) {\n        return attributes(type, UnknownKey.class, (E e) -> Optional.ofNullable(valuesFunc.apply(e))\n            .map(values -> values.stream()\n                .map(UnknownKey::new)\n                .collect(Collectors.toSet())\n            )\n            .orElse(null));\n    }\n\n    private static <E extends Extension, K extends Comparable<K>> IndexAttribute<E, K> attributes(\n        Class<E> objectType, Class<K> keyType, Function<E, Set<K>> valuesFunc\n    ) {\n        return new DefaultIndexAttribute<>(valuesFunc, objectType, keyType, false);\n    }\n\n    private static <E extends Extension, K extends Comparable<K>> IndexAttribute<E, K> attribute(\n        Class<E> objectType, Class<K> keyType, Function<E, K> valueFunc\n    ) {\n        return new DefaultIndexAttribute<>(\n            e -> Optional.ofNullable(valueFunc.apply(e))\n                .map(Set::of)\n                .orElse(null),\n            objectType,\n            keyType,\n            true);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/IndexSpec.java",
    "content": "package run.halo.app.extension.index;\n\nimport com.google.common.base.Objects;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.Extension;\n\n/**\n * An implementation of {@link MultiValueIndexSpec}.\n *\n * @param <E> the type of the extension\n * @param <K> the type of the key\n * @deprecated Use {@link IndexSpecs#multi(String, Class)} instead.\n */\n@Data\n@Accessors(chain = true)\n@Deprecated(forRemoval = true, since = \"2.22.0\")\npublic class IndexSpec<E extends Extension, K extends Comparable<K>>\n    implements ValueIndexSpec<E, K> {\n\n    private String name;\n\n    private IndexAttribute<E, K> indexFunc;\n\n    private OrderType order;\n\n    private boolean unique;\n\n    public Set<K> getValues(E extension) {\n        return indexFunc.getValues(extension);\n    }\n\n    public enum OrderType {\n        ASC,\n        DESC\n    }\n\n    @Override\n    public boolean isNullable() {\n        return true;\n    }\n\n    @Override\n    public Class<K> getKeyType() {\n        return indexFunc.getKeyType();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        IndexSpec indexSpec = (IndexSpec) o;\n        return Objects.equal(name, indexSpec.name);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(name);\n    }\n\n    /**\n     * Normalize to single or multi value index spec.\n     *\n     * @return the normalized index spec\n     */\n    public ValueIndexSpec<E, K> normalize() {\n        if (this.indexFunc.singleValue()) {\n            return IndexSpecs.<E, K>single(name, getKeyType())\n                .unique(unique)\n                .indexFunc(e -> {\n                    var values = getValues(e);\n                    return CollectionUtils.isEmpty(values) ? null : values.iterator().next();\n                })\n                .build();\n        }\n        return IndexSpecs.<E, K>multi(name, getKeyType())\n            .unique(unique)\n            .indexFunc(this::getValues)\n            .build();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/IndexSpecBuilder.java",
    "content": "package run.halo.app.extension.index;\n\nimport run.halo.app.extension.Extension;\n\n/**\n * Index specification builder.\n *\n * @param <E> the type of extension\n * @param <K> the type of index key\n * @author johnniang\n * @since 2.22.0\n */\npublic interface IndexSpecBuilder<\n    E extends Extension,\n    K extends Comparable<K>,\n    B extends IndexSpecBuilder<E, K, B>\n    > {\n\n    /**\n     * Sets whether the index is unique.\n     *\n     * @param unique whether the index is unique, default is false\n     * @return the updated IndexSpecBuilder\n     */\n    B unique(boolean unique);\n\n    /**\n     * Sets whether the index allows null values.\n     *\n     * @param nullable whether the index allows null values, default is true\n     * @return the updated IndexSpecBuilder\n     */\n    B nullable(boolean nullable);\n\n    /**\n     * Builds the value index specification.\n     *\n     * @return the value index specification\n     */\n    ValueIndexSpec<E, K> build();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/IndexSpecs.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.List;\nimport run.halo.app.extension.Extension;\n\n/**\n * An interface that defines a collection of {@link IndexSpec}, and provides methods to add,\n * remove, and get {@link IndexSpec}.\n *\n * @author guqing\n * @since 2.12.0\n */\npublic interface IndexSpecs<E extends Extension> {\n\n    /**\n     * Add a new {@link IndexSpec} to the collection.\n     *\n     * @param indexSpec the index spec to add.\n     * @throws IllegalArgumentException if the index spec with the same name already exists or\n     *                                  the index spec is invalid\n     */\n    default <K extends Comparable<K>> void add(IndexSpec<E, K> indexSpec) {\n        add(indexSpec.normalize());\n    }\n\n    <K extends Comparable<K>> void add(ValueIndexSpec<E, K> indexSpec);\n\n    default <K extends Comparable<K>> void add(IndexSpecBuilder<E, K, ?> builder) {\n        add(builder.build());\n    }\n\n    /**\n     * Get all {@link IndexSpec} in the collection.\n     *\n     * @return all index specs\n     */\n    List<ValueIndexSpec<E, ?>> getIndexSpecs();\n\n    /***\n     * Create a multi-value index spec builder.\n     *\n     * @param name the name of the index spec\n     * @param keyType the type of the keys used in the index spec\n     * @param <E> the type of the extension\n     * @param <K> the type of the key\n     * @return a MultiValueBuilder for the specified index spec\n     */\n    static <E extends Extension, K extends Comparable<K>> MultiValueIndexSpecBuilder<E, K> multi(\n        String name,\n        Class<K> keyType\n    ) {\n        return new MultiValueBuilder<>(name, keyType);\n    }\n\n    /**\n     * Create a single-value index spec builder.\n     *\n     * @param name the name of the index spec\n     * @param keyType the type of the keys used in the index spec\n     * @param <E> the type of the extension\n     * @param <K> the type of the key\n     * @return a SingleValueBuilder for the specified index spec.\n     */\n    static <E extends Extension, K extends Comparable<K>> SingleValueIndexSpecBuilder<E, K> single(\n        String name,\n        Class<K> keyType\n    ) {\n        return new SingleValueBuilder<>(name, keyType);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/IndexedQueryEngine.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.List;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * <p>An interface for querying indexed object records from the index store.</p>\n * <p>It provides a way to retrieve the object records by the given {@link GroupVersionKind} and\n * {@link ListOptions}, the final result will be ordered by the index what {@link ListOptions}\n * used and specified by the {@link PageRequest#getSort()}.</p>\n *\n * @author guqing\n * @since 2.12.0\n * @deprecated Use {@link ReactiveExtensionClient#listAllNames(Class, ListOptions, Sort)}\n * or {@link ReactiveExtensionClient#countBy(Class, ListOptions)}\n * or {@link ReactiveExtensionClient#listTopNames(Class, ListOptions, Sort, int)} instead\n */\n@Deprecated(forRemoval = true, since = \"2.22.0\")\npublic interface IndexedQueryEngine {\n\n    /**\n     * Page retrieve the object records by the given {@link GroupVersionKind} and\n     * {@link ListOptions}.\n     *\n     * @param type the type of the object must exist in\n     * {@link run.halo.app.extension.SchemeManager}.\n     * @param options the list options to use for retrieving the object records.\n     * @param page which page to retrieve and how large the page should be.\n     * @return a collection of {@link Metadata#getName()} for the given page.\n     */\n    ListResult<String> retrieve(GroupVersionKind type, ListOptions options, PageRequest page);\n\n    /**\n     * Retrieve all the object records by the given {@link GroupVersionKind} and\n     * {@link ListOptions}.\n     *\n     * @param type the type of the object must exist in {@link run.halo.app.extension.SchemeManager}\n     * @param options the list options to use for retrieving the object records\n     * @param sort the sort to use for retrieving the object records\n     * @return a collection of {@link Metadata#getName()}\n     */\n    List<String> retrieveAll(GroupVersionKind type, ListOptions options, Sort sort);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/KeyComparator.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Comparator;\nimport org.springframework.lang.Nullable;\n\n@Deprecated(forRemoval = true, since = \"2.22.0\")\nclass KeyComparator implements Comparator<String> {\n    public static final KeyComparator INSTANCE = new KeyComparator();\n\n    @Override\n    public int compare(@Nullable String a, @Nullable String b) {\n        if (a == null && b == null) {\n            return 0;\n        } else if (a == null) {\n            // null less than everything\n            return 1;\n        } else if (b == null) {\n            // null less than everything\n            return -1;\n        }\n        return compareStrings(a, b);\n    }\n\n    private int compareStrings(String a, String b) {\n        int i = 0;\n        int j = 0;\n        while (i < a.length() && j < b.length()) {\n            char charA = a.charAt(i);\n            char charB = b.charAt(j);\n\n            if (Character.isDigit(charA) && Character.isDigit(charB)) {\n                // Both characters are digits, compare as numbers\n                int compareResult = compareNumbers(a, b, i, j);\n                if (compareResult != 0) {\n                    return compareResult;\n                }\n\n                // Move indices past the compared number segments\n                i = moveIndexToNextNonDigit(a, i);\n                j = moveIndexToNextNonDigit(b, j);\n            } else if (charA == charB) {\n                // Characters are the same, continue\n                i++;\n                j++;\n            } else if (Character.isDigit(charA)) {\n                // If charA is digit and charB is not, digit comes first\n                return -1;\n            } else if (Character.isDigit(charB)) {\n                // If charB is digit and charA is not, digit comes first\n                return 1;\n            } else {\n                // Both are non-digits, compare directly\n                return Character.compare(charA, charB);\n            }\n        }\n\n        return Integer.compare(a.length(), b.length());\n    }\n\n    private int compareNumbers(String a, String b, int startA, int startB) {\n        int i = startA;\n        int j = startB;\n\n        // Compare lengths of remaining digits\n        int lengthA = countDigits(a, i);\n        int lengthB = countDigits(b, j);\n        if (lengthA != lengthB) {\n            return Integer.compare(lengthA, lengthB);\n        }\n\n        // Compare digits one by one\n        for (int k = 0; k < lengthA && i < a.length() && j < b.length(); k++, i++, j++) {\n            char charA = a.charAt(i);\n            char charB = b.charAt(j);\n            if (charA != charB) {\n                return Character.compare(charA, charB);\n            }\n        }\n\n        // If both numbers have decimal points, compare decimal parts\n        boolean hasDecimalA = i < a.length() && a.charAt(i) == '.';\n        boolean hasDecimalB = j < b.length() && b.charAt(j) == '.';\n        if (hasDecimalA || hasDecimalB) {\n            return compareDecimalNumbers(a, b, i, j);\n        }\n\n        return 0;\n    }\n\n    private int compareDecimalNumbers(String a, String b, int startA, int startB) {\n        // Find decimal point positions\n        int pointA = a.indexOf('.', startA);\n        int pointB = b.indexOf('.', startB);\n\n        // Compare integer parts before the decimal point\n        int integerComparison = compareIntegerPart(a, b, startA, startB, pointA, pointB);\n        if (integerComparison != 0) {\n            return integerComparison;\n        }\n\n        // Compare fractional parts after the decimal point\n        return compareFractionalPart(a, b, pointA + 1, pointB + 1);\n    }\n\n    private int compareIntegerPart(String a, String b, int startA, int startB, int pointA,\n        int pointB) {\n        int i = startA;\n        int j = startB;\n\n        int lengthA = pointA - i;\n        int lengthB = pointB - j;\n        if (lengthA != lengthB) {\n            return Integer.compare(lengthA, lengthB);\n        }\n\n        while (i < pointA && j < pointB) {\n            char charA = a.charAt(i);\n            char charB = b.charAt(j);\n            if (charA != charB) {\n                return Character.compare(charA, charB);\n            }\n            i++;\n            j++;\n        }\n\n        return 0;\n    }\n\n    private int compareFractionalPart(String a, String b, int i, int j) {\n        while (i < a.length() && j < b.length()\n            && Character.isDigit(a.charAt(i)) && Character.isDigit(b.charAt(j))) {\n            if (a.charAt(i) != b.charAt(j)) {\n                return Character.compare(a.charAt(i), b.charAt(j));\n            }\n            i++;\n            j++;\n        }\n\n        // If one number has more digits left, and they're not all zeroes, it is larger\n        while (i < a.length() && Character.isDigit(a.charAt(i))) {\n            if (a.charAt(i) != '0') {\n                return 1;\n            }\n            i++;\n        }\n        while (j < b.length() && Character.isDigit(b.charAt(j))) {\n            if (b.charAt(j) != '0') {\n                return -1;\n            }\n            j++;\n        }\n\n        return 0;\n    }\n\n    private int countDigits(String s, int start) {\n        int count = 0;\n        while (start < s.length() && Character.isDigit(s.charAt(start))) {\n            count++;\n            start++;\n        }\n        return count;\n    }\n\n    private int moveIndexToNextNonDigit(String s, int index) {\n        while (index < s.length() && (Character.isDigit(s.charAt(index))\n            || s.charAt(index) == '.')) {\n            index++;\n        }\n        return index;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/MultiValueBuilder.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Set;\nimport java.util.function.Function;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.Extension;\n\n/**\n * Builder for {@link MultiValueIndexSpec}.\n *\n * @param <E> the type of extension\n * @param <K> the type of index key\n * @author johnniang\n * @since 2.22.0\n */\nclass MultiValueBuilder<E extends Extension, K extends Comparable<K>>\n    extends AbstractValueIndexSpecBuilder<E, K, MultiValueIndexSpecBuilder<E, K>>\n    implements MultiValueIndexSpecBuilder<E, K> {\n\n    private Function<E, Set<K>> indexFunc;\n\n    MultiValueBuilder(String name, Class<K> keyType) {\n        super(name, keyType);\n    }\n\n    @Override\n    public MultiValueIndexSpecBuilder<E, K> indexFunc(Function<E, Set<K>> indexFunc) {\n        this.indexFunc = indexFunc;\n        return this;\n    }\n\n    @Override\n    public ValueIndexSpec<E, K> build() {\n        Assert.hasText(name, \"Index name must not be blank\");\n        Assert.notNull(keyType, \"Key type must not be null\");\n        Assert.notNull(indexFunc, \"Index function must not be null\");\n\n        return new MultiValueIndexSpec<>() {\n            @Override\n            public String getName() {\n                return name;\n            }\n\n            @Override\n            public boolean isUnique() {\n                return unique;\n            }\n\n            @Override\n            public boolean isNullable() {\n                return nullable;\n            }\n\n            @Override\n            public Class<K> getKeyType() {\n                return keyType;\n            }\n\n            @Override\n            public Set<K> getValues(E extension) {\n                return indexFunc.apply(extension);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/MultiValueIndexSpec.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Set;\nimport org.springframework.lang.Nullable;\nimport run.halo.app.extension.Extension;\n\n/**\n * Multi value index specification.\n *\n * @param <E> the type of extension\n * @param <K> the type of key\n * @author johnniang\n * @since 2.22.0\n */\ninterface MultiValueIndexSpec<E extends Extension, K extends Comparable<K>>\n    extends ValueIndexSpec<E, K> {\n\n    @Nullable\n    Set<K> getValues(E extension);\n\n    static <E extends Extension, K extends Comparable<K>> MultiValueBuilder<E, K> builder(\n        String name, Class<K> keyType) {\n        return new MultiValueBuilder<>(name, keyType);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/MultiValueIndexSpecBuilder.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Set;\nimport java.util.function.Function;\nimport run.halo.app.extension.Extension;\n\n/**\n * Builder for {@link MultiValueIndexSpec}.\n *\n * @param <E> the type of extension\n * @param <K> the type of index key\n * @author johnniang\n * @since 2.22.0\n */\npublic interface MultiValueIndexSpecBuilder<E extends Extension, K extends Comparable<K>>\n    extends IndexSpecBuilder<E, K, MultiValueIndexSpecBuilder<E, K>> {\n\n    /**\n     * Sets the index function.\n     *\n     * @param indexFunc the index function\n     * @return the builder itself\n     */\n    MultiValueIndexSpecBuilder<E, K> indexFunc(Function<E, Set<K>> indexFunc);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/SingleValueBuilder.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.function.Function;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.Extension;\n\n/**\n * Single value index specification builder.\n *\n * @param <E> the type of extension\n * @param <K> the type of index key\n * @author johnniang\n * @since 2.22.0\n */\nclass SingleValueBuilder<E extends Extension, K extends Comparable<K>>\n    extends AbstractValueIndexSpecBuilder<E, K, SingleValueIndexSpecBuilder<E, K>>\n    implements SingleValueIndexSpecBuilder<E, K> {\n\n    private Function<E, K> indexFunc;\n\n    SingleValueBuilder(String name, Class<K> keyType) {\n        super(name, keyType);\n    }\n\n    @Override\n    public SingleValueBuilder<E, K> indexFunc(Function<E, K> indexFunc) {\n        this.indexFunc = indexFunc;\n        return this;\n    }\n\n    @Override\n    public ValueIndexSpec<E, K> build() {\n        Assert.hasText(name, \"Index name must not be blank\");\n        Assert.notNull(keyType, \"Key type must not be null\");\n        Assert.notNull(indexFunc, \"Index function must not be null\");\n\n        return new SingleValueIndexSpec<>() {\n            @Override\n            public K getValue(E extension) {\n                return indexFunc.apply(extension);\n            }\n\n            @Override\n            public String getName() {\n                return name;\n            }\n\n            @Override\n            public boolean isUnique() {\n                return unique;\n            }\n\n            @Override\n            public boolean isNullable() {\n                return nullable;\n            }\n\n            @Override\n            public Class<K> getKeyType() {\n                return keyType;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/SingleValueIndexSpec.java",
    "content": "package run.halo.app.extension.index;\n\nimport org.springframework.lang.Nullable;\nimport run.halo.app.extension.Extension;\n\n/**\n * Single value index specification.\n *\n * @param <E> the type of extension\n * @param <K> the type of key\n * @author johnniang\n * @since 2.22.0\n */\ninterface SingleValueIndexSpec<E extends Extension, K extends Comparable<K>>\n    extends ValueIndexSpec<E, K> {\n\n    @Nullable\n    K getValue(E extension);\n\n    static <E extends Extension, K extends Comparable<K>> SingleValueBuilder<E, K> builder(\n        String name, Class<K> keyType\n    ) {\n        return new SingleValueBuilder<>(name, keyType);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/SingleValueIndexSpecBuilder.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.function.Function;\nimport run.halo.app.extension.Extension;\n\n/**\n * Builder for {@link SingleValueIndexSpec}.\n *\n * @param <E> the type of extension\n * @param <K> the type of index key\n * @author johnniang\n * @since 2.22.0\n */\npublic interface SingleValueIndexSpecBuilder<E extends Extension, K extends Comparable<K>>\n    extends IndexSpecBuilder<E, K, SingleValueIndexSpecBuilder<E, K>> {\n\n    /**\n     * Sets the index function.\n     *\n     * @param indexFunc the index function\n     * @return the builder itself\n     */\n    SingleValueIndexSpecBuilder<E, K> indexFunc(Function<E, K> indexFunc);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/UnknownKey.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Objects;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.lang.Nullable;\n\n/**\n * String key wrapper for nullable string comparison. Only for backward compatibility. May remove\n * in the future.\n *\n * @author johnniang\n * @since 2.22.0\n */\n@Deprecated(forRemoval = true, since = \"2.22.0\")\nrecord UnknownKey(@Nullable String value) implements Comparable<UnknownKey> {\n\n    @Override\n    public int compareTo(@NotNull UnknownKey o) {\n        return KeyComparator.INSTANCE.compare(this.value, o.value);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        UnknownKey unknownKey = (UnknownKey) o;\n        return Objects.equals(value, unknownKey.value);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return Objects.toString(value);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/ValueIndexSpec.java",
    "content": "package run.halo.app.extension.index;\n\nimport run.halo.app.extension.Extension;\n\n/**\n * Specification for a value index on an extension.\n *\n * @param <E> the type of the extension\n * @param <K> the type of the key\n * @author johnniang\n * @since 2.22.0\n */\npublic interface ValueIndexSpec<E extends Extension, K extends Comparable<K>> {\n\n    /**\n     * Gets the name of this index.\n     *\n     * @return the name of this index\n     */\n    String getName();\n\n    /**\n     * Whether this index is unique.\n     *\n     * @return true if this index is unique, false otherwise\n     */\n    boolean isUnique();\n\n    /**\n     * Whether this index allows null values.\n     *\n     * @return true if this index allows null values, false otherwise\n     */\n    boolean isNullable();\n\n    /**\n     * Gets the type of the key.\n     *\n     * @return the type of the key\n     */\n    Class<K> getKeyType();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/AllCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord AllCondition(String indexName) implements Condition {\n\n    @Override\n    public Condition not() {\n        return new NoneCondition(indexName);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"ALL \" + indexName;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/And.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\n/**\n * This condition is only for backward compatibility.\n *\n * @param left left condition\n * @param right right condition\n * @deprecated Use {@link Condition#and(Condition)} instead.\n */\n@Deprecated(forRemoval = true, since = \"2.22.0\")\npublic record And(Condition left, Condition right) implements Condition {\n\n    @Override\n    public Condition not() {\n        return left.not().or(right.not());\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"(\" + left + \" AND \" + right + \")\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/AndCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord AndCondition(Condition left, Condition right) implements Condition {\n\n    public AndCondition {\n        Assert.notNull(left, \"Left condition must not be null\");\n        Assert.notNull(right, \"Right condition must not be null\");\n    }\n\n    @Override\n    public Condition not() {\n        return left.not().or(right.not());\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"(\" + left + \" AND \" + right + \")\";\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/BetweenCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord BetweenCondition(\n    String indexName, Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive)\n    implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new NotBetweenCondition(indexName, fromKey, !fromInclusive, toKey, !toInclusive);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" BETWEEN \"\n            + (fromInclusive ? \"[\" : \"(\")\n            + fromKey\n            + \", \"\n            + toKey\n            + (toInclusive ? \"]\" : \")\");\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/Condition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.springframework.data.relational.core.sql.Visitable;\n\n/**\n * A condition used in querying index.\n * e.g.: {@code metadata.name = 'halo' AND status.published = true}\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface Condition extends Visitable, Query {\n\n    /**\n     * Combine with another condition using AND operator.\n     *\n     * @param another another condition\n     * @return the combined condition\n     */\n    default Condition and(Condition another) {\n        return new AndCondition(this, another);\n    }\n\n    /**\n     * Combine with another condition using OR operator.\n     *\n     * @param another another condition\n     * @return the combined condition\n     */\n    default Condition or(Condition another) {\n        return new OrCondition(this, another);\n    }\n\n    /**\n     * Negate this condition.\n     *\n     * @return the negated condition\n     */\n    default Condition not() {\n        return new NotCondition(this);\n    }\n\n    /**\n     * Creates an empty condition.\n     *\n     * @return an empty condition\n     */\n    static Condition empty() {\n        return new EmptyCondition();\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/EmptyCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord EmptyCondition() implements Condition {\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"EMPTY\";\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/EqualCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord EqualCondition(String indexName, Object key) implements IndexCondition {\n\n    public EqualCondition {\n        Assert.notNull(key, \"Key of \" + indexName + \" must not be null\");\n    }\n\n    @Override\n    public Condition not() {\n        return new NotEqualCondition(indexName, key);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" = \" + key;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/GreaterThanCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord GreaterThanCondition(String indexName, Object lowerBound, boolean inclusive)\n    implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new LessThanCondition(indexName, lowerBound, !inclusive);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + (inclusive ? \" >= \" : \" > \") + lowerBound;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/InCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport java.util.Collection;\nimport java.util.stream.Collectors;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord InCondition(String indexName, Collection<Object> keys) implements IndexCondition {\n\n    public InCondition {\n        Assert.notNull(keys, \"Keys of \" + indexName + \" must not be empty\");\n    }\n\n    @Override\n    public Condition not() {\n        return new NotInCondition(indexName, keys);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" IN (\"\n            + keys.stream().map(Object::toString).collect(Collectors.joining(\", \"))\n            + \")\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/IndexCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\n/**\n * Index condition interface for index-based queries.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface IndexCondition extends Condition {\n\n    /**\n     * Get the index name.\n     *\n     * @return the index name\n     */\n    String indexName();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/IsNotNullCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord IsNotNullCondition(String indexName) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new IsNullCondition(indexName);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" IS NOT NULL\";\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/IsNullCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord IsNullCondition(String indexName) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new IsNotNullCondition(indexName);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" IS NULL\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LabelCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\n/**\n * Label condition interface for label-based queries.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface LabelCondition extends Condition {\n\n    String INDEX_NAME = \"metadata.labels\";\n\n    /**\n     * Get the label key.\n     *\n     * @return the label key\n     */\n    String labelKey();\n\n    @Override\n    LabelCondition not();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LabelEqualsCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord LabelEqualsCondition(String labelKey, String labelValue) implements LabelCondition {\n\n    @Override\n    public LabelCondition not() {\n        return new LabelNotEqualsCondition(labelKey, labelValue);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return INDEX_NAME + \"['\" + labelKey + \"'] = '\" + labelValue + \"'\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LabelExistsCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord LabelExistsCondition(String labelKey) implements LabelCondition {\n\n    @Override\n    public LabelCondition not() {\n        return new LabelNotExistsCondition(labelKey);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"EXISTS \" + INDEX_NAME + \"['\" + labelKey + \"']\";\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LabelInCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport java.util.Collection;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord LabelInCondition(String labelKey, Collection<String> labelValues) implements LabelCondition {\n\n    public LabelInCondition {\n        Assert.notNull(labelValues, \"labelValues of \" + labelKey + \" must not be null\");\n    }\n\n    @Override\n    public LabelCondition not() {\n        return new LabelNotInCondition(labelKey, labelValues);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return INDEX_NAME + \"['\" + labelKey + \"'] IN (\"\n            + String.join(\", \", labelValues.stream().map(v -> \"'\" + v + \"'\").toList())\n            + \")\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LabelNotEqualsCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord LabelNotEqualsCondition(String labelKey, String labelValue) implements LabelCondition {\n\n    @Override\n    public LabelCondition not() {\n        return new LabelEqualsCondition(labelKey, labelValue);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return INDEX_NAME + \"['\" + labelKey + \"'] <> '\" + labelValue + \"'\";\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LabelNotExistsCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord LabelNotExistsCondition(String labelKey) implements LabelCondition {\n\n    @Override\n    public LabelCondition not() {\n        return new LabelExistsCondition(labelKey);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"NOT EXISTS \" + INDEX_NAME + \"['\" + labelKey + \"']\";\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LabelNotInCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport java.util.Collection;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord LabelNotInCondition(String labelKey, Collection<String> labelValues)\n    implements LabelCondition {\n\n    public LabelNotInCondition {\n        Assert.notNull(labelValues, \"labelValues of \" + labelKey + \" must not be null\");\n    }\n\n    @Override\n    public LabelCondition not() {\n        return new LabelInCondition(labelKey, labelValues);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return INDEX_NAME + \"['\" + labelKey + \"'] NOT IN (\"\n            + String.join(\", \", labelValues.stream().map(v -> \"'\" + v + \"'\").toList())\n            + \")\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/LessThanCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord LessThanCondition(String indexName, Object upperBound, boolean inclusive)\n    implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new GreaterThanCondition(indexName, upperBound, !inclusive);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + (inclusive ? \" <= \" : \" < \") + upperBound;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/NoneCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord NoneCondition(String indexName) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new AllCondition(indexName);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"NONE \" + indexName;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/NotBetweenCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord NotBetweenCondition(\n    String indexName, Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive)\n    implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new BetweenCondition(indexName, fromKey, !fromInclusive, toKey, !toInclusive);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" NOT BETWEEN \"\n            + (fromInclusive ? \"[\" : \"(\")\n            + fromKey\n            + \", \"\n            + toKey\n            + (toInclusive ? \"]\" : \")\");\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/NotCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord NotCondition(Condition condition) implements Condition {\n\n    public NotCondition {\n        Assert.notNull(condition, \"Condition must not be null\");\n    }\n\n    @Override\n    public Condition not() {\n        return condition;\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"NOT (\" + condition + \")\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/NotEqualCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord NotEqualCondition(String indexName, Object key) implements IndexCondition {\n\n\n    public NotEqualCondition {\n        Assert.notNull(key, \"Key of \" + indexName + \" must not be null\");\n    }\n\n    @Override\n    public Condition not() {\n        return new EqualCondition(indexName, key);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" != \" + key;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/NotInCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport java.util.Collection;\nimport java.util.stream.Collectors;\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord NotInCondition(String indexName, Collection<Object> keys) implements IndexCondition {\n\n    public NotInCondition {\n        Assert.notNull(keys, \"Keys of \" + indexName + \" must not be empty\");\n    }\n\n    @Override\n    public Condition not() {\n        return new InCondition(indexName, keys);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" NOT IN (\"\n            + keys.stream().map(Object::toString).collect(Collectors.joining(\", \"))\n            + \")\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/OrCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.springframework.util.Assert;\n\nrecord OrCondition(Condition left, Condition right) implements Condition {\n\n    public OrCondition {\n        Assert.notNull(left, \"Left condition must not be null\");\n        Assert.notNull(right, \"Right condition must not be null\");\n    }\n\n    @Override\n    public Condition not() {\n        return left.not().and(right.not());\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return \"(\" + left + \" OR \" + right + \")\";\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/Queries.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport org.springframework.util.Assert;\n\n/**\n * A utility class for building query conditions.\n *\n * <p>\n * Use {@link Condition#not()} to create negated conditions.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic enum Queries {\n    ;\n\n    /**\n     * Combines multiple conditions with a logical AND.\n     *\n     * @param condition the first condition, must not be null\n     * @param additionalConditions additional conditions to combine\n     * @return the combined condition\n     */\n    public static Condition and(Condition condition, Condition... additionalConditions) {\n        Assert.notNull(condition, \"Condition must not be null\");\n        return Arrays.stream(additionalConditions)\n            .reduce(condition, Condition::and);\n    }\n\n    /**\n     * Combines multiple conditions with a logical OR.\n     *\n     * @param condition the first condition, must not be null\n     * @param additionalConditions additional conditions to combine\n     * @return the combined condition\n     */\n    public static Condition or(Condition condition, Condition... additionalConditions) {\n        Assert.notNull(condition, \"Condition must not be null\");\n        return Arrays.stream(additionalConditions)\n            .reduce(condition, Condition::or);\n    }\n\n    /**\n     * Negates the specified condition.\n     *\n     * @param condition the condition to negate, must not be null\n     * @return the negated condition\n     */\n    public static Condition not(Condition condition) {\n        Assert.notNull(condition, \"Condition must not be null\");\n        return condition.not();\n    }\n\n    /**\n     * Creates a \"between\" condition for the specified field name and range values.\n     *\n     * @param fieldName the name of the field\n     * @param fromValue the start value of the range\n     * @param fromInclusive whether the start value is inclusive\n     * @param toValue the end value of the range\n     * @param toInclusive whether the end value is inclusive\n     * @return the \"between\" condition\n     */\n    public static Condition between(String fieldName,\n        Object fromValue, boolean fromInclusive, Object toValue, boolean toInclusive) {\n        return new BetweenCondition(fieldName, fromValue, fromInclusive, toValue, toInclusive);\n    }\n\n    /**\n     * Creates an empty condition that matches all records.\n     *\n     * @return the empty condition\n     */\n    public static Condition empty() {\n        return new EmptyCondition();\n    }\n\n    /**\n     * Creates an \"all\" condition for the specified field name.\n     *\n     * @param fieldName the name of the field\n     * @return the \"all\" condition\n     */\n    public static Condition all(String fieldName) {\n        return new AllCondition(fieldName);\n    }\n\n    /**\n     * Creates an \"is null\" condition for the specified field name.\n     *\n     * @param fieldName the name of the field\n     * @return the \"is null\" condition\n     */\n    public static Condition isNull(String fieldName) {\n        return new IsNullCondition(fieldName);\n    }\n\n    /**\n     * Creates an \"equal\" condition for the specified field name and attribute value.\n     *\n     * @param fieldName the name of the field\n     * @param attributeValue the attribute value, must not be null\n     * @return the \"equal\" condition\n     */\n    public static Condition equal(String fieldName, Object attributeValue) {\n        Assert.notNull(attributeValue,\n            \"Attribute key of field \" + fieldName + \" must not be null\"\n        );\n        return new EqualCondition(fieldName, attributeValue);\n    }\n\n    /**\n     * Creates a \"greater than\" condition for the specified field name and attribute value.\n     *\n     * @param fieldName the name of the field\n     * @param attributeValue the attribute value, must not be null\n     * @param inclusive whether the comparison is inclusive\n     * @return the \"greater than\" condition\n     */\n    public static Condition greaterThan(\n        String fieldName, Object attributeValue, boolean inclusive) {\n        return new GreaterThanCondition(fieldName, attributeValue, inclusive);\n    }\n\n    /**\n     * Creates a \"greater than\" condition for the specified field name and attribute value.\n     *\n     * @param fieldName the name of the field\n     * @param attributeValue the attribute value, must not be null. Which is not inclusive.\n     * @return the \"greater than\" condition\n     */\n    public static Condition greaterThan(String fieldName, Object attributeValue) {\n        return greaterThan(fieldName, attributeValue, false);\n    }\n\n    /**\n     * Creates an \"in\" condition for the specified field name and attribute values.\n     *\n     * @param fieldName the name of the field\n     * @param attributeValue the first attribute value, must not be null. If it's a collection,\n     * it will be treated as the collection of values for the \"in\" condition.\n     * @param additionalValues additional attribute values\n     * @return the \"in\" condition\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static Condition in(\n        String fieldName, Object attributeValue, Object... additionalValues) {\n        Assert.notNull(attributeValue,\n            \"Attribute key of field \" + fieldName + \" must not be null\"\n        );\n        if (attributeValue instanceof Collection<?> collection) {\n            // Allow passing a collection directly\n            return in(fieldName, (Collection<Object>) collection);\n        }\n        if (additionalValues == null) {\n            return equal(fieldName, attributeValue);\n        }\n        var values = new ArrayList<>(additionalValues.length + 1);\n        values.add(attributeValue);\n        values.addAll(Arrays.asList(additionalValues));\n        return in(fieldName, values);\n    }\n\n    /**\n     * Creates an \"in\" condition for the specified field name and collection of values.\n     *\n     * @param fieldName the name of the field\n     * @param values the collection of values, must not be null\n     * @return the \"in\" condition\n     */\n    public static Condition in(String fieldName, Collection<Object> values) {\n        Assert.notNull(values, \"Values must not be null\");\n        if (values.size() == 1) {\n            var value = values.iterator().next();\n            return equal(fieldName, value);\n        }\n        return new InCondition(fieldName, values);\n    }\n\n    /**\n     * Creates a \"less than\" condition for the specified field name and attribute value.\n     *\n     * @param fieldName the name of the field\n     * @param attributeValue the attribute value, must not be null\n     * @param inclusive whether the comparison is inclusive\n     * @return the \"less than\" condition\n     */\n    public static Condition lessThan(String fieldName, Object attributeValue, boolean inclusive) {\n        return new LessThanCondition(fieldName, attributeValue, inclusive);\n    }\n\n    /**\n     * Creates a \"less than\" condition for the specified field name and attribute value.\n     *\n     * @param fieldName the name of the field\n     * @param attributeValue the attribute value, must not be null. Which is not inclusive.\n     * @return the \"less than\" condition\n     */\n    public static Condition lessThan(String fieldName, Object attributeValue) {\n        return lessThan(fieldName, attributeValue, false);\n    }\n\n    /**\n     * Creates a \"not equal\" condition for the specified field name and attribute value.\n     *\n     * @param fieldName the name of the field\n     * @param attributeValue the attribute value, must not be null\n     * @return the \"not equal\" condition\n     */\n    public static Condition notEqual(String fieldName, Object attributeValue) {\n        return new NotEqualCondition(fieldName, attributeValue);\n    }\n\n    /**\n     * Creates a \"starts with\" condition for the specified field name and prefix value.\n     *\n     * @param fieldName the name of the field\n     * @param prefix the prefix value, must not be null\n     * @return the \"starts with\" condition\n     */\n    public static Condition startsWith(String fieldName, String prefix) {\n        return new StringStartsWithCondition(fieldName, prefix);\n    }\n\n    /**\n     * Creates an \"ends with\" condition for the specified field name and suffix value.\n     *\n     * @param fieldName the name of the field\n     * @param suffix the suffix value, must not be null\n     * @return the \"ends with\" condition\n     */\n    public static Condition endsWith(String fieldName, String suffix) {\n        return new StringEndsWithCondition(fieldName, suffix);\n    }\n\n    /**\n     * Creates a \"contains\" condition for the specified field name and substring value.\n     *\n     * @param fieldName the name of the field\n     * @param substring the substring value, must not be null\n     * @return the \"contains\" condition\n     */\n    public static Condition contains(String fieldName, String substring) {\n        return new StringContainsCondition(fieldName, substring);\n    }\n\n    /**\n     * Creates a label condition that checks for the existence of a label with the specified key.\n     *\n     * @param labelKey the label key, must not be null\n     * @return the label existence condition\n     */\n    public static LabelCondition labelExists(String labelKey) {\n        return new LabelExistsCondition(labelKey);\n    }\n\n    /**\n     * Creates a label condition that checks for equality of a label with the specified key and\n     * value.\n     *\n     * @param labelKey the label key, must not be null\n     * @param labelValue the label value, must not be null\n     * @return the label equality condition\n     */\n    public static LabelCondition labelEqual(String labelKey, String labelValue) {\n        return new LabelEqualsCondition(labelKey, labelValue);\n    }\n\n    /**\n     * Creates a label condition that checks if a label with the specified key has a value within\n     * the given collection of values.\n     *\n     * @param labelKey the label key, must not be null\n     * @param labelValues the collection of label values, must not be null\n     * @return the label \"in\" condition\n     */\n    public static LabelCondition labelIn(String labelKey, Collection<String> labelValues) {\n        return new LabelInCondition(labelKey, labelValues);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/Query.java",
    "content": "package run.halo.app.extension.index.query;\n\n/**\n * A {@link Query} is used to build queries for searching indexed objects.\n *\n * <p>\n * Keep this interface is only for backward compatibility.\n *\n * @author guqing\n * @since 2.12.0\n * @deprecated Use {@link Condition} instead.\n */\n@Deprecated(since = \"2.22.0\", forRemoval = true)\npublic interface Query {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/QueryFactory.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.experimental.UtilityClass;\nimport org.springframework.util.Assert;\n\n/**\n * Query factory utility class.\n *\n * @deprecated Use {@link Queries} instead.\n */\n@Deprecated(since = \"2.22.0\", forRemoval = true)\n@UtilityClass\npublic class QueryFactory {\n\n    public static Query all() {\n        return new EmptyCondition();\n    }\n\n    public static Query all(String fieldName) {\n        return new AllCondition(fieldName);\n    }\n\n    public static Query isNull(String fieldName) {\n        return new IsNullCondition(fieldName);\n    }\n\n    public static Query isNotNull(String fieldName) {\n        return new IsNotNullCondition(fieldName);\n    }\n\n    public static Query notEqual(String fieldName, String attributeValue) {\n        return Queries.notEqual(fieldName, attributeValue);\n    }\n\n    public static Query equal(String fieldName, String attributeValue) {\n        return Queries.equal(fieldName, attributeValue);\n    }\n\n    public static Query lessThan(String fieldName, String attributeValue) {\n        return Queries.lessThan(fieldName, attributeValue);\n    }\n\n    public static Query lessThanOrEqual(String fieldName, String attributeValue) {\n        return Queries.lessThan(fieldName, attributeValue, true);\n    }\n\n    public static Query greaterThan(String fieldName, String attributeValue) {\n        return Queries.greaterThan(fieldName, attributeValue);\n    }\n\n    public static Query greaterThanOrEqual(String fieldName, String attributeValue) {\n        return Queries.greaterThan(fieldName, attributeValue, true);\n    }\n\n    public static Query in(String fieldName, String... attributeValues) {\n        return Queries.in(fieldName, Set.of(attributeValues));\n    }\n\n    public static Query in(String fieldName, Collection<String> values) {\n        var convertedValues = values.stream()\n            .map(v -> (Object) v)\n            .collect(Collectors.toSet());\n        return Queries.in(fieldName, convertedValues);\n    }\n\n    public static Query and(Collection<Query> queries) {\n        Assert.notEmpty(queries, \"Queries must not be empty\");\n        if (queries.size() == 1) {\n            return queries.iterator().next();\n        }\n        return queries.stream()\n            .peek(query -> {\n                if (!(query instanceof Condition)) {\n                    throw new IllegalArgumentException(\n                        \"Only Condition instances are supported in AND operations\");\n                }\n            })\n            .map(query -> (Condition) query)\n            .reduce(Condition::and)\n            .orElseThrow(() -> new IllegalArgumentException(\"No Condition found in queries\"));\n    }\n\n    public static And and(Query left, Query right) {\n        Assert.isInstanceOf(Condition.class, left,\n            \"Only Condition instances are supported in AND operations\");\n        Assert.isInstanceOf(Condition.class, right,\n            \"Only Condition instances are supported in AND operations\");\n        return new And((Condition) left, (Condition) right);\n    }\n\n    public static Query and(Query left, Query right, Query... additionalQueries) {\n        var queries = new ArrayList<Query>(2 + additionalQueries.length);\n        queries.add(left);\n        queries.add(right);\n        Collections.addAll(queries, additionalQueries);\n        return and(queries);\n    }\n\n    public static Query and(Query left, Query right, Collection<Query> additionalQueries) {\n        var queries = new ArrayList<Query>(2 + additionalQueries.size());\n        queries.add(left);\n        queries.add(right);\n        queries.addAll(additionalQueries);\n        return and(queries);\n    }\n\n    private static Query or(Collection<Query> queries) {\n        Assert.notEmpty(queries, \"Queries must not be empty\");\n        if (queries.size() == 1) {\n            return queries.iterator().next();\n        }\n        return queries.stream()\n            .peek(query -> {\n                if (!(query instanceof Condition)) {\n                    throw new IllegalArgumentException(\n                        \"Only Condition instances are supported in OR operations\");\n                }\n            })\n            .map(query -> (Condition) query)\n            .reduce(Condition::or)\n            .orElseThrow(() -> new IllegalArgumentException(\"No Condition found in queries\"));\n    }\n\n    public static Query or(Query left, Query right) {\n        return or(List.of(left, right));\n    }\n\n    public static Query or(Query query1, Query query2, Query... additionalQueries) {\n        var queries = new ArrayList<Query>(2 + additionalQueries.length);\n        queries.add(query1);\n        queries.add(query2);\n        Collections.addAll(queries, additionalQueries);\n        return or(queries);\n    }\n\n    public static Query or(Query query1, Query query2, Collection<Query> additionalQueries) {\n        var queries = new ArrayList<Query>(2 + additionalQueries.size());\n        queries.add(query1);\n        queries.add(query2);\n        queries.addAll(additionalQueries);\n        return or(queries);\n    }\n\n    public static Query not(Query query) {\n        Assert.isInstanceOf(Condition.class, query,\n            \"Only Condition instances are supported in NOT operations\");\n        return ((Condition) query).not();\n    }\n\n    public static Query betweenLowerExclusive(String fieldName, String lowerValue,\n        String upperValue) {\n        return Queries.between(fieldName, lowerValue, false, upperValue, true);\n    }\n\n    public static Query betweenUpperExclusive(String fieldName, String lowerValue,\n        String upperValue) {\n        return Queries.between(fieldName, lowerValue, true, upperValue, false);\n    }\n\n    public static Query betweenExclusive(String fieldName, String lowerValue,\n        String upperValue) {\n        return Queries.between(fieldName, lowerValue, false, upperValue, false);\n    }\n\n    public static Query between(String fieldName, String lowerValue, String upperValue) {\n        return Queries.between(fieldName, lowerValue, true, upperValue, true);\n    }\n\n    public static Query startsWith(String fieldName, String value) {\n        return Queries.startsWith(fieldName, value);\n    }\n\n    public static Query endsWith(String fieldName, String value) {\n        return Queries.endsWith(fieldName, value);\n    }\n\n    public static Query contains(String fieldName, String value) {\n        return Queries.contains(fieldName, value);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/StringContainsCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord StringContainsCondition(String indexName, String keyword) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new StringNotContainsCondition(indexName, keyword);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" CONTAINS \" + keyword;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/StringEndsWithCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord StringEndsWithCondition(String indexName, String suffix) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new StringNotEndsWithCondition(indexName, suffix);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" ENDS WITH \" + suffix;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/StringNotContainsCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord StringNotContainsCondition(String indexName, String keyword) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new StringContainsCondition(indexName, keyword);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" NOT CONTAINS \" + keyword;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/StringNotEndsWithCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord StringNotEndsWithCondition(String indexName, String suffix) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new StringEndsWithCondition(indexName, suffix);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" NOT ENDS WITH \" + suffix;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/StringNotStartsWithCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord StringNotStartsWithCondition(String indexName, String prefix) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new StringStartsWithCondition(indexName, prefix);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" NOT STARTS WITH \" + prefix;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/index/query/StringStartsWithCondition.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport org.jetbrains.annotations.NotNull;\n\nrecord StringStartsWithCondition(String indexName, String prefix) implements IndexCondition {\n\n    @Override\n    public Condition not() {\n        return new StringNotStartsWithCondition(indexName, prefix);\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return indexName + \" STARTS WITH \" + prefix;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/IListRequest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.Collections;\nimport java.util.List;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.boot.convert.ApplicationConversionService;\nimport org.springframework.core.convert.ConversionService;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.util.StringUtils;\nimport run.halo.app.extension.PageRequestImpl;\n\npublic interface IListRequest {\n\n    @Schema(description = \"The page number. Zero indicates no page.\")\n    Integer getPage();\n\n    @Schema(description = \"Size of one page. Zero indicates no limit.\")\n    Integer getSize();\n\n    @Schema(description = \"Label selector for filtering.\")\n    List<String> getLabelSelector();\n\n    @Schema(description = \"Field selector for filtering.\")\n    List<String> getFieldSelector();\n\n    class QueryListRequest implements IListRequest {\n\n        protected final MultiValueMap<String, String> queryParams;\n\n        private final ConversionService conversionService =\n            ApplicationConversionService.getSharedInstance();\n\n        public QueryListRequest(MultiValueMap<String, String> queryParams) {\n            this.queryParams = queryParams;\n        }\n\n        @Override\n        public Integer getPage() {\n            var page = queryParams.getFirst(\"page\");\n            if (StringUtils.hasText(page)) {\n                return conversionService.convert(page, Integer.class);\n            }\n            return 0;\n        }\n\n        @Override\n        public Integer getSize() {\n            var size = queryParams.getFirst(\"size\");\n            if (StringUtils.hasText(size)) {\n                return conversionService.convert(size, Integer.class);\n            }\n            return PageRequestImpl.MAX_SIZE;\n        }\n\n        @Override\n        public List<String> getLabelSelector() {\n            return queryParams.getOrDefault(\"labelSelector\", Collections.emptyList());\n        }\n\n        @Override\n        public List<String> getFieldSelector() {\n            return queryParams.getOrDefault(\"fieldSelector\", Collections.emptyList());\n        }\n    }\n\n    static void buildParameters(Builder builder) {\n        builder.parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"page\")\n                .implementation(Integer.class)\n                .required(false)\n                .description(\"Page number. Default is 0.\"))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"size\")\n                .implementation(Integer.class)\n                .required(false)\n                .description(\"Size number. Default is 0.\"))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"labelSelector\")\n                .required(false)\n                .description(\"Label selector. e.g.: hidden!=true\")\n                .implementationArray(String.class))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"fieldSelector\")\n                .required(false)\n                .description(\"Field selector. e.g.: metadata.name==halo\")\n                .implementationArray(String.class)\n            );\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/QueryParamBuildUtil.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.experimental.UtilityClass;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\n@UtilityClass\npublic class QueryParamBuildUtil {\n\n    public static org.springdoc.core.fn.builders.parameter.Builder sortParameter() {\n        return parameterBuilder()\n            .in(ParameterIn.QUERY)\n            .name(\"sort\")\n            .required(false)\n            .description(\"\"\"\n                Sorting criteria in the format: property,(asc|desc). \\\n                Default sort order is ascending. Multiple sort criteria are supported.\\\n                \"\"\")\n            .array(arraySchemaBuilder().schema(schemaBuilder().type(\"string\")));\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/SortableRequest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static run.halo.app.extension.Comparators.compareCreationTimestamp;\nimport static run.halo.app.extension.Comparators.compareName;\nimport static run.halo.app.extension.Comparators.nullsComparator;\nimport static run.halo.app.extension.ExtensionUtil.defaultSort;\nimport static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;\n\nimport io.swagger.v3.oas.annotations.media.ArraySchema;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.Comparator;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.beans.BeanWrapper;\nimport org.springframework.beans.BeanWrapperImpl;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.web.server.ServerWebExchange;\nimport run.halo.app.core.extension.endpoint.SortResolver;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\n\npublic class SortableRequest extends IListRequest.QueryListRequest {\n\n    protected final ServerWebExchange exchange;\n\n    public SortableRequest(ServerWebExchange exchange) {\n        super(exchange.getRequest().getQueryParams());\n        this.exchange = exchange;\n    }\n\n    @ArraySchema(uniqueItems = true,\n        arraySchema = @Schema(name = \"sort\",\n            description = \"Sort property and direction of the list result. Support sorting based \"\n                + \"on attribute name path.\"),\n        schema = @Schema(description = \"like field,asc or field,desc\",\n            implementation = String.class,\n            example = \"metadata.creationTimestamp,desc\"))\n    public Sort getSort() {\n        return SortResolver.defaultInstance.resolve(exchange)\n            .and(defaultSort());\n    }\n\n    /**\n     * Build {@link ListOptions} from query params.\n     *\n     * @return a list options.\n     */\n    public ListOptions toListOptions() {\n        return labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());\n    }\n\n    public PageRequest toPageRequest() {\n        return PageRequestImpl.of(getPage(), getSize(), getSort());\n    }\n\n    /**\n     * Build comparator from sort.\n     *\n     * @param <T> Extension type\n     * @return comparator\n     */\n    public <T extends Extension> Comparator<T> toComparator() {\n        var sort = getSort();\n        var fallbackComparator = Stream.<Comparator<T>>of(\n            compareCreationTimestamp(false),\n            compareName(true)\n        );\n        var comparatorStream = sort.stream().map(order -> {\n            var property = order.getProperty();\n            var direction = order.getDirection();\n            Function<T, Object> function = extension -> {\n                BeanWrapper beanWrapper = new BeanWrapperImpl(extension);\n                return beanWrapper.getPropertyValue(property);\n            };\n            var comparator =\n                Comparator.comparing(function, nullsComparator(direction.isAscending()));\n            if (direction.isDescending()) {\n                comparator = comparator.reversed();\n            }\n            return comparator;\n        });\n        return Stream.concat(comparatorStream, fallbackComparator)\n            .reduce(Comparator::thenComparing)\n            .orElse(null);\n    }\n\n    public static void buildParameters(Builder builder) {\n        IListRequest.buildParameters(builder);\n        builder.parameter(QueryParamBuildUtil.sortParameter());\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport java.util.Objects;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.index.query.Query;\n\npublic record FieldSelector(@Nullable Query query) {\n\n    public FieldSelector(Query query) {\n        this.query = Objects.requireNonNullElseGet(query, Queries::empty);\n    }\n\n    public static FieldSelector of(Query query) {\n        return new FieldSelector(query);\n    }\n\n    public static FieldSelector all() {\n        return new FieldSelector(Queries.empty());\n    }\n\n    public FieldSelector andQuery(Query other) {\n        Assert.isInstanceOf(Condition.class, other, \"Only Condition query is supported\");\n        Assert.isInstanceOf(Condition.class, query, \"Only Condition query is supported\");\n        return of(((Condition) query).and((Condition) other));\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/FieldSelectorConverter.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport java.util.Set;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.Queries;\n\npublic class FieldSelectorConverter implements Converter<SelectorCriteria, Condition> {\n\n    @NonNull\n    @Override\n    public Condition convert(@NonNull SelectorCriteria criteria) {\n        var key = criteria.key();\n        // compatible with old field selector\n        if (\"name\".equals(key)) {\n            key = \"metadata.name\";\n        }\n        switch (criteria.operator()) {\n            case Equals -> {\n                return Queries.equal(key, getSingleValue(criteria));\n            }\n            case NotEquals -> {\n                return Queries.notEqual(key, getSingleValue(criteria));\n            }\n            // compatible with old field selector\n            case IN -> {\n                Set<String> valueArr = defaultIfNull(criteria.values(), Set.of());\n                return Queries.in(key, valueArr);\n            }\n            default -> throw new IllegalArgumentException(\n                \"Unsupported operator: \" + criteria.operator());\n        }\n    }\n\n    String getSingleValue(SelectorCriteria criteria) {\n        if (CollectionUtils.isEmpty(criteria.values())) {\n            throw new IllegalArgumentException(\"No value present for label key: \" + criteria.key());\n        }\n        return criteria.values().iterator().next();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.LabelCondition;\nimport run.halo.app.extension.index.query.Queries;\n\n@Data\n@Accessors(chain = true)\npublic class LabelSelector {\n\n    private List<LabelCondition> conditions;\n\n    @Override\n    public String toString() {\n        if (CollectionUtils.isEmpty(conditions)) {\n            return Condition.empty().toString();\n        }\n        return conditions.stream()\n            .map(c -> (Condition) c)\n            .reduce(Condition::and)\n            .orElseGet(Condition::empty)\n            .toString();\n    }\n\n    /**\n     * Returns a new label selector that is the result of ANDing the current selector with the\n     * given selector.\n     *\n     * @param other the selector to AND with\n     * @return a new label selector\n     */\n    public LabelSelector and(LabelSelector other) {\n        var labelSelector = new LabelSelector();\n        var newConditions = new ArrayList<LabelCondition>();\n        newConditions.addAll(this.conditions);\n        newConditions.addAll(other.conditions);\n        labelSelector.setConditions(newConditions);\n        return labelSelector;\n    }\n\n    public static LabelSelectorBuilder<?> builder() {\n        return new LabelSelectorBuilder<>();\n    }\n\n    public static class LabelSelectorBuilder<T extends LabelSelectorBuilder<T>> {\n        private final List<LabelCondition> conditions = new ArrayList<>();\n\n        public LabelSelectorBuilder() {\n        }\n\n        /**\n         * Create a new label selector builder with the given matchers.\n         */\n        public LabelSelectorBuilder(List<LabelCondition> conditions) {\n            if (conditions != null) {\n                this.conditions.addAll(conditions);\n            }\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private T self() {\n            return (T) this;\n        }\n\n        public T eq(String key, String value) {\n            conditions.add(Queries.labelEqual(key, value));\n            return self();\n        }\n\n        public T notEq(String key, String value) {\n            conditions.add(Queries.labelEqual(key, value).not());\n            return self();\n        }\n\n        public T in(String key, String... values) {\n            conditions.add(Queries.labelIn(key, Arrays.asList(values)));\n            return self();\n        }\n\n        public T notIn(String key, String... values) {\n            conditions.add(Queries.labelIn(key, Arrays.asList(values)).not());\n            return self();\n        }\n\n        public T exists(String key) {\n            conditions.add(Queries.labelExists(key));\n            return self();\n        }\n\n        public T notExists(String key) {\n            conditions.add(Queries.labelExists(key).not());\n            return self();\n        }\n\n        /**\n         * Build the label selector.\n         */\n        public LabelSelector build() {\n            var labelSelector = new LabelSelector();\n            labelSelector.setConditions(conditions);\n            return labelSelector;\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/LabelSelectorConverter.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport java.util.Set;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.index.query.LabelCondition;\nimport run.halo.app.extension.index.query.Queries;\n\npublic class LabelSelectorConverter implements Converter<SelectorCriteria, LabelCondition> {\n\n    @NonNull\n    @Override\n    public LabelCondition convert(@NonNull SelectorCriteria criteria) {\n        switch (criteria.operator()) {\n            case Equals -> {\n                return Queries.labelEqual(criteria.key(), getSingleValue(criteria));\n            }\n            case NotEquals -> {\n                return Queries.labelEqual(criteria.key(), getSingleValue(criteria)).not();\n            }\n            case NotExist -> {\n                return Queries.labelExists(criteria.key()).not();\n            }\n            case Exist -> {\n                return Queries.labelExists(criteria.key());\n            }\n            case IN -> {\n                return Queries.labelIn(criteria.key(), defaultIfNull(criteria.values(), Set.of()));\n            }\n            default ->\n                throw new IllegalArgumentException(\"Unsupported operator: \" + criteria.operator());\n        }\n    }\n\n    String getSingleValue(SelectorCriteria criteria) {\n        if (CollectionUtils.isEmpty(criteria.values())) {\n            throw new IllegalArgumentException(\"No value present for label key: \" + criteria.key());\n        }\n        return criteria.values().iterator().next();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/Operator.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport java.util.Set;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.lang.Nullable;\n\npublic enum Operator implements Converter<String, SelectorCriteria> {\n\n    Equals(\"=\", 3) {\n        @Override\n        @Nullable\n        public SelectorCriteria convert(@Nullable String selector) {\n            if (preFlightCheck(selector, 3)) {\n                var i = selector.indexOf(getOperator());\n                if (i > 0 && (i + getOperator().length()) <= selector.length() - 1) {\n                    String key = selector.substring(0, i);\n                    String value = selector.substring(i + getOperator().length());\n                    return new SelectorCriteria(key, this, Set.of(value));\n                }\n            }\n            return null;\n        }\n    },\n    IN(\"=(\", 2) {\n        @Override\n        public SelectorCriteria convert(String selector) {\n            if (preFlightCheck(selector, 5)) {\n                var idx = selector.indexOf(getOperator());\n                if (idx > 0 && (idx + getOperator().length()) < selector.length() - 2\n                    && selector.charAt(selector.length() - 1) == ')') {\n                    var key = selector.substring(0, idx);\n                    var valuesString =\n                        selector.substring(idx + getOperator().length(), selector.length() - 1);\n                    String[] values = valuesString.split(\",\");\n                    return new SelectorCriteria(key, this, Set.of(values));\n                }\n            }\n            return null;\n        }\n    },\n    NotEquals(\"!=\", 1) {\n        @Override\n        @Nullable\n        public SelectorCriteria convert(@Nullable String selector) {\n            if (preFlightCheck(selector, 4)) {\n                var i = selector.indexOf(getOperator());\n                if (i > 0 && (i + getOperator().length()) < selector.length()) {\n                    String key = selector.substring(0, i);\n                    String value = selector.substring(i + getOperator().length());\n                    return new SelectorCriteria(key, this, Set.of(value));\n                }\n            }\n            return null;\n        }\n    },\n    NotExist(\"!\", 0) {\n        @Override\n        @Nullable\n        public SelectorCriteria convert(@Nullable String selector) {\n            if (preFlightCheck(selector, 2)) {\n                if (selector.startsWith(getOperator())) {\n                    return new SelectorCriteria(selector.substring(1), this, Set.of());\n                }\n            }\n            return null;\n        }\n    },\n    Exist(\"\", Integer.MAX_VALUE) {\n        @Override\n        public SelectorCriteria convert(String selector) {\n            if (preFlightCheck(selector, 1)) {\n                // TODO validate the source with regex in the future\n                return new SelectorCriteria(selector, this, Set.of());\n            }\n            return null;\n        }\n    };\n    private final String operator;\n\n    /**\n     * Parse order.\n     */\n    private final int order;\n\n    Operator(String operator, int order) {\n        this.operator = operator;\n        this.order = order;\n    }\n\n    public String getOperator() {\n        return operator;\n    }\n\n    public int getOrder() {\n        return order;\n    }\n\n    protected boolean preFlightCheck(String selector, int minLength) {\n        return selector != null && selector.length() >= minLength;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/SelectorConverter.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.Objects;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.lang.Nullable;\n\n@Slf4j\npublic class SelectorConverter implements Converter<String, SelectorCriteria> {\n\n    @Override\n    @Nullable\n    public SelectorCriteria convert(@Nullable String selector) {\n        return Arrays.stream(Operator.values())\n            .sorted(Comparator.comparing(Operator::getOrder))\n            .map(operator -> {\n                log.debug(\"Resolving selector: {} with operator: {}\", selector, operator);\n                return operator.convert(selector);\n            })\n            .filter(Objects::nonNull)\n            .findFirst()\n            .orElse(null);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/SelectorCriteria.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport java.util.Set;\n\npublic record SelectorCriteria(String key, Operator operator, Set<String> values) {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.index.query.Condition;\n\npublic final class SelectorUtil {\n\n    private SelectorUtil() {\n    }\n\n    /**\n     * Convert label and field selector expressions to {@link ListOptions}.\n     *\n     * @param labelSelectorTerms label selector expressions\n     * @param fieldSelectorTerms field selector expressions\n     * @return list options(never null)\n     */\n    public static ListOptions labelAndFieldSelectorToListOptions(\n        List<String> labelSelectorTerms, List<String> fieldSelectorTerms) {\n        var selectorConverter = new SelectorConverter();\n\n        var labelConverter = new LabelSelectorConverter();\n        var labelConditions = Optional.ofNullable(labelSelectorTerms)\n            .map(selectors -> selectors.stream()\n                .map(selectorConverter::convert)\n                .filter(Objects::nonNull)\n                .map(labelConverter::convert)\n                .toList())\n            .orElse(List.of());\n\n        var fieldConverter = new FieldSelectorConverter();\n        var fieldCondition = Optional.ofNullable(fieldSelectorTerms)\n            .map(selectors -> selectors.stream()\n                .map(selectorConverter::convert)\n                .filter(Objects::nonNull)\n                .map(fieldConverter::convert)\n                .reduce(Condition::and)\n                .orElse(null)\n            )\n            .orElse(null);\n        var listOptions = new ListOptions();\n        listOptions.setLabelSelector(new LabelSelector().setConditions(labelConditions));\n        if (fieldCondition != null) {\n            listOptions.setFieldSelector(FieldSelector.of(fieldCondition));\n        } else {\n            listOptions.setFieldSelector(FieldSelector.all());\n        }\n        return listOptions;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/AnonymousUserConst.java",
    "content": "package run.halo.app.infra;\n\npublic interface AnonymousUserConst {\n    String PRINCIPAL = \"anonymousUser\";\n\n    String Role = \"anonymous\";\n\n    static boolean isAnonymousUser(String principal) {\n        return PRINCIPAL.equals(principal);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/BackupRootGetter.java",
    "content": "package run.halo.app.infra;\n\nimport java.nio.file.Path;\nimport java.util.function.Supplier;\n\n/**\n * Utility of getting backup root path.\n *\n * @author johnniang\n * @since 2.9.0\n */\npublic interface BackupRootGetter extends Supplier<Path> {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/Condition.java",
    "content": "package run.halo.app.infra;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.NoArgsConstructor;\n\n/**\n * EqualsAndHashCode 排除了lastTransitionTime否则失败时，lastTransitionTime 会被更新\n * 导致 equals 为 false，一直被加入队列.\n *\n * @author guqing\n * @see\n * <a href=\"https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions\">pod-conditions</a>\n * @since 2.0.0\n */\n@Data\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\n@EqualsAndHashCode(exclude = \"lastTransitionTime\")\npublic class Condition {\n    /**\n     * type of condition in CamelCase or in foo.example.com/CamelCase.\n     * example: Ready, Initialized.\n     * maxLength: 316.\n     */\n    @Schema(requiredMode = REQUIRED, maxLength = 316,\n        pattern = \"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(\"\n            + \"([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$\")\n    private String type;\n\n    /**\n     * Status is the status of the condition. Can be True, False, Unknown.\n     */\n    @Schema(requiredMode = REQUIRED)\n    private ConditionStatus status;\n\n    /**\n     * Last time the condition transitioned from one status to another.\n     */\n    @Schema(requiredMode = REQUIRED)\n    private Instant lastTransitionTime;\n\n    /**\n     * Human-readable message indicating details about last transition.\n     * This may be an empty string.\n     */\n    @Schema(maxLength = 32768)\n    @Builder.Default\n    private String message = \"\";\n\n    /**\n     * Unique, one-word, CamelCase reason for the condition's last transition.\n     */\n    @Schema(maxLength = 1024,\n        pattern = \"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$\")\n    @Builder.Default\n    private String reason = \"\";\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/ConditionList.java",
    "content": "package run.halo.app.infra;\n\nimport java.util.AbstractCollection;\nimport java.util.Deque;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.Objects;\nimport java.util.function.Consumer;\nimport org.springframework.lang.NonNull;\n\n/**\n * <p>This {@link ConditionList} to stores multiple {@link Condition}.</p>\n * <p>The element added after is always the first, the first to be removed is always the first to\n * be added.</p>\n * <p>The queue head is the one whose element index is 0</p>\n * Note that: this class is not thread-safe.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class ConditionList extends AbstractCollection<Condition> {\n    private static final int EVICT_THRESHOLD = 20;\n    private final Deque<Condition> conditions = new LinkedList<>();\n\n    @Override\n    public boolean add(@NonNull Condition condition) {\n        if (isSame(conditions.peekFirst(), condition)) {\n            return false;\n        }\n        return conditions.add(condition);\n    }\n\n    public boolean addFirst(@NonNull Condition condition) {\n        if (isSame(conditions.peekFirst(), condition)) {\n            return false;\n        }\n        conditions.addFirst(condition);\n        return true;\n    }\n\n    /**\n     * Add {@param #condition} and evict the first item if the size of conditions is greater than\n     * {@link #EVICT_THRESHOLD}.\n     *\n     * @param condition item to add\n     */\n    public boolean addAndEvictFIFO(@NonNull Condition condition) {\n        return addAndEvictFIFO(condition, EVICT_THRESHOLD);\n    }\n\n    /**\n     * Add {@param #condition} and evict the first item if the size of conditions is greater than\n     * {@param evictThreshold}.\n     *\n     * @param condition item to add\n     */\n    public boolean addAndEvictFIFO(@NonNull Condition condition, int evictThreshold) {\n        var current = getCondition(condition.getType());\n        if (current != null) {\n            // do not update last transition time if status is not changed\n            if (Objects.equals(condition.getStatus(), current.getStatus())) {\n                condition.setLastTransitionTime(current.getLastTransitionTime());\n            }\n        }\n\n        conditions.remove(current);\n        conditions.addFirst(condition);\n\n        while (conditions.size() > evictThreshold) {\n            removeLast();\n        }\n        return true;\n    }\n\n    private Condition getCondition(String type) {\n        for (Condition condition : conditions) {\n            if (condition.getType().equals(type)) {\n                return condition;\n            }\n        }\n        return null;\n    }\n\n\n    public void remove(Condition condition) {\n        conditions.remove(condition);\n    }\n\n    /**\n     * Retrieves, but does not remove, the head of the queue represented by\n     * this deque (in other words, the first element of this deque), or\n     * returns {@code null} if this deque is empty.\n     *\n     * <p>This method is equivalent to {@link #peekFirst()}.\n     *\n     * @return the head of the queue represented by this deque, or\n     * {@code null} if this deque is empty\n     */\n    public Condition peek() {\n        return peekFirst();\n    }\n\n    public Condition peekFirst() {\n        return conditions.peekFirst();\n    }\n\n    public Condition removeLast() {\n        return conditions.removeLast();\n    }\n\n    @Override\n    public void clear() {\n        conditions.clear();\n    }\n\n    public int size() {\n        return conditions.size();\n    }\n\n    private boolean isSame(Condition a, Condition b) {\n        if (a == null || b == null) {\n            return false;\n        }\n        return Objects.equals(a.getType(), b.getType())\n            && Objects.equals(a.getStatus(), b.getStatus())\n            && Objects.equals(a.getReason(), b.getReason())\n            && Objects.equals(a.getMessage(), b.getMessage());\n    }\n\n    @Override\n    public Iterator<Condition> iterator() {\n        return conditions.iterator();\n    }\n\n    @Override\n    public void forEach(Consumer<? super Condition> action) {\n        conditions.forEach(action);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        ConditionList that = (ConditionList) o;\n        return Objects.equals(conditions, that.conditions);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(conditions);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/ConditionStatus.java",
    "content": "package run.halo.app.infra;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic enum ConditionStatus {\n    TRUE,\n    FALSE,\n    UNKNOWN\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/ExternalLinkProcessor.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.URI;\nimport org.springframework.http.HttpRequest;\nimport reactor.core.publisher.Mono;\n\n/**\n * {@link ExternalLinkProcessor} to process an in-site link to an external link.\n *\n * @author guqing\n * @see ExternalUrlSupplier\n * @since 2.9.0\n */\npublic interface ExternalLinkProcessor {\n\n    /**\n     * If the link is in-site link, then process it to an external link with\n     * {@link ExternalUrlSupplier#getRaw()}, otherwise return the original link.\n     *\n     * @param link link to process\n     * @return processed link or original link\n     */\n    String processLink(String link);\n\n    /**\n     * Process the URI to an external URL.\n     * <p>\n     * If the URI is an in-site link, then process it to an external link with\n     * {@link ExternalUrlSupplier#getRaw()} or {@link ExternalUrlSupplier#getURL(HttpRequest)},\n     * otherwise return the original URI.\n     * </p>\n     *\n     * @param uri uri to process\n     * @return processed URI or original URI\n     */\n    Mono<URI> processLink(URI uri);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/ExternalUrlSupplier.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.function.Supplier;\nimport javax.annotation.Nullable;\nimport org.springframework.http.HttpRequest;\n\n/**\n * Represents a supplier of external url configuration.\n *\n * @author johnniang\n */\npublic interface ExternalUrlSupplier extends Supplier<URI> {\n\n    /**\n     * Gets URI according to external URL and use-absolute-permalink properties.\n     *\n     * @return URI \"/\" returned if use-absolute-permalink is false. Or external URL will be\n     * returned.(never null)\n     */\n    @Override\n    URI get();\n\n    /**\n     * Gets URL according to external URL and server request URL.\n     *\n     * @param request represents an HTTP request message, consisting of a method and a URI.\n     * @return External URL will be return if it is provided, or request URI will be returned.\n     * (never null)\n     */\n    URL getURL(HttpRequest request);\n\n    /**\n     * Gets user-configured external URL from <code>HaloProperties#getExternalUrl()</code>.\n     *\n     * @return user-configured external URL or null if it is not provided.\n     */\n    @Nullable\n    URL getRaw();\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/FileCategoryMatcher.java",
    "content": "package run.halo.app.infra;\n\nimport java.util.Set;\n\n/**\n * <p>Classifies files based on their MIME types.</p>\n * <p>It provides different categories such as IMAGE, SVG, AUDIO, VIDEO, ARCHIVE, and DOCUMENT.\n * Each category has a <code>match</code> method that checks if a given MIME type belongs to that\n * category.</p>\n * <p>The categories are defined as follows:</p>\n * <pre>\n * - IMAGE: Matches all image MIME types except for SVG.\n * - SVG: Specifically matches the SVG image MIME type.\n * - AUDIO: Matches all audio MIME types.\n * - VIDEO: Matches all video MIME types.\n * - ARCHIVE: Matches common archive MIME types like zip, rar, tar, etc.\n * - DOCUMENT: Matches common document MIME types like plain text, PDF, Word, Excel, etc.\n * </pre>\n *\n * @author guqing\n * @since 2.18.0\n */\npublic enum FileCategoryMatcher {\n    ALL {\n        @Override\n        public boolean match(String mimeType) {\n            return true;\n        }\n    },\n    IMAGE {\n        @Override\n        public boolean match(String mimeType) {\n            return mimeType.startsWith(\"image/\") && !mimeType.equals(\"image/svg+xml\");\n        }\n    },\n    SVG {\n        @Override\n        public boolean match(String mimeType) {\n            return mimeType.equals(\"image/svg+xml\");\n        }\n    },\n    AUDIO {\n        @Override\n        public boolean match(String mimeType) {\n            return mimeType.startsWith(\"audio/\");\n        }\n    },\n    VIDEO {\n        @Override\n        public boolean match(String mimeType) {\n            return mimeType.startsWith(\"video/\");\n        }\n    },\n    ARCHIVE {\n        static final Set<String> ARCHIVE_MIME_TYPES = Set.of(\n            \"application/zip\",\n            \"application/x-rar-compressed\",\n            \"application/x-tar\",\n            \"application/gzip\",\n            \"application/x-bzip2\",\n            \"application/x-xz\",\n            \"application/x-7z-compressed\"\n        );\n\n        @Override\n        public boolean match(String mimeType) {\n            return ARCHIVE_MIME_TYPES.contains(mimeType);\n        }\n    },\n    DOCUMENT {\n        static final Set<String> DOCUMENT_MIME_TYPES = Set.of(\n            \"text/plain\",\n            \"application/rtf\",\n            \"text/csv\",\n            \"text/xml\",\n            \"application/pdf\",\n            \"application/msword\",\n            \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n            \"application/vnd.ms-excel\",\n            \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n            \"application/vnd.ms-powerpoint\",\n            \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n            \"application/vnd.oasis.opendocument.text\",\n            \"application/vnd.oasis.opendocument.spreadsheet\",\n            \"application/vnd.oasis.opendocument.presentation\"\n        );\n\n        @Override\n        public boolean match(String mimeType) {\n            return DOCUMENT_MIME_TYPES.contains(mimeType);\n        }\n    };\n\n    public abstract boolean match(String mimeType);\n\n    /**\n     * Get the file category matcher by name.\n     */\n    public static FileCategoryMatcher of(String name) {\n        for (var matcher : values()) {\n            if (matcher.name().equalsIgnoreCase(name)) {\n                return matcher;\n            }\n        }\n        throw new IllegalArgumentException(\"Unsupported file category matcher for name: \" + name);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/SystemInfo.java",
    "content": "package run.halo.app.infra;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.net.URL;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\n\n@Data\n@Accessors(chain = true)\npublic class SystemInfo {\n    private String title;\n\n    private String subtitle;\n\n    private String logo;\n\n    private String favicon;\n\n    private URL url;\n\n    private Version version;\n\n    private SeoProp seo;\n\n    private Locale locale;\n\n    private TimeZone timeZone;\n\n    private String activatedThemeName;\n\n    @Data\n    @Accessors(chain = true)\n    public static class SeoProp {\n        private boolean blockSpiders;\n        private String keywords;\n        private String description;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/SystemInfoGetter.java",
    "content": "package run.halo.app.infra;\n\nimport java.util.function.Supplier;\nimport reactor.core.publisher.Mono;\n\npublic interface SystemInfoGetter extends Supplier<Mono<SystemInfo>> {\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/SystemSetting.java",
    "content": "package run.halo.app.infra;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport jakarta.validation.constraints.NotBlank;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.boot.convert.ApplicationConversionService;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * TODO Optimization value acquisition.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class SystemSetting {\n    public static final String SYSTEM_CONFIG_DEFAULT = \"system-default\";\n    public static final String SYSTEM_CONFIG = \"system\";\n\n    @Builder\n    public record Attachment(\n\n        @Nullable\n        UploadOptions console,\n\n        @Nullable\n        UploadOptions uc,\n\n        @Nullable\n        UploadOptions comment,\n\n        @Nullable\n        UploadOptions avatar\n\n    ) {\n\n        public static final String GROUP = \"attachment\";\n\n        @Builder\n        public record UploadOptions(\n\n            @Nullable\n            String groupName,\n\n            @NotBlank\n            String policyName\n\n        ) {\n        }\n\n    }\n\n    @Data\n    public static class Theme {\n        public static final String GROUP = \"theme\";\n\n        private String active;\n    }\n\n    @Data\n    public static class ThemeRouteRules {\n        public static final String GROUP = \"routeRules\";\n\n        private boolean disableThemePreview;\n        private String categories;\n        private String archives;\n        private String post;\n        private String tags;\n\n        public static ThemeRouteRules empty() {\n            ThemeRouteRules rules = new ThemeRouteRules();\n            rules.setPost(\"/archives/{slug}\");\n            rules.setArchives(\"/archives\");\n            rules.setTags(\"/tags\");\n            rules.setCategories(\"/categories\");\n            return rules;\n        }\n    }\n\n    @Data\n    public static class CodeInjection {\n        public static final String GROUP = \"codeInjection\";\n\n        private String globalHead;\n\n        private String contentHead;\n\n        private String footer;\n    }\n\n    @Data\n    public static class Basic {\n        public static final String GROUP = \"basic\";\n        String title;\n        String subtitle;\n        String logo;\n        String favicon;\n        String language;\n        String externalUrl;\n\n        @JsonIgnore\n        public Optional<Locale> useSystemLocale() {\n            return Optional.ofNullable(language)\n                .filter(StringUtils::isNotBlank)\n                .map(Locale::forLanguageTag);\n        }\n    }\n\n    @Data\n    public static class User {\n        public static final String GROUP = \"user\";\n        boolean allowRegistration;\n        boolean mustVerifyEmailOnRegistration;\n        String defaultRole;\n\n        /**\n         * @deprecated since 2.22.0, use {@link Attachment} instead.\n         */\n        @Deprecated(since = \"2.22.0\")\n        String avatarPolicy;\n\n        /**\n         * @deprecated since 2.22.0, use {@link Attachment} instead.\n         */\n        @Deprecated(since = \"2.22.0\")\n        String ucAttachmentPolicy;\n\n        String protectedUsernames;\n    }\n\n    @Data\n    public static class Post {\n        public static final String GROUP = \"post\";\n        Integer postPageSize;\n        Integer archivePageSize;\n        Integer categoryPageSize;\n        Integer tagPageSize;\n        Integer authorPageSize;\n        Boolean review;\n        String slugGenerationStrategy;\n\n        String attachmentPolicyName;\n        String attachmentGroupName;\n    }\n\n    @Data\n    public static class Seo {\n        public static final String GROUP = \"seo\";\n        Boolean blockSpiders;\n        String keywords;\n        String description;\n    }\n\n    @Data\n    public static class Comment {\n        public static final String GROUP = \"comment\";\n        Boolean enable;\n        Boolean requireReviewForNew;\n        Boolean systemUserOnly;\n    }\n\n    @Data\n    public static class Menu {\n        public static final String GROUP = \"menu\";\n        public String primary;\n    }\n\n    @Data\n    public static class AuthProvider {\n        public static final String GROUP = \"authProvider\";\n\n        private List<AuthProviderState> states;\n\n    }\n\n    @Data\n    @Accessors(chain = true)\n    public static class AuthProviderState {\n        private String name;\n        private boolean enabled;\n        private int priority;\n    }\n\n    /**\n     * ExtensionPointEnabled key is metadata name of extension point and value is a list of\n     * extension definition names.\n     */\n    public static class ExtensionPointEnabled extends LinkedHashMap<String, LinkedHashSet<String>> {\n\n        public static final String GROUP = \"extensionPointEnabled\";\n\n    }\n\n    @Nullable\n    public static <T> T get(Map<String, String> data, String key, Class<T> type) {\n        var valueString = data.get(key);\n        if (valueString == null) {\n            return null;\n        }\n        var conversionService = ApplicationConversionService.getSharedInstance();\n        if (conversionService.canConvert(String.class, type)) {\n            return conversionService.convert(valueString, type);\n        }\n        return JsonUtils.jsonToObject(valueString, type);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/SystemVersionSupplier.java",
    "content": "package run.halo.app.infra;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.util.function.Supplier;\n\n/**\n * The supplier to gets the project version.\n * If it cannot be obtained, return 0.0.0.\n *\n * @author guqing\n * @see <a href=\"https://semver.org\">Semantic Versioning 2.0.0</a>\n * @since 2.0.0\n */\npublic interface SystemVersionSupplier extends Supplier<Version> {\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/ValidationUtils.java",
    "content": "package run.halo.app.infra;\n\nimport java.util.regex.Pattern;\nimport lombok.experimental.UtilityClass;\nimport org.springframework.context.i18n.LocaleContextHolder;\nimport org.springframework.validation.BeanPropertyBindingResult;\nimport org.springframework.validation.BindingResult;\nimport org.springframework.validation.Validator;\nimport org.springframework.web.server.ServerWebExchange;\n\n@UtilityClass\npublic class ValidationUtils {\n    public static final String NAME_REGEX =\n        \"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\";\n    public static final Pattern NAME_PATTERN = Pattern.compile(NAME_REGEX);\n\n    /**\n     * {@code A-Z, a-z, 0-9, !@#$%^&*.?} are allowed.\n     */\n    public static final String PASSWORD_REGEX = \"^[A-Za-z0-9!@#$%^&*.?]+$\";\n\n    public static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_REGEX);\n\n    /**\n     * Validate the target object with given locale context.\n     */\n    public static BindingResult validate(Object target, String objectName,\n        Validator validator, ServerWebExchange exchange) {\n        BindingResult bindingResult = new BeanPropertyBindingResult(target, objectName);\n        try {\n            LocaleContextHolder.setLocaleContext(exchange.getLocaleContext());\n            validator.validate(target, bindingResult);\n            return bindingResult;\n        } finally {\n            LocaleContextHolder.resetLocaleContext();\n        }\n    }\n\n    public static BindingResult validate(Object target, Validator validator,\n        ServerWebExchange exchange) {\n        return validate(target, \"form\", validator, exchange);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/model/License.java",
    "content": "package run.halo.app.infra.model;\n\nimport lombok.Data;\n\n/**\n * Common data objects for license.\n */\n@Data\npublic class License {\n    private String name;\n    private String url;\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/utils/FileTypeDetectUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.List;\nimport lombok.experimental.UtilityClass;\nimport org.apache.tika.detect.DefaultDetector;\nimport org.apache.tika.detect.Detector;\nimport org.apache.tika.metadata.Metadata;\nimport org.apache.tika.metadata.TikaCoreProperties;\nimport org.apache.tika.mime.MimeTypeException;\nimport org.apache.tika.mime.MimeTypes;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\n\n@UtilityClass\npublic class FileTypeDetectUtils {\n\n    private static final Detector detector = new DefaultDetector();\n\n    /**\n     * <p>Detects the media type of the given document.</p>\n     * <p>The type detection is based on the content of the given document stream and the name of\n     * the document.</p>\n     *\n     * @param inputStream the document stream must not be null\n     * @throws IOException if the stream can not be read\n     */\n    public static String detectMimeType(InputStream inputStream, String name) throws IOException {\n        Assert.notNull(name, \"The name of the document must not be null\");\n        var metadata = new Metadata();\n        metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, name);\n        return doDetectMimeType(inputStream, metadata);\n    }\n\n    /**\n     * Detect mime type.\n     *\n     * @param inputStream input stream will be closed after detection, must not be null\n     */\n    public static String detectMimeType(InputStream inputStream) throws IOException {\n        return doDetectMimeType(inputStream, new Metadata());\n    }\n\n    private static String doDetectMimeType(InputStream inputStream, Metadata metadata)\n        throws IOException {\n        Assert.notNull(inputStream, \"The inputStream must not be null\");\n        try (var stream = (!inputStream.markSupported()\n            ? new BufferedInputStream(inputStream) : inputStream)) {\n            return detector.detect(stream, metadata).toString();\n        }\n    }\n\n    public static String detectFileExtension(String mimeType) throws MimeTypeException {\n        MimeTypes mimeTypes = MimeTypes.getDefaultMimeTypes();\n        return mimeTypes.forName(mimeType).getExtension();\n    }\n\n    public static List<String> detectFileExtensions(String mimeType) throws MimeTypeException {\n        MimeTypes mimeTypes = MimeTypes.getDefaultMimeTypes();\n        return mimeTypes.forName(mimeType).getExtensions();\n    }\n\n    /**\n     * <p>Get file extension from file name.</p>\n     * <p>The obtained file extension is in lowercase and includes the dot, such as \".jpg\".</p>\n     */\n    @NonNull\n    public static String getFileExtension(String fileName) {\n        Assert.notNull(fileName, \"The fileName must not be null\");\n        int lastDot = fileName.lastIndexOf(\".\");\n        if (lastDot > 0) {\n            return fileName.substring(lastDot).toLowerCase();\n        }\n        return \"\";\n    }\n\n    /**\n     * <p>Recommend to use this method to verify whether the file extension matches the file type\n     * after matching the file type to avoid XSS attacks such as bypassing detection by polyglot\n     * file.</p>\n     *\n     * @param mimeType file mime type,such as \"image/png\"\n     * @param fileName file name,such as \"test.png\"\n     * @see\n     * <a href=\"https://github.com/halo-dev/halo/security/advisories/GHSA-99mc-ch53-pqh9\">CVE Stored XSS</a>\n     * @see <a href=\"https://github.com/halo-dev/halo/pull/7149\">gh-7149</a>\n     */\n    public boolean isValidExtensionForMime(String mimeType, String fileName) {\n        Assert.notNull(mimeType, \"The mimeType must not be null\");\n        Assert.notNull(fileName, \"The fileName must not be null\");\n        String fileExtension = getFileExtension(fileName);\n        try {\n            List<String> detectedExtByMime = detectFileExtensions(mimeType);\n            return detectedExtByMime.stream().anyMatch(ext -> ext.equalsIgnoreCase(fileExtension));\n        } catch (MimeTypeException e) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/utils/GenericClassUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.util.function.Supplier;\nimport net.bytebuddy.ByteBuddy;\nimport net.bytebuddy.description.type.TypeDescription;\n\npublic enum GenericClassUtils {\n    ;\n\n    /**\n     * Generate concrete class of generic class. e.g.: {@code List<String>}\n     *\n     * @param rawClass is generic class, like {@code List.class}\n     * @param parameterType is parameter type of generic class\n     * @param <T> parameter type\n     * @return generated class\n     */\n    public static <T> Class<?> generateConcreteClass(Class<?> rawClass, Class<T> parameterType) {\n        return generateConcreteClass(rawClass, parameterType, () ->\n            parameterType.getName() + rawClass.getSimpleName());\n    }\n\n    /**\n     * Generate concrete class of generic class. e.g.: {@code List<String>}\n     *\n     * @param rawClass is generic class, like {@code List.class}\n     * @param parameterType is parameter type of generic class\n     * @param nameGenerator is generated class name\n     * @param <T> parameter type\n     * @return generated class\n     */\n    public static <T> Class<?> generateConcreteClass(Class<?> rawClass, Class<T> parameterType,\n        Supplier<String> nameGenerator) {\n        var concreteType =\n            TypeDescription.Generic.Builder.parameterizedType(rawClass, parameterType).build();\n        try (var unloaded = new ByteBuddy()\n            .subclass(concreteType)\n            .name(nameGenerator.get())\n            .make()) {\n            return unloaded.load(parameterType.getClassLoader()).getLoaded();\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/utils/JsonParseException.java",
    "content": "package run.halo.app.infra.utils;\n\n/**\n * {@link JsonParseException} thrown when source JSON is invalid.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class JsonParseException extends RuntimeException {\n    public JsonParseException() {\n        super();\n    }\n\n    public JsonParseException(String message) {\n        super(message);\n    }\n\n    public JsonParseException(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n    public JsonParseException(Throwable cause) {\n        super(cause);\n    }\n\n    protected JsonParseException(String message, Throwable cause, boolean enableSuppression,\n        boolean writableStackTrace) {\n        super(message, cause, enableSuppression, writableStackTrace);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/utils/JsonUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport io.swagger.v3.core.util.Json;\nimport java.util.Map;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\n\n/**\n * Json utilities.\n *\n * @author guqing\n * @see JavaTimeModule\n * @since 2.0.0\n * @deprecated Use {@link tools.jackson.databind.json.JsonMapper} directly instead.\n */\n@Deprecated(forRemoval = true, since = \"2.23.0\")\npublic class JsonUtils {\n    public static final ObjectMapper DEFAULT_JSON_MAPPER = Json.mapper();\n\n    private JsonUtils() {\n    }\n\n    public static ObjectMapper mapper() {\n        return DEFAULT_JSON_MAPPER;\n    }\n\n    /**\n     * Converts a map to the object specified type.\n     *\n     * @param sourceMap source map must not be empty\n     * @param type object type must not be null\n     * @param <T> target object type\n     * @return the object specified type\n     */\n    @NonNull\n    public static <T> T mapToObject(@NonNull Map<String, ?> sourceMap, @NonNull Class<T> type) {\n        return DEFAULT_JSON_MAPPER.convertValue(sourceMap, type);\n    }\n\n    /**\n     * Converts object to json format.\n     *\n     * @param source source object must not be null\n     * @return json format of the source object\n     */\n    @NonNull\n    public static String objectToJson(@NonNull Object source) {\n        Assert.notNull(source, \"Source object must not be null\");\n        try {\n            return DEFAULT_JSON_MAPPER.writeValueAsString(source);\n        } catch (JsonProcessingException e) {\n            throw new JsonParseException(e);\n        }\n    }\n\n    /**\n     * Method to deserialize JSON content from given JSON content String.\n     *\n     * @param json json content\n     * @param toValueType object type to convert\n     * @param <T> real type to convert\n     * @return converted object\n     */\n    public static <T> T jsonToObject(String json, Class<T> toValueType) {\n        try {\n            return DEFAULT_JSON_MAPPER.readValue(json, toValueType);\n        } catch (Exception e) {\n            throw new JsonParseException(e);\n        }\n    }\n\n    /**\n     * Method to deserialize JSON content from given JSON content String.\n     *\n     * @param json json content\n     * @param typeReference type reference to convert\n     * @param <T> real type to convert\n     * @return converted object\n     */\n    public static <T> T jsonToObject(String json, TypeReference<T> typeReference) {\n        try {\n            return DEFAULT_JSON_MAPPER.readValue(json, typeReference);\n        } catch (Exception e) {\n            throw new JsonParseException(e);\n        }\n    }\n\n    /**\n     * Method to deserialize JSON content and serialize back from given Object.\n     *\n     * @param source source object to copy\n     * @param <T> real type to deep copy\n     * @return deep copy of the source object\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T deepCopy(T source) {\n        try {\n            return (T) DEFAULT_JSON_MAPPER.readValue(objectToJson(source), source.getClass());\n        } catch (JsonProcessingException e) {\n            throw new JsonParseException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/infra/utils/PathUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport lombok.experimental.UtilityClass;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\n\n/**\n * Http path manipulation tool class.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@UtilityClass\npublic class PathUtils {\n\n    /**\n     * Every HTTP URL conforms to the syntax of a generic URI. The URI generic syntax consists of\n     * components organized hierarchically in order of decreasing significance from left to\n     * right:\n     * <pre>\n     * URI = scheme \":\" [\"//\" authority] path [\"?\" query] [\"#\" fragment]\n     * </pre>\n     * The authority component consists of subcomponents:\n     * <pre>\n     * authority = [userinfo \"@\"] host [\":\" port]\n     * </pre>\n     * Examples of popular schemes include http, https, ftp, mailto, file, data and irc. URI\n     * schemes should be registered with the\n     * <a href=\"https://en.wikipedia.org/wiki/Internet_Assigned_Numbers_Authority\">Internet Assigned Numbers Authority (IANA)</a>, although\n     * non-registered schemes are used in practice.\n     *\n     * @param uriString url or path\n     * @return true if the linkBase is absolute, otherwise false\n     * @see <a href=\"https://en.wikipedia.org/wiki/URL\">URL</a>\n     */\n    public static boolean isAbsoluteUri(final String uriString) {\n        if (StringUtils.isBlank(uriString)) {\n            return false;\n        }\n        try {\n            URI uri = new URI(uriString);\n            return uri.isAbsolute();\n        } catch (URISyntaxException e) {\n            log.debug(\"Failed to parse uri: \" + uriString, e);\n            // ignore this exception\n            return false;\n        }\n    }\n\n    /**\n     * Combine paths based on the passed in path segments parameters.\n     * <br/><br/>\n     * This method doesn't work for Windows system currently.\n     *\n     * @param pathSegments Path segments to be combined\n     * @return the combined path\n     */\n    public static String combinePath(String... pathSegments) {\n        StringBuilder sb = new StringBuilder();\n        for (String path : pathSegments) {\n            if (path == null) {\n                continue;\n            }\n            String s = path.startsWith(\"/\") ? path : \"/\" + path;\n            String segment = s.endsWith(\"/\") ? s.substring(0, s.length() - 1) : s;\n            sb.append(segment);\n        }\n        return sb.toString();\n    }\n\n\n    /**\n     * <p>Append a {@code '/'} if the path does not end with a {@code '/'}.</p>\n     * Examples are as follows:\n     * <pre>\n     *     PathUtils.appendPathSeparatorIfMissing(\"hello\") -> hello/\n     *     PathUtils.appendPathSeparatorIfMissing(\"some-path/\") -> some-path/\n     *     PathUtils.appendPathSeparatorIfMissing(null) -> null\n     * </pre>\n     *\n     * @param path a path\n     * @return A new String if suffix was appended, the same string otherwise.\n     */\n    public static String appendPathSeparatorIfMissing(String path) {\n        return StringUtils.appendIfMissing(path, \"/\", \"/\");\n    }\n\n    /**\n     * <p>Remove the regex in the path pattern placeholder.</p>\n     * <p>For example: </p>\n     * <ul>\n     * <li>'{@code /{year:\\d{4}}/{month:\\d{2}}}' &rarr; '{@code /{year}/{month}}'</li>\n     * <li>'{@code /archives/{year:\\d{4}}/{month:\\d{2}}}' &rarr; '{@code /archives/{year}/{month}\n     * }'</li>\n     * <li>'{@code /archives/{year:\\d{4}}/{slug}}' &rarr; '{@code /archives/{year}/{slug}}'</li>\n     * </ul>\n     *\n     * @param pattern path pattern\n     * @return Simplified path pattern\n     */\n    public static String simplifyPathPattern(String pattern) {\n        if (StringUtils.isBlank(pattern)) {\n            return StringUtils.EMPTY;\n        }\n        String[] parts = StringUtils.split(pattern, '/');\n        for (int i = 0; i < parts.length; i++) {\n            String part = parts[i];\n            if (part.startsWith(\"{\") && part.endsWith(\"}\")) {\n                int colonIdx = part.indexOf(':');\n                if (colonIdx != -1) {\n                    parts[i] = part.substring(0, colonIdx) + part.charAt(part.length() - 1);\n                }\n\n            }\n        }\n        return combinePath(parts);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/migration/Backup.java",
    "content": "package run.halo.app.migration;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"migration.halo.run\", version = \"v1alpha1\", kind = \"Backup\",\n    plural = \"backups\", singular = \"backup\")\npublic class Backup extends AbstractExtension {\n\n    private Spec spec = new Spec();\n\n    private Status status = new Status();\n\n    @Data\n    @Schema(name = \"BackupSpec\")\n    public static class Spec {\n\n        @Schema(description = \"Backup file format. Currently, only zip format is supported.\")\n        private String format;\n\n        private Instant expiresAt;\n\n    }\n\n    @Data\n    @Schema(name = \"BackupStatus\")\n    public static class Status {\n\n        private Phase phase = Phase.PENDING;\n\n        private Instant startTimestamp;\n\n        private Instant completionTimestamp;\n\n        private String failureReason;\n\n        private String failureMessage;\n\n        /**\n         * Size of backup file. Data unit: byte\n         */\n        private Long size;\n\n        /**\n         * Name of backup file.\n         */\n        private String filename;\n    }\n\n    public enum Phase {\n        PENDING,\n        RUNNING,\n        SUCCEEDED,\n        FAILED,\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/migration/Constant.java",
    "content": "package run.halo.app.migration;\n\npublic enum Constant {\n    ;\n\n    public static final String GROUP = \"migration.halo.run\";\n\n    public static final String VERSION = \"v1alpha1\";\n\n    public static final String HOUSE_KEEPER_FINALIZER = \"housekeeper\";\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/notification/NotificationCenter.java",
    "content": "package run.halo.app.notification;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.Subscription;\n\n/**\n * Notification center to notify and manage notifications.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface NotificationCenter {\n\n    /**\n     * Notifies the subscriber with the given reason.\n     *\n     * @param reason reason to notify\n     */\n    Mono<Void> notify(Reason reason);\n\n    /**\n     * Subscribes to the given subject with the given reason.\n     *\n     * @param subscriber subscriber to subscribe to\n     * @param reason interest reason to subscribe\n     * @return a subscription\n     */\n    Mono<Subscription> subscribe(Subscription.Subscriber subscriber,\n        Subscription.InterestReason reason);\n\n    /**\n     * Unsubscribes by the given subject.\n     *\n     * @param subscriber subscriber to unsubscribe\n     */\n    Mono<Void> unsubscribe(Subscription.Subscriber subscriber);\n\n    /**\n     * Unsubscribes by the given subject and reason.\n     *\n     * @param subscriber subscriber to unsubscribe\n     * @param reason reason to unsubscribe\n     */\n    Mono<Void> unsubscribe(Subscription.Subscriber subscriber, Subscription.InterestReason reason);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/notification/NotificationContext.java",
    "content": "package run.halo.app.notification;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.time.Instant;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\npublic class NotificationContext {\n\n    private Message message;\n\n    private ObjectNode receiverConfig;\n\n    private ObjectNode senderConfig;\n\n    @Data\n    public static class Message {\n        private MessagePayload payload;\n\n        private Subject subject;\n\n        private String recipient;\n\n        private Instant timestamp;\n    }\n\n    @Data\n    @Builder\n    public static class Subject {\n        private String apiVersion;\n        private String kind;\n        private String name;\n        private String title;\n        private String url;\n    }\n\n    @Data\n    public static class MessagePayload {\n        private String title;\n\n        private String rawBody;\n\n        private String htmlBody;\n\n        private ReasonAttributes attributes;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/notification/NotificationReasonEmitter.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.function.Consumer;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Reason;\n\n/**\n * {@link NotificationReasonEmitter} to emit notification reason.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface NotificationReasonEmitter {\n\n    /**\n     * Emit a {@link Reason} with {@link ReasonPayload}.\n     *\n     * @param reasonType reason type to emitter must not be blank\n     * @param reasonData reason data must not be null\n     */\n    Mono<Void> emit(String reasonType, Consumer<ReasonPayload.ReasonPayloadBuilder> reasonData);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/notification/ReactiveNotifier.java",
    "content": "package run.halo.app.notification;\n\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Mono;\n\n/**\n * Notifier to notify user.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface ReactiveNotifier extends ExtensionPoint {\n\n    /**\n     * Notify user.\n     *\n     * @param context notification context must not be null\n     */\n    Mono<Void> notify(NotificationContext context);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/notification/ReasonAttributes.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.HashMap;\n\n/**\n * <p>{@link ReasonAttributes} is a map that stores the attributes of the reason.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\npublic class ReasonAttributes extends HashMap<String, Object> {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/notification/ReasonPayload.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport run.halo.app.core.extension.notification.Reason;\n\n/**\n * A value object to hold reason payload.\n *\n * @author guqing\n * @see Reason\n * @since 2.10.0\n */\n@Data\n@AllArgsConstructor\npublic class ReasonPayload {\n    private Reason.Subject subject;\n    private final UserIdentity author;\n    private Map<String, Object> attributes;\n\n    public static ReasonPayloadBuilder builder() {\n        return new ReasonPayloadBuilder();\n    }\n\n    public static class ReasonPayloadBuilder {\n        private Reason.Subject subject;\n        private UserIdentity author;\n        private final Map<String, Object> attributes;\n\n        ReasonPayloadBuilder() {\n            this.attributes = new HashMap<>();\n        }\n\n        public ReasonPayloadBuilder subject(Reason.Subject subject) {\n            this.subject = subject;\n            return this;\n        }\n\n        public ReasonPayloadBuilder attribute(String key, Object value) {\n            this.attributes.put(key, value);\n            return this;\n        }\n\n        public ReasonPayloadBuilder attributes(Map<String, Object> attributes) {\n            this.attributes.putAll(attributes);\n            return this;\n        }\n\n        public ReasonPayloadBuilder author(UserIdentity author) {\n            this.author = author;\n            return this;\n        }\n\n        public ReasonPayload build() {\n            return new ReasonPayload(subject, author, attributes);\n        }\n    }\n}"
  },
  {
    "path": "api/src/main/java/run/halo/app/notification/UserIdentity.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.Optional;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.util.Assert;\nimport run.halo.app.infra.AnonymousUserConst;\n\n/**\n * Identity for user.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic record UserIdentity(String name) {\n    public static final String SEPARATOR = \"#\";\n\n    /**\n     * Create identity with username to identify a user.\n     *\n     * @param username username\n     * @return identity\n     */\n    public static UserIdentity of(String username) {\n        return new UserIdentity(username);\n    }\n\n    /**\n     * <p>Create identity with email to identify a user,\n     * the name will be {@code anonymousUser#email}.</p>\n     * <p>An anonymous user can not be identified by username so we use email to identify it.</p>\n     *\n     * @param email email\n     * @return identity\n     */\n    public static UserIdentity anonymousWithEmail(String email) {\n        Assert.notNull(email, \"Email must not be null\");\n        String name = AnonymousUserConst.PRINCIPAL + SEPARATOR + email;\n        return of(name);\n    }\n\n    public boolean isAnonymous() {\n        return name().startsWith(AnonymousUserConst.PRINCIPAL + SEPARATOR);\n    }\n\n    /**\n     * Gets email if the identity is an anonymous user.\n     *\n     * @return email if the identity is an anonymous user, otherwise empty\n     */\n    public Optional<String> getEmail() {\n        if (isAnonymous()) {\n            return Optional.of(name().substring(name().indexOf(SEPARATOR) + 1))\n                .filter(StringUtils::isNotBlank);\n        }\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/ApiVersion.java",
    "content": "package run.halo.app.plugin;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Api version.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface ApiVersion {\n\n    /**\n     * Api version value.\n     *\n     * @return api version string\n     */\n    String value();\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/BasePlugin.java",
    "content": "package run.halo.app.plugin;\n\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.Plugin;\n\n/**\n * This class will be extended by all plugins and serve as the common class between a plugin and\n * the application.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Getter\n@Slf4j\npublic class BasePlugin extends Plugin {\n\n    protected PluginContext context;\n\n    /**\n     * Constructor a plugin with the given plugin context.\n     *\n     * @param pluginContext plugin context must not be null.\n     */\n    public BasePlugin(PluginContext pluginContext) {\n        this.context = pluginContext;\n    }\n\n    public BasePlugin() {\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/PluginConfigUpdatedEvent.java",
    "content": "package run.halo.app.plugin;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport java.util.Map;\nimport lombok.Builder;\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.ConfigMap;\n\n/**\n * <p>Event that is triggered when the {@link ConfigMap } represented by\n * {@link Plugin.PluginSpec#getConfigMapName()} in the {@link Plugin} is updated.</p>\n * <p>has two properties, oldConfig and newConfig, which represent the {@link ConfigMap#getData()}\n * property value of the {@link ConfigMap}.</p>\n *\n * @author guqing\n * @since 2.17.0\n */\n@Getter\npublic class PluginConfigUpdatedEvent extends ApplicationEvent {\n\n    /**\n     * Old configuration data.\n     *\n     * @deprecated Use {@link #oldSettingValues} and {@link #newSettingValues} instead.\n     */\n    @Deprecated(forRemoval = true, since = \"2.23.0\")\n    private final Map<String, JsonNode> oldConfig;\n\n    /**\n     * New configuration data.\n     *\n     * @deprecated Use {@link #oldSettingValues} and {@link #newSettingValues} instead.\n     */\n    @Deprecated(forRemoval = true, since = \"2.23.0\")\n    private final Map<String, JsonNode> newConfig;\n\n    /**\n     * Old setting values.\n     */\n    private final Map<String, tools.jackson.databind.JsonNode> oldSettingValues;\n\n    /**\n     * New setting values.\n     */\n    private final Map<String, tools.jackson.databind.JsonNode> newSettingValues;\n\n    @Builder\n    public PluginConfigUpdatedEvent(\n        Object source,\n        Map<String, JsonNode> oldConfig,\n        Map<String, JsonNode> newConfig,\n        Map<String, tools.jackson.databind.JsonNode> oldSettingValues,\n        Map<String, tools.jackson.databind.JsonNode> newSettingValues\n    ) {\n        super(source);\n        this.oldConfig = oldConfig;\n        this.newConfig = newConfig;\n        this.oldSettingValues = oldSettingValues;\n        this.newSettingValues = newSettingValues;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/PluginContext.java",
    "content": "package run.halo.app.plugin;\n\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport org.pf4j.RuntimeMode;\n\n/**\n * <p>This class will provide a context for the plugin, which will be used to store some\n * information about the plugin.</p>\n * <p>An instance of this class is provided to plugins in their constructor.</p>\n * <p>It's safe for plugins to keep a reference to the instance for later use.</p>\n * <p>This class facilitates communication with application and plugin manager.</p>\n * <p>Pf4j recommends that you use a custom PluginContext instead of PluginWrapper.</p>\n * <a href=\"https://github.com/pf4j/pf4j/blob/e4d7c7b9ea0c9a32179c3e33da1403228838944f/pf4j/src/main/java/org/pf4j/Plugin.java#L46\">Use application custom PluginContext instead of PluginWrapper</a>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Getter\n@Builder\n@RequiredArgsConstructor\npublic class PluginContext {\n    private final String name;\n\n    private final String configMapName;\n\n    private final String version;\n\n    private final RuntimeMode runtimeMode;\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/PluginsRootGetter.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Path;\nimport java.util.function.Supplier;\n\n/**\n * An interface to get the root path of plugins.\n *\n * @author johnniang\n * @since 2.18.0\n */\npublic interface PluginsRootGetter extends Supplier<Path> {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/ReactiveSettingFetcher.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.Map;\nimport reactor.core.publisher.Mono;\nimport tools.jackson.databind.JsonNode;\n\n/**\n * The {@link ReactiveSettingFetcher} to help plugin fetch own setting configuration.\n *\n * @author guqing\n * @since 2.4.0\n */\npublic interface ReactiveSettingFetcher {\n\n    <T> Mono<T> fetch(String group, Class<T> clazz);\n\n    @Deprecated(forRemoval = true, since = \"2.23.0\")\n    Mono<com.fasterxml.jackson.databind.JsonNode> get(String group);\n\n    /**\n     * Get setting value by group.\n     *\n     * @param group the setting group\n     * @return the setting value or empty if not found\n     */\n    Mono<JsonNode> getSettingValue(String group);\n\n    @Deprecated(forRemoval = true, since = \"2.23.0\")\n    Mono<Map<String, com.fasterxml.jackson.databind.JsonNode>> getValues();\n\n    /**\n     * Get all setting values.\n     *\n     * @return all setting values, never empty\n     */\n    Mono<Map<String, JsonNode>> getSettingValues();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/SettingFetcher.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport run.halo.app.extension.ConfigMap;\nimport tools.jackson.databind.JsonNode;\n\n/**\n * SettingFetcher must be a class instead of an interface due to backward compatibility.\n *\n * @author johnniang\n */\npublic interface SettingFetcher {\n\n    <T> Optional<T> fetch(String group, Class<T> clazz);\n\n    /**\n     * Get values from {@link ConfigMap} by group.\n     *\n     * @param group the setting group\n     * @return the setting value(non-null)\n     * @deprecated use {@link #getSettingValue(String)} instead\n     */\n    @Deprecated(forRemoval = true, since = \"2.23.0\")\n    com.fasterxml.jackson.databind.JsonNode get(String group);\n\n    /**\n     * Get setting value by group.\n     *\n     * @param group the setting group\n     * @return the setting value(non-null)\n     */\n    JsonNode getSettingValue(String group);\n\n    /**\n     * Get values from {@link ConfigMap}.\n     *\n     * @return a unmodifiable map of values(non-null)\n     * @deprecated use {@link #getSettingValues()} instead\n     */\n    @Deprecated(forRemoval = true, since = \"2.23.0\")\n    Map<String, com.fasterxml.jackson.databind.JsonNode> getValues();\n\n    /**\n     * Get all setting values.\n     *\n     * @return all setting values, never null\n     */\n    Map<String, JsonNode> getSettingValues();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/SharedEvent.java",
    "content": "package run.halo.app.plugin;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * <p>It is a symbolic annotation.</p>\n * <p>When the event marked with {@link SharedEvent} annotation is published, it will be\n * broadcast to the application context of the plugin.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface SharedEvent {\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/event/PluginStartedEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * The event that is published when a plugin is really started, and is only for plugin internal use.\n *\n * @author johnniang\n * @since 2.17.0\n */\npublic class PluginStartedEvent extends ApplicationEvent {\n\n    public PluginStartedEvent(Object source) {\n        super(source);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionGetter.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport java.util.List;\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\npublic interface ExtensionGetter {\n\n    /**\n     * Get only one enabled extension from system configuration.\n     *\n     * @param extensionPoint is extension point class.\n     * @return implementation of the corresponding extension point. If no configuration is found,\n     * we will use the default implementation from application context instead.\n     */\n    <T extends ExtensionPoint> Mono<T> getEnabledExtension(Class<T> extensionPoint);\n\n    /**\n     * Get the extension(s) according to the {@code ExtensionPointDefinition} queried\n     * by incoming extension point class.\n     *\n     * @param extensionPoint extension point class\n     * @return implementations of the corresponding extension point.\n     * @throws IllegalArgumentException if the incoming extension point class does not have\n     *                                  the {@code ExtensionPointDefinition}.\n     */\n    <T extends ExtensionPoint> Flux<T> getEnabledExtensions(Class<T> extensionPoint);\n\n    /**\n     * Get all extensions according to extension point class.\n     *\n     * @param extensionPointClass extension point class\n     * @param <T> type of extension point\n     * @return a bunch of extension points.\n     */\n    <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPointClass);\n\n    /**\n     * Get all extensions according to extension point class.\n     *\n     * @param extensionPointClass extension point class\n     * @param <T> type of extension point\n     * @return a bunch of extension points.\n     */\n    <T extends ExtensionPoint> List<T> getExtensionList(Class<T> extensionPointClass);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/HaloDocument.java",
    "content": "package run.halo.app.search;\n\nimport jakarta.validation.constraints.NotBlank;\nimport jakarta.validation.constraints.PastOrPresent;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.Data;\n\n/**\n * Document for search.\n */\n@Data\npublic final class HaloDocument {\n\n    /**\n     * Document ID. It should be unique globally.\n     */\n    @NotBlank\n    private String id;\n\n    /**\n     * Metadata name of the corresponding extension.\n     */\n    @NotBlank\n    private String metadataName;\n\n    /**\n     * Custom metadata. Make sure the map is serializable.\n     */\n    private Map<String, String> annotations;\n\n    /**\n     * Document title.\n     */\n    @NotBlank\n    private String title;\n\n    /**\n     * Document description.\n     */\n    private String description;\n\n    /**\n     * Document content. Safety content, without HTML tag.\n     */\n    @NotBlank\n    private String content;\n\n    /**\n     * Document categories. The item in the list is the category metadata name.\n     */\n    private List<String> categories;\n\n    /**\n     * Document tags. The item in the list is the tag metadata name.\n     */\n    private List<String> tags;\n\n    /**\n     * Whether the document is published.\n     */\n    private boolean published;\n\n    /**\n     * Whether the document is recycled.\n     */\n    private boolean recycled;\n\n    /**\n     * Whether the document is exposed to the public.\n     */\n    private boolean exposed;\n\n    /**\n     * Document owner metadata name.\n     */\n    @NotBlank\n    private String ownerName;\n\n    /**\n     * Document creation timestamp.\n     */\n    @PastOrPresent\n    private Instant creationTimestamp;\n\n    /**\n     * Document update timestamp.\n     */\n    @PastOrPresent\n    private Instant updateTimestamp;\n\n    /**\n     * Document permalink.\n     */\n    @NotBlank\n    private String permalink;\n\n    /**\n     * Document type. e.g.: post.content.halo.run, singlepage.content.halo.run, moment.moment\n     * .halo.run, doc.doc.halo.run.\n     */\n    @NotBlank\n    private String type;\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/HaloDocumentsProvider.java",
    "content": "package run.halo.app.search;\n\nimport org.pf4j.ExtensionPoint;\nimport reactor.core.publisher.Flux;\n\n/**\n * Halo documents provider. This interface is used to rebuild the search index.\n *\n * @author johnniang\n */\npublic interface HaloDocumentsProvider extends ExtensionPoint {\n\n    /**\n     * Fetch all halo documents.\n     *\n     * @return all halo documents\n     */\n    Flux<HaloDocument> fetchAll();\n\n    /**\n     * Get type of documents.\n     *\n     * @return type of documents\n     */\n    String getType();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/SearchEngine.java",
    "content": "package run.halo.app.search;\n\nimport org.pf4j.ExtensionPoint;\n\n/**\n * Search engine is used to index and search halo documents. Meanwhile, it is also an extension\n * point for adding different search engine implementations.\n *\n * @author johnniang\n */\npublic interface SearchEngine extends ExtensionPoint {\n\n    /**\n     * Whether the search engine is available.\n     *\n     * @return true if available, false otherwise\n     */\n    boolean available();\n\n    /**\n     * Add or update halo documents.\n     *\n     * @param haloDocuments halo documents\n     */\n    void addOrUpdate(Iterable<HaloDocument> haloDocuments);\n\n    /**\n     * Delete halo documents by ids.\n     *\n     * @param haloDocIds halo document ids\n     */\n    void deleteDocument(Iterable<String> haloDocIds);\n\n    /**\n     * Delete all halo documents.\n     */\n    void deleteAll();\n\n    /**\n     * Search halo documents.\n     *\n     * @param option search option\n     * @return search result of halo documents\n     */\n    SearchResult search(SearchOption option);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/SearchOption.java",
    "content": "package run.halo.app.search;\n\nimport jakarta.validation.constraints.Max;\nimport jakarta.validation.constraints.Min;\nimport jakarta.validation.constraints.NotBlank;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.Data;\n\n/**\n * Search option. It is used to control search behavior.\n *\n * @author johnniang\n */\n@Data\npublic class SearchOption {\n    /**\n     * Search keyword.\n     */\n    @NotBlank\n    private String keyword;\n\n    /**\n     * Limit of result.\n     */\n    @Min(1)\n    @Max(1000)\n    private int limit = 10;\n\n    /**\n     * Pre HTML tag of highlighted fragment.\n     */\n    private String highlightPreTag = \"<B>\";\n\n    /**\n     * Post HTML tag of highlighted fragment.\n     */\n    private String highlightPostTag = \"</B>\";\n\n    /**\n     * Whether to filter exposed content. If null, it will not filter.\n     */\n    private Boolean filterExposed;\n\n    /**\n     * Whether to filter recycled content. If null, it will not filter.\n     */\n    private Boolean filterRecycled;\n\n    /**\n     * Whether to filter published content. If null, it will not filter.\n     */\n    private Boolean filterPublished;\n\n    /**\n     * Types to include(or). If null, it will include all types.\n     */\n    private List<String> includeTypes;\n\n    /**\n     * Owner names to include(or). If null, it will include all owners.\n     */\n    private List<String> includeOwnerNames;\n\n    /**\n     * Category names to include(and). If null, it will include all categories.\n     */\n    private List<String> includeCategoryNames;\n\n    /**\n     * Tag names to include(and). If null, it will include all tags.\n     */\n    private List<String> includeTagNames;\n\n    /**\n     * Additional annotations for extending search option by other search engines.\n     */\n    private Map<String, String> annotations;\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/SearchResult.java",
    "content": "package run.halo.app.search;\n\nimport java.util.List;\nimport lombok.Data;\n\n@Data\npublic class SearchResult {\n    private List<HaloDocument> hits;\n    private String keyword;\n    private Long total;\n    private int limit;\n    private long processingTimeMillis;\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/SearchService.java",
    "content": "package run.halo.app.search;\n\nimport reactor.core.publisher.Mono;\n\n/**\n * Search service is used to search content.\n *\n * @author johnniang\n * @since 2.17.0\n */\npublic interface SearchService {\n\n    /**\n     * Perform search.\n     *\n     * @param option search option must not be null\n     * @return search result\n     */\n    Mono<SearchResult> search(SearchOption option);\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/event/HaloDocumentAddRequestEvent.java",
    "content": "package run.halo.app.search.event;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.plugin.SharedEvent;\nimport run.halo.app.search.HaloDocument;\n\n@SharedEvent\npublic class HaloDocumentAddRequestEvent extends ApplicationEvent {\n\n    private final Iterable<HaloDocument> documents;\n\n    public HaloDocumentAddRequestEvent(Object source, Iterable<HaloDocument> documents) {\n        super(source);\n        this.documents = documents;\n    }\n\n    public Iterable<HaloDocument> getDocuments() {\n        return documents;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/event/HaloDocumentDeleteRequestEvent.java",
    "content": "package run.halo.app.search.event;\n\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.lang.Nullable;\nimport run.halo.app.plugin.SharedEvent;\n\n@SharedEvent\npublic class HaloDocumentDeleteRequestEvent extends ApplicationEvent {\n\n    private final Iterable<String> docIds;\n\n    /**\n     * Construct a new {@code HaloDocumentDeleteRequestEvent} instance.\n     *\n     * @param source The source of the event.\n     * @param docIds If the document IDs are not provided, all documents will be deleted.\n     */\n    public HaloDocumentDeleteRequestEvent(Object source, @Nullable Iterable<String> docIds) {\n        super(source);\n        this.docIds = docIds;\n    }\n\n    public Iterable<String> getDocIds() {\n        return docIds;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/search/event/HaloDocumentRebuildRequestEvent.java",
    "content": "package run.halo.app.search.event;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.plugin.SharedEvent;\n\n@SharedEvent\npublic class HaloDocumentRebuildRequestEvent extends ApplicationEvent {\n\n    public HaloDocumentRebuildRequestEvent(Object source) {\n        super(source);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/AdditionalWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.core.Ordered;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Contract for interception-style, chained processing of Web requests that may be used to\n * implement cross-cutting, application-agnostic requirements such as security, timeouts, and\n * others.\n *\n * @author guqing\n * @since 2.4.0\n */\npublic interface AdditionalWebFilter extends WebFilter, ExtensionPoint, Ordered {\n\n    /**\n     * Gets the order value of the object.\n     *\n     * @return the order value\n     */\n    default int getOrder() {\n        return Ordered.LOWEST_PRECEDENCE;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/AfterSecurityWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Security web filter for after security.\n *\n * @author johnniang\n * @since 2.18\n */\npublic interface AfterSecurityWebFilter extends WebFilter, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/AnonymousAuthenticationSecurityWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Security web filter for anonymous authentication.\n *\n * @author johnniang\n */\npublic interface AnonymousAuthenticationSecurityWebFilter extends WebFilter, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/AuthenticationSecurityWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Security web filter for normal authentication.\n *\n * @author johnniang\n */\npublic interface AuthenticationSecurityWebFilter extends WebFilter, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/BeforeSecurityWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Security web filter for before security.\n *\n * @author johnniang\n * @since 2.18\n */\npublic interface BeforeSecurityWebFilter extends WebFilter, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/FormLoginSecurityWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Security web filter for form login.\n *\n * @author johnniang\n */\npublic interface FormLoginSecurityWebFilter extends WebFilter, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/HttpBasicSecurityWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Security web filter for HTTP basic.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface HttpBasicSecurityWebFilter extends WebFilter, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/LoginHandlerEnhancer.java",
    "content": "package run.halo.app.security;\n\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>Halo uses this interface to enhance the processing of login success, such as device management\n * and remember me, etc. The login method of the plugin extension needs to call this interface in\n * the processing method of login success to ensure the normal operation of some enhanced\n * functions.</p>\n *\n * @author guqing\n * @since 2.17.0\n */\npublic interface LoginHandlerEnhancer {\n\n    /**\n     * Invoked when login success.\n     *\n     * @param exchange The exchange.\n     * @param successfulAuthentication The successful authentication.\n     */\n    Mono<Void> onLoginSuccess(ServerWebExchange exchange, Authentication successfulAuthentication);\n\n    /**\n     * Invoked when login fails.\n     *\n     * @param exchange The exchange.\n     * @param exception the reason authentication failed\n     */\n    Mono<Void> onLoginFailure(ServerWebExchange exchange, AuthenticationException exception);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/OAuth2AuthorizationCodeSecurityWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.web.server.WebFilter;\n\n/**\n * Security web filter for OAuth2 authorization code.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface OAuth2AuthorizationCodeSecurityWebFilter extends WebFilter, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/PersonalAccessToken.java",
    "content": "package run.halo.app.security;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"security.halo.run\", version = \"v1alpha1\", kind = PersonalAccessToken.KIND,\n    plural = \"personalaccesstokens\", singular = \"personalaccesstoken\")\npublic class PersonalAccessToken extends AbstractExtension {\n\n    public static final String KIND = \"PersonalAccessToken\";\n\n    public static final String PAT_TOKEN_PREFIX = \"pat_\";\n\n    private Spec spec = new Spec();\n\n    @Data\n    @Schema(name = \"PatSpec\")\n    public static class Spec {\n\n        @Schema(requiredMode = REQUIRED)\n        private String name;\n\n        private String description;\n\n        private Instant expiresAt;\n\n        private List<String> roles;\n\n        private List<String> scopes;\n\n        @Schema(requiredMode = REQUIRED)\n        private String username;\n\n        private boolean revoked;\n\n        private Instant revokesAt;\n\n        private Instant lastUsed;\n\n        @Schema(requiredMode = REQUIRED)\n        private String tokenId;\n\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/authentication/CryptoService.java",
    "content": "package run.halo.app.security.authentication;\n\nimport com.nimbusds.jose.jwk.JWK;\nimport reactor.core.publisher.Mono;\n\npublic interface CryptoService {\n\n    /**\n     * Decrypts message with Base64 format.\n     *\n     * @param encryptedMessage is a byte array containing encrypted message.\n     * @return decrypted message.\n     */\n    Mono<byte[]> decrypt(byte[] encryptedMessage);\n\n    /**\n     * Reads public key.\n     *\n     * @return byte array of public key\n     */\n    Mono<byte[]> readPublicKey();\n\n    /**\n     * Gets key ID of private key.\n     *\n     * @return key ID of private key.\n     */\n    String getKeyId();\n\n    /**\n     * Gets JSON Web Keys.\n     *\n     * @return JSON Web Keys\n     */\n    JWK getJwk();\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticationManager.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.security.authentication.ReactiveAuthenticationManager;\n\n/**\n * An extension point for username password authentication.\n * Any non-authentication exception occurs, the default authentication will be used.\n * If you want to skip authentication, please return Mono.empty() directly, the default\n * authentication will be used.\n *\n * @author johnniang\n * @since 2.8\n */\npublic interface UsernamePasswordAuthenticationManager\n    extends ReactiveAuthenticationManager, ExtensionPoint {\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/authentication/oauth2/HaloOAuth2AuthenticationToken.java",
    "content": "package run.halo.app.security.authentication.oauth2;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport lombok.Getter;\nimport org.springframework.security.authentication.AbstractAuthenticationToken;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.core.user.OAuth2User;\n\n/**\n * Halo OAuth2 authentication token which combines {@link UserDetails} and original\n * {@link OAuth2AuthenticationToken}.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class HaloOAuth2AuthenticationToken extends AbstractAuthenticationToken {\n\n    @Getter\n    private final UserDetails userDetails;\n\n    @Getter\n    private final OAuth2AuthenticationToken original;\n\n    /**\n     * Constructs an {@code HaloOAuth2AuthenticationToken} using {@link UserDetails} and original\n     * {@link OAuth2AuthenticationToken}.\n     *\n     * @param userDetails the {@link UserDetails}\n     * @param original the original {@link OAuth2AuthenticationToken}\n     */\n    public HaloOAuth2AuthenticationToken(UserDetails userDetails,\n        OAuth2AuthenticationToken original) {\n        super(combineAuthorities(userDetails, original));\n        this.userDetails = userDetails;\n        this.original = original;\n        setAuthenticated(true);\n    }\n\n    @Override\n    public String getName() {\n        return userDetails.getUsername();\n    }\n\n    @Override\n    public Collection<GrantedAuthority> getAuthorities() {\n        var originalAuthorities = super.getAuthorities();\n        var userDetailsAuthorities = getUserDetails().getAuthorities();\n        var authorities = new ArrayList<GrantedAuthority>(\n            originalAuthorities.size() + userDetailsAuthorities.size()\n        );\n        authorities.addAll(originalAuthorities);\n        authorities.addAll(userDetailsAuthorities);\n        return Collections.unmodifiableList(authorities);\n    }\n\n    @Override\n    public Object getCredentials() {\n        return \"\";\n    }\n\n    @Override\n    public OAuth2User getPrincipal() {\n        return original.getPrincipal();\n    }\n\n    /**\n     * Creates an authenticated {@link HaloOAuth2AuthenticationToken} using {@link UserDetails} and\n     * original {@link OAuth2AuthenticationToken}.\n     *\n     * @param userDetails the {@link UserDetails}\n     * @param original the original {@link OAuth2AuthenticationToken}\n     * @return an authenticated {@link HaloOAuth2AuthenticationToken}\n     */\n    public static HaloOAuth2AuthenticationToken authenticated(\n        UserDetails userDetails, OAuth2AuthenticationToken original\n    ) {\n        return new HaloOAuth2AuthenticationToken(userDetails, original);\n    }\n\n    private static Collection<? extends GrantedAuthority> combineAuthorities(\n        UserDetails userDetails, OAuth2AuthenticationToken original) {\n        var userDetailsAuthorities = userDetails.getAuthorities();\n        var originalAuthorities = original.getAuthorities();\n        var authorities = new ArrayList<GrantedAuthority>(\n            originalAuthorities.size() + userDetailsAuthorities.size()\n        );\n        authorities.addAll(originalAuthorities);\n        authorities.addAll(userDetailsAuthorities);\n        return Collections.unmodifiableList(authorities);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/security/device/DeviceService.java",
    "content": "package run.halo.app.security.device;\n\nimport org.springframework.security.core.Authentication;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\npublic interface DeviceService {\n\n    Mono<Void> loginSuccess(ServerWebExchange exchange, Authentication successfullAuthentication);\n\n    Mono<Void> changeSessionId(ServerWebExchange exchange);\n\n    Mono<Void> revoke(String principalName, String deviceId);\n\n    Mono<Void> revoke(String username);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/Constant.java",
    "content": "package run.halo.app.theme;\n\n/**\n * This class holds constants related to the theme.\n *\n * @author johnniang\n */\npublic enum Constant {\n    ;\n\n    /**\n     * The name of the variable that holds the SEO meta description.\n     */\n    public static final String META_DESCRIPTION_VARIABLE_NAME = \"seoMetaDescription\";\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/ReactivePostContentHandler.java",
    "content": "package run.halo.app.theme;\n\nimport lombok.Builder;\nimport lombok.Data;\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\n\n/**\n * <p>{@link ReactivePostContentHandler} provides a way to extend the content to be displayed in\n * the theme.</p>\n * Plugins can implement this interface to extend the content to be displayed in the theme,\n * including but not limited to adding specific styles, JS libraries, inserting specific content,\n * and intercepting content.\n *\n * @author guqing\n * @since 2.7.0\n */\npublic interface ReactivePostContentHandler extends ExtensionPoint {\n\n    /**\n     * <p>Methods for handling {@link run.halo.app.core.extension.content.Post} content.</p>\n     * <p>For example, you can use this method to change the content for a better display in\n     * theme-side.</p>\n     *\n     * @param postContent content to be handled\n     * @return handled content\n     */\n    Mono<PostContentContext> handle(@NonNull PostContentContext postContent);\n\n    @Data\n    @Builder\n    class PostContentContext {\n        private Post post;\n        private String content;\n        private String raw;\n        private String rawType;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/ReactiveSinglePageContentHandler.java",
    "content": "package run.halo.app.theme;\n\nimport lombok.Builder;\nimport lombok.Data;\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\n\n/**\n * <p>{@link ReactiveSinglePageContentHandler} provides a way to extend the content to be\n * displayed in the theme.</p>\n *\n * @author guqing\n * @see ReactivePostContentHandler\n * @since 2.7.0\n */\npublic interface ReactiveSinglePageContentHandler extends ExtensionPoint {\n\n    /**\n     * <p>Methods for handling {@link run.halo.app.core.extension.content.SinglePage} content.</p>\n     * <p>For example, you can use this method to change the content for a better display in\n     * theme-side.</p>\n     *\n     * @param singlePageContent content to be handled\n     * @return handled content\n     */\n    Mono<SinglePageContentContext> handle(@NonNull SinglePageContentContext singlePageContent);\n\n    @Data\n    @Builder\n    class SinglePageContentContext {\n        private SinglePage singlePage;\n        private String content;\n        private String raw;\n        private String rawType;\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/TemplateNameResolver.java",
    "content": "package run.halo.app.theme;\n\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>The {@link TemplateNameResolver} is used to resolve template name.</p>\n * <code>Halo</code> has a theme mechanism, template files are provided by different themes, so\n * we need a method to determine whether the template file exists in the activated theme and if\n * it does not exist, provide a default template name.\n *\n * @author guqing\n * @since 2.11.0\n */\npublic interface TemplateNameResolver {\n\n    /**\n     * Resolve template name if exists or default template name in classpath.\n     *\n     * @param exchange exchange to resolve theme to use\n     * @param name template\n     * @return template name if exists or default template name in classpath\n     */\n    Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name);\n\n    /**\n     * Resolve template name if exists or default template given.\n     *\n     * @param exchange exchange to resolve theme to use\n     * @param name template name\n     * @param defaultName default template name to use if given template name not exists\n     * @return template name if exists or default template name given\n     */\n    Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name,\n        String defaultName);\n\n    /**\n     * Determine whether the template file exists in the current theme.\n     *\n     * @param exchange exchange to resolve theme to use\n     * @param name template name\n     * @return <code>true</code> if the template file exists in the current theme, false otherwise\n     */\n    Mono<Boolean> isTemplateAvailableInTheme(ServerWebExchange exchange, String name);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/dialect/CommentWidget.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.pf4j.ExtensionPoint;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\n\n/**\n * Comment widget extension point to extend the &#x3C;halo:comment /&#x3E; tag of the theme-side.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface CommentWidget extends ExtensionPoint {\n\n    String ENABLE_COMMENT_ATTRIBUTE = CommentWidget.class.getName() + \".ENABLE\";\n\n    void render(ITemplateContext context, IProcessableElementTag tag,\n        IElementTagStructureHandler structureHandler);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/dialect/ElementTagPostProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.pf4j.ExtensionPoint;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport reactor.core.publisher.Mono;\n\n/**\n * An extension point for post-processing element tag.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface ElementTagPostProcessor extends ExtensionPoint {\n\n    /**\n     * <p>\n     * Execute the processor.\n     * </p>\n     * <p>\n     * The {@link IProcessableElementTag} object argument is immutable, so all modifications to\n     * this object or any\n     * instructions to be given to the engine should be done through the specified\n     * {@link org.thymeleaf.model.IModelFactory} model factory in context.\n     * </p>\n     * <p>\n     * Don't forget to return the new tag after processing or\n     * {@link reactor.core.publisher.Mono#empty()} if not processable.\n     * </p>\n     *\n     * @param context the template context.\n     * @param tag the event this processor is executing on.\n     * @return a {@link reactor.core.publisher.Mono} that will complete when processing finishes\n     * or empty mono if not support.\n     */\n    Mono<IProcessableElementTag> process(\n        ITemplateContext context,\n        final IProcessableElementTag tag\n    );\n\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/dialect/TemplateFooterProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.pf4j.ExtensionPoint;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport reactor.core.publisher.Mono;\n\n/**\n * Theme template <code>footer</code> tag snippet injection processor.\n *\n * @author guqing\n * @since 2.17.0\n */\npublic interface TemplateFooterProcessor extends ExtensionPoint {\n\n    Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,\n        IElementTagStructureHandler structureHandler, IModel model);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/dialect/TemplateHeadProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.pf4j.ExtensionPoint;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\n\n/**\n * Theme template <code>head</code> tag snippet injection processor.\n * <p>Head processor is processed order by {@link org.springframework.core.annotation.Order}\n * annotation, Higher order will be processed first and so that low-priority processor can be\n * overwritten head tag written by high-priority processor.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@FunctionalInterface\npublic interface TemplateHeadProcessor extends ExtensionPoint {\n\n    Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler);\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/finders/Finder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.core.annotation.AliasFor;\nimport org.springframework.stereotype.Service;\n\n/**\n * Template model data finder for theme.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Service\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface Finder {\n\n    /**\n     * The name of the theme model variable.\n     *\n     * @return variable name, class simple name if not specified\n     */\n    @AliasFor(annotation = Service.class)\n    String value() default \"\";\n}"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/finders/vo/ExtensionVoOperator.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * An operator for extension value object.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface ExtensionVoOperator {\n\n    @NonNull\n    MetadataOperator getMetadata();\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/router/ModelConst.java",
    "content": "package run.halo.app.theme.router;\n\n/**\n * Static variable keys for view model.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic enum ModelConst {\n    ;\n    public static final String TEMPLATE_ID = \"_templateId\";\n    public static final String POWERED_BY_HALO_TEMPLATE_ENGINE = \"poweredByHaloTemplateEngine\";\n\n    /**\n     * This key is used to prevent caching from cache plugins.\n     */\n    public static final String NO_CACHE = \"HALO_TEMPLATE_ENGINE.NO_CACHE\";\n\n    public static final Integer DEFAULT_PAGE_SIZE = 10;\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/router/PageUrlUtils.java",
    "content": "package run.halo.app.theme.router;\n\nimport java.util.Objects;\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.commons.lang3.math.NumberUtils;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.infra.utils.PathUtils;\n\n/**\n * A utility class for template page url.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class PageUrlUtils {\n    public static final String PAGE_PART = \"page\";\n\n    public static int pageNum(ServerRequest request) {\n        if (isPageUrl(request.path())) {\n            String pageNum = StringUtils.substringAfterLast(request.path(), \"/page/\");\n            return NumberUtils.toInt(pageNum, 1);\n        }\n        return 1;\n    }\n\n    public static boolean isPageUrl(String path) {\n        String[] split = StringUtils.split(path, \"/\");\n        if (split.length > 1) {\n            return PAGE_PART.equals(split[split.length - 2])\n                && NumberUtils.isDigits(split[split.length - 1]);\n        }\n        return false;\n    }\n\n    public static long totalPage(ListResult<?> list) {\n        return (list.getTotal() - 1) / list.getSize() + 1;\n    }\n\n    /**\n     * Gets next page url with path.\n     *\n     * @param path request path\n     * @return request path with next page part\n     */\n    public static String nextPageUrl(String path, long total) {\n        String[] segments = StringUtils.split(path, \"/\");\n        long defaultPage = Math.min(2, Math.max(total, 1));\n        if (segments.length > 1) {\n            String pagePart = segments[segments.length - 2];\n            if (PAGE_PART.equals(pagePart)) {\n                int pageNumIndex = segments.length - 1;\n                String pageNum = segments[pageNumIndex];\n                segments[pageNumIndex] = toNextPage(pageNum, total);\n                return PathUtils.combinePath(segments);\n            }\n            return appendPagePart(PathUtils.combinePath(segments), defaultPage);\n        }\n        return appendPagePart(PathUtils.combinePath(segments), defaultPage);\n    }\n\n    /**\n     * Gets previous page url with path.\n     *\n     * @param path request path\n     * @return request path with previous page part\n     */\n    public static String prevPageUrl(String path) {\n        String[] segments = StringUtils.split(path, \"/\");\n        if (segments.length > 1) {\n            String pagePart = segments[segments.length - 2];\n            if (PAGE_PART.equals(pagePart)) {\n                int pageNumIndex = segments.length - 1;\n                String pageNum = segments[pageNumIndex];\n                int prevPage = toPrevPage(pageNum);\n                segments[pageNumIndex] = String.valueOf(prevPage);\n                if (prevPage == 1) {\n                    segments = ArrayUtils.subarray(segments, 0, pageNumIndex - 1);\n                }\n                if (segments.length == 0) {\n                    return \"/\";\n                }\n                return PathUtils.combinePath(segments);\n            }\n        }\n        return Objects.toString(path, \"/\");\n    }\n\n    private static String appendPagePart(String path, long page) {\n        return PathUtils.combinePath(path, PAGE_PART, String.valueOf(page));\n    }\n\n    private static String toNextPage(String pageStr, long total) {\n        long page = Math.min(parseInt(pageStr) + 1, Math.max(total, 1));\n        return String.valueOf(page);\n    }\n\n    private static int toPrevPage(String pageStr) {\n        return Math.max(parseInt(pageStr) - 1, 1);\n    }\n\n    private static int parseInt(String pageStr) {\n        if (!NumberUtils.isParsable(pageStr)) {\n            throw new IllegalArgumentException(\"Page number must be a number\");\n        }\n        return NumberUtils.toInt(pageStr, 1);\n    }\n}\n"
  },
  {
    "path": "api/src/main/java/run/halo/app/theme/router/UrlContextListResult.java",
    "content": "package run.halo.app.theme.router;\n\nimport java.util.List;\nimport lombok.Getter;\nimport lombok.ToString;\nimport run.halo.app.extension.ListResult;\n\n/**\n * Page wrapper with next and previous url.\n *\n * @param <T> the type of the list item.\n * @author guqing\n * @since 2.0.0\n */\n@Getter\n@ToString(callSuper = true)\npublic class UrlContextListResult<T> extends ListResult<T> {\n    private final String nextUrl;\n    private final String prevUrl;\n\n    public UrlContextListResult(int page, int size, long total, List<T> items, String nextUrl,\n        String prevUrl) {\n        super(page, size, total, items);\n        this.nextUrl = nextUrl;\n        this.prevUrl = prevUrl;\n    }\n\n    public static class Builder<T> {\n        private int page;\n        private int size;\n        private long total;\n        private List<T> items;\n        private String nextUrl;\n        private String prevUrl;\n\n        public Builder<T> page(int page) {\n            this.page = page;\n            return this;\n        }\n\n        public Builder<T> size(int size) {\n            this.size = size;\n            return this;\n        }\n\n        public Builder<T> total(long total) {\n            this.total = total;\n            return this;\n        }\n\n        public Builder<T> items(List<T> items) {\n            this.items = items;\n            return this;\n        }\n\n        public Builder<T> nextUrl(String nextUrl) {\n            this.nextUrl = nextUrl;\n            return this;\n        }\n\n        public Builder<T> prevUrl(String prevUrl) {\n            this.prevUrl = prevUrl;\n            return this;\n        }\n\n        /**\n         * Assign value with list result.\n         *\n         * @param listResult list result\n         * @return builder\n         */\n        public Builder<T> listResult(ListResult<T> listResult) {\n            this.page = listResult.getPage();\n            this.size = listResult.getSize();\n            this.total = listResult.getTotal();\n            this.items = listResult.getItems();\n            return this;\n        }\n\n        public UrlContextListResult<T> build() {\n            return new UrlContextListResult<>(page, size, total, items, nextUrl, prevUrl);\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/core/extension/content/PostTest.java",
    "content": "package run.halo.app.core.extension.content;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport run.halo.app.extension.Metadata;\n\nclass PostTest {\n\n    @ParameterizedTest\n    @MethodSource(\"isRecycledProvider\")\n    void isRecycledTest(Metadata metadata, boolean expected) {\n        assertEquals(expected, Post.isRecycled(metadata));\n    }\n\n    static Stream<Arguments> isRecycledProvider() {\n        Function<Consumer<Metadata>, Metadata> metadataCreator =\n            metadataConsumer -> {\n                var metadata = new Metadata();\n                metadataConsumer.accept(metadata);\n                return metadata;\n            };\n        return Stream.of(\n            Arguments.of(metadataCreator.apply(metadata -> {\n            }), false),\n            Arguments.of(metadataCreator.apply(metadata ->\n                    metadata.setLabels(Map.of(Post.DELETED_LABEL, \"false\"))),\n                false),\n            Arguments.of(metadataCreator.apply(metadata ->\n                    metadata.setLabels(Map.of(Post.DELETED_LABEL, \"invalid\"))),\n                false),\n            Arguments.of(metadataCreator.apply(metadata -> {\n                metadata.setLabels(Map.of(Post.DELETED_LABEL, \"true\"));\n            }), true)\n        );\n\n    }\n\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/core/extension/notification/SubscriptionTest.java",
    "content": "package run.halo.app.core.extension.notification;\n\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link Subscription}.\n *\n * @author guqing\n * @since 2.13.0\n */\nclass SubscriptionTest {\n\n    @Test\n    void reasonSubjectToStringTest() {\n        Subscription.ReasonSubject subject = new Subscription.ReasonSubject();\n        subject.setApiVersion(\"v1\");\n        subject.setKind(\"Kind\");\n        subject.setName(\"Name\");\n\n        String expected = \"Kind#v1/Name\";\n        String actual = subject.toString();\n\n        assertThat(actual).isEqualTo(expected);\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/ExtensionUtilTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\n\nclass ExtensionUtilTest {\n\n    @Test\n    void testIsNotDeleted() {\n        var ext = mock(ExtensionOperator.class);\n\n        when(ext.getMetadata()).thenReturn(null);\n        assertFalse(ExtensionUtil.isDeleted(ext));\n\n        var metadata = mock(Metadata.class);\n        when(ext.getMetadata()).thenReturn(metadata);\n        when(metadata.getDeletionTimestamp()).thenReturn(null);\n        assertFalse(ExtensionUtil.isDeleted(ext));\n\n        when(metadata.getDeletionTimestamp()).thenReturn(Instant.now());\n        assertTrue(ExtensionUtil.isDeleted(ext));\n    }\n\n    @Test\n    void addFinalizers() {\n        var metadata = new Metadata();\n        assertNull(metadata.getFinalizers());\n        assertTrue(ExtensionUtil.addFinalizers(metadata, Set.of(\"fake\")));\n\n        assertEquals(Set.of(\"fake\"), metadata.getFinalizers());\n\n        assertFalse(ExtensionUtil.addFinalizers(metadata, Set.of(\"fake\")));\n        assertEquals(Set.of(\"fake\"), metadata.getFinalizers());\n\n        assertTrue(ExtensionUtil.addFinalizers(metadata, Set.of(\"another-fake\")));\n        assertEquals(Set.of(\"fake\", \"another-fake\"), metadata.getFinalizers());\n    }\n\n    @Test\n    void removeFinalizers() {\n        var metadata = new Metadata();\n        assertFalse(ExtensionUtil.removeFinalizers(metadata, Set.of(\"fake\")));\n        assertNull(metadata.getFinalizers());\n\n        metadata.setFinalizers(new HashSet<>(Set.of(\"fake\")));\n        assertTrue(ExtensionUtil.removeFinalizers(metadata, Set.of(\"fake\")));\n        assertEquals(Set.of(), metadata.getFinalizers());\n    }\n\n    @Test\n    void hasDoNotOverwriteLabelTests() {\n        var extension = mock(ExtensionOperator.class);\n        when(extension.getMetadata()).thenReturn(null);\n        assertFalse(ExtensionUtil.hasDoNotOverwriteLabel(extension));\n\n        var metadata = mock(Metadata.class);\n        when(extension.getMetadata()).thenReturn(metadata);\n        when(metadata.getLabels()).thenReturn(null);\n        assertFalse(ExtensionUtil.hasDoNotOverwriteLabel(extension));\n\n        when(metadata.getLabels()).thenReturn(\n            Map.of(ExtensionUtil.DO_NOT_OVERWRITE_LABEL, \"false\")\n        );\n        assertFalse(ExtensionUtil.hasDoNotOverwriteLabel(extension));\n\n        when(metadata.getLabels()).thenReturn(\n            Map.of(ExtensionUtil.DO_NOT_OVERWRITE_LABEL, \"true\")\n        );\n        assertTrue(ExtensionUtil.hasDoNotOverwriteLabel(extension));\n\n        when(metadata.getLabels()).thenReturn(\n            Map.of(ExtensionUtil.DO_NOT_OVERWRITE_LABEL, \"TrUe\")\n        );\n        assertTrue(ExtensionUtil.hasDoNotOverwriteLabel(extension));\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/FakeExtension.java",
    "content": "package run.halo.app.extension;\n\n@GVK(group = \"fake.halo.run\",\n    version = \"v1alpha1\",\n    kind = \"Fake\",\n    plural = \"fakes\",\n    singular = \"fake\")\npublic class FakeExtension extends AbstractExtension {\n\n    public static FakeExtension createFake(String name) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        var fake = new FakeExtension();\n        fake.setMetadata(metadata);\n        return fake;\n    }\n\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/ListOptionsTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Test for {@link ListOptions}.\n *\n * @author guqing\n * @since 2.17.0\n */\nclass ListOptionsTest {\n\n    @Nested\n    class ListOptionsBuilderTest {\n\n        @Test\n        void shouldBuildWithFieldAndLabelSelectors() {\n            var listOptions = ListOptions.builder()\n                .labelSelector()\n                .eq(\"key-1\", \"value-1\")\n                .notEq(\"key-2\", \"value-1\")\n                .exists(\"key-3\")\n                .end()\n                .andQuery(equal(\"spec.slug\", \"fake-slug\"))\n                .orQuery(equal(\"spec.slug\", \"test\"))\n                .build();\n            assertEquals(\"\"\"\n                ((spec.slug = fake-slug OR spec.slug = test) \\\n                AND ((metadata.labels['key-1'] = 'value-1' \\\n                AND metadata.labels['key-2'] <> 'value-1') \\\n                AND EXISTS metadata.labels['key-3']))\\\n                \"\"\", listOptions.toCondition().toString());\n        }\n\n        @Test\n        void shouldBuildLabelSelectorOnly() {\n            var listOptions = ListOptions.builder()\n                .labelSelector()\n                .notEq(\"key-2\", \"value-1\")\n                .end()\n                .build();\n            assertEquals(\"\"\"\n                metadata.labels['key-2'] <> 'value-1'\\\n                \"\"\", listOptions.toCondition().toString());\n        }\n\n        @Test\n        void shouldBuildFieldSelectorOnly() {\n            var listOptions = ListOptions.builder()\n                .andQuery(equal(\"spec.slug\", \"fake-slug\"))\n                .orQuery(equal(\"spec.slug\", \"test\"))\n                .build();\n            assertEquals(\"\"\"\n                (spec.slug = fake-slug OR spec.slug = test)\\\n                \"\"\", listOptions.toCondition().toString());\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/PageRequestImplTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.springframework.data.domain.Sort;\n\nclass PageRequestImplTest {\n\n    @ParameterizedTest\n    @ValueSource(\n        ints = {0, -1, -10, -100, -1000, -10000}\n    )\n    void shouldBeCompatibleZeroAndNegativePageNumber(int pageNumber) {\n        var page = new PageRequestImpl(pageNumber, 10, Sort.unsorted());\n        assertEquals(1, page.getPageNumber());\n        assertEquals(10, page.getPageSize());\n    }\n\n    @ParameterizedTest\n    @ValueSource(\n        ints = {0, -1, -10, -100, -1000, -10000}\n    )\n    void shouldBeCompatibleNegativePageSize(int size) {\n        var page = new PageRequestImpl(10, size, Sort.unsorted());\n        assertEquals(10, page.getPageNumber());\n        assertEquals(PageRequestImpl.MAX_SIZE, page.getPageSize());\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/SecretTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLMapper;\nimport java.util.Map;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link Secret}.\n *\n * @author guqing\n * @since 2.4.0\n */\nclass SecretTest {\n\n    @Test\n    void serialize() throws JSONException {\n        Secret secret = new Secret();\n        secret.setMetadata(new Metadata());\n        secret.getMetadata().setName(\"test-secret\");\n        secret.setType(Secret.SECRET_TYPE_OPAQUE);\n        secret.setData(Map.of(\"password\", \"admin\".getBytes()));\n        String s = JsonUtils.objectToJson(secret);\n        JSONAssert.assertEquals(testJsonString(), s, true);\n    }\n\n    @Test\n    void deserialize() {\n        String s = testJsonString();\n        Secret secret = JsonUtils.jsonToObject(s, Secret.class);\n        assertThat(secret).isNotNull();\n        assertThat(secret.getMetadata().getName()).isEqualTo(\"test-secret\");\n        assertThat(secret.getType()).isEqualTo(Secret.SECRET_TYPE_OPAQUE);\n        assertThat(secret.getData()).containsEntry(\"password\", \"admin\".getBytes());\n    }\n\n    @Test\n    void deserializeWithUnstructured() throws JsonProcessingException {\n        Secret secret = Unstructured.OBJECT_MAPPER.readValue(testJsonString(), Secret.class);\n        assertThat(secret.getMetadata().getName()).isEqualTo(\"test-secret\");\n        assertThat(secret.getType()).isEqualTo(Secret.SECRET_TYPE_OPAQUE);\n        assertThat(secret.getData()).containsEntry(\"password\", \"admin\".getBytes());\n    }\n\n    @Test\n    void deserializeYamlWithStringData() throws JsonProcessingException {\n        String s = \"\"\"\n            apiVersion: v1alpha1\n            kind: Secret\n            metadata:\n              name: secret-basic-auth\n            type: halo.run/basic-auth\n            stringData:\n              username: admin\n              password: t0p-Secret\n            \"\"\";\n        Secret secret = new YAMLMapper().readValue(s, Secret.class);\n        assertThat(secret.getMetadata().getName()).isEqualTo(\"secret-basic-auth\");\n        assertThat(secret.getType()).isEqualTo(\"halo.run/basic-auth\");\n        assertThat(secret.getStringData()).containsEntry(\"username\", \"admin\");\n        assertThat(secret.getStringData()).containsEntry(\"password\", \"t0p-Secret\");\n    }\n\n    private String testJsonString() {\n        return \"\"\"\n            {\n                \"apiVersion\": \"v1alpha1\",\n                \"kind\": \"Secret\",\n                \"metadata\": {\n                    \"name\": \"test-secret\"\n                },\n                \"type\": \"Opaque\",\n                \"data\": {\n                    \"password\": \"YWRtaW4=\"\n                }\n            }\n            \"\"\";\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/controller/ControllerBuilderTest.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.FakeExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass ControllerBuilderTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @Test\n    void buildWithNullReconciler() {\n        assertThrows(IllegalArgumentException.class,\n            () -> new ControllerBuilder(null, client).build(), \"Reconciler must not be null\");\n    }\n\n    @Test\n    void buildWithNullClient() {\n        assertThrows(IllegalArgumentException.class,\n            () -> new ControllerBuilder(new FakeReconciler(), null).build());\n    }\n\n    @Test\n    void buildTest() {\n        assertThrows(IllegalArgumentException.class,\n            () -> new ControllerBuilder(new FakeReconciler(), client)\n                .build(),\n            \"Extension must not be null\");\n\n        assertNotNull(fakeBuilder().build());\n\n        assertNotNull(fakeBuilder()\n            .syncAllOnStart(true)\n            .nowSupplier(Instant::now)\n            .minDelay(Duration.ofMillis(5))\n            .maxDelay(Duration.ofSeconds(1000))\n            .build());\n\n        assertNotNull(fakeBuilder()\n            .syncAllOnStart(true)\n            .minDelay(Duration.ofMillis(5))\n            .maxDelay(Duration.ofSeconds(1000))\n            .onAddMatcher(null)\n            .onUpdateMatcher(null)\n            .onDeleteMatcher(null)\n            .build()\n        );\n    }\n\n    @Test\n    void invalidMinDelayAndMaxDelay() {\n        assertThrows(IllegalArgumentException.class,\n            () -> fakeBuilder()\n                .minDelay(Duration.ofSeconds(2))\n                .maxDelay(Duration.ofSeconds(1))\n                .build(),\n            \"Min delay must be less than or equal to max delay\");\n\n        assertNotNull(fakeBuilder()\n            .minDelay(null)\n            .maxDelay(Duration.ofSeconds(1))\n            .build());\n\n        assertNotNull(fakeBuilder()\n            .minDelay(Duration.ofSeconds(1))\n            .maxDelay(null)\n            .build());\n\n        assertNotNull(fakeBuilder()\n            .minDelay(Duration.ofSeconds(-1))\n            .build());\n\n        assertNotNull(fakeBuilder()\n            .maxDelay(Duration.ofSeconds(-1))\n            .build());\n    }\n\n    ControllerBuilder fakeBuilder() {\n        return new ControllerBuilder(new FakeReconciler(), client)\n            .extension(new FakeExtension());\n    }\n\n    static class FakeReconciler implements Reconciler<Reconciler.Request> {\n\n        @Override\n        public Result reconcile(Request request) {\n            return new Reconciler.Result(false, null);\n        }\n\n        @Override\n        public Controller setupWith(ControllerBuilder builder) {\n            return null;\n        }\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/controller/DefaultControllerTest.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyLong;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.extension.controller.Reconciler.Result;\nimport run.halo.app.extension.controller.RequestQueue.DelayedEntry;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultControllerTest {\n\n    @Mock\n    RequestQueue<Request> queue;\n\n    @Mock\n    Reconciler<Request> reconciler;\n\n    @Mock\n    RequestSynchronizer synchronizer;\n\n    @Mock\n    ExecutorService executor;\n\n    Instant now = Instant.now();\n\n    Duration minRetryAfter = Duration.ofMillis(100);\n\n    Duration maxRetryAfter = Duration.ofSeconds(10);\n\n    DefaultController<Request> controller;\n\n    @BeforeEach\n    void setUp() {\n        controller = createController(1);\n\n        assertFalse(controller.isDisposed());\n        assertFalse(controller.isStarted());\n    }\n\n    DefaultController<Request> createController(int workerCount) {\n        return new DefaultController<>(\"fake-controller\", reconciler, queue, synchronizer,\n            () -> now, minRetryAfter, maxRetryAfter, executor, workerCount);\n    }\n\n    @Test\n    void shouldReturnRightName() {\n        assertEquals(\"fake-controller\", controller.getName());\n    }\n\n    @Nested\n    class WorkerTest {\n\n        @Test\n        void shouldCreateCorrectName() {\n            var worker = controller.new Worker();\n            assertEquals(\"fake-controller-worker-1\", worker.getName());\n            worker = controller.new Worker();\n            assertEquals(\"fake-controller-worker-2\", worker.getName());\n            worker = controller.new Worker();\n            assertEquals(\"fake-controller-worker-3\", worker.getName());\n        }\n\n        @Test\n        void shouldRunCorrectlyIfReconcilerReturnsNoReEnqueue() throws InterruptedException {\n            when(queue.take()).thenReturn(new DelayedEntry<>(\n                    new Request(\"fake-request\"), Duration.ofSeconds(1), () -> now\n                ))\n                .thenThrow(InterruptedException.class);\n            when(reconciler.reconcile(any(Request.class))).thenReturn(new Result(false, null));\n\n            controller.new Worker().run();\n\n            verify(synchronizer, never()).start();\n            verify(queue, times(2)).take();\n            verify(queue, times(0)).add(any());\n            verify(queue, times(1)).done(any());\n            verify(reconciler, times(1)).reconcile(eq(new Request(\"fake-request\")));\n        }\n\n        @Test\n        void shouldRunCorrectlyIfReconcilerReturnsReEnqueue() throws InterruptedException {\n            when(queue.take()).thenReturn(new DelayedEntry<>(\n                    new Request(\"fake-request\"), Duration.ofSeconds(1), () -> now\n                ))\n                .thenThrow(InterruptedException.class);\n            when(queue.add(any())).thenReturn(true);\n            when(reconciler.reconcile(any(Request.class))).thenReturn(new Result(true, null));\n\n            controller.new Worker().run();\n\n            verify(synchronizer, never()).start();\n            verify(queue, times(2)).take();\n            verify(queue, times(1)).done(any());\n            verify(queue, times(1)).add(argThat(de ->\n                de.getEntry().name().equals(\"fake-request\")\n                    && de.getRetryAfter().equals(Duration.ofSeconds(2))));\n            verify(reconciler, times(1)).reconcile(any(Request.class));\n        }\n\n        @Test\n        void shouldReRunIfReconcilerThrowException() throws InterruptedException {\n            when(queue.take()).thenReturn(new DelayedEntry<>(\n                    new Request(\"fake-request\"), Duration.ofSeconds(1), () -> now\n                ))\n                .thenThrow(InterruptedException.class);\n            when(queue.add(any())).thenReturn(true);\n            when(reconciler.reconcile(any(Request.class))).thenThrow(RuntimeException.class);\n\n            controller.new Worker().run();\n\n            verify(synchronizer, never()).start();\n            verify(queue, times(2)).take();\n            verify(queue, times(1)).done(any());\n            verify(queue, times(1)).add(argThat(de ->\n                de.getEntry().name().equals(\"fake-request\")\n                    && de.getRetryAfter().equals(Duration.ofSeconds(2))));\n            verify(reconciler, times(1)).reconcile(any(Request.class));\n        }\n\n        @Test\n        void canReRunIfReconcilerThrowRequeueException() throws InterruptedException {\n            when(queue.take()).thenReturn(new DelayedEntry<>(\n                    new Request(\"fake-request\"), Duration.ofSeconds(1), () -> now\n                ))\n                .thenThrow(InterruptedException.class);\n            when(queue.add(any())).thenReturn(true);\n            var expectException = new RequeueException(Result.requeue(Duration.ofSeconds(2)));\n            when(reconciler.reconcile(any(Request.class))).thenThrow(expectException);\n\n            controller.new Worker().run();\n\n            verify(synchronizer, never()).start();\n            verify(queue, times(2)).take();\n            verify(queue).done(any());\n            verify(queue).add(argThat(de ->\n                de.getEntry().name().equals(\"fake-request\")\n                    && de.getRetryAfter().equals(Duration.ofSeconds(2))));\n            verify(reconciler).reconcile(any(Request.class));\n        }\n\n        @Test\n        void doNotReRunIfReconcilerThrowsRequeueExceptionWithoutRequeue()\n            throws InterruptedException {\n            when(queue.take()).thenReturn(new DelayedEntry<>(\n                    new Request(\"fake-request\"), Duration.ofSeconds(1), () -> now\n                ))\n                .thenThrow(InterruptedException.class);\n            var expectException = new RequeueException(Result.doNotRetry());\n            when(reconciler.reconcile(any(Request.class))).thenThrow(expectException);\n\n            controller.new Worker().run();\n\n            verify(synchronizer, never()).start();\n            verify(queue, times(2)).take();\n            verify(queue).done(any());\n\n            verify(queue, never()).add(any());\n            verify(reconciler).reconcile(any(Request.class));\n        }\n\n        @Test\n        void shouldSetMinRetryAfterWhenTakeZeroDelayedEntry() throws InterruptedException {\n            when(queue.take()).thenReturn(new DelayedEntry<>(\n                    new Request(\"fake-request\"), minRetryAfter.minusMillis(1), () -> now\n                ))\n                .thenThrow(InterruptedException.class);\n            when(queue.add(any())).thenReturn(true);\n            when(reconciler.reconcile(any(Request.class))).thenReturn(new Result(true, null));\n\n            controller.new Worker().run();\n\n            verify(synchronizer, never()).start();\n            verify(queue, times(2)).take();\n            verify(queue, times(1)).done(any());\n            verify(queue, times(1)).add(argThat(de ->\n                de.getEntry().name().equals(\"fake-request\")\n                    && de.getRetryAfter().equals(minRetryAfter)));\n            verify(reconciler, times(1)).reconcile(any(Request.class));\n        }\n\n        @Test\n        void shouldSetMaxRetryAfterWhenTakeGreaterThanMaxRetryAfterDelayedEntry()\n            throws InterruptedException {\n            when(queue.take()).thenReturn(new DelayedEntry<>(\n                    new Request(\"fake-request\"), maxRetryAfter.plusMillis(1), () -> now\n                ))\n                .thenThrow(InterruptedException.class);\n            when(queue.add(any())).thenReturn(true);\n            when(reconciler.reconcile(any(Request.class))).thenReturn(new Result(true, null));\n\n            controller.new Worker().run();\n\n            verify(synchronizer, never()).start();\n            verify(queue, times(2)).take();\n            verify(queue, times(1)).done(any());\n            verify(queue, times(1)).add(argThat(de ->\n                de.getEntry().name().equals(\"fake-request\")\n                    && de.getRetryAfter().equals(maxRetryAfter)));\n            verify(reconciler, times(1)).reconcile(any(Request.class));\n        }\n\n    }\n\n    @Test\n    void shouldDisposeCorrectlyIfShutdownInTime() throws InterruptedException {\n        when(executor.awaitTermination(anyLong(), any())).thenReturn(true);\n\n        controller.dispose();\n\n        assertTrue(controller.isDisposed());\n        assertFalse(controller.isStarted());\n\n        verify(synchronizer, times(1)).dispose();\n        verify(queue, times(1)).dispose();\n        verify(executor).shutdownNow();\n        verify(executor, never()).shutdown();\n        verify(executor, times(1)).awaitTermination(anyLong(), any());\n    }\n\n    @Test\n    void shouldDisposeCorrectlyIfNotShutdownInTime() throws InterruptedException {\n        when(executor.awaitTermination(anyLong(), any()))\n            .thenReturn(false)\n            .thenReturn(true);\n\n        controller.dispose();\n\n        assertTrue(controller.isDisposed());\n        assertFalse(controller.isStarted());\n\n        verify(synchronizer).dispose();\n        verify(queue).dispose();\n        verify(executor).shutdown();\n        verify(executor).shutdownNow();\n        verify(executor, times(2)).awaitTermination(anyLong(), any());\n    }\n\n    @Test\n    void shouldDisposeCorrectlyEvenIfTimeoutAwaitTermination() throws InterruptedException {\n        when(executor.awaitTermination(anyLong(), any()))\n            .thenThrow(InterruptedException.class)\n            .thenReturn(true);\n\n        controller.dispose();\n\n        assertTrue(controller.isDisposed());\n        assertFalse(controller.isStarted());\n\n        verify(synchronizer, times(1)).dispose();\n        verify(queue, times(1)).dispose();\n        verify(executor).shutdown();\n        verify(executor, times(1)).shutdownNow();\n        verify(executor, times(2)).awaitTermination(anyLong(), any());\n    }\n\n    @Test\n    void shouldStartCorrectly() throws InterruptedException {\n        doAnswer(invocation -> {\n            // simulate starting synchronizer\n            invocation.getArgument(0, Runnable.class).run();\n            return null;\n        }).doAnswer(invocation -> {\n            // simulate starting worker\n            return null;\n        }).when(executor).execute(any(Runnable.class));\n\n        controller.start();\n        assertTrue(controller.isStarted());\n        assertFalse(controller.isDisposed());\n\n        verify(synchronizer).start();\n        verify(executor, times(2)).execute(any(Runnable.class));\n    }\n\n    @Test\n    void shouldNotStartWhenDisposed() throws InterruptedException {\n        when(executor.awaitTermination(1, TimeUnit.MINUTES)).thenReturn(true);\n        controller.dispose();\n        controller.start();\n        assertFalse(controller.isStarted());\n        assertTrue(controller.isDisposed());\n\n        verify(executor, times(0)).execute(any(Runnable.class));\n    }\n\n    @Test\n    void shouldCreateMultiWorkers() {\n        controller = createController(5);\n        controller.start();\n        verify(executor, times(5)).execute(any(DefaultController.Worker.class));\n    }\n\n    @Test\n    void shouldFailToCreateControllerDueToInvalidWorkerCount() {\n        assertThrows(IllegalArgumentException.class, () -> createController(0));\n        assertThrows(IllegalArgumentException.class, () -> createController(-1));\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/controller/DefaultDelayQueueTest.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.extension.controller.RequestQueue.DelayedEntry;\n\nclass DefaultDelayQueueTest {\n\n    Instant now = Instant.now();\n\n    DefaultQueue<Request> queue;\n\n    final Duration minDelay = Duration.ofMillis(1);\n\n    @BeforeEach\n    void setUp() {\n        queue = new DefaultQueue<>(() -> now, minDelay);\n    }\n\n    @Test\n    void addImmediatelyTest() {\n        var request = newRequest(\"fake-name\");\n        var added = queue.addImmediately(request);\n        assertTrue(added);\n        assertEquals(1, queue.size());\n        var delayedEntry = queue.peek();\n        assertNotNull(delayedEntry);\n        assertEquals(newRequest(\"fake-name\"), delayedEntry.getEntry());\n        assertEquals(minDelay, delayedEntry.getRetryAfter());\n        assertEquals(minDelay.toMillis(), delayedEntry.getDelay(TimeUnit.MILLISECONDS));\n    }\n\n    @Test\n    void addWithDelaySmallerThanMinDelay() {\n        var request = newRequest(\"fake-name\");\n        var added = queue.add(new DelayedEntry<>(request, Duration.ofNanos(1), () -> now));\n        assertTrue(added);\n        assertEquals(1, queue.size());\n        var delayedEntry = queue.peek();\n        assertNotNull(delayedEntry);\n        assertEquals(newRequest(\"fake-name\"), delayedEntry.getEntry());\n        assertEquals(minDelay, delayedEntry.getRetryAfter());\n        assertEquals(minDelay.toMillis(), delayedEntry.getDelay(TimeUnit.MILLISECONDS));\n    }\n\n    @Test\n    void addWithDelayGreaterThanMinDelay() {\n        var request = newRequest(\"fake-name\");\n        var added = queue.add(new DelayedEntry<>(request, minDelay.plusMillis(1), () -> now));\n        assertTrue(added);\n        assertEquals(1, queue.size());\n        var delayedEntry = queue.peek();\n        assertNotNull(delayedEntry);\n        assertEquals(newRequest(\"fake-name\"), delayedEntry.getEntry());\n        assertEquals(minDelay.plusMillis(1), delayedEntry.getRetryAfter());\n        assertEquals(minDelay.plusMillis(1).toMillis(),\n            delayedEntry.getDelay(TimeUnit.MILLISECONDS));\n    }\n\n    @Test\n    void shouldNotAddAfterDisposing() {\n        assertFalse(queue.isDisposed());\n        queue.dispose();\n        assertTrue(queue.isDisposed());\n        var request = newRequest(\"fake-name\");\n        var added = queue.add(new DelayedEntry<>(request, minDelay, () -> now));\n        assertFalse(added);\n        assertEquals(0, queue.size());\n    }\n\n    @Test\n    void shouldNotAddRepeatedlyIfNotDone() throws InterruptedException {\n        queue = new DefaultQueue<>(() -> now, Duration.ZERO);\n        var fakeEntry = new DelayedEntry<>(newRequest(\"fake-name\"), Duration.ZERO,\n            () -> this.now);\n\n        queue.add(fakeEntry);\n        assertEquals(1, queue.size());\n        assertEquals(fakeEntry, queue.peek());\n        queue.take();\n        assertEquals(0, queue.size());\n\n        queue.add(fakeEntry);\n        assertEquals(0, queue.size());\n\n        queue.done(newRequest(\"fake-name\"));\n        queue.add(fakeEntry);\n        assertEquals(1, queue.size());\n        assertEquals(fakeEntry, queue.peek());\n    }\n\n    @Test\n    void shouldNotAddIfHavingEarlierEntryInQueue() {\n        queue = new DefaultQueue<>(() -> now, Duration.ZERO);\n        var fakeEntry = new DelayedEntry<>(newRequest(\"fake-name\"), Duration.ZERO,\n            () -> this.now);\n\n        assertTrue(queue.add(fakeEntry));\n        assertEquals(1, queue.size());\n        assertEquals(fakeEntry, queue.peek());\n\n        assertFalse(queue.add(fakeEntry));\n        var laterEntry = new DelayedEntry<>(newRequest(\"fake-name\"), Duration.ofMillis(100),\n            () -> this.now);\n        assertFalse(queue.add(laterEntry));\n    }\n\n    @Test\n    void shouldAddIfHavingLaterEntryInQueue() {\n        queue = new DefaultQueue<>(() -> now, Duration.ZERO);\n        var fakeEntry = new DelayedEntry<>(newRequest(\"fake-name\"), Duration.ofMillis(100),\n            () -> this.now);\n\n        assertTrue(queue.add(fakeEntry));\n        assertEquals(1, queue.size());\n        assertEquals(fakeEntry, queue.peek());\n\n        assertFalse(queue.add(fakeEntry));\n        var laterEntry = new DelayedEntry<>(newRequest(\"fake-name\"), Duration.ofMillis(99),\n            () -> this.now);\n        assertTrue(queue.add(laterEntry));\n    }\n\n    Request newRequest(String name) {\n        return new Request(name);\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/controller/DelayedEntryTest.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.controller.RequestQueue.DelayedEntry;\n\nclass DelayedEntryTest {\n\n    Instant now = Instant.now();\n\n    @Test\n    void createDelayedEntry() {\n        var delayedEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(100), () -> now);\n        assertEquals(100, delayedEntry.getDelay(TimeUnit.MILLISECONDS));\n        assertEquals(Duration.ofMillis(100), delayedEntry.getRetryAfter());\n        assertEquals(now.plusMillis(100), delayedEntry.getReadyAt());\n        assertEquals(\"fake\", delayedEntry.getEntry());\n\n        delayedEntry = new DelayedEntry<>(\"fake\", now.plus(Duration.ofSeconds(1)), () -> now);\n        assertEquals(1000, delayedEntry.getDelay(TimeUnit.MILLISECONDS));\n        assertEquals(Duration.ofMillis(1000), delayedEntry.getRetryAfter());\n    }\n\n    @Test\n    void compareWithGreaterDelay() {\n        var firstDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(100), () -> now);\n        var secondDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(200), () -> now);\n\n        assertTrue(firstDelayEntry.compareTo(secondDelayEntry) < 0);\n    }\n\n    @Test\n    void compareWithSameDelay() {\n        var firstDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(100), () -> now);\n        var secondDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(100), () -> now);\n\n        assertEquals(0, firstDelayEntry.compareTo(secondDelayEntry));\n    }\n\n    @Test\n    void compareWithLessDelay() {\n        var firstDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(200), () -> now);\n        var secondDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(100), () -> now);\n\n        assertTrue(firstDelayEntry.compareTo(secondDelayEntry) > 0);\n    }\n\n    @Test\n    void shouldBeEqualWithNameOnly() {\n        var firstDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(200), () -> now);\n        var secondDelayEntry = new DelayedEntry<>(\"fake\", Duration.ofMillis(100), Instant::now);\n\n        assertEquals(firstDelayEntry, secondDelayEntry);\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/controller/ExtensionWatcherTest.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.extension.FakeExtension.createFake;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.DefaultExtensionMatcher;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.WatcherExtensionMatchers;\nimport run.halo.app.extension.controller.Reconciler.Request;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionWatcherTest {\n\n    @Mock\n    RequestQueue<Request> queue;\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    WatcherExtensionMatchers matchers;\n\n    @InjectMocks\n    ExtensionWatcher watcher;\n\n    private DefaultExtensionMatcher getEmptyMatcher() {\n        return DefaultExtensionMatcher.builder(client,\n                GroupVersionKind.fromExtension(FakeExtension.class))\n            .build();\n    }\n\n    @Test\n    void shouldAddExtensionWhenAddPredicateAlwaysTrue() {\n        when(matchers.onAddMatcher()).thenReturn(getEmptyMatcher());\n        watcher.onAdd(createFake(\"fake-name\"));\n\n        verify(matchers, times(1)).onAddMatcher();\n        verify(queue, times(1)).addImmediately(\n            argThat(request -> request.name().equals(\"fake-name\")));\n        verify(queue, times(0)).add(any());\n    }\n\n    @Test\n    void shouldNotAddExtensionWhenAddPredicateAlwaysFalse() {\n        var type = GroupVersionKind.fromAPIVersionAndKind(\"v1alpha1\", \"User\");\n        when(matchers.onAddMatcher()).thenReturn(\n            DefaultExtensionMatcher.builder(client, type).build());\n        watcher.onAdd(createFake(\"fake-name\"));\n\n        verify(matchers, times(1)).onAddMatcher();\n        verify(queue, times(0)).add(any());\n        verify(queue, times(0)).addImmediately(any());\n    }\n\n    @Test\n    void shouldNotAddExtensionWhenWatcherIsDisposed() {\n        watcher.dispose();\n        watcher.onAdd(createFake(\"fake-name\"));\n\n        verify(matchers, times(0)).onAddMatcher();\n        verify(queue, times(0)).addImmediately(any());\n        verify(queue, times(0)).add(any());\n    }\n\n    @Test\n    void shouldUpdateExtensionWhenUpdatePredicateAlwaysTrue() {\n        when(matchers.onUpdateMatcher()).thenReturn(getEmptyMatcher());\n        watcher.onUpdate(createFake(\"old-fake-name\"), createFake(\"new-fake-name\"));\n\n        verify(matchers, times(1)).onUpdateMatcher();\n        verify(queue, times(1)).addImmediately(\n            argThat(request -> request.name().equals(\"new-fake-name\")));\n        verify(queue, times(0)).add(any());\n    }\n\n    @Test\n    void shouldUpdateExtensionWhenUpdatePredicateAlwaysFalse() {\n        var type = GroupVersionKind.fromAPIVersionAndKind(\"v1alpha1\", \"User\");\n        when(matchers.onUpdateMatcher()).thenReturn(\n            DefaultExtensionMatcher.builder(client, type).build());\n        watcher.onUpdate(createFake(\"old-fake-name\"), createFake(\"new-fake-name\"));\n\n        verify(matchers, times(1)).onUpdateMatcher();\n        verify(queue, times(0)).add(any());\n        verify(queue, times(0)).addImmediately(any());\n    }\n\n    @Test\n    void shouldNotUpdateExtensionWhenWatcherIsDisposed() {\n        watcher.dispose();\n        watcher.onUpdate(createFake(\"old-fake-name\"), createFake(\"new-fake-name\"));\n\n        verify(matchers, times(0)).onUpdateMatcher();\n        verify(queue, times(0)).add(any());\n        verify(queue, times(0)).addImmediately(any());\n    }\n\n    @Test\n    void shouldDeleteExtensionWhenDeletePredicateAlwaysTrue() {\n        when(matchers.onDeleteMatcher()).thenReturn(getEmptyMatcher());\n        watcher.onDelete(createFake(\"fake-name\"));\n\n        verify(matchers, times(1)).onDeleteMatcher();\n        verify(queue, times(1)).addImmediately(\n            argThat(request -> request.name().equals(\"fake-name\")));\n        verify(queue, times(0)).add(any());\n    }\n\n    @Test\n    void shouldDeleteExtensionWhenDeletePredicateAlwaysFalse() {\n        var type = GroupVersionKind.fromAPIVersionAndKind(\"v1alpha1\", \"User\");\n        when(matchers.onDeleteMatcher()).thenReturn(\n            DefaultExtensionMatcher.builder(client, type).build());\n        watcher.onDelete(createFake(\"fake-name\"));\n\n        verify(matchers, times(1)).onDeleteMatcher();\n        verify(queue, times(0)).add(any());\n        verify(queue, times(0)).addImmediately(any());\n    }\n\n    @Test\n    void shouldNotDeleteExtensionWhenWatcherIsDisposed() {\n        watcher.dispose();\n        watcher.onDelete(createFake(\"fake-name\"));\n\n        verify(matchers, times(0)).onDeleteMatcher();\n        verify(queue, times(0)).add(any());\n        verify(queue, times(0)).addImmediately(any());\n    }\n\n    @Test\n    void shouldInvokeDisposeHookIfRegistered() {\n        var mockHook = mock(Runnable.class);\n        watcher.registerDisposeHook(mockHook);\n        verify(mockHook, times(0)).run();\n\n        watcher.dispose();\n        verify(mockHook, times(1)).run();\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/controller/RequestSynchronizerTest.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Watcher;\n\n@ExtendWith(MockitoExtension.class)\nclass RequestSynchronizerTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    Watcher watcher;\n\n    RequestSynchronizer synchronizer;\n\n    @BeforeEach\n    void setUp() {\n        synchronizer =\n            new RequestSynchronizer(true, client, new FakeExtension(), watcher, new ListOptions());\n        assertFalse(synchronizer.isDisposed());\n        assertFalse(synchronizer.isStarted());\n    }\n\n    @Test\n    void shouldStartCorrectlyWhenSyncingAllOnStart() {\n        when(client.listTopNames(\n            eq(FakeExtension.class), isA(ListOptions.class), isA(Sort.class), any(Integer.class))\n        ).thenReturn(List.of(\"fake-01\", \"fake-02\"));\n\n        synchronizer.start();\n\n        assertTrue(synchronizer.isStarted());\n        assertFalse(synchronizer.isDisposed());\n        verify(watcher, times(2)).onAdd(isA(Reconciler.Request.class));\n        verify(client, times(1)).watch(same(watcher));\n    }\n\n    @Test\n    void shouldStartCorrectlyWhenNotSyncingAllOnStart() {\n        synchronizer =\n            new RequestSynchronizer(false, client, new FakeExtension(), watcher, new ListOptions());\n        assertFalse(synchronizer.isDisposed());\n        assertFalse(synchronizer.isStarted());\n\n        synchronizer.start();\n\n        assertTrue(synchronizer.isStarted());\n        assertFalse(synchronizer.isDisposed());\n\n        verify(client, times(0)).list(any(), any(), any());\n        verify(watcher, times(0)).onAdd(isA(Reconciler.Request.class));\n        verify(client, times(1)).watch(any(Watcher.class));\n    }\n\n    @Test\n    void shouldDisposeCorrectly() {\n        synchronizer.start();\n        assertFalse(synchronizer.isDisposed());\n        assertTrue(synchronizer.isStarted());\n\n        synchronizer.dispose();\n\n        assertTrue(synchronizer.isDisposed());\n        assertTrue(synchronizer.isStarted());\n        verify(watcher, times(1)).dispose();\n    }\n\n    @Test\n    void shouldNotStartAfterDisposing() {\n        synchronizer.dispose();\n        synchronizer.start();\n\n        verify(client, times(0)).list(any(), any(), any());\n        verify(watcher, times(0)).onAdd(isA(Reconciler.Request.class));\n        verify(client, times(0)).watch(any());\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/index/IndexAttributeFactoryTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Metadata;\n\n/**\n * Tests for {@link IndexAttributeFactory}.\n *\n * @author guqing\n * @since 2.12.0\n */\nclass IndexAttributeFactoryTest {\n\n    @Test\n    void shouldCreateMultiValueAttribute() {\n        var attribute = IndexAttributeFactory.multiValueAttribute(FakeExtension.class,\n            FakeExtension::getTags);\n        assertThat(attribute).isNotNull();\n        assertThat(attribute.getObjectType()).isEqualTo(FakeExtension.class);\n        var extension = new FakeExtension();\n        extension.setMetadata(new Metadata());\n        extension.getMetadata().setName(\"fake-name-1\");\n        extension.setTags(Set.of(\"tag1\", \"tag2\"));\n        Assertions.assertEquals(\n            Set.of(new UnknownKey(\"tag1\"), new UnknownKey(\"tag2\")),\n            attribute.getValues(extension)\n        );\n    }\n\n    @Test\n    void shouldCreateSingleValueAttribute() {\n        var attribute = IndexAttributeFactory.simpleAttribute(FakeExtension.class,\n            FakeExtension::getName);\n        assertThat(attribute).isNotNull();\n        assertThat(attribute.getObjectType()).isEqualTo(FakeExtension.class);\n        var extension = new FakeExtension();\n        extension.setMetadata(new Metadata());\n        extension.getMetadata().setName(\"fake-name-2\");\n        extension.setName(\"Fake Extension Name\");\n        Assertions.assertEquals(\n            Set.of(new UnknownKey(\"Fake Extension Name\")),\n            attribute.getValues(extension)\n        );\n    }\n\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    @GVK(group = \"test\", version = \"v1\", kind = \"FakeExtension\", plural = \"fakes\",\n        singular = \"fake\")\n    static class FakeExtension extends AbstractExtension {\n\n        private Set<String> tags;\n\n        private String name;\n\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/index/IndexSpecTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * Tests for {@link IndexSpec}.\n *\n * @author guqing\n * @since 2.12.0\n */\nclass IndexSpecTest {\n\n    @Test\n    void equalsVerifier() {\n        var spec1 = new IndexSpec()\n            .setName(\"metadata.name\")\n            .setOrder(IndexSpec.OrderType.ASC)\n            .setIndexFunc(IndexAttributeFactory.simpleAttribute(FakeExtension.class,\n                e -> e.getMetadata().getName())\n            )\n            .setUnique(true);\n\n        var spec2 = new IndexSpec()\n            .setName(\"metadata.name\")\n            .setOrder(IndexSpec.OrderType.ASC)\n            .setIndexFunc(IndexAttributeFactory.simpleAttribute(FakeExtension.class,\n                e -> e.getMetadata().getName())\n            )\n            .setUnique(true);\n\n        assertThat(spec1).isEqualTo(spec2);\n        assertThat(spec1.equals(spec2)).isTrue();\n        assertThat(spec1.hashCode()).isEqualTo(spec2.hashCode());\n\n        var spec3 = new IndexSpec()\n            .setName(\"metadata.name\")\n            .setOrder(IndexSpec.OrderType.DESC)\n            .setIndexFunc(IndexAttributeFactory.simpleAttribute(FakeExtension.class,\n                e -> e.getMetadata().getName())\n            )\n            .setUnique(false);\n        assertThat(spec1).isEqualTo(spec3);\n        assertThat(spec1.equals(spec3)).isTrue();\n        assertThat(spec1.hashCode()).isEqualTo(spec3.hashCode());\n\n        var spec4 = new IndexSpec()\n            .setName(\"slug\")\n            .setOrder(IndexSpec.OrderType.ASC)\n            .setIndexFunc(IndexAttributeFactory.simpleAttribute(FakeExtension.class,\n                e -> e.getMetadata().getName())\n            )\n            .setUnique(true);\n        assertThat(spec1.equals(spec4)).isFalse();\n        assertThat(spec1).isNotEqualTo(spec4);\n    }\n\n    @Test\n    void equalAnotherObject() {\n        var spec3 = new IndexSpec()\n            .setName(\"metadata.name\");\n        assertThat(spec3.equals(new Object())).isFalse();\n    }\n\n    @Test\n    void shouldNormalizeToSingleValueIndexSpec() {\n        var spec = new IndexSpec()\n            .setName(\"metadata.name\")\n            .setOrder(IndexSpec.OrderType.ASC)\n            .setIndexFunc(IndexAttributeFactory.simpleAttribute(FakeExtension.class,\n                e -> e.getMetadata().getName())\n            )\n            .setUnique(true);\n\n        var normalized = spec.normalize();\n\n        assertEquals(\"metadata.name\", normalized.getName());\n        assertTrue(normalized.isUnique());\n        assertTrue(normalized.isNullable());\n        assertInstanceOf(SingleValueIndexSpec.class, normalized);\n    }\n\n    @Test\n    void shouldNormalizeToMultiValueIndexSpec() {\n        var spec = new IndexSpec()\n            .setName(\"slug\")\n            .setOrder(IndexSpec.OrderType.ASC)\n            .setIndexFunc(IndexAttributeFactory.multiValueAttribute(FakeExtension.class,\n                e -> Set.of(e.getSlug())))\n            .setUnique(false);\n\n        var normalized = spec.normalize();\n\n        assertEquals(\"slug\", normalized.getName());\n        assertFalse(normalized.isUnique());\n        assertTrue(normalized.isNullable());\n        assertInstanceOf(MultiValueIndexSpec.class, normalized);\n    }\n\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    @GVK(group = \"test\", version = \"v1\", kind = \"Fake\", plural = \"fakes\", singular = \"fake\")\n    static class FakeExtension extends AbstractExtension {\n        private String slug;\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/index/KeyComparatorTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.TreeMap;\nimport java.util.TreeSet;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.RepeatedTest;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link KeyComparator}.\n *\n * @author guqing\n * @since 2.12.0\n */\nclass KeyComparatorTest {\n    private final KeyComparator comparator = KeyComparator.INSTANCE;\n\n    @Test\n    void keyComparator() {\n        String[] strings = {\"103\", \"101\", \"102\", \"1011\", \"1013\", \"1021\", \"1022\", \"1012\", \"1023\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(\n            new String[] {\"101\", \"102\", \"103\", \"1011\", \"1012\", \"1013\", \"1021\", \"1022\", \"1023\"});\n\n        Arrays.sort(strings, comparator.reversed());\n        assertThat(strings).isEqualTo(\n            new String[] {\"1023\", \"1022\", \"1021\", \"1013\", \"1012\", \"1011\", \"103\", \"102\", \"101\"});\n\n        // but if we use natural order, the result is:\n        Arrays.sort(strings, Comparator.naturalOrder());\n        assertThat(strings).isEqualTo(\n            new String[] {\"101\", \"1011\", \"1012\", \"1013\", \"102\", \"1021\", \"1022\", \"1023\", \"103\"});\n    }\n\n    @Test\n    void keyComparator2() {\n        String[] strings =\n            {\"moment-101\", \"moment-102\", \"moment-103\", \"moment-1011\", \"moment-1013\", \"moment-1021\",\n                \"moment-1022\", \"moment-1012\", \"moment-1023\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(new String[] {\"moment-101\", \"moment-102\", \"moment-103\",\n            \"moment-1011\", \"moment-1012\", \"moment-1013\", \"moment-1021\", \"moment-1022\",\n            \"moment-1023\"});\n\n        // date sort\n        strings =\n            new String[] {\"2022-01-15\", \"2022-02-01\", \"2021-12-25\", \"2022-01-01\", \"2022-01-02\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(\n            new String[] {\"2021-12-25\", \"2022-01-01\", \"2022-01-02\", \"2022-01-15\", \"2022-02-01\"});\n\n        // alphabet and number sort\n        strings = new String[] {\"abc123\", \"abc45\", \"abc9\", \"abc100\", \"abc20\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(\n            new String[] {\"abc9\", \"abc20\", \"abc45\", \"abc100\", \"abc123\"});\n\n        // test for pure alphabet sort\n        strings = new String[] {\"xyz\", \"abc\", \"def\", \"abcde\", \"xyzabc\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(new String[] {\"abc\", \"abcde\", \"def\", \"xyz\", \"xyzabc\"});\n\n        // test for empty string\n        strings = new String[] {\"\", \"abc\", \"123\", \"xyz\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(new String[] {\"\", \"123\", \"abc\", \"xyz\"});\n\n        // test for the same string\n        strings = new String[] {\"abc\", \"abc\", \"abc\", \"abc\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(new String[] {\"abc\", \"abc\", \"abc\", \"abc\"});\n\n        // test for null element\n        strings = new String[] {null, \"abc\", \"123\", \"xyz\"};\n        Arrays.sort(strings, comparator);\n        assertThat(strings).isEqualTo(new String[] {\"123\", \"abc\", \"xyz\", null});\n    }\n\n    @Test\n    void complexStringTest() {\n        var strings = new String[] {\n            \"1719560085223\",\n            \"1719564195757\",\n            \"AJHQ9JKT\",\n            \"1719565849173\",\n            \"5InykKCe\",\n            \"123123123\",\n            \"adJhTqEo\",\n            \"123123\",\n            \"Ahvcq7Wn\",\n            \"asda\",\n            \"b5jHcxfe\"\n        };\n        Arrays.sort(strings, comparator);\n        assertThat(strings).containsExactly(\n            \"5InykKCe\",\n            \"123123\",\n            \"123123123\",\n            \"1719560085223\",\n            \"1719564195757\",\n            \"1719565849173\",\n            \"AJHQ9JKT\",\n            \"Ahvcq7Wn\",\n            \"adJhTqEo\",\n            \"asda\",\n            \"b5jHcxfe\"\n        );\n    }\n\n    @Test\n    void complexButSkewedStringTest() {\n        var strings = new String[] {\n            \"chu-shi-hua-gong-neng-you-hua-halo-2.9.0-fa-bu\",\n            \"cxcc\",\n            \"d\",\n            \"dddd\",\n            \"ddddd\",\n            \"de-dao\",\n            \"dENMr6tX\",\n            \"dian-shang-ke-fu\",\n            \"dong-tai-she-ji-shi-xi-25jie\",\n            \"eeeeeeee\",\n            \"ejqRrTp4\",\n            \"Fh8Jd09T\",\n            \"g5gZaGvS\",\n        };\n        Arrays.sort(strings, comparator);\n        assertThat(strings).containsExactly(\n            \"Fh8Jd09T\",\n            \"chu-shi-hua-gong-neng-you-hua-halo-2.9.0-fa-bu\",\n            \"cxcc\",\n            \"d\",\n            \"dENMr6tX\",\n            \"dddd\",\n            \"ddddd\",\n            \"de-dao\",\n            \"dian-shang-ke-fu\",\n            \"dong-tai-she-ji-shi-xi-25jie\",\n            \"eeeeeeee\",\n            \"ejqRrTp4\",\n            \"g5gZaGvS\"\n        );\n    }\n\n    @Test\n    void mixLetterCaseStringTest() {\n        var strings = new String[] {\n            \"VpLBxBJ7\", \"AJHQ9JKT\", \"asda\", \"Tq5EgH2V\", \"Fh8Jd09T\", \"J7KMLQeK\", \"adJhTqEo\",\n            \"Ahvcq7Wn\",\n        };\n        Arrays.sort(strings, comparator);\n        assertThat(strings).containsExactly(\n            \"AJHQ9JKT\", \"Ahvcq7Wn\", \"Fh8Jd09T\", \"J7KMLQeK\", \"Tq5EgH2V\", \"VpLBxBJ7\", \"adJhTqEo\",\n            \"asda\"\n        );\n    }\n\n    @Test\n    void mixLetterCaseAndNumberTest() {\n        var strings = new String[] {\n            \"1719565849173\", \"1719564195757\", \"1703040584263\",\n            \"AJHQ9JKT\", \"Ahvcq7Wn\", \"Fh8Jd09T\", \"adJhTqEo\",\n            \"asda\", \"1703053590063\", \"1702955288482\",\n            \"zhi-chi-bei-fen-hui-fu-halo-2.8.0-fa-bu\",\n            \"zhi-chi-ge-ren-zhong-xin-halo-2.11.0-fa-bu\",\n            \"J7KMLQeK\", \"Tq5EgH2V\", \"VpLBxBJ7\",\n            \"b5jHcxfe\", \"cao-ni-ma-a-huang-jian-ming\", \"chu-ji-ying-jian-kai-fa\",\n            \"ddddd\", \"de-dao\", \"dian-shang-ke-fu\", \"eeeeeeee\", \"ejqRrTp4\",\n            \"halo-maintainer-2023-nian-du-bang-dan\", \"hello-halo\", \"hello-world\",\n            \"dong-tai-she-ji-shi-xi-25jie\", \"halo-nuan-dong-li-yu-quan-chang-qi-zhe-qi\", \"hello\",\n            \"kai-fang-gong-gong-api-halo-2.5.0-fa-bu\",\n            \"xing-neng-you-hua-yu-gong-neng-gai-jin-halo-2.13-fa-bu\", \"ye-wu-tuo-zhan-jing-li\",\n            \"ying-qu-jing-mei-zhou-bian-halo-ying-yong-shi-chang-zhu-ti-you-jiang-zheng-ji\",\n            \"zhi-chi-bao-chi-deng-lu-hui-hua-halo-2.16.0-fa-bu\",\n        };\n        Arrays.sort(strings, comparator);\n        assertThat(strings).containsExactly(\n            \"1702955288482\",\n            \"1703040584263\",\n            \"1703053590063\",\n            \"1719564195757\",\n            \"1719565849173\",\n            \"AJHQ9JKT\",\n            \"Ahvcq7Wn\",\n            \"Fh8Jd09T\",\n            \"J7KMLQeK\",\n            \"Tq5EgH2V\",\n            \"VpLBxBJ7\",\n            \"adJhTqEo\",\n            \"asda\",\n            \"b5jHcxfe\",\n            \"cao-ni-ma-a-huang-jian-ming\",\n            \"chu-ji-ying-jian-kai-fa\",\n            \"ddddd\",\n            \"de-dao\",\n            \"dian-shang-ke-fu\",\n            \"dong-tai-she-ji-shi-xi-25jie\",\n            \"eeeeeeee\",\n            \"ejqRrTp4\",\n            \"halo-maintainer-2023-nian-du-bang-dan\",\n            \"halo-nuan-dong-li-yu-quan-chang-qi-zhe-qi\",\n            \"hello\",\n            \"hello-halo\",\n            \"hello-world\",\n            \"kai-fang-gong-gong-api-halo-2.5.0-fa-bu\",\n            \"xing-neng-you-hua-yu-gong-neng-gai-jin-halo-2.13-fa-bu\",\n            \"ye-wu-tuo-zhan-jing-li\",\n            \"ying-qu-jing-mei-zhou-bian-halo-ying-yong-shi-chang-zhu-ti-you-jiang-zheng-ji\",\n            \"zhi-chi-bao-chi-deng-lu-hui-hua-halo-2.16.0-fa-bu\",\n            \"zhi-chi-bei-fen-hui-fu-halo-2.8.0-fa-bu\",\n            \"zhi-chi-ge-ren-zhong-xin-halo-2.11.0-fa-bu\");\n    }\n\n    @Test\n    public void sortingWithComplexStringsTest() {\n        List<String> strings = Arrays.asList(\"abc10\", \"abc2\", \"abc1\", \"abc20\", \"abc100\");\n        strings.sort(comparator);\n        assertThat(strings).containsExactly(\"abc1\", \"abc2\", \"abc10\", \"abc20\", \"abc100\");\n    }\n\n    @Test\n    public void sortingWithDecimalStringsTest() {\n        List<String> strings =\n            Arrays.asList(\"1.2\", \"1.10\", \"1.1\", \"1.20\", \"1.02\", \"1.22\", \"1.001\", \"1.002\");\n        strings.sort(comparator);\n        assertThat(strings).containsExactly(\"1.001\", \"1.002\", \"1.02\", \"1.1\", \"1.10\", \"1.2\", \"1.20\",\n            \"1.22\");\n    }\n\n    @Test\n    public void treeSetWithComparatorTest() {\n        TreeSet<String> set = new TreeSet<>(comparator);\n        set.add(\"abc123\");\n        set.add(\"abc1\");\n        set.add(\"abc12\");\n        set.add(\"abc2\");\n\n        assertThat(set).containsExactly(\"abc1\", \"abc2\", \"abc12\", \"abc123\");\n    }\n\n    @Test\n    public void testTreeMap_WithComparator() {\n        TreeMap<String, String> map = new TreeMap<>(comparator);\n        map.put(\"2024-08-29\", \"date1\");\n        map.put(\"2024-08-28\", \"date2\");\n        map.put(\"2024-08-30\", \"date3\");\n\n        assertThat(map.keySet()).containsExactly(\"2024-08-28\", \"2024-08-29\", \"2024-08-30\");\n        assertThat(map.get(\"2024-08-29\")).isEqualTo(\"date1\");\n    }\n\n    @Test\n    public void integerPartDifferentTest() {\n        // Create strings with different integer parts to cover the compareIntegerPart code\n        // block\n        String[] strings = {\"abc10\", \"abc2\", \"abc1\", \"abc20\", \"abc10022229\"};\n        Arrays.sort(strings, comparator);\n\n        String[] expectedOrder = {\"abc1\", \"abc2\", \"abc10\", \"abc20\", \"abc10022229\"};\n        assertThat(strings).containsExactly(expectedOrder);\n    }\n\n    @Test\n    public void integerPartDifferentWithDecimalTest() {\n        // To specifically test integer part comparison\n        String str1 = \"abc12.5\";\n        String str2 = \"abc11.5\";\n\n        // Compare should return a positive number since \"12\" > \"11\"\n        assertThat(comparator.compare(str1, str2)).isPositive();\n\n        String str3 = \"abc11.5\";\n        String str4 = \"abc12.5\";\n\n        // Compare should return a negative number since \"11\" < \"12\"\n        assertThat(comparator.compare(str3, str4)).isNegative();\n\n        // Test for multiple decimal points\n        assertThat(comparator.compare(\"1.23.4\", \"1.23\")).isGreaterThan(0);\n        assertThat(comparator.compare(\"1.23\", \"1.23.4\")).isLessThan(0);\n\n        assertThat(comparator.compare(\"1..23\", \"1.23\")).isLessThan(0);\n        assertThat(comparator.compare(\"1.23..\", \"1.23\")).isGreaterThan(0);\n\n        assertThat(comparator.compare(\"\", \"1.23\")).isLessThan(0);\n        assertThat(comparator.compare(\"1.23\", \"\")).isGreaterThan(0);\n\n        assertThat(comparator.compare(\"1.23\", \"1.23\")).isZero();\n    }\n\n    @Nested\n    class ComparatorCharacteristicTest {\n        @Test\n        public void reflexiveTest() {\n            // Reflexive: a == a should always return 0\n            assertThat(comparator.compare(\"test\", \"test\")).isZero();\n            assertThat(comparator.compare(\"\", \"\")).isZero();\n            assertThat(comparator.compare(\"123\", \"123\")).isZero();\n            assertThat(comparator.compare(null, null)).isZero();\n        }\n\n        @Test\n        public void symmetricTest() {\n            // Symmetric: a > b implies b < a\n            assertThat(comparator.compare(\"123\", \"test\")).isNegative();\n            assertThat(comparator.compare(\"test\", \"123\")).isPositive();\n\n            assertThat(comparator.compare(\"1.023\", \"1.23\")).isNegative();\n            assertThat(comparator.compare(\"1.23\", \"1.023\")).isPositive();\n        }\n\n        @Test\n        public void transitiveTest() {\n            // Transitive: a > b and b > c implies a > c\n            assertThat(comparator.compare(\"test2\", \"test1\")).isPositive();\n            assertThat(comparator.compare(\"test1\", \"test0\")).isPositive();\n            assertThat(comparator.compare(\"test2\", \"test0\")).isPositive();\n        }\n\n        @RepeatedTest(50)\n        public void consistencyTest() {\n            // Consistency: a == b should always return 0 if not changed\n            assertThat(comparator.compare(\"123abc\", \"123abc\")).isZero();\n            assertThat(comparator.compare(\"test\", \"test\")).isZero();\n            assertThat(comparator.compare(\"123abc\", \"123abc\"))\n                .isEqualTo(comparator.compare(\"123abc\", \"123abc\"));\n        }\n\n        @Test\n        public void withNumbersTest() {\n            // Numbers should be compared numerically\n            assertThat(comparator.compare(\"item2\", \"item10\")).isNegative();\n            assertThat(comparator.compare(\"item10\", \"item2\")).isPositive();\n            assertThat(comparator.compare(\"item10\", \"item10\")).isZero();\n        }\n\n        @Test\n        public void mixedContentTest() {\n            // Mixed content comparison\n            assertThat(comparator.compare(\"abc123\", \"abc124\")).isNegative();\n            assertThat(comparator.compare(\"abc124\", \"abc123\")).isPositive();\n            assertThat(comparator.compare(\"abc123\", \"abc123\")).isZero();\n        }\n\n        @Test\n        public void nullHandlingTest() {\n            // Null handling\n            assertThat(comparator.compare(null, \"test\")).isPositive();\n            assertThat(comparator.compare(\"test\", null)).isNegative();\n            assertThat(comparator.compare(null, null)).isZero();\n        }\n\n        @Test\n        public void lengthDifferenceTest() {\n            // Length difference should affect comparison\n            assertThat(comparator.compare(\"test\", \"testa\")).isNegative();\n            assertThat(comparator.compare(\"testa\", \"test\")).isPositive();\n        }\n\n        @Test\n        public void specialCharactersTest() {\n            // Special character comparison\n            assertThat(comparator.compare(\"a#1\", \"a#2\")).isNegative();\n            assertThat(comparator.compare(\"a#2\", \"a#1\")).isPositive();\n            assertThat(comparator.compare(\"a#1\", \"a#1\")).isZero();\n        }\n\n        @Test\n        public void emptyStringsTest() {\n            // Empty string comparison\n            assertThat(comparator.compare(\"\", \"test\")).isNegative();\n            assertThat(comparator.compare(\"test\", \"\")).isPositive();\n            assertThat(comparator.compare(\"\", \"\")).isZero();\n        }\n    }\n\n    @Nested\n    class ComparatorEdgeTest {\n        @Test\n        public void pureNumbersTest() {\n            assertThat(comparator.compare(\"123\", \"123\")).isEqualTo(0);\n            assertThat(comparator.compare(\"123\", \"124\")).isLessThan(0);\n            assertThat(comparator.compare(\"124\", \"123\")).isGreaterThan(0);\n            // Leading zeros\n            assertThat(comparator.compare(\"00123\", \"123\") > 0).isTrue();\n            assertThat(comparator.compare(\"0\", \"0\")).isEqualTo(0);\n            assertThat(comparator.compare(\"0\", \"0000\")).isLessThan(0);\n            assertThat(comparator.compare(\"0x\", \"0\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"0\", \"1\")).isLessThan(0);\n            assertThat(comparator.compare(\"1\", \"0\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"001\", \"0\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"0x5e\", \"0000\")).isLessThan(0);\n        }\n\n        @Test\n        public void mumbersWithOverflowTest() {\n            // Max long value\n            String largeNumber1 = \"9223372036854775807\";\n            // One more than max long value\n            String largeNumber2 = \"9223372036854775808\";\n            assertThat(comparator.compare(largeNumber1, largeNumber2)).isLessThan(0);\n            assertThat(comparator.compare(largeNumber2, largeNumber1)).isGreaterThan(0);\n\n            // large number str comparison\n            assertThat(comparator.compare(\"123456789012345678901234567890\",\n                \"123456789012345678901234567891\")).isLessThan(0);\n            assertThat(comparator.compare(\"123456789012345678901234567890\",\n                \"123456789012345678901234567891\")).isNotPositive();\n            assertThat(comparator.compare(\"999999999999999999999999999999\",\n                \"999999999999999999999999999998\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"999999999999999999999999999999\",\n                \"999999999999999999999999999998\")).isNotNegative();\n            assertThat(comparator.compare(\"9999999999999999999999999999999999999999999999\",\n                \"9999999999999999999999999999999999999999999998\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"100000000000000000000000000000\",\n                \"100000000000000000000000000000\")).isEqualTo(0);\n\n            // This specific case is to test the overflow for a real-world scenario\n            assertThat(comparator.compare(\"5InykKCe\", \"1710683457874\") < 0).isTrue();\n            assertThat(comparator.compare(\"5InykKce\", \"1717477435943\") > 0).isFalse();\n\n            assertThat(comparator.compare(\"0\",\n                \"9999999999999999999999999999999999999999999998\")).isLessThan(0);\n            assertThat(comparator.compare(\"9999999999999999999999999999999999999999999998\",\n                \"0\")).isGreaterThan(0);\n        }\n\n        @Test\n        public void decimalStringsTest() {\n            assertThat(comparator.compare(\"123.45\", \"123.45\")).isEqualTo(0);\n            assertThat(comparator.compare(\"123.45\", \"123.46\")).isLessThan(0);\n            assertThat(comparator.compare(\"123.46\", \"123.45\")).isGreaterThan(0);\n            // Decimal equivalence\n            assertThat(comparator.compare(\"123.5\", \"123.50\")).isLessThan(0);\n            assertThat(comparator.compare(\"123.0005\", \"123.00050\")).isLessThan(0);\n        }\n\n        @Test\n        public void lettersAndNumbersTest() {\n            assertThat(comparator.compare(\"abc123\", \"abc123\")).isEqualTo(0);\n            assertThat(comparator.compare(\"abc123\", \"abc124\")).isLessThan(0);\n            assertThat(comparator.compare(\"abc124\", \"abc123\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"abc123\", \"abcd123\")).isLessThan(0);\n        }\n\n        @Test\n        public void pureLettersTest() {\n            assertThat(comparator.compare(\"abc\", \"abc\")).isEqualTo(0);\n            assertThat(comparator.compare(\"abc\", \"abcd\")).isLessThan(0);\n            assertThat(comparator.compare(\"abcd\", \"abc\")).isGreaterThan(0);\n            // Case sensitivity\n            assertThat(comparator.compare(\"ABC\", \"abc\")).isLessThan(0);\n        }\n\n        @Test\n        public void dateStringsTest() {\n            assertThat(comparator.compare(\"2024-08-29\", \"2024-08-29\")).isEqualTo(0);\n            assertThat(comparator.compare(\"2024-08-29\", \"2024-08-30\")).isLessThan(0);\n            assertThat(comparator.compare(\"2024-08-30\", \"2024-08-29\")).isGreaterThan(0);\n\n            // Time comparison\n            assertThat(comparator.compare(\"2024-08-29T12:00:00.000Z\", \"2024-08-29T12:00:00.001Z\"))\n                .isLessThan(0);\n            assertThat(comparator.compare(\"2024-08-29T12:00:00.001Z\", \"2024-08-29T12:00:00.000Z\"))\n                .isGreaterThan(0);\n            assertThat(comparator.compare(\"2024-08-29T12:00:00.000Z\", \"2024-08-29T12:00:01.000Z\"))\n                .isLessThan(0);\n            assertThat(comparator.compare(\"2024-08-29T12:00:01.000Z\", \"2024-08-29T12:00:00.000Z\"))\n                .isGreaterThan(0);\n            assertThat(comparator.compare(\"2024-08-29T12:00:00.000Z\", \"2024-08-29T12:01:00.000Z\"))\n                .isLessThan(0);\n            assertThat(comparator.compare(\"2024-08-29T12:01:00.000Z\", \"2024-08-29T12:00:00.000Z\"))\n                .isGreaterThan(0);\n            assertThat(comparator.compare(\"2024-08-29T12:00:00.000Z\", \"2024-08-29T13:00:00.000Z\"))\n                .isLessThan(0);\n            assertThat(comparator.compare(\"2024-08-29T13:00:00.000Z\", \"2024-08-29T12:00:00.000Z\"))\n                .isGreaterThan(0);\n            assertThat(comparator.compare(\"2024-08-29T12:00:00.000Z\", \"2024-08-30T12:00:00.000Z\"))\n                .isLessThan(0);\n            assertThat(comparator.compare(\"2024-08-30T12:00:00.000Z\", \"2024-08-29T12:00:00.000Z\"))\n                .isGreaterThan(0);\n        }\n\n        @Test\n        public void booleanStringsTest() {\n            assertThat(comparator.compare(\"true\", \"false\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"false\", \"true\")).isLessThan(0);\n            assertThat(comparator.compare(\"true\", \"true\")).isEqualTo(0);\n            assertThat(comparator.compare(\"false\", \"false\")).isEqualTo(0);\n        }\n\n        @Test\n        public void complexMixedStringsTest() {\n            assertThat(comparator.compare(\"abc123xyz456\", \"abc123xyz456\")).isEqualTo(0);\n            assertThat(comparator.compare(\"abc123xyz456\", \"abc124xyz456\")).isLessThan(0);\n            assertThat(comparator.compare(\"abc124xyz456\", \"abc123xyz456\")).isGreaterThan(0);\n            assertThat(comparator.compare(\"abc123xyz456\", \"abc123xyz457\")).isLessThan(0);\n        }\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/index/MultiValueBuilderTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Collections;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.FakeExtension;\n\nclass MultiValueBuilderTest {\n\n    @Test\n    void throwIfNoNameProvided() {\n        assertThrows(IllegalArgumentException.class,\n            () -> new MultiValueBuilder<FakeExtension, String>(null, String.class)\n        );\n    }\n\n    @Test\n    void throwIfNoKeyTypeProvided() {\n        assertThrows(IllegalArgumentException.class,\n            () -> new MultiValueBuilder<FakeExtension, String>(\"metadata.name\", null)\n        );\n    }\n\n    @Test\n    void throwIfNoIndexFuncProvided() {\n        var builder = new MultiValueBuilder<FakeExtension, String>(\"metadata.name\", String.class);\n        assertThrows(IllegalArgumentException.class, builder::build);\n    }\n\n    @Test\n    void shouldBuildCorrectly() {\n        var builder = new MultiValueBuilder<FakeExtension, String>(\"metadata.name\", String.class)\n            .indexFunc(e -> Collections.singleton(e.getMetadata().getName()));\n        var indexSpec = builder.build();\n        assertNotNull(indexSpec);\n        assertInstanceOf(MultiValueIndexSpec.class, indexSpec);\n        assertEquals(String.class, indexSpec.getKeyType());\n        assertEquals(\"metadata.name\", indexSpec.getName());\n        assertFalse(indexSpec.isUnique());\n        assertTrue(indexSpec.isNullable());\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/index/SingleValueBuilderTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.FakeExtension;\n\nclass SingleValueBuilderTest {\n\n    @Test\n    void throwIfNoNameProvided() {\n        assertThrows(IllegalArgumentException.class,\n            () -> new SingleValueBuilder<FakeExtension, String>(null, String.class)\n        );\n    }\n\n    @Test\n    void throwIfNoKeyTypeProvided() {\n        assertThrows(IllegalArgumentException.class, () ->\n            new SingleValueBuilder<FakeExtension, String>(\"metadata.name\", null)\n        );\n    }\n\n    @Test\n    void throwIfNoIndexFuncProvided() {\n        var builder = new SingleValueBuilder<FakeExtension, String>(\"metadata.name\", String.class);\n        assertThrows(IllegalArgumentException.class, builder::build);\n    }\n\n    @Test\n    void shouldBuildCorrectly() {\n        var builder = new SingleValueBuilder<FakeExtension, String>(\"metadata.name\", String.class)\n            .indexFunc(e -> e.getMetadata().getName());\n        var indexSpec = builder.build();\n        assertNotNull(indexSpec);\n        assertInstanceOf(SingleValueIndexSpec.class, indexSpec);\n        assertEquals(String.class, indexSpec.getKeyType());\n        assertEquals(\"metadata.name\", indexSpec.getName());\n        assertFalse(indexSpec.isUnique());\n        assertTrue(indexSpec.isNullable());\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/index/UnknownKeyTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.Objects;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\n\nclass UnknownKeyTest {\n\n    @ParameterizedTest\n    @CsvSource({\n        \"a, a\",\n        \"b, b\",\n        \",\",\n        \"abc, abc\",\n        \"2025, 2025\",\n        \"true, true\",\n        \"2025-10-20, 2025-10-20\",\n        \"特殊字符, 特殊字符\"\n    })\n    void shouldEqualsWorkCorrectly(String key1, String key2) {\n        assertEquals(new UnknownKey(key1), new UnknownKey(key2));\n        assertEquals(new UnknownKey(null), new UnknownKey(null));\n    }\n\n    @ParameterizedTest\n    @CsvSource(\n        {\n            \"a, a, 0\",\n            \"a, b, -1\",\n            \"b, a, 1\",\n            \"abc, abd, -1\",\n            \"abd, abc, 1\",\n            \"0.1, 0.1, 0\",\n            \"0.1, 0.2, -1\",\n            \"0.2, 0.1, 1\",\n            \"true, true, 0\",\n            \"false, true, -1\",\n            \"true, false, 1\",\n            \"-0.1, -0.1, 0\",\n            \"-1, -1, 0\",\n            \"2025, 2025, 0\",\n            \"2025, 2026, -1\",\n            \"2026, 2025, 1\",\n            \"2025-10-20, 2025-10-20, 0\",\n            \"2025-10-20, 2025-10-21, -1\",\n            \"2025-10-21, 2025-10-20, 1\",\n            \"特殊字符A, 特殊字符B, -1\",\n            \"特殊字符B, 特殊字符A, 1\"\n        }\n    )\n    void shouldCompareCorrectly(String key1, String key2, int expected) {\n        var compare = new UnknownKey(key1).compareTo(new UnknownKey(key2));\n        assertTrue(Objects.equals(expected, compare) || expected * compare > 0);\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/index/query/QueriesTest.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nclass QueriesTest {\n\n    @Test\n    void shouldBuildAndConditionStaticMethod() {\n        var condition = Queries.and(\n            Queries.equal(\"status\", \"active\"),\n            Queries.greaterThan(\"age\", 18)\n        );\n        assertEquals(\"(status = active AND age > 18)\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildOrConditionStaticMethod() {\n        var condition = Queries.or(\n            Queries.lessThan(\"price\", 50, false),\n            Queries.equal(\"category\", \"Books\")\n        );\n        assertEquals(\"(price < 50 OR category = Books)\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotConditionStaticMethod() {\n        var condition = Queries.not(Queries.equal(\"role\", \"Admin\"));\n        assertEquals(\"role != Admin\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildBetweenCondition() {\n        var condition = Queries.between(\"age\", 18, true, 30, false);\n        assertEquals(\"age BETWEEN [18, 30)\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildEqualCondition() {\n        var condition = Queries.equal(\"name\", \"John\");\n        assertEquals(\"name = John\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildInCondition() {\n        var condition = Queries.in(\"status\", \"active\", \"pending\");\n        assertEquals(\"status IN (active, pending)\", condition.toString());\n\n        condition = Queries.in(\"status\", List.<String>of(\"active\", \"pending\"));\n        assertEquals(\"status IN (active, pending)\", condition.toString());\n    }\n\n    @Test\n    void shouldRefineInConditionWithSingleValueToEqualCondition() {\n        var condition = Queries.in(\"status\", \"active\");\n        assertEquals(\"status = active\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildGreaterThanCondition() {\n        var condition = Queries.greaterThan(\"score\", 85);\n        assertEquals(\"score > 85\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLessThanCondition() {\n        var condition = Queries.lessThan(\"price\", 100, true);\n        assertEquals(\"price <= 100\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLessThanConditionExclusive() {\n        var condition = Queries.lessThan(\"price\", 100);\n        assertEquals(\"price < 100\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildEmptyCondition() {\n        var condition = Queries.empty();\n        assertEquals(\"EMPTY\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildAllCondition() {\n        var condition = Queries.all(\"tags\");\n        assertEquals(\"ALL tags\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotEqualCondition() {\n        var condition = Queries.notEqual(\"name\", \"Alice\");\n        assertEquals(\"name != Alice\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotBetweenCondition() {\n        var condition = Queries.between(\"age\", 20, false, 40, true).not();\n        assertEquals(\"age NOT BETWEEN [20, 40)\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotInCondition() {\n        var condition = Queries.in(\"status\", \"inactive\", \"banned\").not();\n        assertEquals(\"status NOT IN (inactive, banned)\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotAllCondition() {\n        var condition = Queries.all(\"categories\").not();\n        assertEquals(\"NONE categories\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildStartsWithCondition() {\n        var condition = Queries.startsWith(\"username\", \"admin\");\n        assertEquals(\"username STARTS WITH admin\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotStartsWithCondition() {\n        var condition = Queries.startsWith(\"username\", \"guest\").not();\n        assertEquals(\"username NOT STARTS WITH guest\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildEndsWithCondition() {\n        var condition = Queries.endsWith(\"email\", \"@example.com\");\n        assertEquals(\"email ENDS WITH @example.com\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotEndsWithCondition() {\n        var condition = Queries.endsWith(\"email\", \"@spam.com\").not();\n        assertEquals(\"email NOT ENDS WITH @spam.com\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildContainsCondition() {\n        var condition = Queries.contains(\"description\", \"important\");\n        assertEquals(\"description CONTAINS important\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildNotContainsCondition() {\n        var condition = Queries.contains(\"notes\", \"confidential\").not();\n        assertEquals(\"notes NOT CONTAINS confidential\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildAndCondition() {\n        var condition = Queries.equal(\"status\", \"active\")\n            .and(Queries.greaterThan(\"age\", 18));\n        assertEquals(\"(status = active AND age > 18)\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildOrCondition() {\n        var condition = Queries.lessThan(\"price\", 50, false)\n            .or(Queries.equal(\"category\", \"Books\"));\n        assertEquals(\"(price < 50 OR category = Books)\", condition.toString());\n    }\n\n\n    @Test\n    void shouldBuildNotCondition() {\n        var condition = Queries.equal(\"role\", \"Admin\").not();\n        assertEquals(\"role != Admin\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLabelExistsCondition() {\n        var condition = Queries.labelExists(\"premiumUser\");\n        assertEquals(\"EXISTS metadata.labels['premiumUser']\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLabelNotExistsCondition() {\n        var condition = Queries.labelExists(\"betaTester\").not();\n        assertEquals(\"NOT EXISTS metadata.labels['betaTester']\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLabelEqualCondition() {\n        var condition = Queries.labelEqual(\"region\", \"CN\");\n        assertEquals(\"metadata.labels['region'] = 'CN'\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLabelNotEqualCondition() {\n        var condition = Queries.labelEqual(\"tier\", \"gold\").not();\n        assertEquals(\"metadata.labels['tier'] <> 'gold'\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLabelInCondition() {\n        var condition = Queries.labelIn(\"env\", List.of(\"prod\", \"staging\"));\n        assertEquals(\"metadata.labels['env'] IN ('prod', 'staging')\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildLabelNotInCondition() {\n        var condition = Queries.labelIn(\"version\", List.of(\"v1\", \"v2\")).not();\n        assertEquals(\"metadata.labels['version'] NOT IN ('v1', 'v2')\", condition.toString());\n    }\n\n    @Test\n    void shouldBuildChainedConditions() {\n        var condition = Queries.equal(\"name\", \"Bob\")\n            .and(Queries.greaterThan(\"age\", 25))\n            .or(Queries.in(\"status\", \"active\", \"pending\"));\n        assertEquals(\n            \"((name = Bob AND age > 25) OR status IN (active, pending))\", condition.toString()\n        );\n    }\n\n    @Test\n    void shouldBuildComplexCondition() {\n        var condition = Queries.between(\"salary\", 50000, true, 100000, false)\n            .and(Queries.equal(\"department\", \"Engineering\").not())\n            .or(Queries.in(\"role\", \"Manager\", \"Director\"));\n        assertEquals(\"\"\"\n            ((salary BETWEEN [50000, 100000) AND department != Engineering) OR role IN (Manager,\\\n             Director))\\\n            \"\"\", condition.toString()\n        );\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/indexer/DefaultIndexEngineTest.java",
    "content": "package run.halo.app.extension.indexer;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.PriorityQueue;\nimport org.junit.jupiter.api.Test;\n\nclass DefaultIndexEngineTest {\n\n    @Test\n    void priorityQueueTest() {\n        int n = 3;\n        var pq = new PriorityQueue<>(n, Comparator.<Integer>naturalOrder().reversed());\n        List.of(5, 4, 3, 2, 1).forEach(i -> {\n            pq.offer(i);\n            if (pq.size() > n) {\n                pq.poll();\n            }\n        });\n        var result = new ArrayList<>();\n        while (!pq.isEmpty()) {\n            result.addFirst(pq.poll());\n        }\n        result.forEach(System.out::println);\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/indexer/LabelIndexImplTest.java",
    "content": "package run.halo.app.extension.indexer;\n\nimport java.util.concurrent.ConcurrentSkipListMap;\nimport org.junit.jupiter.api.Test;\n\nclass LabelIndexImplTest {\n\n    @Test\n    void stringPrefixTest() {\n        var map = new ConcurrentSkipListMap<String, String>();\n        map.put(\"a@b\", \"1\");\n        map.put(\"a@c\", \"2\");\n        map.put(\"a@cdefg\", \"2\");\n        map.put(\"a@d\", \"3\");\n        map.put(\"b@d\", \"4\");\n\n        var subMap = map.subMap(\"a@\", \"a@\" + Character.MAX_VALUE);\n        subMap.sequencedKeySet().forEach(System.out::println);\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/router/selector/LabelSelectorTest.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link LabelSelector}.\n *\n * @author guqing\n * @since 2.17.0\n */\nclass LabelSelectorTest {\n\n    @Test\n    void builderTest() {\n        var labelSelector = LabelSelector.builder()\n            .eq(\"a\", \"v1\")\n            .in(\"b\", \"v2\", \"v3\")\n            .build();\n        assertThat(labelSelector.toString())\n            .isEqualTo(\"\"\"\n                (metadata.labels['a'] = 'v1' AND metadata.labels['b'] IN ('v2', 'v3'))\\\n                \"\"\");\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/router/selector/OperatorTest.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static run.halo.app.extension.router.selector.Operator.Equals;\nimport static run.halo.app.extension.router.selector.Operator.Exist;\nimport static run.halo.app.extension.router.selector.Operator.IN;\nimport static run.halo.app.extension.router.selector.Operator.NotEquals;\nimport static run.halo.app.extension.router.selector.Operator.NotExist;\n\nimport java.util.List;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\nclass OperatorTest {\n\n    @Test\n    void shouldConvertCorrectly() {\n        record TestCase(String source, Operator converter, SelectorCriteria expected) {\n        }\n\n        List.of(\n            new TestCase(\"\", Equals, null),\n            new TestCase(\"=\", Equals, null),\n            new TestCase(\"=value\", Equals, null),\n            new TestCase(\"name=\", Equals, null),\n            new TestCase(\"name=value\", Equals,\n                new SelectorCriteria(\"name\", Equals, Set.of(\"value\"))),\n            new TestCase(\"name=v\", Equals,\n                new SelectorCriteria(\"name\", Equals, Set.of(\"v\"))),\n\n            new TestCase(\"\", NotEquals, null),\n            new TestCase(\"=\", NotEquals, null),\n            new TestCase(\"!\", NotEquals, null),\n            new TestCase(\"!=\", NotEquals, null),\n            new TestCase(\"!=value\", NotEquals, null),\n            new TestCase(\"name!=\", NotEquals, null),\n            new TestCase(\"name!=value\", NotEquals,\n                new SelectorCriteria(\"name\", NotEquals, Set.of(\"value\"))),\n\n            new TestCase(\"\", NotExist, null),\n            new TestCase(\"!\", NotExist, null),\n            new TestCase(\"!name\", NotExist, new SelectorCriteria(\"name\", NotExist, Set.of())),\n            new TestCase(\"name\", NotExist, null),\n            new TestCase(\"na!me\", NotExist, null),\n            new TestCase(\"name!\", NotExist, null),\n            new TestCase(\"name!=1\", NotEquals,\n                new SelectorCriteria(\"name\", NotEquals, Set.of(\"1\"))),\n            new TestCase(\"name!=12\", NotEquals,\n                new SelectorCriteria(\"name\", NotEquals, Set.of(\"12\"))),\n\n            new TestCase(\"name\", Exist, new SelectorCriteria(\"name\", Exist, Set.of())),\n            new TestCase(\"\", Exist, null),\n            new TestCase(\"!\", Exist, new SelectorCriteria(\"!\", Exist, Set.of())),\n            new TestCase(\"a\", Exist, new SelectorCriteria(\"a\", Exist, Set.of())),\n\n            new TestCase(\"name\", IN, null),\n            new TestCase(\"name=(fake-name)\", IN,\n                new SelectorCriteria(\"name\", IN, Set.of(\"fake-name\"))),\n            new TestCase(\"name=(first-name,second-name)\", IN,\n                new SelectorCriteria(\"name\", IN, Set.of(\"first-name\", \"second-name\")))\n        ).forEach(testCase -> {\n            log.debug(\"Testing: {}\", testCase);\n            assertEquals(testCase.expected(), testCase.converter().convert(testCase.source()));\n        });\n    }\n\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/extension/router/selector/SelectorConverterTest.java",
    "content": "package run.halo.app.extension.router.selector;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static run.halo.app.extension.router.selector.Operator.Equals;\n\nimport java.util.List;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\nclass SelectorConverterTest {\n\n    SelectorConverter converter = new SelectorConverter();\n\n    @Test\n    void shouldConvertCorrectly() {\n        record TestCase(String selector, SelectorCriteria expected) {\n        }\n\n        List.of(\n            new TestCase(\"\", null),\n            new TestCase(\"name=value\",\n                new SelectorCriteria(\"name\", Equals, Set.of(\"value\"))),\n            new TestCase(\"name!=value\",\n                new SelectorCriteria(\"name\", Operator.NotEquals, Set.of(\"value\"))),\n            new TestCase(\"name\",\n                new SelectorCriteria(\"name\", Operator.Exist, Set.of())),\n            new TestCase(\"!name\",\n                new SelectorCriteria(\"name\", Operator.NotExist, Set.of())),\n            new TestCase(\"name\",\n                new SelectorCriteria(\"name\", Operator.Exist, Set.of())),\n            new TestCase(\"name!=\",\n                new SelectorCriteria(\"name!=\", Operator.Exist, Set.of())),\n            new TestCase(\"==\",\n                new SelectorCriteria(\"==\", Operator.Exist, Set.of()))\n        ).forEach(testCase -> {\n            log.debug(\"Testing: {}\", testCase);\n            assertEquals(testCase.expected, converter.convert(testCase.selector));\n        });\n    }\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/infra/utils/GenericClassUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListResult;\n\nclass GenericClassUtilsTest {\n\n    @Test\n    void generateConcreteClass() {\n        var clazz = GenericClassUtils.generateConcreteClass(ListResult.class, Post.class,\n            () -> Post.class.getName() + \"List\");\n        assertEquals(\"run.halo.app.core.extension.content.PostList\", clazz.getName());\n        assertEquals(\"run.halo.app.core.extension.content\", clazz.getPackageName());\n    }\n\n}"
  },
  {
    "path": "api/src/test/java/run/halo/app/infra/utils/JsonUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link JsonUtils}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class JsonUtilsTest {\n\n    @Test\n    public void serializerTime() {\n        Instant now = Instant.now();\n        String instantStr = JsonUtils.objectToJson(now);\n        assertThat(instantStr).isNotNull();\n\n        String localDateTimeStr = JsonUtils.objectToJson(LocalDateTime.now());\n        assertThat(localDateTimeStr).isNotNull();\n    }\n\n    @Test\n    @SuppressWarnings(\"rawtypes\")\n    public void deserializerArrayString() {\n        String s = \"[\\\"hello\\\", \\\"world\\\"]\";\n        List list = JsonUtils.jsonToObject(s, List.class);\n        assertThat(list).isEqualTo(List.of(\"hello\", \"world\"));\n    }\n}\n"
  },
  {
    "path": "api/src/test/java/run/halo/app/infra/utils/PathUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link PathUtils}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass PathUtilsTest {\n\n    @Test\n    void combinePath() {\n        Map<String, String> combinePathCases = getCombinePathCases();\n        combinePathCases.forEach((segments, expected) -> {\n            String s = PathUtils.combinePath(segments.split(\",\"));\n            assertThat(s).isEqualTo(expected);\n        });\n\n        String s = PathUtils.combinePath(\"a\", \"\", \"c\");\n        assertThat(s).isEqualTo(\"/a/c\");\n    }\n\n    private Map<String, String> getCombinePathCases() {\n        Map<String, String> combinePathCases = new HashMap<>();\n        combinePathCases.put(\"a,b,c\", \"/a/b/c\");\n        combinePathCases.put(\"/a,b,c\", \"/a/b/c\");\n        combinePathCases.put(\"/a,b/,c\", \"/a/b/c\");\n        combinePathCases.put(\"/a,/b/,c\", \"/a/b/c\");\n        return combinePathCases;\n    }\n\n    @Test\n    void appendPathSeparatorIfMissing() {\n        String s = PathUtils.appendPathSeparatorIfMissing(\"a\");\n        assertThat(s).isEqualTo(\"a/\");\n\n        s = PathUtils.appendPathSeparatorIfMissing(\"a/\");\n        assertThat(s).isEqualTo(\"a/\");\n\n        s = PathUtils.appendPathSeparatorIfMissing(null);\n        assertThat(s).isEqualTo(null);\n    }\n\n    @Test\n    void simplifyPathPattern() {\n        assertThat(PathUtils.simplifyPathPattern(\"/a/b/c\")).isEqualTo(\"/a/b/c\");\n        assertThat(PathUtils.simplifyPathPattern(\"/a/{b}/c\")).isEqualTo(\"/a/{b}/c\");\n        assertThat(PathUtils.simplifyPathPattern(\"/a/{b}/*\")).isEqualTo(\"/a/{b}/*\");\n        assertThat(PathUtils.simplifyPathPattern(\"/archives/{year:\\\\d{4}}/{month:\\\\d{2}}\"))\n            .isEqualTo(\"/archives/{year}/{month}\");\n        assertThat(PathUtils.simplifyPathPattern(\"/archives/{year:\\\\d{4}}/{slug}\"))\n            .isEqualTo(\"/archives/{year}/{slug}\");\n        assertThat(PathUtils.simplifyPathPattern(\"/archives/{year:\\\\d{4}}/page/{page:\\\\d+}\"))\n            .isEqualTo(\"/archives/{year}/page/{page}\");\n    }\n\n    @Test\n    void isAbsoluteUri() {\n        String[] absoluteUris = new String[] {\n            \"ftp://ftp.is.co.za/rfc/rfc1808.txt\",\n            \"http://www.ietf.org/rfc/rfc2396.txt\",\n            \"ldap://[2001:db8::7]/c=GB?objectClass?one\",\n            \"mailto:John.Doe@example.com\",\n            \"news:comp.infosystems.www.servers.unix\",\n            \"tel:+1-816-555-1212\",\n            \"telnet://192.0.2.16:80/\",\n            \"urn:oasis:names:specification:docbook:dtd:xml:4.1.2\",\n            \"data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh\",\n            \"irc://irc.example.com:6667/#some-channel\",\n            \"ircs://irc.example.com:6667/#some-channel\",\n            \"irc6://irc.example.com:6667/#some-channel\"\n        };\n        for (String uri : absoluteUris) {\n            assertThat(PathUtils.isAbsoluteUri(uri)).isTrue();\n        }\n\n        String[] paths = new String[] {\n            \"//example.com/path/resource.txt\",\n            \"/path/resource.txt\",\n            \"path/resource.txt\",\n            \"../resource.txt\",\n            \"./resource.txt\",\n            \"resource.txt\",\n            \"#fragment\",\n            \"\",\n            null\n        };\n        for (String path : paths) {\n            assertThat(PathUtils.isAbsoluteUri(path)).isFalse();\n        }\n    }\n}"
  },
  {
    "path": "api-docs/openapi/v3_0/aggregated.json",
    "content": "{\n  \"openapi\": \"3.0.1\",\n  \"info\": {\n    \"title\": \"Halo\",\n    \"version\": \"2.23.0-SNAPSHOT\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:8091\",\n      \"description\": \"Generated server url\"\n    }\n  ],\n  \"security\": [\n    {\n      \"basicAuth\": [],\n      \"bearerAuth\": []\n    }\n  ],\n  \"paths\": {\n    \"/api/v1alpha1/annotationsettings\": {\n      \"get\": {\n        \"description\": \"List AnnotationSetting\",\n        \"operationId\": \"listAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSettingList\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsettings\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create AnnotationSetting\",\n        \"operationId\": \"createAnnotationSetting\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AnnotationSetting\"\n              }\n            }\n          },\n          \"description\": \"Fresh annotationsetting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsettings created just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/annotationsettings/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete AnnotationSetting\",\n        \"operationId\": \"deleteAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response annotationsetting deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get AnnotationSetting\",\n        \"operationId\": \"getAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response single annotationsetting\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch AnnotationSetting\",\n        \"operationId\": \"patchAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsetting patched just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update AnnotationSetting\",\n        \"operationId\": \"updateAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AnnotationSetting\"\n              }\n            }\n          },\n          \"description\": \"Updated annotationsetting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsettings updated just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/configmaps\": {\n      \"get\": {\n        \"description\": \"List ConfigMap\",\n        \"operationId\": \"listConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMapList\"\n                }\n              }\n            },\n            \"description\": \"Response configmaps\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ConfigMap\",\n        \"operationId\": \"createConfigMap\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ConfigMap\"\n              }\n            }\n          },\n          \"description\": \"Fresh configmap\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response configmaps created just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/configmaps/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ConfigMap\",\n        \"operationId\": \"deleteConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response configmap deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ConfigMap\",\n        \"operationId\": \"getConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response single configmap\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ConfigMap\",\n        \"operationId\": \"patchConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response configmap patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ConfigMap\",\n        \"operationId\": \"updateConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ConfigMap\"\n              }\n            }\n          },\n          \"description\": \"Updated configmap\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response configmaps updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menuitems\": {\n      \"get\": {\n        \"description\": \"List MenuItem\",\n        \"operationId\": \"listMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItemList\"\n                }\n              }\n            },\n            \"description\": \"Response menuitems\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create MenuItem\",\n        \"operationId\": \"createMenuItem\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/MenuItem\"\n              }\n            }\n          },\n          \"description\": \"Fresh menuitem\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response menuitems created just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menuitems/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete MenuItem\",\n        \"operationId\": \"deleteMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response menuitem deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get MenuItem\",\n        \"operationId\": \"getMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response single menuitem\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch MenuItem\",\n        \"operationId\": \"patchMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response menuitem patched just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update MenuItem\",\n        \"operationId\": \"updateMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/MenuItem\"\n              }\n            }\n          },\n          \"description\": \"Updated menuitem\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response menuitems updated just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menus\": {\n      \"get\": {\n        \"description\": \"List Menu\",\n        \"operationId\": \"listMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuList\"\n                }\n              }\n            },\n            \"description\": \"Response menus\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Menu\",\n        \"operationId\": \"createMenu\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Menu\"\n              }\n            }\n          },\n          \"description\": \"Fresh menu\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response menus created just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menus/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Menu\",\n        \"operationId\": \"deleteMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response menu deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Menu\",\n        \"operationId\": \"getMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response single menu\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Menu\",\n        \"operationId\": \"patchMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response menu patched just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Menu\",\n        \"operationId\": \"updateMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Menu\"\n              }\n            }\n          },\n          \"description\": \"Updated menu\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response menus updated just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/rolebindings\": {\n      \"get\": {\n        \"description\": \"List RoleBinding\",\n        \"operationId\": \"listRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBindingList\"\n                }\n              }\n            },\n            \"description\": \"Response rolebindings\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create RoleBinding\",\n        \"operationId\": \"createRoleBinding\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RoleBinding\"\n              }\n            }\n          },\n          \"description\": \"Fresh rolebinding\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response rolebindings created just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/rolebindings/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete RoleBinding\",\n        \"operationId\": \"deleteRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response rolebinding deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get RoleBinding\",\n        \"operationId\": \"getRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response single rolebinding\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch RoleBinding\",\n        \"operationId\": \"patchRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response rolebinding patched just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update RoleBinding\",\n        \"operationId\": \"updateRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RoleBinding\"\n              }\n            }\n          },\n          \"description\": \"Updated rolebinding\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response rolebindings updated just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/roles\": {\n      \"get\": {\n        \"description\": \"List Role\",\n        \"operationId\": \"listRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleList\"\n                }\n              }\n            },\n            \"description\": \"Response roles\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Role\",\n        \"operationId\": \"createRole\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Role\"\n              }\n            }\n          },\n          \"description\": \"Fresh role\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response roles created just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/roles/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Role\",\n        \"operationId\": \"deleteRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response role deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Role\",\n        \"operationId\": \"getRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response single role\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Role\",\n        \"operationId\": \"patchRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response role patched just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Role\",\n        \"operationId\": \"updateRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Role\"\n              }\n            }\n          },\n          \"description\": \"Updated role\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response roles updated just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/secrets\": {\n      \"get\": {\n        \"description\": \"List Secret\",\n        \"operationId\": \"listSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SecretList\"\n                }\n              }\n            },\n            \"description\": \"Response secrets\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Secret\",\n        \"operationId\": \"createSecret\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Secret\"\n              }\n            }\n          },\n          \"description\": \"Fresh secret\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response secrets created just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/secrets/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Secret\",\n        \"operationId\": \"deleteSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response secret deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Secret\",\n        \"operationId\": \"getSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response single secret\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Secret\",\n        \"operationId\": \"patchSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response secret patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Secret\",\n        \"operationId\": \"updateSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Secret\"\n              }\n            }\n          },\n          \"description\": \"Updated secret\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response secrets updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/settings\": {\n      \"get\": {\n        \"description\": \"List Setting\",\n        \"operationId\": \"listSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SettingList\"\n                }\n              }\n            },\n            \"description\": \"Response settings\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Setting\",\n        \"operationId\": \"createSetting\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Setting\"\n              }\n            }\n          },\n          \"description\": \"Fresh setting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response settings created just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/settings/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Setting\",\n        \"operationId\": \"deleteSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response setting deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Setting\",\n        \"operationId\": \"getSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response single setting\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Setting\",\n        \"operationId\": \"patchSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response setting patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Setting\",\n        \"operationId\": \"updateSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Setting\"\n              }\n            }\n          },\n          \"description\": \"Updated setting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response settings updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/users\": {\n      \"get\": {\n        \"description\": \"List User\",\n        \"operationId\": \"listUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserList\"\n                }\n              }\n            },\n            \"description\": \"Response users\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create User\",\n        \"operationId\": \"createUser\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/User\"\n              }\n            }\n          },\n          \"description\": \"Fresh user\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response users created just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/users/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete User\",\n        \"operationId\": \"deleteUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response user deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get User\",\n        \"operationId\": \"getUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response single user\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch User\",\n        \"operationId\": \"patchUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response user patched just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update User\",\n        \"operationId\": \"updateUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/User\"\n              }\n            }\n          },\n          \"description\": \"Updated user\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response users updated just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/attachments\": {\n      \"get\": {\n        \"operationId\": \"SearchAttachments\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Filter attachments without group. This parameter will ignore group parameter.\",\n            \"in\": \"query\",\n            \"name\": \"ungrouped\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          },\n          {\n            \"description\": \"Keyword for searching.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Acceptable media types.\",\n            \"in\": \"query\",\n            \"name\": \"accepts\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AttachmentList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/attachments/-/upload-from-url\": {\n      \"post\": {\n        \"operationId\": \"ExternalTransferAttachment\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UploadFromUrlRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/attachments/upload\": {\n      \"post\": {\n        \"operationId\": \"UploadAttachment\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/IUploadRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/auth-providers\": {\n      \"get\": {\n        \"description\": \"Lists all auth providers\",\n        \"operationId\": \"listAuthProviders\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/ListedAuthProvider\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/auth-providers/{name}/disable\": {\n      \"put\": {\n        \"description\": \"Disables an auth provider\",\n        \"operationId\": \"disableAuthProvider\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/auth-providers/{name}/enable\": {\n      \"put\": {\n        \"description\": \"Enables an auth provider\",\n        \"operationId\": \"enableAuthProvider\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/comments\": {\n      \"get\": {\n        \"description\": \"List comments.\",\n        \"operationId\": \"ListComments\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Comments filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Commenter kind.\",\n            \"in\": \"query\",\n            \"name\": \"ownerKind\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Commenter name.\",\n            \"in\": \"query\",\n            \"name\": \"ownerName\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedCommentList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create a comment.\",\n        \"operationId\": \"CreateComment\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CommentRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/comments/{name}/reply\": {\n      \"post\": {\n        \"description\": \"Create a reply.\",\n        \"operationId\": \"CreateReply\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReplyRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/indices/-/rebuild\": {\n      \"post\": {\n        \"description\": \"Rebuild all indices\",\n        \"operationId\": \"RebuildAllIndices\",\n        \"responses\": {},\n        \"tags\": [\n          \"IndicesV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/notifiers/{name}/sender-config\": {\n      \"get\": {\n        \"description\": \"Fetch sender config of notifier\",\n        \"operationId\": \"FetchSenderConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Save sender config of notifier\",\n        \"operationId\": \"SaveSenderConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins\": {\n      \"get\": {\n        \"description\": \"List plugins using query criteria and sort params\",\n        \"operationId\": \"ListPlugins\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Keyword of plugin name or description\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Whether the plugin is enabled\",\n            \"in\": \"query\",\n            \"name\": \"enabled\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PluginList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css\": {\n      \"get\": {\n        \"description\": \"Merge all CSS bundles of enabled plugins into one.\",\n        \"operationId\": \"fetchCssBundle\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js\": {\n      \"get\": {\n        \"description\": \"Merge all JS bundles of enabled plugins into one.\",\n        \"operationId\": \"fetchJsBundle\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri\": {\n      \"post\": {\n        \"description\": \"Install a plugin from uri.\",\n        \"operationId\": \"InstallPluginFromUri\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/InstallFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/install\": {\n      \"post\": {\n        \"description\": \"Install a plugin by uploading a Jar file.\",\n        \"operationId\": \"InstallPlugin\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PluginInstallRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/json-config\": {\n      \"get\": {\n        \"description\": \"Fetch converted json config of plugin by configured configMapName.\",\n        \"operationId\": \"fetchPluginJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update the config of plugin setting.\",\n        \"operationId\": \"updatePluginJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"content\": {},\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/plugin-state\": {\n      \"put\": {\n        \"description\": \"Change the running state of a plugin by name.\",\n        \"operationId\": \"ChangePluginRunningState\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PluginRunningStateRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/reload\": {\n      \"put\": {\n        \"description\": \"Reload a plugin by name.\",\n        \"operationId\": \"reloadPlugin\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/reset-config\": {\n      \"put\": {\n        \"description\": \"Reset the configMap of plugin setting.\",\n        \"operationId\": \"ResetPluginConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/setting\": {\n      \"get\": {\n        \"description\": \"Fetch setting of plugin.\",\n        \"operationId\": \"fetchPluginSetting\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/upgrade\": {\n      \"post\": {\n        \"description\": \"Upgrade a plugin by uploading a Jar file\",\n        \"operationId\": \"UpgradePlugin\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PluginInstallRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {},\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/upgrade-from-uri\": {\n      \"post\": {\n        \"description\": \"Upgrade a plugin from uri.\",\n        \"operationId\": \"UpgradePluginFromUri\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UpgradeFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"List posts.\",\n        \"operationId\": \"ListPosts\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Posts filtered by publish phase.\",\n            \"in\": \"query\",\n            \"name\": \"publishPhase\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"DRAFT\",\n                \"PENDING_APPROVAL\",\n                \"PUBLISHED\",\n                \"FAILED\"\n              ]\n            }\n          },\n          {\n            \"description\": \"Posts filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Posts filtered by category including sub-categories.\",\n            \"in\": \"query\",\n            \"name\": \"categoryWithChildren\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Draft a post.\",\n        \"operationId\": \"DraftPost\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PostRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}\": {\n      \"put\": {\n        \"description\": \"Update a post.\",\n        \"operationId\": \"UpdateDraftPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PostRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/content\": {\n      \"delete\": {\n        \"description\": \"Delete a content for post.\",\n        \"operationId\": \"deletePostContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Fetch content of post.\",\n        \"operationId\": \"fetchPostContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update a post\\u0027s content.\",\n        \"operationId\": \"UpdatePostContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Content\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/head-content\": {\n      \"get\": {\n        \"description\": \"Fetch head content of post.\",\n        \"operationId\": \"fetchPostHeadContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/publish\": {\n      \"put\": {\n        \"description\": \"Publish a post.\",\n        \"operationId\": \"PublishPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Head snapshot name of content.\",\n            \"in\": \"query\",\n            \"name\": \"headSnapshot\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"async\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/recycle\": {\n      \"put\": {\n        \"description\": \"Recycle a post.\",\n        \"operationId\": \"RecyclePost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/release-content\": {\n      \"get\": {\n        \"description\": \"Fetch release content of post.\",\n        \"operationId\": \"fetchPostReleaseContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/revert-content\": {\n      \"put\": {\n        \"description\": \"Revert to specified snapshot for post content.\",\n        \"operationId\": \"revertToSpecifiedSnapshotForPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RevertSnapshotForPostParam\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/snapshot\": {\n      \"get\": {\n        \"description\": \"List all snapshots for post content.\",\n        \"operationId\": \"listPostSnapshots\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/ListedSnapshotDto\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish\": {\n      \"put\": {\n        \"description\": \"UnPublish a post.\",\n        \"operationId\": \"UnpublishPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/replies\": {\n      \"get\": {\n        \"description\": \"List replies.\",\n        \"operationId\": \"ListReplies\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Replies filtered by commentName.\",\n            \"in\": \"query\",\n            \"name\": \"commentName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedReplyList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages\": {\n      \"get\": {\n        \"description\": \"List single pages.\",\n        \"operationId\": \"ListSinglePages\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by contributor.\",\n            \"in\": \"query\",\n            \"name\": \"contributor\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by publish phase.\",\n            \"in\": \"query\",\n            \"name\": \"publishPhase\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"DRAFT\",\n                \"PENDING_APPROVAL\",\n                \"PUBLISHED\",\n                \"FAILED\"\n              ]\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by visibility.\",\n            \"in\": \"query\",\n            \"name\": \"visible\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"PUBLIC\",\n                \"INTERNAL\",\n                \"PRIVATE\"\n              ]\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedSinglePageList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Draft a single page.\",\n        \"operationId\": \"DraftSinglePage\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePageRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}\": {\n      \"put\": {\n        \"description\": \"Update a single page.\",\n        \"operationId\": \"UpdateDraftSinglePage\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePageRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content\": {\n      \"delete\": {\n        \"description\": \"Delete a content for post.\",\n        \"operationId\": \"deleteSinglePageContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Fetch content of single page.\",\n        \"operationId\": \"fetchSinglePageContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update a single page\\u0027s content.\",\n        \"operationId\": \"UpdateSinglePageContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Content\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/head-content\": {\n      \"get\": {\n        \"description\": \"Fetch head content of single page.\",\n        \"operationId\": \"fetchSinglePageHeadContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/publish\": {\n      \"put\": {\n        \"description\": \"Publish a single page.\",\n        \"operationId\": \"PublishSinglePage\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/release-content\": {\n      \"get\": {\n        \"description\": \"Fetch release content of single page.\",\n        \"operationId\": \"fetchSinglePageReleaseContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/revert-content\": {\n      \"put\": {\n        \"description\": \"Revert to specified snapshot for single page content.\",\n        \"operationId\": \"revertToSpecifiedSnapshotForSinglePage\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RevertSnapshotForSingleParam\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/snapshot\": {\n      \"get\": {\n        \"description\": \"List all snapshots for single page content.\",\n        \"operationId\": \"listSinglePageSnapshots\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/ListedSnapshotDto\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/stats\": {\n      \"get\": {\n        \"description\": \"Get stats.\",\n        \"operationId\": \"getStats\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DashboardStats\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/tags\": {\n      \"get\": {\n        \"description\": \"List Post Tags.\",\n        \"operationId\": \"ListPostTags\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Post tags filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes\": {\n      \"get\": {\n        \"description\": \"List themes.\",\n        \"operationId\": \"ListThemes\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Whether to list uninstalled themes.\",\n            \"in\": \"query\",\n            \"name\": \"uninstalled\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ThemeList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/-/activation\": {\n      \"get\": {\n        \"description\": \"Fetch the activated theme.\",\n        \"operationId\": \"fetchActivatedTheme\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/-/install-from-uri\": {\n      \"post\": {\n        \"description\": \"Install a theme from uri.\",\n        \"operationId\": \"InstallThemeFromUri\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/InstallFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/install\": {\n      \"post\": {\n        \"description\": \"Install a theme by uploading a zip file.\",\n        \"operationId\": \"InstallTheme\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ThemeInstallRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/activation\": {\n      \"put\": {\n        \"description\": \"Activate a theme by name.\",\n        \"operationId\": \"activateTheme\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/invalidate-cache\": {\n      \"put\": {\n        \"description\": \"Invalidate theme template cache.\",\n        \"operationId\": \"InvalidateCache\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/json-config\": {\n      \"get\": {\n        \"description\": \"Fetch converted json config of theme by configured configMapName.\",\n        \"operationId\": \"fetchThemeJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update the configMap of theme setting.\",\n        \"operationId\": \"updateThemeJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"content\": {},\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/reload\": {\n      \"put\": {\n        \"description\": \"Reload theme setting.\",\n        \"operationId\": \"Reload\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/reset-config\": {\n      \"put\": {\n        \"description\": \"Reset the configMap of theme setting.\",\n        \"operationId\": \"ResetThemeConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/setting\": {\n      \"get\": {\n        \"description\": \"Fetch setting of theme.\",\n        \"operationId\": \"fetchThemeSetting\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade\": {\n      \"post\": {\n        \"description\": \"Upgrade theme\",\n        \"operationId\": \"UpgradeTheme\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UpgradeRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {},\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade-from-uri\": {\n      \"post\": {\n        \"description\": \"Upgrade a theme from uri.\",\n        \"operationId\": \"UpgradeThemeFromUri\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UpgradeFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users\": {\n      \"get\": {\n        \"description\": \"List users\",\n        \"operationId\": \"ListUsers\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Keyword to search\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Role name\",\n            \"in\": \"query\",\n            \"name\": \"role\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserEndpoint.ListedUserList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Creates a new user.\",\n        \"operationId\": \"CreateUser\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CreateUserRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-\": {\n      \"get\": {\n        \"description\": \"Get current user detail\",\n        \"operationId\": \"GetCurrentUserDetail\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DetailedUser\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update current user profile, but password.\",\n        \"operationId\": \"UpdateCurrentUser\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/User\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-/password\": {\n      \"put\": {\n        \"description\": \"Change own password of user.\",\n        \"operationId\": \"ChangeOwnPassword\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ChangeOwnPasswordRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-/send-email-verification-code\": {\n      \"post\": {\n        \"description\": \"Send email verification code for user\",\n        \"operationId\": \"SendEmailVerificationCode\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/EmailVerifyRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-/verify-email\": {\n      \"post\": {\n        \"description\": \"Verify email for user by code.\",\n        \"operationId\": \"VerifyEmail\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VerifyCodeRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}\": {\n      \"get\": {\n        \"description\": \"Get user detail by name\",\n        \"operationId\": \"GetUserDetail\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DetailedUser\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}/avatar\": {\n      \"delete\": {\n        \"description\": \"delete user avatar\",\n        \"operationId\": \"DeleteUserAvatar\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"upload user avatar\",\n        \"operationId\": \"UploadUserAvatar\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/IAvatarUploadRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}/password\": {\n      \"put\": {\n        \"description\": \"Change anyone password of user for admin.\",\n        \"operationId\": \"ChangeAnyonePassword\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user. If the name is equal to \\u0027-\\u0027, it will change the password of current user.\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ChangePasswordRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}/permissions\": {\n      \"get\": {\n        \"description\": \"Get permissions of user\",\n        \"operationId\": \"GetPermissions\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserPermission\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Grant permissions to user\",\n        \"operationId\": \"GrantPermission\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/GrantRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/categories\": {\n      \"get\": {\n        \"description\": \"Lists categories.\",\n        \"operationId\": \"queryCategories\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CategoryVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/categories/{name}\": {\n      \"get\": {\n        \"description\": \"Gets category by name.\",\n        \"operationId\": \"queryCategoryByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Category name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CategoryVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/categories/{name}/posts\": {\n      \"get\": {\n        \"description\": \"Lists posts by category name.\",\n        \"operationId\": \"queryPostsByCategoryName\",\n        \"parameters\": [\n          {\n            \"description\": \"Category name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"Lists posts.\",\n        \"operationId\": \"queryPosts\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/posts/{name}\": {\n      \"get\": {\n        \"description\": \"Gets a post by name.\",\n        \"operationId\": \"queryPostByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PostVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/posts/{name}/navigation\": {\n      \"get\": {\n        \"description\": \"Gets a post navigation by name.\",\n        \"operationId\": \"queryPostNavigationByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NavigationPostVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/singlepages\": {\n      \"get\": {\n        \"description\": \"Lists single pages\",\n        \"operationId\": \"querySinglePages\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedSinglePageVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/singlepages/{name}\": {\n      \"get\": {\n        \"description\": \"Gets single page by name\",\n        \"operationId\": \"querySinglePageByName\",\n        \"parameters\": [\n          {\n            \"description\": \"SinglePage name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePageVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/tags\": {\n      \"get\": {\n        \"description\": \"Lists tags\",\n        \"operationId\": \"queryTags\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/tags/{name}\": {\n      \"get\": {\n        \"description\": \"Gets tag by name\",\n        \"operationId\": \"queryTagByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Tag name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/tags/{name}/posts\": {\n      \"get\": {\n        \"description\": \"Lists posts by tag name\",\n        \"operationId\": \"queryPostsByTagName\",\n        \"parameters\": [\n          {\n            \"description\": \"Tag name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/comments\": {\n      \"get\": {\n        \"description\": \"List comments.\",\n        \"operationId\": \"ListComments_1\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"The comment subject group.\",\n            \"in\": \"query\",\n            \"name\": \"group\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The comment subject version.\",\n            \"in\": \"query\",\n            \"name\": \"version\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The comment subject kind.\",\n            \"in\": \"query\",\n            \"name\": \"kind\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The comment subject name.\",\n            \"in\": \"query\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Whether to include replies. Default is false.\",\n            \"in\": \"query\",\n            \"name\": \"withReplies\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          },\n          {\n            \"description\": \"Reply size of the comment, default is 10, only works when withReplies is true.\",\n            \"in\": \"query\",\n            \"name\": \"replySize\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CommentWithReplyVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create a comment.\",\n        \"operationId\": \"CreateComment_1\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CommentRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/comments/{name}\": {\n      \"get\": {\n        \"description\": \"Get a comment.\",\n        \"operationId\": \"GetComment\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CommentVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/comments/{name}/reply\": {\n      \"get\": {\n        \"description\": \"List comment replies.\",\n        \"operationId\": \"ListCommentReplies\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReplyVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create a reply.\",\n        \"operationId\": \"CreateReply_1\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReplyRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/indices/-/search\": {\n      \"post\": {\n        \"description\": \"Search indices.\",\n        \"operationId\": \"IndicesSearch\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SearchOption\"\n              }\n            }\n          },\n          \"description\": \"Please note that the \\\"filterPublished\\\", \\\"filterExposed\\\" and \\\"filterRecycled\\\" fields are ignored in this endpoint.\"\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SearchResult\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"IndexV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/menus/-\": {\n      \"get\": {\n        \"description\": \"Gets primary menu.\",\n        \"operationId\": \"queryPrimaryMenu\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/menus/{name}\": {\n      \"get\": {\n        \"description\": \"Gets menu by name.\",\n        \"operationId\": \"queryMenuByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Menu name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/stats/-\": {\n      \"get\": {\n        \"description\": \"Gets site stats\",\n        \"operationId\": \"queryStats\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SiteStatsVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/trackers/counter\": {\n      \"post\": {\n        \"description\": \"Count an extension resource visits.\",\n        \"operationId\": \"count\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CounterRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MetricsV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/trackers/downvote\": {\n      \"post\": {\n        \"description\": \"Downvote an extension resource.\",\n        \"operationId\": \"downvote\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VoteRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MetricsV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/trackers/upvote\": {\n      \"post\": {\n        \"description\": \"Upvote an extension resource.\",\n        \"operationId\": \"upvote\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VoteRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MetricsV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/notifiers/{name}/receiver-config\": {\n      \"get\": {\n        \"description\": \"Fetch receiver config of notifier\",\n        \"operationId\": \"FetchReceiverConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Save receiver config of notifier\",\n        \"operationId\": \"SaveReceiverConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/subscriptions/{name}/unsubscribe\": {\n      \"get\": {\n        \"description\": \"Unsubscribe a subscription\",\n        \"operationId\": \"Unsubscribe\",\n        \"parameters\": [\n          {\n            \"description\": \"Subscription name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Unsubscribe token\",\n            \"in\": \"query\",\n            \"name\": \"token\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notification-preferences\": {\n      \"get\": {\n        \"description\": \"List notification preferences for the authenticated user.\",\n        \"operationId\": \"ListUserNotificationPreferences\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonTypeNotifierMatrix\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Save notification preferences for the authenticated user.\",\n        \"operationId\": \"SaveUserNotificationPreferences\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReasonTypeNotifierCollectionRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonTypeNotifierMatrix\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications\": {\n      \"get\": {\n        \"description\": \"List notifications for the authenticated user.\",\n        \"operationId\": \"ListUserNotifications\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/-/mark-specified-as-read\": {\n      \"put\": {\n        \"description\": \"Mark the specified notifications as read.\",\n        \"operationId\": \"MarkNotificationsAsRead\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/MarkSpecifiedRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete the specified notification.\",\n        \"operationId\": \"DeleteSpecifiedNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Notification name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}/mark-as-read\": {\n      \"put\": {\n        \"description\": \"Mark the specified notification as read.\",\n        \"operationId\": \"MarkNotificationAsRead\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Notification name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.plugin.halo.run/v1alpha1/plugins/{name}/available\": {\n      \"get\": {\n        \"description\": \"Gets plugin available by name.\",\n        \"operationId\": \"queryPluginAvailableByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Plugin name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"boolean\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri\": {\n      \"get\": {\n        \"description\": \"Get thumbnail by URI\",\n        \"operationId\": \"GetThumbnailByUri\",\n        \"parameters\": [\n          {\n            \"description\": \"The URI of the image\",\n            \"in\": \"query\",\n            \"name\": \"uri\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The size of the thumbnail\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"S\",\n                \"M\",\n                \"L\",\n                \"XL\"\n              ]\n            }\n          },\n          {\n            \"description\": \"The width of the thumbnail, if \\u0027size\\u0027 is not provided, this parameter will be used to determine the size\",\n            \"in\": \"query\",\n            \"name\": \"width\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"enum\": [\n                \"400\",\n                \"800\",\n                \"1200\",\n                \"1600\"\n              ]\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"binary\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/authproviders\": {\n      \"get\": {\n        \"description\": \"List AuthProvider\",\n        \"operationId\": \"listAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProviderList\"\n                }\n              }\n            },\n            \"description\": \"Response authproviders\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create AuthProvider\",\n        \"operationId\": \"createAuthProvider\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AuthProvider\"\n              }\n            }\n          },\n          \"description\": \"Fresh authprovider\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response authproviders created just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/authproviders/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete AuthProvider\",\n        \"operationId\": \"deleteAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response authprovider deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get AuthProvider\",\n        \"operationId\": \"getAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response single authprovider\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch AuthProvider\",\n        \"operationId\": \"patchAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response authprovider patched just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update AuthProvider\",\n        \"operationId\": \"updateAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AuthProvider\"\n              }\n            }\n          },\n          \"description\": \"Updated authprovider\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response authproviders updated just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/userconnections\": {\n      \"get\": {\n        \"description\": \"List UserConnection\",\n        \"operationId\": \"listUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnectionList\"\n                }\n              }\n            },\n            \"description\": \"Response userconnections\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create UserConnection\",\n        \"operationId\": \"createUserConnection\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UserConnection\"\n              }\n            }\n          },\n          \"description\": \"Fresh userconnection\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response userconnections created just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/userconnections/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete UserConnection\",\n        \"operationId\": \"deleteUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response userconnection deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get UserConnection\",\n        \"operationId\": \"getUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response single userconnection\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch UserConnection\",\n        \"operationId\": \"patchUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response userconnection patched just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update UserConnection\",\n        \"operationId\": \"updateUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UserConnection\"\n              }\n            }\n          },\n          \"description\": \"Updated userconnection\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response userconnections updated just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}\": {\n      \"get\": {\n        \"description\": \"Get system config by group\",\n        \"operationId\": \"getSystemConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of the system config\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              },\n              \"application/json\": {}\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemConfigV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update system config by group\",\n        \"operationId\": \"updateSystemConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of the system config\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"204 NO_CONTENT\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemConfigV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.migration.halo.run/v1alpha1/backup-files\": {\n      \"get\": {\n        \"description\": \"Get backup files from backup root.\",\n        \"operationId\": \"getBackupFiles\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/BackupFile\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MigrationV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.migration.halo.run/v1alpha1/backups/{name}/files/{filename}\": {\n      \"get\": {\n        \"operationId\": \"DownloadBackups\",\n        \"parameters\": [\n          {\n            \"description\": \"Backup name.\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Backup filename.\",\n            \"in\": \"path\",\n            \"name\": \"filename\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"MigrationV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.migration.halo.run/v1alpha1/restorations\": {\n      \"post\": {\n        \"description\": \"Restore backup by uploading file or providing download link or backup name.\",\n        \"operationId\": \"RestoreBackup\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RestoreRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {},\n        \"tags\": [\n          \"MigrationV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.notification.halo.run/v1alpha1/notifiers/default-email-notifier/verify-connection\": {\n      \"post\": {\n        \"description\": \"Verify email sender config.\",\n        \"operationId\": \"VerifyEmailSenderConfig\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/EmailConfigValidationRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.security.halo.run/v1alpha1/users/{username}/disable\": {\n      \"post\": {\n        \"description\": \"Disable user by username\",\n        \"operationId\": \"DisableUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"The user has been disabled.\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.security.halo.run/v1alpha1/users/{username}/enable\": {\n      \"post\": {\n        \"description\": \"Enable user by username\",\n        \"operationId\": \"EnableUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"The user has been enabled.\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.storage.halo.run/v1alpha1/attachments/-/upload\": {\n      \"post\": {\n        \"description\": \"Upload attachment endpoint for console.\",\n        \"operationId\": \"uploadAttachmentForConsole\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UploadForm\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.storage.halo.run/v1alpha1/policies/{name}/configs/{group}\": {\n      \"get\": {\n        \"description\": \"Get policy config by group\",\n        \"operationId\": \"getPolicyConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of the policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Name of the group\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PolicyAlpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update policy config by group\",\n        \"operationId\": \"updatePolicyConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of the policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Name of the group\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"PolicyAlpha1Console\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/categories\": {\n      \"get\": {\n        \"description\": \"List Category\",\n        \"operationId\": \"listCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CategoryList\"\n                }\n              }\n            },\n            \"description\": \"Response categories\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Category\",\n        \"operationId\": \"createCategory\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Category\"\n              }\n            }\n          },\n          \"description\": \"Fresh category\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response categories created just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/categories/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Category\",\n        \"operationId\": \"deleteCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response category deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Category\",\n        \"operationId\": \"getCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response single category\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Category\",\n        \"operationId\": \"patchCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response category patched just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Category\",\n        \"operationId\": \"updateCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Category\"\n              }\n            }\n          },\n          \"description\": \"Updated category\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response categories updated just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/comments\": {\n      \"get\": {\n        \"description\": \"List Comment\",\n        \"operationId\": \"listComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CommentList\"\n                }\n              }\n            },\n            \"description\": \"Response comments\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Comment\",\n        \"operationId\": \"createComment\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Comment\"\n              }\n            }\n          },\n          \"description\": \"Fresh comment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response comments created just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/comments/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Comment\",\n        \"operationId\": \"deleteComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response comment deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Comment\",\n        \"operationId\": \"getComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response single comment\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Comment\",\n        \"operationId\": \"patchComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response comment patched just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Comment\",\n        \"operationId\": \"updateComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Comment\"\n              }\n            }\n          },\n          \"description\": \"Updated comment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response comments updated just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"List Post\",\n        \"operationId\": \"listPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PostList\"\n                }\n              }\n            },\n            \"description\": \"Response posts\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Post\",\n        \"operationId\": \"createPost\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          },\n          \"description\": \"Fresh post\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response posts created just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/posts/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Post\",\n        \"operationId\": \"deletePost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response post deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Post\",\n        \"operationId\": \"getPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response single post\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Post\",\n        \"operationId\": \"patchPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response post patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Post\",\n        \"operationId\": \"updatePost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          },\n          \"description\": \"Updated post\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response posts updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/replies\": {\n      \"get\": {\n        \"description\": \"List Reply\",\n        \"operationId\": \"listReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReplyList\"\n                }\n              }\n            },\n            \"description\": \"Response replies\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Reply\",\n        \"operationId\": \"createReply\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reply\"\n              }\n            }\n          },\n          \"description\": \"Fresh reply\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response replies created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/replies/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Reply\",\n        \"operationId\": \"deleteReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reply deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Reply\",\n        \"operationId\": \"getReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response single reply\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Reply\",\n        \"operationId\": \"patchReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response reply patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Reply\",\n        \"operationId\": \"updateReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reply\"\n              }\n            }\n          },\n          \"description\": \"Updated reply\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response replies updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/singlepages\": {\n      \"get\": {\n        \"description\": \"List SinglePage\",\n        \"operationId\": \"listSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePageList\"\n                }\n              }\n            },\n            \"description\": \"Response singlepages\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create SinglePage\",\n        \"operationId\": \"createSinglePage\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePage\"\n              }\n            }\n          },\n          \"description\": \"Fresh singlepage\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response singlepages created just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/singlepages/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete SinglePage\",\n        \"operationId\": \"deleteSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response singlepage deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get SinglePage\",\n        \"operationId\": \"getSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response single singlepage\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch SinglePage\",\n        \"operationId\": \"patchSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response singlepage patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update SinglePage\",\n        \"operationId\": \"updateSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePage\"\n              }\n            }\n          },\n          \"description\": \"Updated singlepage\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response singlepages updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/snapshots\": {\n      \"get\": {\n        \"description\": \"List Snapshot\",\n        \"operationId\": \"listSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SnapshotList\"\n                }\n              }\n            },\n            \"description\": \"Response snapshots\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Snapshot\",\n        \"operationId\": \"createSnapshot\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Snapshot\"\n              }\n            }\n          },\n          \"description\": \"Fresh snapshot\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response snapshots created just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/snapshots/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Snapshot\",\n        \"operationId\": \"deleteSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response snapshot deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Snapshot\",\n        \"operationId\": \"getSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response single snapshot\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Snapshot\",\n        \"operationId\": \"patchSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response snapshot patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Snapshot\",\n        \"operationId\": \"updateSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Snapshot\"\n              }\n            }\n          },\n          \"description\": \"Updated snapshot\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response snapshots updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/tags\": {\n      \"get\": {\n        \"description\": \"List Tag\",\n        \"operationId\": \"listTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagList\"\n                }\n              }\n            },\n            \"description\": \"Response tags\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Tag\",\n        \"operationId\": \"createTag\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Tag\"\n              }\n            }\n          },\n          \"description\": \"Fresh tag\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response tags created just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/tags/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Tag\",\n        \"operationId\": \"deleteTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response tag deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Tag\",\n        \"operationId\": \"getTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response single tag\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Tag\",\n        \"operationId\": \"patchTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response tag patched just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Tag\",\n        \"operationId\": \"updateTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Tag\"\n              }\n            }\n          },\n          \"description\": \"Updated tag\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response tags updated just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/metrics.halo.run/v1alpha1/counters\": {\n      \"get\": {\n        \"description\": \"List Counter\",\n        \"operationId\": \"listCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CounterList\"\n                }\n              }\n            },\n            \"description\": \"Response counters\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Counter\",\n        \"operationId\": \"createCounter\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Counter\"\n              }\n            }\n          },\n          \"description\": \"Fresh counter\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response counters created just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/metrics.halo.run/v1alpha1/counters/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Counter\",\n        \"operationId\": \"deleteCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response counter deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Counter\",\n        \"operationId\": \"getCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response single counter\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Counter\",\n        \"operationId\": \"patchCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response counter patched just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Counter\",\n        \"operationId\": \"updateCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Counter\"\n              }\n            }\n          },\n          \"description\": \"Updated counter\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response counters updated just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/migration.halo.run/v1alpha1/backups\": {\n      \"get\": {\n        \"description\": \"List Backup\",\n        \"operationId\": \"listBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/BackupList\"\n                }\n              }\n            },\n            \"description\": \"Response backups\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Backup\",\n        \"operationId\": \"createBackup\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Backup\"\n              }\n            }\n          },\n          \"description\": \"Fresh backup\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response backups created just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/migration.halo.run/v1alpha1/backups/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Backup\",\n        \"operationId\": \"deleteBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response backup deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Backup\",\n        \"operationId\": \"getBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response single backup\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Backup\",\n        \"operationId\": \"patchBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response backup patched just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Backup\",\n        \"operationId\": \"updateBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Backup\"\n              }\n            }\n          },\n          \"description\": \"Updated backup\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response backups updated just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifications\": {\n      \"get\": {\n        \"description\": \"List Notification\",\n        \"operationId\": \"listNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationList\"\n                }\n              }\n            },\n            \"description\": \"Response notifications\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Notification\",\n        \"operationId\": \"createNotification\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Notification\"\n              }\n            }\n          },\n          \"description\": \"Fresh notification\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response notifications created just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifications/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Notification\",\n        \"operationId\": \"deleteNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response notification deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Notification\",\n        \"operationId\": \"getNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response single notification\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Notification\",\n        \"operationId\": \"patchNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response notification patched just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Notification\",\n        \"operationId\": \"updateNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Notification\"\n              }\n            }\n          },\n          \"description\": \"Updated notification\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response notifications updated just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notificationtemplates\": {\n      \"get\": {\n        \"description\": \"List NotificationTemplate\",\n        \"operationId\": \"listNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplateList\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplates\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create NotificationTemplate\",\n        \"operationId\": \"createNotificationTemplate\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotificationTemplate\"\n              }\n            }\n          },\n          \"description\": \"Fresh notificationtemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplates created just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notificationtemplates/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete NotificationTemplate\",\n        \"operationId\": \"deleteNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response notificationtemplate deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get NotificationTemplate\",\n        \"operationId\": \"getNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response single notificationtemplate\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch NotificationTemplate\",\n        \"operationId\": \"patchNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplate patched just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update NotificationTemplate\",\n        \"operationId\": \"updateNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotificationTemplate\"\n              }\n            }\n          },\n          \"description\": \"Updated notificationtemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplates updated just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifierDescriptors\": {\n      \"get\": {\n        \"description\": \"List NotifierDescriptor\",\n        \"operationId\": \"listNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptorList\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptors\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create NotifierDescriptor\",\n        \"operationId\": \"createNotifierDescriptor\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n              }\n            }\n          },\n          \"description\": \"Fresh notifierDescriptor\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptors created just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifierDescriptors/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete NotifierDescriptor\",\n        \"operationId\": \"deleteNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response notifierDescriptor deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get NotifierDescriptor\",\n        \"operationId\": \"getNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response single notifierDescriptor\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch NotifierDescriptor\",\n        \"operationId\": \"patchNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptor patched just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update NotifierDescriptor\",\n        \"operationId\": \"updateNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n              }\n            }\n          },\n          \"description\": \"Updated notifierDescriptor\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptors updated just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasons\": {\n      \"get\": {\n        \"description\": \"List Reason\",\n        \"operationId\": \"listReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonList\"\n                }\n              }\n            },\n            \"description\": \"Response reasons\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Reason\",\n        \"operationId\": \"createReason\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reason\"\n              }\n            }\n          },\n          \"description\": \"Fresh reason\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response reasons created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasons/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Reason\",\n        \"operationId\": \"deleteReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reason deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Reason\",\n        \"operationId\": \"getReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response single reason\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Reason\",\n        \"operationId\": \"patchReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response reason patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Reason\",\n        \"operationId\": \"updateReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reason\"\n              }\n            }\n          },\n          \"description\": \"Updated reason\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response reasons updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasontypes\": {\n      \"get\": {\n        \"description\": \"List ReasonType\",\n        \"operationId\": \"listReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonTypeList\"\n                }\n              }\n            },\n            \"description\": \"Response reasontypes\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ReasonType\",\n        \"operationId\": \"createReasonType\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReasonType\"\n              }\n            }\n          },\n          \"description\": \"Fresh reasontype\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response reasontypes created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasontypes/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ReasonType\",\n        \"operationId\": \"deleteReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reasontype deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ReasonType\",\n        \"operationId\": \"getReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response single reasontype\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ReasonType\",\n        \"operationId\": \"patchReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response reasontype patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ReasonType\",\n        \"operationId\": \"updateReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReasonType\"\n              }\n            }\n          },\n          \"description\": \"Updated reasontype\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response reasontypes updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/subscriptions\": {\n      \"get\": {\n        \"description\": \"List Subscription\",\n        \"operationId\": \"listSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SubscriptionList\"\n                }\n              }\n            },\n            \"description\": \"Response subscriptions\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Subscription\",\n        \"operationId\": \"createSubscription\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Subscription\"\n              }\n            }\n          },\n          \"description\": \"Fresh subscription\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response subscriptions created just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/subscriptions/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Subscription\",\n        \"operationId\": \"deleteSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response subscription deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Subscription\",\n        \"operationId\": \"getSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response single subscription\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Subscription\",\n        \"operationId\": \"patchSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response subscription patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Subscription\",\n        \"operationId\": \"updateSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Subscription\"\n              }\n            }\n          },\n          \"description\": \"Updated subscription\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response subscriptions updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensiondefinitions\": {\n      \"get\": {\n        \"description\": \"List ExtensionDefinition\",\n        \"operationId\": \"listExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinitionList\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinitions\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ExtensionDefinition\",\n        \"operationId\": \"createExtensionDefinition\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n              }\n            }\n          },\n          \"description\": \"Fresh extensiondefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinitions created just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensiondefinitions/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ExtensionDefinition\",\n        \"operationId\": \"deleteExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response extensiondefinition deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ExtensionDefinition\",\n        \"operationId\": \"getExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response single extensiondefinition\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ExtensionDefinition\",\n        \"operationId\": \"patchExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinition patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ExtensionDefinition\",\n        \"operationId\": \"updateExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n              }\n            }\n          },\n          \"description\": \"Updated extensiondefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinitions updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions\": {\n      \"get\": {\n        \"description\": \"List ExtensionPointDefinition\",\n        \"operationId\": \"listExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinitionList\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinitions\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ExtensionPointDefinition\",\n        \"operationId\": \"createExtensionPointDefinition\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n              }\n            }\n          },\n          \"description\": \"Fresh extensionpointdefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinitions created just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ExtensionPointDefinition\",\n        \"operationId\": \"deleteExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response extensionpointdefinition deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ExtensionPointDefinition\",\n        \"operationId\": \"getExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response single extensionpointdefinition\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ExtensionPointDefinition\",\n        \"operationId\": \"patchExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinition patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ExtensionPointDefinition\",\n        \"operationId\": \"updateExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n              }\n            }\n          },\n          \"description\": \"Updated extensionpointdefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinitions updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/plugins\": {\n      \"get\": {\n        \"description\": \"List Plugin\",\n        \"operationId\": \"listPlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PluginList\"\n                }\n              }\n            },\n            \"description\": \"Response plugins\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Plugin\",\n        \"operationId\": \"createPlugin\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Plugin\"\n              }\n            }\n          },\n          \"description\": \"Fresh plugin\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response plugins created just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/plugins/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Plugin\",\n        \"operationId\": \"deletePlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response plugin deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Plugin\",\n        \"operationId\": \"getPlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response single plugin\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Plugin\",\n        \"operationId\": \"patchPlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response plugin patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Plugin\",\n        \"operationId\": \"updatePlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Plugin\"\n              }\n            }\n          },\n          \"description\": \"Updated plugin\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response plugins updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/reverseproxies\": {\n      \"get\": {\n        \"description\": \"List ReverseProxy\",\n        \"operationId\": \"listReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxyList\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxies\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ReverseProxy\",\n        \"operationId\": \"createReverseProxy\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReverseProxy\"\n              }\n            }\n          },\n          \"description\": \"Fresh reverseproxy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxies created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/reverseproxies/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ReverseProxy\",\n        \"operationId\": \"deleteReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reverseproxy deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ReverseProxy\",\n        \"operationId\": \"getReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response single reverseproxy\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ReverseProxy\",\n        \"operationId\": \"patchReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxy patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ReverseProxy\",\n        \"operationId\": \"updateReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReverseProxy\"\n              }\n            }\n          },\n          \"description\": \"Updated reverseproxy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxies updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/devices\": {\n      \"get\": {\n        \"description\": \"List Device\",\n        \"operationId\": \"listDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DeviceList\"\n                }\n              }\n            },\n            \"description\": \"Response devices\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Device\",\n        \"operationId\": \"createDevice\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Device\"\n              }\n            }\n          },\n          \"description\": \"Fresh device\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response devices created just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/devices/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Device\",\n        \"operationId\": \"deleteDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response device deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Device\",\n        \"operationId\": \"getDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response single device\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Device\",\n        \"operationId\": \"patchDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response device patched just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Device\",\n        \"operationId\": \"updateDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Device\"\n              }\n            }\n          },\n          \"description\": \"Updated device\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response devices updated just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/personalaccesstokens\": {\n      \"get\": {\n        \"description\": \"List PersonalAccessToken\",\n        \"operationId\": \"listPersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessTokenList\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstokens\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create PersonalAccessToken\",\n        \"operationId\": \"createPersonalAccessToken\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n              }\n            }\n          },\n          \"description\": \"Fresh personalaccesstoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstokens created just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/personalaccesstokens/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete PersonalAccessToken\",\n        \"operationId\": \"deletePersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response personalaccesstoken deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get PersonalAccessToken\",\n        \"operationId\": \"getPersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response single personalaccesstoken\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch PersonalAccessToken\",\n        \"operationId\": \"patchPersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstoken patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update PersonalAccessToken\",\n        \"operationId\": \"updatePersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n              }\n            }\n          },\n          \"description\": \"Updated personalaccesstoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstokens updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/remembermetokens\": {\n      \"get\": {\n        \"description\": \"List RememberMeToken\",\n        \"operationId\": \"listRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeTokenList\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetokens\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create RememberMeToken\",\n        \"operationId\": \"createRememberMeToken\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RememberMeToken\"\n              }\n            }\n          },\n          \"description\": \"Fresh remembermetoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetokens created just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/remembermetokens/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete RememberMeToken\",\n        \"operationId\": \"deleteRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response remembermetoken deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get RememberMeToken\",\n        \"operationId\": \"getRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response single remembermetoken\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch RememberMeToken\",\n        \"operationId\": \"patchRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetoken patched just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update RememberMeToken\",\n        \"operationId\": \"updateRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RememberMeToken\"\n              }\n            }\n          },\n          \"description\": \"Updated remembermetoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetokens updated just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/attachments\": {\n      \"get\": {\n        \"description\": \"List Attachment\",\n        \"operationId\": \"listAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AttachmentList\"\n                }\n              }\n            },\n            \"description\": \"Response attachments\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Attachment\",\n        \"operationId\": \"createAttachment\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Attachment\"\n              }\n            }\n          },\n          \"description\": \"Fresh attachment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response attachments created just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/attachments/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Attachment\",\n        \"operationId\": \"deleteAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response attachment deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Attachment\",\n        \"operationId\": \"getAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response single attachment\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Attachment\",\n        \"operationId\": \"patchAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response attachment patched just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Attachment\",\n        \"operationId\": \"updateAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Attachment\"\n              }\n            }\n          },\n          \"description\": \"Updated attachment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response attachments updated just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/groups\": {\n      \"get\": {\n        \"description\": \"List Group\",\n        \"operationId\": \"listGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/GroupList\"\n                }\n              }\n            },\n            \"description\": \"Response groups\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Group\",\n        \"operationId\": \"createGroup\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Group\"\n              }\n            }\n          },\n          \"description\": \"Fresh group\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response groups created just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/groups/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Group\",\n        \"operationId\": \"deleteGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response group deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Group\",\n        \"operationId\": \"getGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response single group\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Group\",\n        \"operationId\": \"patchGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response group patched just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Group\",\n        \"operationId\": \"updateGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Group\"\n              }\n            }\n          },\n          \"description\": \"Updated group\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response groups updated just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/localthumbnails\": {\n      \"get\": {\n        \"description\": \"List LocalThumbnail\",\n        \"operationId\": \"listLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnailList\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnails\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create LocalThumbnail\",\n        \"operationId\": \"createLocalThumbnail\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/LocalThumbnail\"\n              }\n            }\n          },\n          \"description\": \"Fresh localthumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnails created just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/localthumbnails/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete LocalThumbnail\",\n        \"operationId\": \"deleteLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response localthumbnail deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get LocalThumbnail\",\n        \"operationId\": \"getLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response single localthumbnail\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch LocalThumbnail\",\n        \"operationId\": \"patchLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnail patched just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update LocalThumbnail\",\n        \"operationId\": \"updateLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/LocalThumbnail\"\n              }\n            }\n          },\n          \"description\": \"Updated localthumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnails updated just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policies\": {\n      \"get\": {\n        \"description\": \"List Policy\",\n        \"operationId\": \"listPolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyList\"\n                }\n              }\n            },\n            \"description\": \"Response policies\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Policy\",\n        \"operationId\": \"createPolicy\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Policy\"\n              }\n            }\n          },\n          \"description\": \"Fresh policy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response policies created just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policies/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Policy\",\n        \"operationId\": \"deletePolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response policy deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Policy\",\n        \"operationId\": \"getPolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response single policy\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Policy\",\n        \"operationId\": \"patchPolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response policy patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Policy\",\n        \"operationId\": \"updatePolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Policy\"\n              }\n            }\n          },\n          \"description\": \"Updated policy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response policies updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policytemplates\": {\n      \"get\": {\n        \"description\": \"List PolicyTemplate\",\n        \"operationId\": \"listPolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplateList\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplates\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create PolicyTemplate\",\n        \"operationId\": \"createPolicyTemplate\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PolicyTemplate\"\n              }\n            }\n          },\n          \"description\": \"Fresh policytemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplates created just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policytemplates/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete PolicyTemplate\",\n        \"operationId\": \"deletePolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response policytemplate deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get PolicyTemplate\",\n        \"operationId\": \"getPolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response single policytemplate\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch PolicyTemplate\",\n        \"operationId\": \"patchPolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplate patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update PolicyTemplate\",\n        \"operationId\": \"updatePolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PolicyTemplate\"\n              }\n            }\n          },\n          \"description\": \"Updated policytemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplates updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/thumbnails\": {\n      \"get\": {\n        \"description\": \"List Thumbnail\",\n        \"operationId\": \"listThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ThumbnailList\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnails\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Thumbnail\",\n        \"operationId\": \"createThumbnail\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Thumbnail\"\n              }\n            }\n          },\n          \"description\": \"Fresh thumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnails created just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/thumbnails/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Thumbnail\",\n        \"operationId\": \"deleteThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response thumbnail deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Thumbnail\",\n        \"operationId\": \"getThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response single thumbnail\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Thumbnail\",\n        \"operationId\": \"patchThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnail patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Thumbnail\",\n        \"operationId\": \"updateThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Thumbnail\"\n              }\n            }\n          },\n          \"description\": \"Updated thumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnails updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/theme.halo.run/v1alpha1/themes\": {\n      \"get\": {\n        \"description\": \"List Theme\",\n        \"operationId\": \"listTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ThemeList\"\n                }\n              }\n            },\n            \"description\": \"Response themes\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Theme\",\n        \"operationId\": \"createTheme\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Theme\"\n              }\n            }\n          },\n          \"description\": \"Fresh theme\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response themes created just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/theme.halo.run/v1alpha1/themes/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Theme\",\n        \"operationId\": \"deleteTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response theme deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Theme\",\n        \"operationId\": \"getTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response single theme\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Theme\",\n        \"operationId\": \"patchTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response theme patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Theme\",\n        \"operationId\": \"updateTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Theme\"\n              }\n            }\n          },\n          \"description\": \"Updated theme\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response themes updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/uc.api.auth.halo.run/v1alpha1/user-connections/{registerId}/disconnect\": {\n      \"put\": {\n        \"description\": \"Disconnect my connection from a third-party platform.\",\n        \"operationId\": \"DisconnectMyConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"The registration ID of the third-party platform.\",\n            \"in\": \"path\",\n            \"name\": \"registerId\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/UserConnection\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"List posts owned by the current user.\",\n        \"operationId\": \"ListMyPosts\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Posts filtered by publish phase.\",\n            \"in\": \"query\",\n            \"name\": \"publishPhase\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"DRAFT\",\n                \"PENDING_APPROVAL\",\n                \"PUBLISHED\",\n                \"FAILED\"\n              ]\n            }\n          },\n          {\n            \"description\": \"Posts filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Posts filtered by category including sub-categories.\",\n            \"in\": \"query\",\n            \"name\": \"categoryWithChildren\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create my post. If you want to create a post with content, please set\\n annotation: \\\"content.halo.run/content-json\\\" into annotations and refer\\n to Content for corresponding data type.\\n\",\n        \"operationId\": \"CreateMyPost\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}\": {\n      \"get\": {\n        \"description\": \"Get post that belongs to the current user.\",\n        \"operationId\": \"GetMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update my post.\",\n        \"operationId\": \"UpdateMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/draft\": {\n      \"get\": {\n        \"description\": \"Get my post draft.\",\n        \"operationId\": \"GetMyPostDraft\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Should include patched content and raw or not.\",\n            \"in\": \"query\",\n            \"name\": \"patched\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update draft of my post. Please make sure set annotation:\\n\\\"content.halo.run/content-json\\\" into annotations and refer to\\nContent for corresponding data type.\\n\",\n        \"operationId\": \"UpdateMyPostDraft\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Snapshot\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/publish\": {\n      \"put\": {\n        \"description\": \"Publish my post.\",\n        \"operationId\": \"PublishMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/recycle\": {\n      \"delete\": {\n        \"description\": \"Move my post to recycle bin.\",\n        \"operationId\": \"RecycleMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/unpublish\": {\n      \"put\": {\n        \"description\": \"Unpublish my post.\",\n        \"operationId\": \"UnpublishMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/snapshots/{name}\": {\n      \"get\": {\n        \"description\": \"Get snapshot for one post.\",\n        \"operationId\": \"GetSnapshotForPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Snapshot name.\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Post name.\",\n            \"in\": \"query\",\n            \"name\": \"postName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Should include patched content and raw or not.\",\n            \"in\": \"query\",\n            \"name\": \"patched\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.halo.run/v1alpha1/annotationsettings\": {\n      \"get\": {\n        \"description\": \"List available AnnotationSettings for the given targetRef. The available AnnotationSettings are determined by the currently activated theme and started plugins.\",\n        \"operationId\": \"listAvailableAnnotationSettings\",\n        \"parameters\": [\n          {\n            \"description\": \"The targetRef of the AnnotationSetting. e.g.: \\u0027content.halo.run/Post\",\n            \"in\": \"query\",\n            \"name\": \"targetRef\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1AlphaUc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.halo.run/v1alpha1/user-preferences/{group}\": {\n      \"get\": {\n        \"description\": \"Get my preference by group.\",\n        \"operationId\": \"getMyPreference\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of user preference, e.g. `notification`.\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserPreferenceV1alpha1Uc\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Create or update my preference by group.\",\n        \"operationId\": \"updateMyPreference\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of user preference, e.g. `notification`.\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content, preference updated successfully.\"\n          }\n        },\n        \"tags\": [\n          \"UserPreferenceV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings\": {\n      \"get\": {\n        \"description\": \"Get Two-factor authentication settings.\",\n        \"operationId\": \"GetTwoFactorAuthenticationSettings\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings/disabled\": {\n      \"put\": {\n        \"description\": \"Disable Two-factor authentication\",\n        \"operationId\": \"DisableTwoFactor\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PasswordRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings/enabled\": {\n      \"put\": {\n        \"description\": \"Enable Two-factor authentication\",\n        \"operationId\": \"EnableTwoFactor\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PasswordRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp\": {\n      \"post\": {\n        \"description\": \"Configure a TOTP\",\n        \"operationId\": \"ConfigurerTotp\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/TotpRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp/-\": {\n      \"delete\": {\n        \"operationId\": \"DeleteTotp\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PasswordRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp/auth-link\": {\n      \"get\": {\n        \"description\": \"Get TOTP auth link, including secret\",\n        \"operationId\": \"GetTotpAuthLink\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TotpAuthLinkResponse\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/devices\": {\n      \"get\": {\n        \"description\": \"List all user devices\",\n        \"operationId\": \"ListDevices\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/UserDevice\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/devices/{deviceId}\": {\n      \"delete\": {\n        \"description\": \"Revoke a own device\",\n        \"operationId\": \"RevokeDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Device ID\",\n            \"in\": \"path\",\n            \"name\": \"deviceId\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204 NO_CONTENT\": {\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens\": {\n      \"get\": {\n        \"description\": \"Obtain PAT list.\",\n        \"operationId\": \"ObtainPats\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Generate a PAT.\",\n        \"operationId\": \"GeneratePat\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete a PAT\",\n        \"operationId\": \"DeletePat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Obtain a PAT.\",\n        \"operationId\": \"ObtainPat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}/actions/restoration\": {\n      \"put\": {\n        \"description\": \"Restore a PAT.\",\n        \"operationId\": \"RestorePat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}/actions/revocation\": {\n      \"put\": {\n        \"description\": \"Revoke a PAT\",\n        \"operationId\": \"RevokePat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.storage.halo.run/v1alpha1/attachments\": {\n      \"get\": {\n        \"description\": \"List attachments of the current user uploaded.\",\n        \"operationId\": \"ListMyAttachments\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Filter attachments without group. This parameter will ignore group parameter.\",\n            \"in\": \"query\",\n            \"name\": \"ungrouped\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          },\n          {\n            \"description\": \"Keyword for searching.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Acceptable media types.\",\n            \"in\": \"query\",\n            \"name\": \"accepts\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AttachmentList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"deprecated\": true,\n        \"description\": \"Create attachment for the given post. Deprecated in favor of /attachments/-/upload.\",\n        \"operationId\": \"CreateAttachmentForPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Wait for permalink.\",\n            \"in\": \"query\",\n            \"name\": \"waitForPermalink\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PostAttachmentRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload\": {\n      \"post\": {\n        \"description\": \"Upload attachment to user center storage.\",\n        \"operationId\": \"UploadAttachmentForUc\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UploadForm\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload-from-url\": {\n      \"post\": {\n        \"deprecated\": true,\n        \"description\": \"Upload attachment from the given URL.\\nDeprecated in favor of /attachments/-/upload.\",\n        \"operationId\": \"ExternalTransferAttachment_1\",\n        \"parameters\": [\n          {\n            \"description\": \"Wait for permalink.\",\n            \"in\": \"query\",\n            \"name\": \"waitForPermalink\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UcUploadFromUrlRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/system/setup\": {\n      \"get\": {\n        \"description\": \"Jump to setup page\",\n        \"operationId\": \"JumpToSetupPage\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"text/html\": {\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemV1alpha1Public\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Setup system\",\n        \"operationId\": \"SetupSystem\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SetupRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"content\": {},\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"SystemV1alpha1Public\"\n        ]\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"AddOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"add\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"AnnotationSetting\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AnnotationSettingSpec\"\n          }\n        }\n      },\n      \"AnnotationSettingList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/AnnotationSetting\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AnnotationSettingSpec\": {\n        \"required\": [\n          \"formSchema\",\n          \"targetRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"formSchema\": {\n            \"minLength\": 1,\n            \"type\": \"array\",\n            \"items\": {\n              \"minLength\": 1,\n              \"type\": \"object\"\n            }\n          },\n          \"targetRef\": {\n            \"$ref\": \"#/components/schemas/GroupKind\"\n          }\n        }\n      },\n      \"Attachment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AttachmentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/AttachmentStatus\"\n          }\n        }\n      },\n      \"AttachmentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Attachment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AttachmentSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of attachment\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"Group name\"\n          },\n          \"mediaType\": {\n            \"type\": \"string\",\n            \"description\": \"Media type of attachment\"\n          },\n          \"ownerName\": {\n            \"type\": \"string\",\n            \"description\": \"Name of User who uploads the attachment\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Policy name\"\n          },\n          \"size\": {\n            \"minimum\": 0,\n            \"type\": \"integer\",\n            \"description\": \"Size of attachment. Unit is Byte\",\n            \"format\": \"int64\"\n          },\n          \"tags\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Tags of attachment\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Tag name\"\n            }\n          }\n        }\n      },\n      \"AttachmentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\",\n            \"description\": \"Permalink of attachment.\\nIf it is in local storage, the public URL will be set.\\nIf it is in s3 storage, the Object URL will be set.\\n\"\n          },\n          \"thumbnails\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"AuthProvider\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AuthProviderSpec\"\n          }\n        },\n        \"description\": \"Auth provider extension.\"\n      },\n      \"AuthProviderList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/AuthProvider\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AuthProviderSpec\": {\n        \"required\": [\n          \"authType\",\n          \"authenticationUrl\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"authType\": {\n            \"type\": \"string\",\n            \"description\": \"Auth type: form or oauth2.\",\n            \"enum\": [\n              \"FORM\",\n              \"OAUTH2\"\n            ]\n          },\n          \"authenticationUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Authentication url of the auth provider\"\n          },\n          \"bindingUrl\": {\n            \"type\": \"string\"\n          },\n          \"configMapRef\": {\n            \"$ref\": \"#/components/schemas/ConfigMapRef\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of the auth provider\"\n          },\n          \"helpPage\": {\n            \"type\": \"string\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"method\": {\n            \"type\": \"string\"\n          },\n          \"rememberMeSupport\": {\n            \"type\": \"boolean\"\n          },\n          \"settingRef\": {\n            \"$ref\": \"#/components/schemas/SettingRef\"\n          },\n          \"unbindUrl\": {\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Author\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Backup\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/BackupSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/BackupStatus\"\n          }\n        }\n      },\n      \"BackupFile\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Filename of backup file.\"\n          },\n          \"lastModifiedTime\": {\n            \"type\": \"string\",\n            \"description\": \"Last modified time of backup file.\",\n            \"format\": \"date-time\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of backup file.\",\n            \"format\": \"int64\"\n          }\n        },\n        \"description\": \"Backup file.\"\n      },\n      \"BackupList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Backup\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"BackupSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"expiresAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"format\": {\n            \"type\": \"string\",\n            \"description\": \"Backup file format. Currently, only zip format is supported.\"\n          }\n        }\n      },\n      \"BackupStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"completionTimestamp\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"failureMessage\": {\n            \"type\": \"string\"\n          },\n          \"failureReason\": {\n            \"type\": \"string\"\n          },\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Name of backup file.\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PENDING\",\n              \"RUNNING\",\n              \"SUCCEEDED\",\n              \"FAILED\"\n            ]\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of backup file. Data unit: byte\",\n            \"format\": \"int64\"\n          },\n          \"startTimestamp\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          }\n        }\n      },\n      \"Category\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CategorySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CategoryStatus\"\n          }\n        }\n      },\n      \"CategoryList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Category\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CategorySpec\": {\n        \"required\": [\n          \"displayName\",\n          \"priority\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eWhether to hide the category from the category list.\\u003c/p\\u003e\\n \\u003cp\\u003eWhen set to true, the category including its subcategories and related posts will\\n not be displayed in the category list, but it can still be accessed by permalink.\\u003c/p\\u003e\\n \\u003cp\\u003eLimitation: It only takes effect on the theme-side categorized list and it only\\n allows to be set to true on the first level(root node) of categories.\\u003c/p\\u003e\"\n          },\n          \"postTemplate\": {\n            \"maxLength\": 255,\n            \"type\": \"string\",\n            \"description\": \"\\u003cp\\u003eUsed to specify the template for the posts associated with the category.\\u003c/p\\u003e\\n \\u003cp\\u003eThe priority is not as high as that of the post.\\u003c/p\\u003e\\n \\u003cp\\u003eIf the post also specifies a template, the post\\u0027s template will prevail.\\u003c/p\\u003e\"\n          },\n          \"preventParentPostCascadeQuery\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eif a category is queried for related posts, the default behavior is to\\n query all posts under the category including its subcategories, but if this field is\\n set to true, cascade query behavior will be terminated here.\\u003c/p\\u003e\\n \\u003cp\\u003eFor example, if a category has subcategories A and B, and A has subcategories C and\\n D and C marked this field as true, when querying posts under A category,all posts under A\\n and B will be queried, but C and D will not be queried.\\u003c/p\\u003e\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"maxLength\": 255,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CategoryStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的已发布且公开的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"CategoryVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CategorySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CategoryStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link Category Category}.\"\n      },\n      \"CategoryVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CategoryVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ChangeOwnPasswordRequest\": {\n        \"required\": [\n          \"oldPassword\",\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"oldPassword\": {\n            \"type\": \"string\",\n            \"description\": \"Old password.\"\n          },\n          \"password\": {\n            \"minLength\": 5,\n            \"type\": \"string\",\n            \"description\": \"New password.\"\n          }\n        }\n      },\n      \"ChangePasswordRequest\": {\n        \"required\": [\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"password\": {\n            \"minLength\": 5,\n            \"type\": \"string\",\n            \"description\": \"New password.\"\n          }\n        }\n      },\n      \"Comment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        }\n      },\n      \"CommentEmailOwner\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\",\n            \"description\": \"avatar for comment owner\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"display name for comment owner\"\n          },\n          \"email\": {\n            \"type\": \"string\",\n            \"description\": \"email for comment owner\"\n          },\n          \"website\": {\n            \"type\": \"string\",\n            \"description\": \"website for comment owner\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eThe creator info of the comment.\\u003c/p\\u003e\\n This {@link CommentEmailOwner CommentEmailOwner} is only applicable to the user who is allowed to comment\\n without logging in.\"\n      },\n      \"CommentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Comment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CommentOwner\": {\n        \"required\": [\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"maxLength\": 64,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentRequest\": {\n        \"required\": [\n          \"content\",\n          \"raw\",\n          \"subjectRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentEmailOwner\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        },\n        \"description\": \"Request parameter object for {@link Comment Comment}.\"\n      },\n      \"CommentSpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"subjectRef\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"lastReadTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentStats\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"comment stats value object.\"\n      },\n      \"CommentStatsVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"comment stats value object.\"\n      },\n      \"CommentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"hasNewReply\": {\n            \"type\": \"boolean\"\n          },\n          \"lastReplyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"replyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"unreadReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visibleReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"CommentVo\": {\n        \"required\": [\n          \"metadata\",\n          \"owner\",\n          \"spec\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"CommentVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CommentVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CommentWithReplyVo\": {\n        \"required\": [\n          \"metadata\",\n          \"owner\",\n          \"spec\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"replies\": {\n            \"$ref\": \"#/components/schemas/ListResultReplyVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"CommentWithReplyVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CommentWithReplyVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Condition\": {\n        \"required\": [\n          \"lastTransitionTime\",\n          \"status\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"lastTransitionTime\": {\n            \"type\": \"string\",\n            \"description\": \"Last time the condition transitioned from one status to another.\",\n            \"format\": \"date-time\"\n          },\n          \"message\": {\n            \"maxLength\": 32768,\n            \"type\": \"string\",\n            \"description\": \"Human-readable message indicating details about last transition.\\n This may be an empty string.\"\n          },\n          \"reason\": {\n            \"maxLength\": 1024,\n            \"pattern\": \"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$\",\n            \"type\": \"string\",\n            \"description\": \"Unique, one-word, CamelCase reason for the condition\\u0027s last transition.\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"description\": \"Status is the status of the condition. Can be True, False, Unknown.\",\n            \"enum\": [\n              \"TRUE\",\n              \"FALSE\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"type\": {\n            \"maxLength\": 316,\n            \"pattern\": \"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$\",\n            \"type\": \"string\",\n            \"description\": \"type of condition in CamelCase or in foo.example.com/CamelCase.\\n example: Ready, Initialized.\\n maxLength: 316.\"\n          }\n        },\n        \"description\": \"EqualsAndHashCode 排除了lastTransitionTime否则失败时，lastTransitionTime 会被更新\\n 导致 equals 为 false，一直被加入队列.\"\n      },\n      \"ConfigMap\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"data\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eConfigMap holds configuration data to consume.\\u003c/p\\u003e\"\n      },\n      \"ConfigMapList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ConfigMap\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ConfigMapRef\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Content\": {\n        \"required\": [\n          \"content\",\n          \"raw\",\n          \"rawType\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ContentUpdateParam\": {\n        \"required\": [\n          \"content\",\n          \"raw\",\n          \"rawType\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ContentVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A value object for Content from {@link Snapshot Snapshot}.\"\n      },\n      \"ContentWrapper\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"type\": \"string\"\n          },\n          \"snapshotName\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Contributor\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Contributor from user.\"\n      },\n      \"ContributorVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"bio\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A value object for {@link run.halo.app.core.extension.User run.halo.app.core.extension.User}.\"\n      },\n      \"CopyOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"copy\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"Counter\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"approvedComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"downvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"totalComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"A counter for number of requests by extension resource name.\"\n      },\n      \"CounterList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Counter\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CounterRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"hostname\": {\n            \"type\": \"string\"\n          },\n          \"language\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"plural\": {\n            \"type\": \"string\"\n          },\n          \"referrer\": {\n            \"type\": \"string\"\n          },\n          \"screen\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CreateUserRequest\": {\n        \"required\": [\n          \"email\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"bio\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          },\n          \"phone\": {\n            \"type\": \"string\"\n          },\n          \"roles\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"CustomTemplates\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"category\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          },\n          \"page\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          },\n          \"post\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          }\n        }\n      },\n      \"DashboardStats\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"approvedComments\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"comments\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"posts\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"upvotes\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"users\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"visits\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"DetailedUser\": {\n        \"required\": [\n          \"roles\",\n          \"user\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"user\": {\n            \"$ref\": \"#/components/schemas/User\"\n          }\n        }\n      },\n      \"Device\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/DeviceSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/DeviceStatus\"\n          }\n        }\n      },\n      \"DeviceList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Device\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"DeviceSpec\": {\n        \"required\": [\n          \"ipAddress\",\n          \"principalName\",\n          \"sessionId\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"ipAddress\": {\n            \"maxLength\": 129,\n            \"type\": \"string\"\n          },\n          \"lastAccessedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"lastAuthenticatedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"principalName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"rememberMeSeriesId\": {\n            \"type\": \"string\"\n          },\n          \"sessionId\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"userAgent\": {\n            \"maxLength\": 500,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"DeviceStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"browser\": {\n            \"type\": \"string\"\n          },\n          \"os\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"EmailConfigValidationRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Gets email display name.\"\n          },\n          \"enable\": {\n            \"type\": \"boolean\"\n          },\n          \"encryption\": {\n            \"type\": \"string\"\n          },\n          \"host\": {\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          },\n          \"port\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"sender\": {\n            \"type\": \"string\",\n            \"description\": \"Gets email sender address.\"\n          },\n          \"username\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"EmailVerifyRequest\": {\n        \"required\": [\n          \"email\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"email\": {\n            \"type\": \"string\",\n            \"format\": \"email\"\n          }\n        }\n      },\n      \"Excerpt\": {\n        \"required\": [\n          \"autoGenerate\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"autoGenerate\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Extension\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          }\n        },\n        \"description\": \"Extension is an interface which represents an Extension. It contains setters and getters of\\n GroupVersionKind and Metadata.\"\n      },\n      \"ExtensionDefinition\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ExtensionSpec\"\n          }\n        },\n        \"description\": \"Extension definition.\\n An {@link ExtensionDefinition ExtensionDefinition} is a type of metadata that provides additional information about\\n an extension. An extension is a way to add new functionality to an existing class, structure,\\n enumeration, or protocol type without needing to subclass it.\"\n      },\n      \"ExtensionDefinitionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ExtensionPointDefinition\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ExtensionPointSpec\"\n          }\n        },\n        \"description\": \"Extension point definition.\\n An {@link ExtensionPointDefinition ExtensionPointDefinition} is a concept used in \\u003ccode\\u003eHalo\\u003c/code\\u003e to allow for the\\n dynamic extension of system. It defines a location within \\u003ccode\\u003eHalo\\u003c/code\\u003e where\\n additional functionality can be added through the use of plugins or extensions.\"\n      },\n      \"ExtensionPointDefinitionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ExtensionPointSpec\": {\n        \"required\": [\n          \"className\",\n          \"displayName\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"className\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"icon\": {\n            \"type\": \"string\"\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"SINGLETON\",\n              \"MULTI_INSTANCE\"\n            ]\n          }\n        }\n      },\n      \"ExtensionSpec\": {\n        \"required\": [\n          \"className\",\n          \"displayName\",\n          \"extensionPointName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"className\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"extensionPointName\": {\n            \"type\": \"string\"\n          },\n          \"icon\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"FileReverseProxyProvider\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"directory\": {\n            \"type\": \"string\"\n          },\n          \"filename\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"GrantRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"roles\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"Group\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/GroupSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/GroupStatus\"\n          }\n        }\n      },\n      \"GroupKind\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"is group name of Extension.\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"is kind name of Extension.\"\n          }\n        },\n        \"description\": \"GroupKind contains group and kind data only.\"\n      },\n      \"GroupList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Group\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"GroupSpec\": {\n        \"required\": [\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of group\"\n          }\n        }\n      },\n      \"GroupStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"totalAttachments\": {\n            \"minimum\": 0,\n            \"type\": \"integer\",\n            \"description\": \"Total of attachments under the current group\",\n            \"format\": \"int64\"\n          },\n          \"updateTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Update timestamp of the group\",\n            \"format\": \"date-time\"\n          }\n        }\n      },\n      \"HaloDocument\": {\n        \"required\": [\n          \"content\",\n          \"id\",\n          \"metadataName\",\n          \"ownerName\",\n          \"permalink\",\n          \"title\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Custom metadata. Make sure the map is serializable.\"\n          },\n          \"categories\": {\n            \"type\": \"array\",\n            \"description\": \"Document categories. The item in the list is the category metadata name.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document content. Safety content, without HTML tag.\"\n          },\n          \"creationTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Document creation timestamp.\",\n            \"format\": \"date-time\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"description\": \"Document description.\"\n          },\n          \"exposed\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether the document is exposed to the public.\"\n          },\n          \"id\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document ID. It should be unique globally.\"\n          },\n          \"metadataName\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Metadata name of the corresponding extension.\"\n          },\n          \"ownerName\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document owner metadata name.\"\n          },\n          \"permalink\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document permalink.\"\n          },\n          \"published\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether the document is published.\"\n          },\n          \"recycled\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether the document is recycled.\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"description\": \"Document tags. The item in the list is the tag metadata name.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document title.\"\n          },\n          \"type\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document type. e.g.: post.content.halo.run, singlepage.content.halo.run, moment.moment\\n .halo.run, doc.doc.halo.run.\"\n          },\n          \"updateTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Document update timestamp.\",\n            \"format\": \"date-time\"\n          }\n        },\n        \"description\": \"Document for search.\"\n      },\n      \"IAvatarUploadRequest\": {\n        \"required\": [\n          \"file\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          }\n        }\n      },\n      \"IUploadRequest\": {\n        \"required\": [\n          \"file\",\n          \"policyName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the group to which the attachment belongs\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Storage policy name\"\n          }\n        }\n      },\n      \"InstallFromUriRequest\": {\n        \"required\": [\n          \"uri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"uri\"\n          }\n        }\n      },\n      \"InterestReason\": {\n        \"required\": [\n          \"reasonType\",\n          \"subject\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"expression\": {\n            \"type\": \"string\",\n            \"description\": \"The expression to be interested in\"\n          },\n          \"reasonType\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the reason definition to be interested in\"\n          },\n          \"subject\": {\n            \"$ref\": \"#/components/schemas/InterestReasonSubject\"\n          }\n        },\n        \"description\": \"The reason to be interested in\"\n      },\n      \"InterestReasonSubject\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"if name is not specified, it presents all subjects of the specified reason type and custom resources\"\n          }\n        },\n        \"description\": \"The subject name of reason type to be interested in\"\n      },\n      \"JsonPatch\": {\n        \"minItems\": 1,\n        \"uniqueItems\": true,\n        \"type\": \"array\",\n        \"description\": \"JSON schema for JSONPatch operations\",\n        \"items\": {\n          \"oneOf\": [\n            {\n              \"$ref\": \"#/components/schemas/AddOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/ReplaceOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/TestOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/RemoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/MoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/CopyOperation\"\n            }\n          ]\n        }\n      },\n      \"License\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"url\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Common data objects for license.\"\n      },\n      \"ListResultReplyVo\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReplyVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedAuthProvider\": {\n        \"required\": [\n          \"displayName\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"authType\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"FORM\",\n              \"OAUTH2\"\n            ]\n          },\n          \"authenticationUrl\": {\n            \"type\": \"string\"\n          },\n          \"bindingUrl\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"enabled\": {\n            \"type\": \"boolean\"\n          },\n          \"helpPage\": {\n            \"type\": \"string\"\n          },\n          \"isBound\": {\n            \"type\": \"boolean\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"privileged\": {\n            \"type\": \"boolean\"\n          },\n          \"supportsBinding\": {\n            \"type\": \"boolean\"\n          },\n          \"unbindingUrl\": {\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A listed value object for {@link run.halo.app.core.extension.AuthProvider run.halo.app.core.extension.AuthProvider}.\"\n      },\n      \"ListedComment\": {\n        \"required\": [\n          \"comment\",\n          \"owner\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"comment\": {\n            \"$ref\": \"#/components/schemas/Comment\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStats\"\n          },\n          \"subject\": {\n            \"$ref\": \"#/components/schemas/Extension\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedCommentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedComment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedPost\": {\n        \"required\": [\n          \"categories\",\n          \"contributors\",\n          \"owner\",\n          \"post\",\n          \"stats\",\n          \"tags\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Category\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Contributor\"\n            }\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/Contributor\"\n          },\n          \"post\": {\n            \"$ref\": \"#/components/schemas/Post\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/Stats\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Tag\"\n            }\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedPostList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedPost\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedPostVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CategoryVo\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TagVo\"\n            }\n          }\n        },\n        \"description\": \"A value object for {@link Post Post}.\"\n      },\n      \"ListedPostVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedPostVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedReply\": {\n        \"required\": [\n          \"owner\",\n          \"reply\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"reply\": {\n            \"$ref\": \"#/components/schemas/Reply\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStats\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedReplyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedReply\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedSinglePage\": {\n        \"required\": [\n          \"contributors\",\n          \"owner\",\n          \"page\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Contributor\"\n            }\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/Contributor\"\n          },\n          \"page\": {\n            \"$ref\": \"#/components/schemas/SinglePage\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/Stats\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedSinglePageList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedSinglePage\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedSinglePageVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SinglePageSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/SinglePageStatus\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedSinglePageVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedSinglePageVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedSnapshotDto\": {\n        \"required\": [\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ListedSnapshotSpec\"\n          }\n        }\n      },\n      \"ListedSnapshotSpec\": {\n        \"required\": [\n          \"owner\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"modifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ListedUser\": {\n        \"required\": [\n          \"roles\",\n          \"user\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"user\": {\n            \"$ref\": \"#/components/schemas/User\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"LocalThumbnail\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/LocalThumbnailSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/LocalThumbnailStatus\"\n          }\n        }\n      },\n      \"LocalThumbnailList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/LocalThumbnail\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"LocalThumbnailSpec\": {\n        \"required\": [\n          \"filePath\",\n          \"imageSignature\",\n          \"imageUri\",\n          \"size\",\n          \"thumbSignature\",\n          \"thumbnailUri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"filePath\": {\n            \"type\": \"string\",\n            \"description\": \"Consider the compatibility of the system and migration, use unix-style relative paths\\n here.\"\n          },\n          \"imageSignature\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"A hash signature for the image uri.\"\n          },\n          \"imageUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"size\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"S\",\n              \"M\",\n              \"L\",\n              \"XL\"\n            ]\n          },\n          \"thumbSignature\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"A hash signature for the thumbnail uri.\"\n          },\n          \"thumbnailUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"LocalThumbnailStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PENDING\",\n              \"SUCCEEDED\",\n              \"FAILED\"\n            ]\n          }\n        }\n      },\n      \"MarkSpecifiedRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"names\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"Menu\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuSpec\"\n          }\n        }\n      },\n      \"MenuItem\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuItemSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/MenuItemStatus\"\n          }\n        }\n      },\n      \"MenuItemList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/MenuItem\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"MenuItemSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Children of this menu item\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"The name of menu item child\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"The display name of menu item.\"\n          },\n          \"href\": {\n            \"type\": \"string\",\n            \"description\": \"The href of this menu item.\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"description\": \"The priority is for ordering.\",\n            \"format\": \"int32\"\n          },\n          \"target\": {\n            \"type\": \"string\",\n            \"description\": \"The \\u003ca\\u003e target attribute of this menu item.\",\n            \"enum\": [\n              \"_blank\",\n              \"_self\",\n              \"_parent\",\n              \"_top\"\n            ]\n          },\n          \"targetRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        }\n      },\n      \"MenuItemStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Calculated Display name of menu item.\"\n          },\n          \"href\": {\n            \"type\": \"string\",\n            \"description\": \"Calculated href of manu item.\"\n          }\n        }\n      },\n      \"MenuItemVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/MenuItemVo\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Gets menu item\\u0027s display name.\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"parentName\": {\n            \"type\": \"string\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuItemSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/MenuItemStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link MenuItem MenuItem}.\"\n      },\n      \"MenuList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Menu\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"MenuSpec\": {\n        \"required\": [\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"The display name of the menu.\"\n          },\n          \"menuItems\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Menu items of this menu.\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Name of menu item.\"\n            }\n          }\n        }\n      },\n      \"MenuVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"menuItems\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/MenuItemVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuSpec\"\n          }\n        },\n        \"description\": \"A value object for {@link Menu Menu}.\"\n      },\n      \"Metadata\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Annotations are like key-value format.\"\n          },\n          \"creationTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Creation timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"deletionTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Deletion timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"finalizers\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"string\",\n              \"nullable\": true\n            }\n          },\n          \"generateName\": {\n            \"type\": \"string\",\n            \"description\": \"The name field will be generated automatically according to the given generateName field\"\n          },\n          \"labels\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Labels are like key-value format.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Metadata name\"\n          },\n          \"version\": {\n            \"type\": \"integer\",\n            \"description\": \"Current version of the Extension. It will be bumped up every update.\",\n            \"format\": \"int64\",\n            \"nullable\": true\n          }\n        },\n        \"description\": \"Metadata of Extension.\"\n      },\n      \"MoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"move\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"NavigationPostVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"next\": {\n            \"$ref\": \"#/components/schemas/ListedPostVo\"\n          },\n          \"previous\": {\n            \"$ref\": \"#/components/schemas/ListedPostVo\"\n          }\n        },\n        \"description\": \"Post navigation vo to hold previous and next item.\"\n      },\n      \"Notification\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/NotificationSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link Notification Notification} is a custom extension that used to store notification information for\\n inner use, it\\u0027s on-site notification.\\u003c/p\\u003e\\n\\n \\u003cp\\u003eSupports the following operations:\\u003c/p\\u003e\\n \\u003cul\\u003e\\n    \\u003cli\\u003eMarked as read: {@link NotificationSpec#setUnread(boolean) NotificationSpec#setUnread(boolean)}\\u003c/li\\u003e\\n    \\u003cli\\u003eGet the last read time: {@link NotificationSpec#getLastReadAt NotificationSpec#getLastReadAt()}\\u003c/li\\u003e\\n    \\u003cli\\u003eFilter by recipient: {@link NotificationSpec#getRecipient NotificationSpec#getRecipient()}\\u003c/li\\u003e\\n \\u003c/ul\\u003e\"\n      },\n      \"NotificationList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Notification\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"NotificationSpec\": {\n        \"required\": [\n          \"htmlContent\",\n          \"rawContent\",\n          \"reason\",\n          \"recipient\",\n          \"title\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"htmlContent\": {\n            \"type\": \"string\"\n          },\n          \"lastReadAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"rawContent\": {\n            \"type\": \"string\"\n          },\n          \"reason\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"The name of reason\"\n          },\n          \"recipient\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"The name of user\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"unread\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"NotificationTemplate\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/NotificationTemplateSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link NotificationTemplate NotificationTemplate} is a custom extension that defines a notification template.\\u003c/p\\u003e\\n \\u003cp\\u003eIt describes the notification template\\u0027s name, description, and the template content.\\u003c/p\\u003e\\n \\u003cp\\u003e{@link Spec#getReasonSelector Spec#getReasonSelector()} is used to select the template by reasonType and language,\\n if multiple templates are matched, the best match will be selected. This is useful when you\\n want to override the default template.\\u003c/p\\u003e\"\n      },\n      \"NotificationTemplateList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/NotificationTemplate\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"NotificationTemplateSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"reasonSelector\": {\n            \"$ref\": \"#/components/schemas/ReasonSelector\"\n          },\n          \"template\": {\n            \"$ref\": \"#/components/schemas/TemplateContent\"\n          }\n        }\n      },\n      \"NotifierDescriptor\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/NotifierDescriptorSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link NotifierDescriptor NotifierDescriptor} is a custom extension that defines a notifier.\\u003c/p\\u003e\\n \\u003cp\\u003eIt describes the notifier\\u0027s name, description, and the extension name of the notifier to\\n let the user know what the notifier is and what it can do in the UI and also let the\\n \\u003ccode\\u003eNotificationCenter\\u003c/code\\u003e know how to load the notifier and prepare the notifier\\u0027s settings.\\u003c/p\\u003e\"\n      },\n      \"NotifierDescriptorList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"NotifierDescriptorSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"notifierExtName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"notifierExtName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"receiverSettingRef\": {\n            \"$ref\": \"#/components/schemas/NotifierSettingRef\"\n          },\n          \"senderSettingRef\": {\n            \"$ref\": \"#/components/schemas/NotifierSettingRef\"\n          }\n        }\n      },\n      \"NotifierInfo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"NotifierSettingRef\": {\n        \"required\": [\n          \"group\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"OwnerInfo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Comment owner info.\"\n      },\n      \"PasswordRequest\": {\n        \"required\": [\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"password\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PatSpec\": {\n        \"required\": [\n          \"name\",\n          \"tokenId\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"expiresAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"lastUsed\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"revoked\": {\n            \"type\": \"boolean\"\n          },\n          \"revokesAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"scopes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"tokenId\": {\n            \"type\": \"string\"\n          },\n          \"username\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PersonalAccessToken\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PatSpec\"\n          }\n        }\n      },\n      \"PersonalAccessTokenList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Plugin\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PluginSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PluginStatus\"\n          }\n        },\n        \"description\": \"A custom resource for Plugin.\"\n      },\n      \"PluginAuthor\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PluginInstallRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"presetName\": {\n            \"type\": \"string\",\n            \"description\": \"Plugin preset name. We will find the plugin from plugin presets\"\n          },\n          \"source\": {\n            \"type\": \"string\",\n            \"description\": \"Install source. Default is file.\",\n            \"enum\": [\n              \"FILE\",\n              \"PRESET\",\n              \"URL\"\n            ]\n          }\n        }\n      },\n      \"PluginList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Plugin\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PluginRunningStateRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"async\": {\n            \"type\": \"boolean\"\n          },\n          \"enable\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"PluginSpec\": {\n        \"required\": [\n          \"version\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"author\": {\n            \"$ref\": \"#/components/schemas/PluginAuthor\"\n          },\n          \"configMapName\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"enabled\": {\n            \"type\": \"boolean\"\n          },\n          \"homepage\": {\n            \"type\": \"string\"\n          },\n          \"issues\": {\n            \"type\": \"string\"\n          },\n          \"license\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/License\"\n            }\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"pluginDependencies\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"repo\": {\n            \"type\": \"string\"\n          },\n          \"requires\": {\n            \"type\": \"string\",\n            \"description\": \"SemVer format.\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"pattern\": \"^(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)(?:-((?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+([0-9a-zA-Z-]+(?:\\\\.[0-9a-zA-Z-]+)*))?$\",\n            \"type\": \"string\",\n            \"description\": \"plugin version.\"\n          }\n        }\n      },\n      \"PluginStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"entry\": {\n            \"type\": \"string\"\n          },\n          \"lastProbeState\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"CREATED\",\n              \"DISABLED\",\n              \"RESOLVED\",\n              \"STARTED\",\n              \"STOPPED\",\n              \"FAILED\",\n              \"UNLOADED\"\n            ]\n          },\n          \"lastStartTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"loadLocation\": {\n            \"type\": \"string\",\n            \"description\": \"Load location of the plugin, often a path.\",\n            \"format\": \"uri\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PENDING\",\n              \"STARTING\",\n              \"CREATED\",\n              \"DISABLING\",\n              \"DISABLED\",\n              \"RESOLVED\",\n              \"STARTED\",\n              \"STOPPED\",\n              \"FAILED\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"stylesheet\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Policy\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PolicySpec\"\n          }\n        }\n      },\n      \"PolicyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Policy\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PolicyRule\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiGroups\": {\n            \"type\": \"array\",\n            \"description\": \"APIGroups is the name of the APIGroup that contains the resources.\\n If multiple API groups are specified, any action requested against one of the enumerated\\n resources in any API group will be allowed.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"nonResourceURLs\": {\n            \"type\": \"array\",\n            \"description\": \"NonResourceURLs is a set of partial urls that a user should have access to.\\n *s are allowed, but only as the full, final step in the path\\n If an action is not a resource API request, then the URL is split on \\u0027/\\u0027 and is checked\\n against the NonResourceURLs to look for a match.\\n Since non-resource URLs are not namespaced, this field is only applicable for\\n ClusterRoles referenced from a ClusterRoleBinding.\\n Rules can either apply to API resources (such as \\\"pods\\\" or \\\"secrets\\\") or non-resource\\n URL paths (such as \\\"/api\\\"),  but not both.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"resourceNames\": {\n            \"type\": \"array\",\n            \"description\": \"ResourceNames is an optional white list of names that the rule applies to.  An empty set\\n means that everything is allowed.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"resources\": {\n            \"type\": \"array\",\n            \"description\": \"Resources is a list of resources this rule applies to.  \\u0027*\\u0027 represents all resources in\\n the specified apiGroups.\\n \\u0027*\\u0026#47;foo\\u0027 represents the subresource \\u0027foo\\u0027 for all resources in the specified\\n apiGroups.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"verbs\": {\n            \"type\": \"array\",\n            \"description\": \"about who the rule applies to or which namespace the rule applies to.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        \"description\": \"PolicyRule holds information that describes a policy rule, but does not contain information\\n about whom the rule applies to or which namespace the rule applies to.\"\n      },\n      \"PolicySpec\": {\n        \"required\": [\n          \"displayName\",\n          \"templateName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"configMapName\": {\n            \"type\": \"string\",\n            \"description\": \"Reference name of ConfigMap extension\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of policy\"\n          },\n          \"templateName\": {\n            \"type\": \"string\",\n            \"description\": \"Reference name of PolicyTemplate\"\n          }\n        }\n      },\n      \"PolicyTemplate\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PolicyTemplateSpec\"\n          }\n        }\n      },\n      \"PolicyTemplateList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/PolicyTemplate\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PolicyTemplateSpec\": {\n        \"required\": [\n          \"settingName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Post\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003ePost extension.\\u003c/p\\u003e\"\n      },\n      \"PostAttachmentRequest\": {\n        \"required\": [\n          \"file\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"postName\": {\n            \"type\": \"string\",\n            \"description\": \"Post name.\"\n          },\n          \"singlePageName\": {\n            \"type\": \"string\",\n            \"description\": \"Single page name.\"\n          }\n        }\n      },\n      \"PostList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Post\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PostRequest\": {\n        \"required\": [\n          \"content\",\n          \"post\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentUpdateParam\"\n          },\n          \"post\": {\n            \"$ref\": \"#/components/schemas/Post\"\n          }\n        },\n        \"description\": \"Post and content data for creating and updating post.\"\n      },\n      \"PostSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"文章引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"PostStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PostVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CategoryVo\"\n            }\n          },\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentVo\"\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TagVo\"\n            }\n          }\n        },\n        \"description\": \"A value object for {@link Post Post}.\"\n      },\n      \"Reason\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReasonSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link Reason Reason} is a custom extension that defines a reason for a notification, It represents\\n an instance of a {@link ReasonType ReasonType}.\\u003c/p\\u003e\\n \\u003cp\\u003eIt can be understood as an event that triggers a notification.\\u003c/p\\u003e\"\n      },\n      \"ReasonAttributes\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"empty\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"description\": \"Attributes used to transfer data\"\n      },\n      \"ReasonList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Reason\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReasonProperty\": {\n        \"required\": [\n          \"name\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"optional\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"type\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonSelector\": {\n        \"required\": [\n          \"language\",\n          \"reasonType\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"language\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"default\": \"default\"\n          },\n          \"reasonType\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonSpec\": {\n        \"required\": [\n          \"author\",\n          \"reasonType\",\n          \"subject\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"attributes\": {\n            \"$ref\": \"#/components/schemas/ReasonAttributes\"\n          },\n          \"author\": {\n            \"type\": \"string\"\n          },\n          \"reasonType\": {\n            \"type\": \"string\"\n          },\n          \"subject\": {\n            \"$ref\": \"#/components/schemas/ReasonSubject\"\n          }\n        }\n      },\n      \"ReasonSubject\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"name\",\n          \"title\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"type\": \"string\"\n          },\n          \"url\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonType\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReasonTypeSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link ReasonType ReasonType} is a custom extension that defines a type of reason.\\u003c/p\\u003e\\n \\u003cp\\u003eOne {@link ReasonType ReasonType} can have multiple {@link Reason Reason}s to notify.\\u003c/p\\u003e\"\n      },\n      \"ReasonTypeInfo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"uiPermissions\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"ReasonTypeList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonType\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReasonTypeNotifierCollectionRequest\": {\n        \"required\": [\n          \"reasonTypeNotifiers\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"reasonTypeNotifiers\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonTypeNotifierRequest\"\n            }\n          }\n        }\n      },\n      \"ReasonTypeNotifierMatrix\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"notifiers\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/NotifierInfo\"\n            }\n          },\n          \"reasonTypes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonTypeInfo\"\n            }\n          },\n          \"stateMatrix\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"boolean\"\n              }\n            }\n          }\n        }\n      },\n      \"ReasonTypeNotifierRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"notifiers\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"reasonType\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonTypeSpec\": {\n        \"required\": [\n          \"description\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"properties\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonProperty\"\n            }\n          }\n        }\n      },\n      \"Ref\": {\n        \"required\": [\n          \"group\",\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"Extension group\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Extension kind\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Extension name. This field is mandatory\"\n          },\n          \"version\": {\n            \"type\": \"string\",\n            \"description\": \"Extension version\"\n          }\n        },\n        \"description\": \"Extension reference object. The name is mandatory\"\n      },\n      \"RememberMeToken\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/RememberMeTokenSpec\"\n          }\n        }\n      },\n      \"RememberMeTokenList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/RememberMeToken\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RememberMeTokenSpec\": {\n        \"required\": [\n          \"series\",\n          \"tokenValue\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"lastUsed\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"series\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"tokenValue\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"username\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"RemoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"remove\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"ReplaceOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"replace\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Reply\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReplySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/ReplyStatus\"\n          }\n        }\n      },\n      \"ReplyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Reply\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReplyRequest\": {\n        \"required\": [\n          \"content\",\n          \"raw\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentEmailOwner\"\n          },\n          \"quoteReply\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A request parameter object for {@link Reply Reply}.\"\n      },\n      \"ReplySpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"commentName\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"commentName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"quoteReply\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReplyStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReplyVo\": {\n        \"required\": [\n          \"metadata\",\n          \"owner\",\n          \"spec\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReplySpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStatsVo\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ReplyVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReplyVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RestoreRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"backupName\": {\n            \"type\": \"string\",\n            \"description\": \"Backup metadata name.\"\n          },\n          \"downloadUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Remote backup HTTP URL.\"\n          },\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Filename of backup file in backups root.\"\n          }\n        }\n      },\n      \"ReverseProxy\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"rules\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReverseProxyRule\"\n            }\n          }\n        },\n        \"description\": \"\\u003cp\\u003eThe reverse proxy custom resource is used to configure a path to proxy it to a directory or\\n file.\\u003c/p\\u003e\\n \\u003cp\\u003eHTTP proxy may be added in the future.\\u003c/p\\u003e\"\n      },\n      \"ReverseProxyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReverseProxy\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReverseProxyRule\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"$ref\": \"#/components/schemas/FileReverseProxyProvider\"\n          },\n          \"path\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"RevertSnapshotForPostParam\": {\n        \"required\": [\n          \"snapshotName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"snapshotName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"RevertSnapshotForSingleParam\": {\n        \"required\": [\n          \"snapshotName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"snapshotName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Role\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"rules\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"rules\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/PolicyRule\"\n            }\n          }\n        }\n      },\n      \"RoleBinding\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"roleRef\": {\n            \"$ref\": \"#/components/schemas/RoleRef\"\n          },\n          \"subjects\": {\n            \"type\": \"array\",\n            \"description\": \"Subjects holds references to the objects the role applies to.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Subject\"\n            }\n          }\n        },\n        \"description\": \"RoleBinding references a role, but does not contain it.\\n It can reference a Role in the global.\\n It adds who information via Subjects.\"\n      },\n      \"RoleBindingList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/RoleBinding\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RoleList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RoleRef\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiGroup\": {\n            \"type\": \"string\",\n            \"description\": \"APIGroup is the group for the resource being referenced.\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Kind is the type of resource being referenced.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Name is the name of resource being referenced.\"\n          }\n        },\n        \"description\": \"RoleRef contains information that points to the role being used.\"\n      },\n      \"SearchOption\": {\n        \"required\": [\n          \"keyword\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Additional annotations for extending search option by other search engines.\"\n          },\n          \"filterExposed\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether to filter exposed content. If null, it will not filter.\"\n          },\n          \"filterPublished\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether to filter published content. If null, it will not filter.\"\n          },\n          \"filterRecycled\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether to filter recycled content. If null, it will not filter.\"\n          },\n          \"highlightPostTag\": {\n            \"type\": \"string\",\n            \"description\": \"Post HTML tag of highlighted fragment.\"\n          },\n          \"highlightPreTag\": {\n            \"type\": \"string\",\n            \"description\": \"Pre HTML tag of highlighted fragment.\"\n          },\n          \"includeCategoryNames\": {\n            \"type\": \"array\",\n            \"description\": \"Category names to include(and). If null, it will include all categories.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"includeOwnerNames\": {\n            \"type\": \"array\",\n            \"description\": \"Owner names to include(or). If null, it will include all owners.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"includeTagNames\": {\n            \"type\": \"array\",\n            \"description\": \"Tag names to include(and). If null, it will include all tags.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"includeTypes\": {\n            \"type\": \"array\",\n            \"description\": \"Types to include(or). If null, it will include all types.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"keyword\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Search keyword.\"\n          },\n          \"limit\": {\n            \"maximum\": 1000,\n            \"minimum\": 1,\n            \"type\": \"integer\",\n            \"description\": \"Limit of result.\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"Search option. It is used to control search behavior.\"\n      },\n      \"SearchResult\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"hits\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/HaloDocument\"\n            }\n          },\n          \"keyword\": {\n            \"type\": \"string\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"processingTimeMillis\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Secret\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"data\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\",\n              \"format\": \"byte\"\n            },\n            \"description\": \"\\u003cp\\u003eThe total bytes of the values in\\n the Data field must be less than {@link run.halo.app.extension.Secret#MAX_SECRET_SIZE #MAX_SECRET_SIZE} bytes.\\u003c/p\\u003e\\n \\u003cp\\u003e\\u003ccode\\u003edata\\u003c/code\\u003e contains the secret data. Each key must consist of alphanumeric\\n characters, \\u0027-\\u0027, \\u0027_\\u0027 or \\u0027.\\u0027. The serialized form of the secret data is a\\n base64 encoded string, representing the arbitrary (possibly non-string)\\n data value here. Described in\\n \\u003ca href\\u003d\\\"https://tools.ietf.org/html/rfc4648#section-4\\\"\\u003erfc4648#section-4\\u003c/a\\u003e\\n \\u003c/p\\u003e\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"stringData\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"\\u003ccode\\u003estringData\\u003c/code\\u003e allows specifying non-binary secret data in string form.\\n It is provided as a write-only input field for convenience.\\n All keys and values are merged into the data field on write, overwriting any existing\\n values.\\n The stringData field is never output when reading from the API.\"\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"description\": \"Used to facilitate programmatic handling of secret data.\\n More info:\\n \\u003ca href\\u003d\\\"https://kubernetes.io/docs/concepts/configuration/secret/#secret-types\\\"\\u003esecret-types\\u003c/a\\u003e\"\n          }\n        },\n        \"description\": \"Secret is a small piece of sensitive data which should be kept secret, such as a password,\\n a token, or a key.\"\n      },\n      \"SecretList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Secret\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Setting\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SettingSpec\"\n          }\n        },\n        \"description\": \"{@link Setting Setting} is a custom extension to generate forms based on configuration.\"\n      },\n      \"SettingForm\": {\n        \"minLength\": 1,\n        \"required\": [\n          \"formSchema\",\n          \"group\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"formSchema\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\"\n            }\n          },\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"label\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SettingList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Setting\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SettingRef\": {\n        \"required\": [\n          \"group\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SettingSpec\": {\n        \"required\": [\n          \"forms\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"forms\": {\n            \"minLength\": 1,\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/SettingForm\"\n            }\n          }\n        }\n      },\n      \"SetupRequest\": {\n        \"required\": [\n          \"externalUrl\",\n          \"password\",\n          \"siteTitle\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"email\": {\n            \"type\": \"string\",\n            \"format\": \"email\"\n          },\n          \"externalUrl\": {\n            \"type\": \"string\"\n          },\n          \"language\": {\n            \"pattern\": \"^(zh-CN|zh-TW|en|es)$\",\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"maxLength\": 257,\n            \"minLength\": 5,\n            \"pattern\": \"^[A-Za-z0-9!@#$%^\\u0026*.?]+$\",\n            \"type\": \"string\"\n          },\n          \"siteTitle\": {\n            \"maxLength\": 80,\n            \"minLength\": 0,\n            \"type\": \"string\"\n          },\n          \"username\": {\n            \"maxLength\": 63,\n            \"minLength\": 4,\n            \"pattern\": \"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\",\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SinglePage\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SinglePageSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/SinglePageStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eSingle page extension.\\u003c/p\\u003e\"\n      },\n      \"SinglePageList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/SinglePage\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SinglePageRequest\": {\n        \"required\": [\n          \"content\",\n          \"page\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentUpdateParam\"\n          },\n          \"page\": {\n            \"$ref\": \"#/components/schemas/SinglePage\"\n          }\n        },\n        \"description\": \"A request parameter for {@link SinglePage SinglePage}.\"\n      },\n      \"SinglePageSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"SinglePageStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SinglePageVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentVo\"\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SinglePageSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/SinglePageStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link SinglePage SinglePage}.\"\n      },\n      \"SiteStatsVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"category\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"comment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"post\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"A value object for site stats.\"\n      },\n      \"SnapShotSpec\": {\n        \"required\": [\n          \"owner\",\n          \"rawType\",\n          \"subjectRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"contentPatch\": {\n            \"type\": \"string\"\n          },\n          \"contributors\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"parentSnapshotName\": {\n            \"type\": \"string\"\n          },\n          \"rawPatch\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"maxLength\": 50,\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"such as: markdown | html | json | asciidoc | latex.\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        }\n      },\n      \"Snapshot\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SnapShotSpec\"\n          }\n        }\n      },\n      \"SnapshotList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Snapshot\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Stats\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"approvedComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"totalComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"Stats value object.\"\n      },\n      \"StatsVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"comment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"Stats value object.\"\n      },\n      \"Subject\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiGroup\": {\n            \"type\": \"string\",\n            \"description\": \"APIGroup holds the API group of the referenced subject.\\n Defaults to \\\"\\\" for ServiceAccount subjects.\\n Defaults to \\\"rbac.authorization.halo.run\\\" for User and Group subjects.\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Kind of object being referenced. Values defined by this API group are \\\"User\\\", \\\"Group\\\",\\n and \\\"ServiceAccount\\\".\\n If the Authorizer does not recognize the kind value, the Authorizer should report\\n an error.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Name of the object being referenced.\"\n          }\n        }\n      },\n      \"Subscription\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SubscriptionSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link Subscription Subscription} is a custom extension that defines a subscriber to be notified when a\\n certain {@link Reason Reason} is triggered.\\u003c/p\\u003e\\n \\u003cp\\u003eIt holds a {@link Subscriber Subscriber} to the user to be notified, a {@link InterestReason InterestReason} to\\n subscribe to.\\u003c/p\\u003e\"\n      },\n      \"SubscriptionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Subscription\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SubscriptionSpec\": {\n        \"required\": [\n          \"reason\",\n          \"subscriber\",\n          \"unsubscribeToken\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"disabled\": {\n            \"type\": \"boolean\",\n            \"description\": \"Perhaps users need to unsubscribe and interact without receiving notifications again\"\n          },\n          \"reason\": {\n            \"$ref\": \"#/components/schemas/InterestReason\"\n          },\n          \"subscriber\": {\n            \"$ref\": \"#/components/schemas/SubscriptionSubscriber\"\n          },\n          \"unsubscribeToken\": {\n            \"type\": \"string\",\n            \"description\": \"The token to unsubscribe\"\n          }\n        }\n      },\n      \"SubscriptionSubscriber\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"The subscriber to be notified\"\n      },\n      \"Tag\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/TagSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/TagStatus\"\n          }\n        }\n      },\n      \"TagList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Tag\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"TagSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"color\": {\n            \"pattern\": \"^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$\",\n            \"type\": \"string\",\n            \"description\": \"Color regex explanation.\\n \\u003cpre\\u003e\\n ^                 # start of the line\\n #                 # start with a number sign `#`\\n (                 # start of (group 1)\\n   [a-fA-F0-9]{6}  # support z-f, A-F and 0-9, with a length of 6\\n   |               # or\\n   [a-fA-F0-9]{3}  # support z-f, A-F and 0-9, with a length of 3\\n )                 # end of (group 1)\\n $                 # end of the line\\n \\u003c/pre\\u003e\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TagStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"TagVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/TagSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/TagStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link Tag Tag}.\"\n      },\n      \"TagVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TagVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"TemplateContent\": {\n        \"required\": [\n          \"title\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"htmlBody\": {\n            \"type\": \"string\"\n          },\n          \"rawBody\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TemplateDescriptor\": {\n        \"required\": [\n          \"file\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"file\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"screenshot\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Type used to describe custom template page.\"\n      },\n      \"TestOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"test\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Theme\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ThemeSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/ThemeStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eTheme extension.\\u003c/p\\u003e\"\n      },\n      \"ThemeInstallRequest\": {\n        \"type\": \"object\"\n      },\n      \"ThemeList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Theme\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ThemeSpec\": {\n        \"required\": [\n          \"author\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"author\": {\n            \"$ref\": \"#/components/schemas/Author\"\n          },\n          \"configMapName\": {\n            \"type\": \"string\"\n          },\n          \"customTemplates\": {\n            \"$ref\": \"#/components/schemas/CustomTemplates\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"homepage\": {\n            \"type\": \"string\"\n          },\n          \"issues\": {\n            \"type\": \"string\"\n          },\n          \"license\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/License\"\n            }\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"repo\": {\n            \"type\": \"string\"\n          },\n          \"requires\": {\n            \"type\": \"string\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ThemeStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"location\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"READY\",\n              \"FAILED\",\n              \"UNKNOWN\"\n            ]\n          }\n        }\n      },\n      \"Thumbnail\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ThumbnailSpec\"\n          }\n        }\n      },\n      \"ThumbnailList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Thumbnail\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ThumbnailSpec\": {\n        \"required\": [\n          \"imageSignature\",\n          \"imageUri\",\n          \"size\",\n          \"thumbnailUri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"imageSignature\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"imageUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"size\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"S\",\n              \"M\",\n              \"L\",\n              \"XL\"\n            ]\n          },\n          \"thumbnailUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TotpAuthLinkResponse\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"authLink\": {\n            \"type\": \"string\",\n            \"description\": \"QR Code with base64 encoded.\",\n            \"format\": \"uri\"\n          },\n          \"rawSecret\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TotpRequest\": {\n        \"required\": [\n          \"code\",\n          \"password\",\n          \"secret\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"secret\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TwoFactorAuthSettings\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"available\": {\n            \"type\": \"boolean\",\n            \"description\": \"Check if 2FA is available.\"\n          },\n          \"emailVerified\": {\n            \"type\": \"boolean\"\n          },\n          \"enabled\": {\n            \"type\": \"boolean\"\n          },\n          \"totpConfigured\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"UcUploadFromUrlRequest\": {\n        \"required\": [\n          \"url\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Custom file name\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"format\": \"url\"\n          }\n        }\n      },\n      \"UpgradeFromUriRequest\": {\n        \"required\": [\n          \"uri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"uri\"\n          }\n        }\n      },\n      \"UpgradeRequest\": {\n        \"required\": [\n          \"file\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          }\n        }\n      },\n      \"UploadForm\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"description\": \"The file to upload. If not provided, the url will be used.\",\n            \"format\": \"binary\"\n          },\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"The filename to use when uploading from url. If not provided, the filename will be\\n extracted from the url.\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"The url to upload from. If not provided, the file will be used.\"\n          }\n        },\n        \"description\": \"Upload form from console. The file and url are mutually exclusive. If both are provided,\\n the file will be used.\"\n      },\n      \"UploadFromUrlRequest\": {\n        \"required\": [\n          \"policyName\",\n          \"url\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Custom file name\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the group to which the attachment belongs\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Storage policy name\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"format\": \"url\"\n          }\n        }\n      },\n      \"User\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/UserSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/UserStatus\"\n          }\n        },\n        \"description\": \"The extension represents user details of Halo.\"\n      },\n      \"UserConnection\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/UserConnectionSpec\"\n          }\n        },\n        \"description\": \"User connection extension.\"\n      },\n      \"UserConnectionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/UserConnection\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"UserConnectionSpec\": {\n        \"required\": [\n          \"providerUserId\",\n          \"registrationId\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"providerUserId\": {\n            \"type\": \"string\",\n            \"description\": \"The unique identifier for the user\\u0027s connection to the OAuth provider.\\n for example, the user\\u0027s GitHub id.\"\n          },\n          \"registrationId\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the OAuth provider (e.g. Google, Facebook, Twitter).\"\n          },\n          \"updatedAt\": {\n            \"type\": \"string\",\n            \"description\": \"The time when the user connection was last updated.\",\n            \"format\": \"date-time\"\n          },\n          \"username\": {\n            \"type\": \"string\",\n            \"description\": \"The {@link Metadata#getName Metadata#getName()} of the user associated with the OAuth connection.\"\n          }\n        }\n      },\n      \"UserDevice\": {\n        \"required\": [\n          \"active\",\n          \"currentDevice\",\n          \"device\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"active\": {\n            \"type\": \"boolean\"\n          },\n          \"currentDevice\": {\n            \"type\": \"boolean\"\n          },\n          \"device\": {\n            \"$ref\": \"#/components/schemas/Device\"\n          }\n        }\n      },\n      \"UserEndpoint.ListedUserList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedUser\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"UserList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/User\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"UserPermission\": {\n        \"required\": [\n          \"permissions\",\n          \"roles\",\n          \"uiPermissions\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"permissions\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"uiPermissions\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"UserSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"email\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"bio\": {\n            \"type\": \"string\"\n          },\n          \"disabled\": {\n            \"type\": \"boolean\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"emailVerified\": {\n            \"type\": \"boolean\"\n          },\n          \"loginHistoryLimit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          },\n          \"phone\": {\n            \"type\": \"string\"\n          },\n          \"registeredAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"totpEncryptedSecret\": {\n            \"type\": \"string\"\n          },\n          \"twoFactorAuthEnabled\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"UserStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"VerifyCodeRequest\": {\n        \"required\": [\n          \"code\",\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"VoteRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"plural\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"basicAuth\": {\n        \"scheme\": \"basic\",\n        \"type\": \"http\"\n      },\n      \"bearerAuth\": {\n        \"bearerFormat\": \"JWT\",\n        \"scheme\": \"bearer\",\n        \"type\": \"http\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "api-docs/openapi/v3_0/apis_console.api_v1alpha1.json",
    "content": "{\n  \"openapi\": \"3.0.1\",\n  \"info\": {\n    \"title\": \"Halo\",\n    \"version\": \"2.23.0-SNAPSHOT\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:8091\",\n      \"description\": \"Generated server url\"\n    }\n  ],\n  \"security\": [\n    {\n      \"basicAuth\": [],\n      \"bearerAuth\": []\n    }\n  ],\n  \"paths\": {\n    \"/apis/api.console.halo.run/v1alpha1/attachments\": {\n      \"get\": {\n        \"operationId\": \"SearchAttachments\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Filter attachments without group. This parameter will ignore group parameter.\",\n            \"in\": \"query\",\n            \"name\": \"ungrouped\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          },\n          {\n            \"description\": \"Keyword for searching.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Acceptable media types.\",\n            \"in\": \"query\",\n            \"name\": \"accepts\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AttachmentList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/attachments/-/upload-from-url\": {\n      \"post\": {\n        \"operationId\": \"ExternalTransferAttachment\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UploadFromUrlRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/attachments/upload\": {\n      \"post\": {\n        \"operationId\": \"UploadAttachment\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/IUploadRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/auth-providers\": {\n      \"get\": {\n        \"description\": \"Lists all auth providers\",\n        \"operationId\": \"listAuthProviders\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/ListedAuthProvider\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/auth-providers/{name}/disable\": {\n      \"put\": {\n        \"description\": \"Disables an auth provider\",\n        \"operationId\": \"disableAuthProvider\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/auth-providers/{name}/enable\": {\n      \"put\": {\n        \"description\": \"Enables an auth provider\",\n        \"operationId\": \"enableAuthProvider\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/comments\": {\n      \"get\": {\n        \"description\": \"List comments.\",\n        \"operationId\": \"ListComments\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Comments filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Commenter kind.\",\n            \"in\": \"query\",\n            \"name\": \"ownerKind\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Commenter name.\",\n            \"in\": \"query\",\n            \"name\": \"ownerName\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedCommentList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create a comment.\",\n        \"operationId\": \"CreateComment\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CommentRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/comments/{name}/reply\": {\n      \"post\": {\n        \"description\": \"Create a reply.\",\n        \"operationId\": \"CreateReply\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReplyRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/indices/-/rebuild\": {\n      \"post\": {\n        \"description\": \"Rebuild all indices\",\n        \"operationId\": \"RebuildAllIndices\",\n        \"responses\": {},\n        \"tags\": [\n          \"IndicesV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/notifiers/{name}/sender-config\": {\n      \"get\": {\n        \"description\": \"Fetch sender config of notifier\",\n        \"operationId\": \"FetchSenderConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Save sender config of notifier\",\n        \"operationId\": \"SaveSenderConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins\": {\n      \"get\": {\n        \"description\": \"List plugins using query criteria and sort params\",\n        \"operationId\": \"ListPlugins\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Keyword of plugin name or description\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Whether the plugin is enabled\",\n            \"in\": \"query\",\n            \"name\": \"enabled\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PluginList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css\": {\n      \"get\": {\n        \"description\": \"Merge all CSS bundles of enabled plugins into one.\",\n        \"operationId\": \"fetchCssBundle\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js\": {\n      \"get\": {\n        \"description\": \"Merge all JS bundles of enabled plugins into one.\",\n        \"operationId\": \"fetchJsBundle\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri\": {\n      \"post\": {\n        \"description\": \"Install a plugin from uri.\",\n        \"operationId\": \"InstallPluginFromUri\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/InstallFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/install\": {\n      \"post\": {\n        \"description\": \"Install a plugin by uploading a Jar file.\",\n        \"operationId\": \"InstallPlugin\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PluginInstallRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/json-config\": {\n      \"get\": {\n        \"description\": \"Fetch converted json config of plugin by configured configMapName.\",\n        \"operationId\": \"fetchPluginJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update the config of plugin setting.\",\n        \"operationId\": \"updatePluginJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"content\": {},\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/plugin-state\": {\n      \"put\": {\n        \"description\": \"Change the running state of a plugin by name.\",\n        \"operationId\": \"ChangePluginRunningState\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PluginRunningStateRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/reload\": {\n      \"put\": {\n        \"description\": \"Reload a plugin by name.\",\n        \"operationId\": \"reloadPlugin\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/reset-config\": {\n      \"put\": {\n        \"description\": \"Reset the configMap of plugin setting.\",\n        \"operationId\": \"ResetPluginConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/setting\": {\n      \"get\": {\n        \"description\": \"Fetch setting of plugin.\",\n        \"operationId\": \"fetchPluginSetting\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/upgrade\": {\n      \"post\": {\n        \"description\": \"Upgrade a plugin by uploading a Jar file\",\n        \"operationId\": \"UpgradePlugin\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PluginInstallRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {},\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/plugins/{name}/upgrade-from-uri\": {\n      \"post\": {\n        \"description\": \"Upgrade a plugin from uri.\",\n        \"operationId\": \"UpgradePluginFromUri\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UpgradeFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"List posts.\",\n        \"operationId\": \"ListPosts\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Posts filtered by publish phase.\",\n            \"in\": \"query\",\n            \"name\": \"publishPhase\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"DRAFT\",\n                \"PENDING_APPROVAL\",\n                \"PUBLISHED\",\n                \"FAILED\"\n              ]\n            }\n          },\n          {\n            \"description\": \"Posts filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Posts filtered by category including sub-categories.\",\n            \"in\": \"query\",\n            \"name\": \"categoryWithChildren\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Draft a post.\",\n        \"operationId\": \"DraftPost\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PostRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}\": {\n      \"put\": {\n        \"description\": \"Update a post.\",\n        \"operationId\": \"UpdateDraftPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PostRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/content\": {\n      \"delete\": {\n        \"description\": \"Delete a content for post.\",\n        \"operationId\": \"deletePostContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Fetch content of post.\",\n        \"operationId\": \"fetchPostContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update a post\\u0027s content.\",\n        \"operationId\": \"UpdatePostContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Content\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/head-content\": {\n      \"get\": {\n        \"description\": \"Fetch head content of post.\",\n        \"operationId\": \"fetchPostHeadContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/publish\": {\n      \"put\": {\n        \"description\": \"Publish a post.\",\n        \"operationId\": \"PublishPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Head snapshot name of content.\",\n            \"in\": \"query\",\n            \"name\": \"headSnapshot\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"async\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/recycle\": {\n      \"put\": {\n        \"description\": \"Recycle a post.\",\n        \"operationId\": \"RecyclePost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/release-content\": {\n      \"get\": {\n        \"description\": \"Fetch release content of post.\",\n        \"operationId\": \"fetchPostReleaseContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/revert-content\": {\n      \"put\": {\n        \"description\": \"Revert to specified snapshot for post content.\",\n        \"operationId\": \"revertToSpecifiedSnapshotForPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RevertSnapshotForPostParam\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/snapshot\": {\n      \"get\": {\n        \"description\": \"List all snapshots for post content.\",\n        \"operationId\": \"listPostSnapshots\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/ListedSnapshotDto\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish\": {\n      \"put\": {\n        \"description\": \"UnPublish a post.\",\n        \"operationId\": \"UnpublishPost\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/replies\": {\n      \"get\": {\n        \"description\": \"List replies.\",\n        \"operationId\": \"ListReplies\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Replies filtered by commentName.\",\n            \"in\": \"query\",\n            \"name\": \"commentName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedReplyList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages\": {\n      \"get\": {\n        \"description\": \"List single pages.\",\n        \"operationId\": \"ListSinglePages\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by contributor.\",\n            \"in\": \"query\",\n            \"name\": \"contributor\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by publish phase.\",\n            \"in\": \"query\",\n            \"name\": \"publishPhase\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"DRAFT\",\n                \"PENDING_APPROVAL\",\n                \"PUBLISHED\",\n                \"FAILED\"\n              ]\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by visibility.\",\n            \"in\": \"query\",\n            \"name\": \"visible\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"PUBLIC\",\n                \"INTERNAL\",\n                \"PRIVATE\"\n              ]\n            }\n          },\n          {\n            \"description\": \"SinglePages filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedSinglePageList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Draft a single page.\",\n        \"operationId\": \"DraftSinglePage\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePageRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}\": {\n      \"put\": {\n        \"description\": \"Update a single page.\",\n        \"operationId\": \"UpdateDraftSinglePage\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePageRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content\": {\n      \"delete\": {\n        \"description\": \"Delete a content for post.\",\n        \"operationId\": \"deleteSinglePageContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Fetch content of single page.\",\n        \"operationId\": \"fetchSinglePageContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"snapshotName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update a single page\\u0027s content.\",\n        \"operationId\": \"UpdateSinglePageContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Content\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/head-content\": {\n      \"get\": {\n        \"description\": \"Fetch head content of single page.\",\n        \"operationId\": \"fetchSinglePageHeadContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/publish\": {\n      \"put\": {\n        \"description\": \"Publish a single page.\",\n        \"operationId\": \"PublishSinglePage\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/release-content\": {\n      \"get\": {\n        \"description\": \"Fetch release content of single page.\",\n        \"operationId\": \"fetchSinglePageReleaseContent\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ContentWrapper\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/revert-content\": {\n      \"put\": {\n        \"description\": \"Revert to specified snapshot for single page content.\",\n        \"operationId\": \"revertToSpecifiedSnapshotForSinglePage\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RevertSnapshotForSingleParam\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/snapshot\": {\n      \"get\": {\n        \"description\": \"List all snapshots for single page content.\",\n        \"operationId\": \"listSinglePageSnapshots\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/ListedSnapshotDto\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/stats\": {\n      \"get\": {\n        \"description\": \"Get stats.\",\n        \"operationId\": \"getStats\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DashboardStats\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/tags\": {\n      \"get\": {\n        \"description\": \"List Post Tags.\",\n        \"operationId\": \"ListPostTags\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Post tags filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes\": {\n      \"get\": {\n        \"description\": \"List themes.\",\n        \"operationId\": \"ListThemes\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Whether to list uninstalled themes.\",\n            \"in\": \"query\",\n            \"name\": \"uninstalled\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ThemeList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/-/activation\": {\n      \"get\": {\n        \"description\": \"Fetch the activated theme.\",\n        \"operationId\": \"fetchActivatedTheme\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/-/install-from-uri\": {\n      \"post\": {\n        \"description\": \"Install a theme from uri.\",\n        \"operationId\": \"InstallThemeFromUri\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/InstallFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/install\": {\n      \"post\": {\n        \"description\": \"Install a theme by uploading a zip file.\",\n        \"operationId\": \"InstallTheme\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ThemeInstallRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/activation\": {\n      \"put\": {\n        \"description\": \"Activate a theme by name.\",\n        \"operationId\": \"activateTheme\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/invalidate-cache\": {\n      \"put\": {\n        \"description\": \"Invalidate theme template cache.\",\n        \"operationId\": \"InvalidateCache\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/json-config\": {\n      \"get\": {\n        \"description\": \"Fetch converted json config of theme by configured configMapName.\",\n        \"operationId\": \"fetchThemeJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update the configMap of theme setting.\",\n        \"operationId\": \"updateThemeJsonConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"content\": {},\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/reload\": {\n      \"put\": {\n        \"description\": \"Reload theme setting.\",\n        \"operationId\": \"Reload\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/reset-config\": {\n      \"put\": {\n        \"description\": \"Reset the configMap of theme setting.\",\n        \"operationId\": \"ResetThemeConfig\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/setting\": {\n      \"get\": {\n        \"description\": \"Fetch setting of theme.\",\n        \"operationId\": \"fetchThemeSetting\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade\": {\n      \"post\": {\n        \"description\": \"Upgrade theme\",\n        \"operationId\": \"UpgradeTheme\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UpgradeRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {},\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade-from-uri\": {\n      \"post\": {\n        \"description\": \"Upgrade a theme from uri.\",\n        \"operationId\": \"UpgradeThemeFromUri\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UpgradeFromUriRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users\": {\n      \"get\": {\n        \"description\": \"List users\",\n        \"operationId\": \"ListUsers\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Keyword to search\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Role name\",\n            \"in\": \"query\",\n            \"name\": \"role\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserEndpoint.ListedUserList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Creates a new user.\",\n        \"operationId\": \"CreateUser\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CreateUserRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-\": {\n      \"get\": {\n        \"description\": \"Get current user detail\",\n        \"operationId\": \"GetCurrentUserDetail\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DetailedUser\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update current user profile, but password.\",\n        \"operationId\": \"UpdateCurrentUser\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/User\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-/password\": {\n      \"put\": {\n        \"description\": \"Change own password of user.\",\n        \"operationId\": \"ChangeOwnPassword\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ChangeOwnPasswordRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-/send-email-verification-code\": {\n      \"post\": {\n        \"description\": \"Send email verification code for user\",\n        \"operationId\": \"SendEmailVerificationCode\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/EmailVerifyRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/-/verify-email\": {\n      \"post\": {\n        \"description\": \"Verify email for user by code.\",\n        \"operationId\": \"VerifyEmail\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VerifyCodeRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}\": {\n      \"get\": {\n        \"description\": \"Get user detail by name\",\n        \"operationId\": \"GetUserDetail\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DetailedUser\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}/avatar\": {\n      \"delete\": {\n        \"description\": \"delete user avatar\",\n        \"operationId\": \"DeleteUserAvatar\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"upload user avatar\",\n        \"operationId\": \"UploadUserAvatar\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/IAvatarUploadRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}/password\": {\n      \"put\": {\n        \"description\": \"Change anyone password of user for admin.\",\n        \"operationId\": \"ChangeAnyonePassword\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user. If the name is equal to \\u0027-\\u0027, it will change the password of current user.\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ChangePasswordRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/api.console.halo.run/v1alpha1/users/{name}/permissions\": {\n      \"get\": {\n        \"description\": \"Get permissions of user\",\n        \"operationId\": \"GetPermissions\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserPermission\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Grant permissions to user\",\n        \"operationId\": \"GrantPermission\",\n        \"parameters\": [\n          {\n            \"description\": \"User name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/GrantRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}\": {\n      \"get\": {\n        \"description\": \"Get system config by group\",\n        \"operationId\": \"getSystemConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of the system config\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              },\n              \"application/json\": {}\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemConfigV1alpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update system config by group\",\n        \"operationId\": \"updateSystemConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of the system config\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"204 NO_CONTENT\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemConfigV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.migration.halo.run/v1alpha1/backup-files\": {\n      \"get\": {\n        \"description\": \"Get backup files from backup root.\",\n        \"operationId\": \"getBackupFiles\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/BackupFile\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MigrationV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.migration.halo.run/v1alpha1/backups/{name}/files/{filename}\": {\n      \"get\": {\n        \"operationId\": \"DownloadBackups\",\n        \"parameters\": [\n          {\n            \"description\": \"Backup name.\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Backup filename.\",\n            \"in\": \"path\",\n            \"name\": \"filename\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"MigrationV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.migration.halo.run/v1alpha1/restorations\": {\n      \"post\": {\n        \"description\": \"Restore backup by uploading file or providing download link or backup name.\",\n        \"operationId\": \"RestoreBackup\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RestoreRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {},\n        \"tags\": [\n          \"MigrationV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.notification.halo.run/v1alpha1/notifiers/default-email-notifier/verify-connection\": {\n      \"post\": {\n        \"description\": \"Verify email sender config.\",\n        \"operationId\": \"VerifyEmailSenderConfig\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/EmailConfigValidationRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.security.halo.run/v1alpha1/users/{username}/disable\": {\n      \"post\": {\n        \"description\": \"Disable user by username\",\n        \"operationId\": \"DisableUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"The user has been disabled.\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.security.halo.run/v1alpha1/users/{username}/enable\": {\n      \"post\": {\n        \"description\": \"Enable user by username\",\n        \"operationId\": \"EnableUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"The user has been enabled.\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.storage.halo.run/v1alpha1/attachments/-/upload\": {\n      \"post\": {\n        \"description\": \"Upload attachment endpoint for console.\",\n        \"operationId\": \"uploadAttachmentForConsole\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UploadForm\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Console\"\n        ]\n      }\n    },\n    \"/apis/console.api.storage.halo.run/v1alpha1/policies/{name}/configs/{group}\": {\n      \"get\": {\n        \"description\": \"Get policy config by group\",\n        \"operationId\": \"getPolicyConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of the policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Name of the group\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PolicyAlpha1Console\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update policy config by group\",\n        \"operationId\": \"updatePolicyConfigByGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of the policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Name of the group\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No Content\"\n          }\n        },\n        \"tags\": [\n          \"PolicyAlpha1Console\"\n        ]\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"AddOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"add\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Attachment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AttachmentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/AttachmentStatus\"\n          }\n        }\n      },\n      \"AttachmentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Attachment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AttachmentSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of attachment\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"Group name\"\n          },\n          \"mediaType\": {\n            \"type\": \"string\",\n            \"description\": \"Media type of attachment\"\n          },\n          \"ownerName\": {\n            \"type\": \"string\",\n            \"description\": \"Name of User who uploads the attachment\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Policy name\"\n          },\n          \"size\": {\n            \"minimum\": 0,\n            \"type\": \"integer\",\n            \"description\": \"Size of attachment. Unit is Byte\",\n            \"format\": \"int64\"\n          },\n          \"tags\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Tags of attachment\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Tag name\"\n            }\n          }\n        }\n      },\n      \"AttachmentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\",\n            \"description\": \"Permalink of attachment.\\nIf it is in local storage, the public URL will be set.\\nIf it is in s3 storage, the Object URL will be set.\\n\"\n          },\n          \"thumbnails\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"AuthProvider\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AuthProviderSpec\"\n          }\n        },\n        \"description\": \"Auth provider extension.\"\n      },\n      \"AuthProviderSpec\": {\n        \"required\": [\n          \"authType\",\n          \"authenticationUrl\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"authType\": {\n            \"type\": \"string\",\n            \"description\": \"Auth type: form or oauth2.\",\n            \"enum\": [\n              \"FORM\",\n              \"OAUTH2\"\n            ]\n          },\n          \"authenticationUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Authentication url of the auth provider\"\n          },\n          \"bindingUrl\": {\n            \"type\": \"string\"\n          },\n          \"configMapRef\": {\n            \"$ref\": \"#/components/schemas/ConfigMapRef\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of the auth provider\"\n          },\n          \"helpPage\": {\n            \"type\": \"string\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"method\": {\n            \"type\": \"string\"\n          },\n          \"rememberMeSupport\": {\n            \"type\": \"boolean\"\n          },\n          \"settingRef\": {\n            \"$ref\": \"#/components/schemas/SettingRef\"\n          },\n          \"unbindUrl\": {\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Author\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"BackupFile\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Filename of backup file.\"\n          },\n          \"lastModifiedTime\": {\n            \"type\": \"string\",\n            \"description\": \"Last modified time of backup file.\",\n            \"format\": \"date-time\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of backup file.\",\n            \"format\": \"int64\"\n          }\n        },\n        \"description\": \"Backup file.\"\n      },\n      \"Category\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CategorySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CategoryStatus\"\n          }\n        }\n      },\n      \"CategorySpec\": {\n        \"required\": [\n          \"displayName\",\n          \"priority\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eWhether to hide the category from the category list.\\u003c/p\\u003e\\n \\u003cp\\u003eWhen set to true, the category including its subcategories and related posts will\\n not be displayed in the category list, but it can still be accessed by permalink.\\u003c/p\\u003e\\n \\u003cp\\u003eLimitation: It only takes effect on the theme-side categorized list and it only\\n allows to be set to true on the first level(root node) of categories.\\u003c/p\\u003e\"\n          },\n          \"postTemplate\": {\n            \"maxLength\": 255,\n            \"type\": \"string\",\n            \"description\": \"\\u003cp\\u003eUsed to specify the template for the posts associated with the category.\\u003c/p\\u003e\\n \\u003cp\\u003eThe priority is not as high as that of the post.\\u003c/p\\u003e\\n \\u003cp\\u003eIf the post also specifies a template, the post\\u0027s template will prevail.\\u003c/p\\u003e\"\n          },\n          \"preventParentPostCascadeQuery\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eif a category is queried for related posts, the default behavior is to\\n query all posts under the category including its subcategories, but if this field is\\n set to true, cascade query behavior will be terminated here.\\u003c/p\\u003e\\n \\u003cp\\u003eFor example, if a category has subcategories A and B, and A has subcategories C and\\n D and C marked this field as true, when querying posts under A category,all posts under A\\n and B will be queried, but C and D will not be queried.\\u003c/p\\u003e\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"maxLength\": 255,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CategoryStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的已发布且公开的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"ChangeOwnPasswordRequest\": {\n        \"required\": [\n          \"oldPassword\",\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"oldPassword\": {\n            \"type\": \"string\",\n            \"description\": \"Old password.\"\n          },\n          \"password\": {\n            \"minLength\": 5,\n            \"type\": \"string\",\n            \"description\": \"New password.\"\n          }\n        }\n      },\n      \"ChangePasswordRequest\": {\n        \"required\": [\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"password\": {\n            \"minLength\": 5,\n            \"type\": \"string\",\n            \"description\": \"New password.\"\n          }\n        }\n      },\n      \"Comment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        }\n      },\n      \"CommentEmailOwner\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\",\n            \"description\": \"avatar for comment owner\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"display name for comment owner\"\n          },\n          \"email\": {\n            \"type\": \"string\",\n            \"description\": \"email for comment owner\"\n          },\n          \"website\": {\n            \"type\": \"string\",\n            \"description\": \"website for comment owner\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eThe creator info of the comment.\\u003c/p\\u003e\\n This {@link CommentEmailOwner CommentEmailOwner} is only applicable to the user who is allowed to comment\\n without logging in.\"\n      },\n      \"CommentOwner\": {\n        \"required\": [\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"maxLength\": 64,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentRequest\": {\n        \"required\": [\n          \"content\",\n          \"raw\",\n          \"subjectRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentEmailOwner\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        },\n        \"description\": \"Request parameter object for {@link Comment Comment}.\"\n      },\n      \"CommentSpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"subjectRef\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"lastReadTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentStats\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"comment stats value object.\"\n      },\n      \"CommentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"hasNewReply\": {\n            \"type\": \"boolean\"\n          },\n          \"lastReplyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"replyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"unreadReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visibleReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"Condition\": {\n        \"required\": [\n          \"lastTransitionTime\",\n          \"status\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"lastTransitionTime\": {\n            \"type\": \"string\",\n            \"description\": \"Last time the condition transitioned from one status to another.\",\n            \"format\": \"date-time\"\n          },\n          \"message\": {\n            \"maxLength\": 32768,\n            \"type\": \"string\",\n            \"description\": \"Human-readable message indicating details about last transition.\\n This may be an empty string.\"\n          },\n          \"reason\": {\n            \"maxLength\": 1024,\n            \"pattern\": \"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$\",\n            \"type\": \"string\",\n            \"description\": \"Unique, one-word, CamelCase reason for the condition\\u0027s last transition.\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"description\": \"Status is the status of the condition. Can be True, False, Unknown.\",\n            \"enum\": [\n              \"TRUE\",\n              \"FALSE\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"type\": {\n            \"maxLength\": 316,\n            \"pattern\": \"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$\",\n            \"type\": \"string\",\n            \"description\": \"type of condition in CamelCase or in foo.example.com/CamelCase.\\n example: Ready, Initialized.\\n maxLength: 316.\"\n          }\n        },\n        \"description\": \"EqualsAndHashCode 排除了lastTransitionTime否则失败时，lastTransitionTime 会被更新\\n 导致 equals 为 false，一直被加入队列.\"\n      },\n      \"ConfigMap\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"data\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eConfigMap holds configuration data to consume.\\u003c/p\\u003e\"\n      },\n      \"ConfigMapRef\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Content\": {\n        \"required\": [\n          \"content\",\n          \"raw\",\n          \"rawType\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ContentUpdateParam\": {\n        \"required\": [\n          \"content\",\n          \"raw\",\n          \"rawType\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ContentWrapper\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"type\": \"string\"\n          },\n          \"snapshotName\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Contributor\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Contributor from user.\"\n      },\n      \"CopyOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"copy\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"CreateUserRequest\": {\n        \"required\": [\n          \"email\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"bio\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          },\n          \"phone\": {\n            \"type\": \"string\"\n          },\n          \"roles\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"CustomTemplates\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"category\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          },\n          \"page\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          },\n          \"post\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          }\n        }\n      },\n      \"DashboardStats\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"approvedComments\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"comments\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"posts\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"upvotes\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"users\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"visits\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"DetailedUser\": {\n        \"required\": [\n          \"roles\",\n          \"user\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"user\": {\n            \"$ref\": \"#/components/schemas/User\"\n          }\n        }\n      },\n      \"EmailConfigValidationRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Gets email display name.\"\n          },\n          \"enable\": {\n            \"type\": \"boolean\"\n          },\n          \"encryption\": {\n            \"type\": \"string\"\n          },\n          \"host\": {\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          },\n          \"port\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"sender\": {\n            \"type\": \"string\",\n            \"description\": \"Gets email sender address.\"\n          },\n          \"username\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"EmailVerifyRequest\": {\n        \"required\": [\n          \"email\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"email\": {\n            \"type\": \"string\",\n            \"format\": \"email\"\n          }\n        }\n      },\n      \"Excerpt\": {\n        \"required\": [\n          \"autoGenerate\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"autoGenerate\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Extension\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          }\n        },\n        \"description\": \"Extension is an interface which represents an Extension. It contains setters and getters of\\n GroupVersionKind and Metadata.\"\n      },\n      \"GrantRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"roles\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"IAvatarUploadRequest\": {\n        \"required\": [\n          \"file\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          }\n        }\n      },\n      \"IUploadRequest\": {\n        \"required\": [\n          \"file\",\n          \"policyName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the group to which the attachment belongs\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Storage policy name\"\n          }\n        }\n      },\n      \"InstallFromUriRequest\": {\n        \"required\": [\n          \"uri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"uri\"\n          }\n        }\n      },\n      \"JsonPatch\": {\n        \"minItems\": 1,\n        \"uniqueItems\": true,\n        \"type\": \"array\",\n        \"description\": \"JSON schema for JSONPatch operations\",\n        \"items\": {\n          \"oneOf\": [\n            {\n              \"$ref\": \"#/components/schemas/AddOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/ReplaceOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/TestOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/RemoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/MoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/CopyOperation\"\n            }\n          ]\n        }\n      },\n      \"License\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"url\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Common data objects for license.\"\n      },\n      \"ListedAuthProvider\": {\n        \"required\": [\n          \"displayName\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"authType\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"FORM\",\n              \"OAUTH2\"\n            ]\n          },\n          \"authenticationUrl\": {\n            \"type\": \"string\"\n          },\n          \"bindingUrl\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"enabled\": {\n            \"type\": \"boolean\"\n          },\n          \"helpPage\": {\n            \"type\": \"string\"\n          },\n          \"isBound\": {\n            \"type\": \"boolean\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"privileged\": {\n            \"type\": \"boolean\"\n          },\n          \"supportsBinding\": {\n            \"type\": \"boolean\"\n          },\n          \"unbindingUrl\": {\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A listed value object for {@link run.halo.app.core.extension.AuthProvider run.halo.app.core.extension.AuthProvider}.\"\n      },\n      \"ListedComment\": {\n        \"required\": [\n          \"comment\",\n          \"owner\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"comment\": {\n            \"$ref\": \"#/components/schemas/Comment\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStats\"\n          },\n          \"subject\": {\n            \"$ref\": \"#/components/schemas/Extension\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedCommentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedComment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedPost\": {\n        \"required\": [\n          \"categories\",\n          \"contributors\",\n          \"owner\",\n          \"post\",\n          \"stats\",\n          \"tags\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Category\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Contributor\"\n            }\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/Contributor\"\n          },\n          \"post\": {\n            \"$ref\": \"#/components/schemas/Post\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/Stats\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Tag\"\n            }\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedPostList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedPost\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedReply\": {\n        \"required\": [\n          \"owner\",\n          \"reply\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"reply\": {\n            \"$ref\": \"#/components/schemas/Reply\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStats\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedReplyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedReply\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedSinglePage\": {\n        \"required\": [\n          \"contributors\",\n          \"owner\",\n          \"page\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Contributor\"\n            }\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/Contributor\"\n          },\n          \"page\": {\n            \"$ref\": \"#/components/schemas/SinglePage\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/Stats\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedSinglePageList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedSinglePage\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedSnapshotDto\": {\n        \"required\": [\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ListedSnapshotSpec\"\n          }\n        }\n      },\n      \"ListedSnapshotSpec\": {\n        \"required\": [\n          \"owner\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"modifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ListedUser\": {\n        \"required\": [\n          \"roles\",\n          \"user\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"user\": {\n            \"$ref\": \"#/components/schemas/User\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"Metadata\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Annotations are like key-value format.\"\n          },\n          \"creationTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Creation timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"deletionTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Deletion timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"finalizers\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"string\",\n              \"nullable\": true\n            }\n          },\n          \"generateName\": {\n            \"type\": \"string\",\n            \"description\": \"The name field will be generated automatically according to the given generateName field\"\n          },\n          \"labels\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Labels are like key-value format.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Metadata name\"\n          },\n          \"version\": {\n            \"type\": \"integer\",\n            \"description\": \"Current version of the Extension. It will be bumped up every update.\",\n            \"format\": \"int64\",\n            \"nullable\": true\n          }\n        },\n        \"description\": \"Metadata of Extension.\"\n      },\n      \"MoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"move\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"OwnerInfo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Comment owner info.\"\n      },\n      \"Plugin\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PluginSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PluginStatus\"\n          }\n        },\n        \"description\": \"A custom resource for Plugin.\"\n      },\n      \"PluginAuthor\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PluginInstallRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"presetName\": {\n            \"type\": \"string\",\n            \"description\": \"Plugin preset name. We will find the plugin from plugin presets\"\n          },\n          \"source\": {\n            \"type\": \"string\",\n            \"description\": \"Install source. Default is file.\",\n            \"enum\": [\n              \"FILE\",\n              \"PRESET\",\n              \"URL\"\n            ]\n          }\n        }\n      },\n      \"PluginList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Plugin\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PluginRunningStateRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"async\": {\n            \"type\": \"boolean\"\n          },\n          \"enable\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"PluginSpec\": {\n        \"required\": [\n          \"version\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"author\": {\n            \"$ref\": \"#/components/schemas/PluginAuthor\"\n          },\n          \"configMapName\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"enabled\": {\n            \"type\": \"boolean\"\n          },\n          \"homepage\": {\n            \"type\": \"string\"\n          },\n          \"issues\": {\n            \"type\": \"string\"\n          },\n          \"license\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/License\"\n            }\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"pluginDependencies\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"repo\": {\n            \"type\": \"string\"\n          },\n          \"requires\": {\n            \"type\": \"string\",\n            \"description\": \"SemVer format.\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"pattern\": \"^(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)(?:-((?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+([0-9a-zA-Z-]+(?:\\\\.[0-9a-zA-Z-]+)*))?$\",\n            \"type\": \"string\",\n            \"description\": \"plugin version.\"\n          }\n        }\n      },\n      \"PluginStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"entry\": {\n            \"type\": \"string\"\n          },\n          \"lastProbeState\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"CREATED\",\n              \"DISABLED\",\n              \"RESOLVED\",\n              \"STARTED\",\n              \"STOPPED\",\n              \"FAILED\",\n              \"UNLOADED\"\n            ]\n          },\n          \"lastStartTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"loadLocation\": {\n            \"type\": \"string\",\n            \"description\": \"Load location of the plugin, often a path.\",\n            \"format\": \"uri\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PENDING\",\n              \"STARTING\",\n              \"CREATED\",\n              \"DISABLING\",\n              \"DISABLED\",\n              \"RESOLVED\",\n              \"STARTED\",\n              \"STOPPED\",\n              \"FAILED\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"stylesheet\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PolicyRule\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiGroups\": {\n            \"type\": \"array\",\n            \"description\": \"APIGroups is the name of the APIGroup that contains the resources.\\n If multiple API groups are specified, any action requested against one of the enumerated\\n resources in any API group will be allowed.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"nonResourceURLs\": {\n            \"type\": \"array\",\n            \"description\": \"NonResourceURLs is a set of partial urls that a user should have access to.\\n *s are allowed, but only as the full, final step in the path\\n If an action is not a resource API request, then the URL is split on \\u0027/\\u0027 and is checked\\n against the NonResourceURLs to look for a match.\\n Since non-resource URLs are not namespaced, this field is only applicable for\\n ClusterRoles referenced from a ClusterRoleBinding.\\n Rules can either apply to API resources (such as \\\"pods\\\" or \\\"secrets\\\") or non-resource\\n URL paths (such as \\\"/api\\\"),  but not both.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"resourceNames\": {\n            \"type\": \"array\",\n            \"description\": \"ResourceNames is an optional white list of names that the rule applies to.  An empty set\\n means that everything is allowed.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"resources\": {\n            \"type\": \"array\",\n            \"description\": \"Resources is a list of resources this rule applies to.  \\u0027*\\u0027 represents all resources in\\n the specified apiGroups.\\n \\u0027*\\u0026#47;foo\\u0027 represents the subresource \\u0027foo\\u0027 for all resources in the specified\\n apiGroups.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"verbs\": {\n            \"type\": \"array\",\n            \"description\": \"about who the rule applies to or which namespace the rule applies to.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        \"description\": \"PolicyRule holds information that describes a policy rule, but does not contain information\\n about whom the rule applies to or which namespace the rule applies to.\"\n      },\n      \"Post\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003ePost extension.\\u003c/p\\u003e\"\n      },\n      \"PostRequest\": {\n        \"required\": [\n          \"content\",\n          \"post\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentUpdateParam\"\n          },\n          \"post\": {\n            \"$ref\": \"#/components/schemas/Post\"\n          }\n        },\n        \"description\": \"Post and content data for creating and updating post.\"\n      },\n      \"PostSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"文章引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"PostStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Ref\": {\n        \"required\": [\n          \"group\",\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"Extension group\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Extension kind\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Extension name. This field is mandatory\"\n          },\n          \"version\": {\n            \"type\": \"string\",\n            \"description\": \"Extension version\"\n          }\n        },\n        \"description\": \"Extension reference object. The name is mandatory\"\n      },\n      \"RemoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"remove\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"ReplaceOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"replace\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Reply\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReplySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/ReplyStatus\"\n          }\n        }\n      },\n      \"ReplyRequest\": {\n        \"required\": [\n          \"content\",\n          \"raw\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentEmailOwner\"\n          },\n          \"quoteReply\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A request parameter object for {@link Reply Reply}.\"\n      },\n      \"ReplySpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"commentName\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"commentName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"quoteReply\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReplyStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RestoreRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"backupName\": {\n            \"type\": \"string\",\n            \"description\": \"Backup metadata name.\"\n          },\n          \"downloadUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Remote backup HTTP URL.\"\n          },\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Filename of backup file in backups root.\"\n          }\n        }\n      },\n      \"RevertSnapshotForPostParam\": {\n        \"required\": [\n          \"snapshotName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"snapshotName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"RevertSnapshotForSingleParam\": {\n        \"required\": [\n          \"snapshotName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"snapshotName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Role\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"rules\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"rules\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/PolicyRule\"\n            }\n          }\n        }\n      },\n      \"Setting\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SettingSpec\"\n          }\n        },\n        \"description\": \"{@link Setting Setting} is a custom extension to generate forms based on configuration.\"\n      },\n      \"SettingForm\": {\n        \"minLength\": 1,\n        \"required\": [\n          \"formSchema\",\n          \"group\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"formSchema\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\"\n            }\n          },\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"label\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SettingRef\": {\n        \"required\": [\n          \"group\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SettingSpec\": {\n        \"required\": [\n          \"forms\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"forms\": {\n            \"minLength\": 1,\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/SettingForm\"\n            }\n          }\n        }\n      },\n      \"SinglePage\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SinglePageSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/SinglePageStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eSingle page extension.\\u003c/p\\u003e\"\n      },\n      \"SinglePageRequest\": {\n        \"required\": [\n          \"content\",\n          \"page\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentUpdateParam\"\n          },\n          \"page\": {\n            \"$ref\": \"#/components/schemas/SinglePage\"\n          }\n        },\n        \"description\": \"A request parameter for {@link SinglePage SinglePage}.\"\n      },\n      \"SinglePageSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"SinglePageStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Stats\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"approvedComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"totalComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"Stats value object.\"\n      },\n      \"Tag\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/TagSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/TagStatus\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"TagList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Tag\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"TagSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"color\": {\n            \"pattern\": \"^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$\",\n            \"type\": \"string\",\n            \"description\": \"Color regex explanation.\\n \\u003cpre\\u003e\\n ^                 # start of the line\\n #                 # start with a number sign `#`\\n (                 # start of (group 1)\\n   [a-fA-F0-9]{6}  # support z-f, A-F and 0-9, with a length of 6\\n   |               # or\\n   [a-fA-F0-9]{3}  # support z-f, A-F and 0-9, with a length of 3\\n )                 # end of (group 1)\\n $                 # end of the line\\n \\u003c/pre\\u003e\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TagStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"TemplateDescriptor\": {\n        \"required\": [\n          \"file\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"file\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"screenshot\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Type used to describe custom template page.\"\n      },\n      \"TestOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"test\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Theme\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ThemeSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/ThemeStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eTheme extension.\\u003c/p\\u003e\"\n      },\n      \"ThemeInstallRequest\": {\n        \"type\": \"object\"\n      },\n      \"ThemeList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Theme\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ThemeSpec\": {\n        \"required\": [\n          \"author\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"author\": {\n            \"$ref\": \"#/components/schemas/Author\"\n          },\n          \"configMapName\": {\n            \"type\": \"string\"\n          },\n          \"customTemplates\": {\n            \"$ref\": \"#/components/schemas/CustomTemplates\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"homepage\": {\n            \"type\": \"string\"\n          },\n          \"issues\": {\n            \"type\": \"string\"\n          },\n          \"license\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/License\"\n            }\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"repo\": {\n            \"type\": \"string\"\n          },\n          \"requires\": {\n            \"type\": \"string\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ThemeStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"location\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"READY\",\n              \"FAILED\",\n              \"UNKNOWN\"\n            ]\n          }\n        }\n      },\n      \"UpgradeFromUriRequest\": {\n        \"required\": [\n          \"uri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"uri\": {\n            \"type\": \"string\",\n            \"format\": \"uri\"\n          }\n        }\n      },\n      \"UpgradeRequest\": {\n        \"required\": [\n          \"file\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          }\n        }\n      },\n      \"UploadForm\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"description\": \"The file to upload. If not provided, the url will be used.\",\n            \"format\": \"binary\"\n          },\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"The filename to use when uploading from url. If not provided, the filename will be\\n extracted from the url.\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"The url to upload from. If not provided, the file will be used.\"\n          }\n        },\n        \"description\": \"Upload form from console. The file and url are mutually exclusive. If both are provided,\\n the file will be used.\"\n      },\n      \"UploadFromUrlRequest\": {\n        \"required\": [\n          \"policyName\",\n          \"url\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Custom file name\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the group to which the attachment belongs\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Storage policy name\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"format\": \"url\"\n          }\n        }\n      },\n      \"User\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/UserSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/UserStatus\"\n          }\n        },\n        \"description\": \"The extension represents user details of Halo.\"\n      },\n      \"UserEndpoint.ListedUserList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedUser\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"UserPermission\": {\n        \"required\": [\n          \"permissions\",\n          \"roles\",\n          \"uiPermissions\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"permissions\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"uiPermissions\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"UserSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"email\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"bio\": {\n            \"type\": \"string\"\n          },\n          \"disabled\": {\n            \"type\": \"boolean\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"emailVerified\": {\n            \"type\": \"boolean\"\n          },\n          \"loginHistoryLimit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          },\n          \"phone\": {\n            \"type\": \"string\"\n          },\n          \"registeredAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"totpEncryptedSecret\": {\n            \"type\": \"string\"\n          },\n          \"twoFactorAuthEnabled\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"UserStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"VerifyCodeRequest\": {\n        \"required\": [\n          \"code\",\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"basicAuth\": {\n        \"scheme\": \"basic\",\n        \"type\": \"http\"\n      },\n      \"bearerAuth\": {\n        \"bearerFormat\": \"JWT\",\n        \"scheme\": \"bearer\",\n        \"type\": \"http\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "api-docs/openapi/v3_0/apis_extension.api_v1alpha1.json",
    "content": "{\n  \"openapi\": \"3.0.1\",\n  \"info\": {\n    \"title\": \"Halo\",\n    \"version\": \"2.23.0-SNAPSHOT\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:8091\",\n      \"description\": \"Generated server url\"\n    }\n  ],\n  \"security\": [\n    {\n      \"basicAuth\": [],\n      \"bearerAuth\": []\n    }\n  ],\n  \"paths\": {\n    \"/api/v1alpha1/annotationsettings\": {\n      \"get\": {\n        \"description\": \"List AnnotationSetting\",\n        \"operationId\": \"listAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSettingList\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsettings\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create AnnotationSetting\",\n        \"operationId\": \"createAnnotationSetting\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AnnotationSetting\"\n              }\n            }\n          },\n          \"description\": \"Fresh annotationsetting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsettings created just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/annotationsettings/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete AnnotationSetting\",\n        \"operationId\": \"deleteAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response annotationsetting deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get AnnotationSetting\",\n        \"operationId\": \"getAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response single annotationsetting\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch AnnotationSetting\",\n        \"operationId\": \"patchAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsetting patched just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update AnnotationSetting\",\n        \"operationId\": \"updateAnnotationSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of annotationsetting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AnnotationSetting\"\n              }\n            }\n          },\n          \"description\": \"Updated annotationsetting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                }\n              }\n            },\n            \"description\": \"Response annotationsettings updated just now\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/configmaps\": {\n      \"get\": {\n        \"description\": \"List ConfigMap\",\n        \"operationId\": \"listConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMapList\"\n                }\n              }\n            },\n            \"description\": \"Response configmaps\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ConfigMap\",\n        \"operationId\": \"createConfigMap\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ConfigMap\"\n              }\n            }\n          },\n          \"description\": \"Fresh configmap\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response configmaps created just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/configmaps/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ConfigMap\",\n        \"operationId\": \"deleteConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response configmap deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ConfigMap\",\n        \"operationId\": \"getConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response single configmap\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ConfigMap\",\n        \"operationId\": \"patchConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response configmap patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ConfigMap\",\n        \"operationId\": \"updateConfigMap\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of configmap\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ConfigMap\"\n              }\n            }\n          },\n          \"description\": \"Updated configmap\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ConfigMap\"\n                }\n              }\n            },\n            \"description\": \"Response configmaps updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ConfigMapV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menuitems\": {\n      \"get\": {\n        \"description\": \"List MenuItem\",\n        \"operationId\": \"listMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItemList\"\n                }\n              }\n            },\n            \"description\": \"Response menuitems\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create MenuItem\",\n        \"operationId\": \"createMenuItem\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/MenuItem\"\n              }\n            }\n          },\n          \"description\": \"Fresh menuitem\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response menuitems created just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menuitems/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete MenuItem\",\n        \"operationId\": \"deleteMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response menuitem deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get MenuItem\",\n        \"operationId\": \"getMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response single menuitem\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch MenuItem\",\n        \"operationId\": \"patchMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response menuitem patched just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update MenuItem\",\n        \"operationId\": \"updateMenuItem\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menuitem\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/MenuItem\"\n              }\n            }\n          },\n          \"description\": \"Updated menuitem\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuItem\"\n                }\n              }\n            },\n            \"description\": \"Response menuitems updated just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuItemV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menus\": {\n      \"get\": {\n        \"description\": \"List Menu\",\n        \"operationId\": \"listMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuList\"\n                }\n              }\n            },\n            \"description\": \"Response menus\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Menu\",\n        \"operationId\": \"createMenu\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Menu\"\n              }\n            }\n          },\n          \"description\": \"Fresh menu\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response menus created just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/menus/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Menu\",\n        \"operationId\": \"deleteMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response menu deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Menu\",\n        \"operationId\": \"getMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response single menu\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Menu\",\n        \"operationId\": \"patchMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response menu patched just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Menu\",\n        \"operationId\": \"updateMenu\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of menu\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Menu\"\n              }\n            }\n          },\n          \"description\": \"Updated menu\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Menu\"\n                }\n              }\n            },\n            \"description\": \"Response menus updated just now\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/rolebindings\": {\n      \"get\": {\n        \"description\": \"List RoleBinding\",\n        \"operationId\": \"listRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBindingList\"\n                }\n              }\n            },\n            \"description\": \"Response rolebindings\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create RoleBinding\",\n        \"operationId\": \"createRoleBinding\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RoleBinding\"\n              }\n            }\n          },\n          \"description\": \"Fresh rolebinding\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response rolebindings created just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/rolebindings/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete RoleBinding\",\n        \"operationId\": \"deleteRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response rolebinding deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get RoleBinding\",\n        \"operationId\": \"getRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response single rolebinding\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch RoleBinding\",\n        \"operationId\": \"patchRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response rolebinding patched just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update RoleBinding\",\n        \"operationId\": \"updateRoleBinding\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of rolebinding\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RoleBinding\"\n              }\n            }\n          },\n          \"description\": \"Updated rolebinding\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleBinding\"\n                }\n              }\n            },\n            \"description\": \"Response rolebindings updated just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleBindingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/roles\": {\n      \"get\": {\n        \"description\": \"List Role\",\n        \"operationId\": \"listRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RoleList\"\n                }\n              }\n            },\n            \"description\": \"Response roles\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Role\",\n        \"operationId\": \"createRole\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Role\"\n              }\n            }\n          },\n          \"description\": \"Fresh role\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response roles created just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/roles/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Role\",\n        \"operationId\": \"deleteRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response role deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Role\",\n        \"operationId\": \"getRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response single role\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Role\",\n        \"operationId\": \"patchRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response role patched just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Role\",\n        \"operationId\": \"updateRole\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of role\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Role\"\n              }\n            }\n          },\n          \"description\": \"Updated role\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Role\"\n                }\n              }\n            },\n            \"description\": \"Response roles updated just now\"\n          }\n        },\n        \"tags\": [\n          \"RoleV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/secrets\": {\n      \"get\": {\n        \"description\": \"List Secret\",\n        \"operationId\": \"listSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SecretList\"\n                }\n              }\n            },\n            \"description\": \"Response secrets\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Secret\",\n        \"operationId\": \"createSecret\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Secret\"\n              }\n            }\n          },\n          \"description\": \"Fresh secret\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response secrets created just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/secrets/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Secret\",\n        \"operationId\": \"deleteSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response secret deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Secret\",\n        \"operationId\": \"getSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response single secret\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Secret\",\n        \"operationId\": \"patchSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response secret patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Secret\",\n        \"operationId\": \"updateSecret\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of secret\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Secret\"\n              }\n            }\n          },\n          \"description\": \"Updated secret\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Secret\"\n                }\n              }\n            },\n            \"description\": \"Response secrets updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SecretV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/settings\": {\n      \"get\": {\n        \"description\": \"List Setting\",\n        \"operationId\": \"listSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SettingList\"\n                }\n              }\n            },\n            \"description\": \"Response settings\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Setting\",\n        \"operationId\": \"createSetting\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Setting\"\n              }\n            }\n          },\n          \"description\": \"Fresh setting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response settings created just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/settings/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Setting\",\n        \"operationId\": \"deleteSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response setting deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Setting\",\n        \"operationId\": \"getSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response single setting\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Setting\",\n        \"operationId\": \"patchSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response setting patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Setting\",\n        \"operationId\": \"updateSetting\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of setting\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Setting\"\n              }\n            }\n          },\n          \"description\": \"Updated setting\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Setting\"\n                }\n              }\n            },\n            \"description\": \"Response settings updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SettingV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/users\": {\n      \"get\": {\n        \"description\": \"List User\",\n        \"operationId\": \"listUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserList\"\n                }\n              }\n            },\n            \"description\": \"Response users\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create User\",\n        \"operationId\": \"createUser\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/User\"\n              }\n            }\n          },\n          \"description\": \"Fresh user\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response users created just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      }\n    },\n    \"/api/v1alpha1/users/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete User\",\n        \"operationId\": \"deleteUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response user deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get User\",\n        \"operationId\": \"getUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response single user\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch User\",\n        \"operationId\": \"patchUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response user patched just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update User\",\n        \"operationId\": \"updateUser\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of user\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/User\"\n              }\n            }\n          },\n          \"description\": \"Updated user\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/User\"\n                }\n              }\n            },\n            \"description\": \"Response users updated just now\"\n          }\n        },\n        \"tags\": [\n          \"UserV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/authproviders\": {\n      \"get\": {\n        \"description\": \"List AuthProvider\",\n        \"operationId\": \"listAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProviderList\"\n                }\n              }\n            },\n            \"description\": \"Response authproviders\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create AuthProvider\",\n        \"operationId\": \"createAuthProvider\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AuthProvider\"\n              }\n            }\n          },\n          \"description\": \"Fresh authprovider\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response authproviders created just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/authproviders/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete AuthProvider\",\n        \"operationId\": \"deleteAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response authprovider deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get AuthProvider\",\n        \"operationId\": \"getAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response single authprovider\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch AuthProvider\",\n        \"operationId\": \"patchAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response authprovider patched just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update AuthProvider\",\n        \"operationId\": \"updateAuthProvider\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of authprovider\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/AuthProvider\"\n              }\n            }\n          },\n          \"description\": \"Updated authprovider\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AuthProvider\"\n                }\n              }\n            },\n            \"description\": \"Response authproviders updated just now\"\n          }\n        },\n        \"tags\": [\n          \"AuthProviderV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/userconnections\": {\n      \"get\": {\n        \"description\": \"List UserConnection\",\n        \"operationId\": \"listUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnectionList\"\n                }\n              }\n            },\n            \"description\": \"Response userconnections\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create UserConnection\",\n        \"operationId\": \"createUserConnection\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UserConnection\"\n              }\n            }\n          },\n          \"description\": \"Fresh userconnection\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response userconnections created just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/auth.halo.run/v1alpha1/userconnections/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete UserConnection\",\n        \"operationId\": \"deleteUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response userconnection deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get UserConnection\",\n        \"operationId\": \"getUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response single userconnection\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch UserConnection\",\n        \"operationId\": \"patchUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response userconnection patched just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update UserConnection\",\n        \"operationId\": \"updateUserConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of userconnection\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UserConnection\"\n              }\n            }\n          },\n          \"description\": \"Updated userconnection\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UserConnection\"\n                }\n              }\n            },\n            \"description\": \"Response userconnections updated just now\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/categories\": {\n      \"get\": {\n        \"description\": \"List Category\",\n        \"operationId\": \"listCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CategoryList\"\n                }\n              }\n            },\n            \"description\": \"Response categories\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Category\",\n        \"operationId\": \"createCategory\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Category\"\n              }\n            }\n          },\n          \"description\": \"Fresh category\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response categories created just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/categories/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Category\",\n        \"operationId\": \"deleteCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response category deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Category\",\n        \"operationId\": \"getCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response single category\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Category\",\n        \"operationId\": \"patchCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response category patched just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Category\",\n        \"operationId\": \"updateCategory\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of category\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Category\"\n              }\n            }\n          },\n          \"description\": \"Updated category\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Category\"\n                }\n              }\n            },\n            \"description\": \"Response categories updated just now\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/comments\": {\n      \"get\": {\n        \"description\": \"List Comment\",\n        \"operationId\": \"listComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CommentList\"\n                }\n              }\n            },\n            \"description\": \"Response comments\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Comment\",\n        \"operationId\": \"createComment\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Comment\"\n              }\n            }\n          },\n          \"description\": \"Fresh comment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response comments created just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/comments/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Comment\",\n        \"operationId\": \"deleteComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response comment deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Comment\",\n        \"operationId\": \"getComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response single comment\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Comment\",\n        \"operationId\": \"patchComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response comment patched just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Comment\",\n        \"operationId\": \"updateComment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of comment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Comment\"\n              }\n            }\n          },\n          \"description\": \"Updated comment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"Response comments updated just now\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"List Post\",\n        \"operationId\": \"listPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PostList\"\n                }\n              }\n            },\n            \"description\": \"Response posts\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Post\",\n        \"operationId\": \"createPost\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          },\n          \"description\": \"Fresh post\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response posts created just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/posts/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Post\",\n        \"operationId\": \"deletePost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response post deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Post\",\n        \"operationId\": \"getPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response single post\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Post\",\n        \"operationId\": \"patchPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response post patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Post\",\n        \"operationId\": \"updatePost\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of post\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          },\n          \"description\": \"Updated post\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"Response posts updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/replies\": {\n      \"get\": {\n        \"description\": \"List Reply\",\n        \"operationId\": \"listReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReplyList\"\n                }\n              }\n            },\n            \"description\": \"Response replies\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Reply\",\n        \"operationId\": \"createReply\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reply\"\n              }\n            }\n          },\n          \"description\": \"Fresh reply\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response replies created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/replies/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Reply\",\n        \"operationId\": \"deleteReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reply deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Reply\",\n        \"operationId\": \"getReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response single reply\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Reply\",\n        \"operationId\": \"patchReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response reply patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Reply\",\n        \"operationId\": \"updateReply\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reply\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reply\"\n              }\n            }\n          },\n          \"description\": \"Updated reply\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"Response replies updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReplyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/singlepages\": {\n      \"get\": {\n        \"description\": \"List SinglePage\",\n        \"operationId\": \"listSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePageList\"\n                }\n              }\n            },\n            \"description\": \"Response singlepages\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create SinglePage\",\n        \"operationId\": \"createSinglePage\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePage\"\n              }\n            }\n          },\n          \"description\": \"Fresh singlepage\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response singlepages created just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/singlepages/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete SinglePage\",\n        \"operationId\": \"deleteSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response singlepage deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get SinglePage\",\n        \"operationId\": \"getSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response single singlepage\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch SinglePage\",\n        \"operationId\": \"patchSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response singlepage patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update SinglePage\",\n        \"operationId\": \"updateSinglePage\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of singlepage\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SinglePage\"\n              }\n            }\n          },\n          \"description\": \"Updated singlepage\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePage\"\n                }\n              }\n            },\n            \"description\": \"Response singlepages updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/snapshots\": {\n      \"get\": {\n        \"description\": \"List Snapshot\",\n        \"operationId\": \"listSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SnapshotList\"\n                }\n              }\n            },\n            \"description\": \"Response snapshots\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Snapshot\",\n        \"operationId\": \"createSnapshot\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Snapshot\"\n              }\n            }\n          },\n          \"description\": \"Fresh snapshot\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response snapshots created just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/snapshots/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Snapshot\",\n        \"operationId\": \"deleteSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response snapshot deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Snapshot\",\n        \"operationId\": \"getSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response single snapshot\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Snapshot\",\n        \"operationId\": \"patchSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response snapshot patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Snapshot\",\n        \"operationId\": \"updateSnapshot\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of snapshot\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Snapshot\"\n              }\n            }\n          },\n          \"description\": \"Updated snapshot\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"Response snapshots updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/tags\": {\n      \"get\": {\n        \"description\": \"List Tag\",\n        \"operationId\": \"listTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagList\"\n                }\n              }\n            },\n            \"description\": \"Response tags\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Tag\",\n        \"operationId\": \"createTag\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Tag\"\n              }\n            }\n          },\n          \"description\": \"Fresh tag\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response tags created just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/content.halo.run/v1alpha1/tags/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Tag\",\n        \"operationId\": \"deleteTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response tag deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Tag\",\n        \"operationId\": \"getTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response single tag\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Tag\",\n        \"operationId\": \"patchTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response tag patched just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Tag\",\n        \"operationId\": \"updateTag\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of tag\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Tag\"\n              }\n            }\n          },\n          \"description\": \"Updated tag\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            },\n            \"description\": \"Response tags updated just now\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/metrics.halo.run/v1alpha1/counters\": {\n      \"get\": {\n        \"description\": \"List Counter\",\n        \"operationId\": \"listCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CounterList\"\n                }\n              }\n            },\n            \"description\": \"Response counters\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Counter\",\n        \"operationId\": \"createCounter\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Counter\"\n              }\n            }\n          },\n          \"description\": \"Fresh counter\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response counters created just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/metrics.halo.run/v1alpha1/counters/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Counter\",\n        \"operationId\": \"deleteCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response counter deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Counter\",\n        \"operationId\": \"getCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response single counter\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Counter\",\n        \"operationId\": \"patchCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response counter patched just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Counter\",\n        \"operationId\": \"updateCounter\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of counter\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Counter\"\n              }\n            }\n          },\n          \"description\": \"Updated counter\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Counter\"\n                }\n              }\n            },\n            \"description\": \"Response counters updated just now\"\n          }\n        },\n        \"tags\": [\n          \"CounterV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/migration.halo.run/v1alpha1/backups\": {\n      \"get\": {\n        \"description\": \"List Backup\",\n        \"operationId\": \"listBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/BackupList\"\n                }\n              }\n            },\n            \"description\": \"Response backups\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Backup\",\n        \"operationId\": \"createBackup\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Backup\"\n              }\n            }\n          },\n          \"description\": \"Fresh backup\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response backups created just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/migration.halo.run/v1alpha1/backups/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Backup\",\n        \"operationId\": \"deleteBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response backup deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Backup\",\n        \"operationId\": \"getBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response single backup\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Backup\",\n        \"operationId\": \"patchBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response backup patched just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Backup\",\n        \"operationId\": \"updateBackup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of backup\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Backup\"\n              }\n            }\n          },\n          \"description\": \"Updated backup\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Backup\"\n                }\n              }\n            },\n            \"description\": \"Response backups updated just now\"\n          }\n        },\n        \"tags\": [\n          \"BackupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifications\": {\n      \"get\": {\n        \"description\": \"List Notification\",\n        \"operationId\": \"listNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationList\"\n                }\n              }\n            },\n            \"description\": \"Response notifications\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Notification\",\n        \"operationId\": \"createNotification\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Notification\"\n              }\n            }\n          },\n          \"description\": \"Fresh notification\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response notifications created just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifications/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Notification\",\n        \"operationId\": \"deleteNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response notification deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Notification\",\n        \"operationId\": \"getNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response single notification\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Notification\",\n        \"operationId\": \"patchNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response notification patched just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Notification\",\n        \"operationId\": \"updateNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notification\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Notification\"\n              }\n            }\n          },\n          \"description\": \"Updated notification\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"Response notifications updated just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notificationtemplates\": {\n      \"get\": {\n        \"description\": \"List NotificationTemplate\",\n        \"operationId\": \"listNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplateList\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplates\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create NotificationTemplate\",\n        \"operationId\": \"createNotificationTemplate\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotificationTemplate\"\n              }\n            }\n          },\n          \"description\": \"Fresh notificationtemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplates created just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notificationtemplates/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete NotificationTemplate\",\n        \"operationId\": \"deleteNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response notificationtemplate deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get NotificationTemplate\",\n        \"operationId\": \"getNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response single notificationtemplate\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch NotificationTemplate\",\n        \"operationId\": \"patchNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplate patched just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update NotificationTemplate\",\n        \"operationId\": \"updateNotificationTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notificationtemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotificationTemplate\"\n              }\n            }\n          },\n          \"description\": \"Updated notificationtemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response notificationtemplates updated just now\"\n          }\n        },\n        \"tags\": [\n          \"NotificationTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifierDescriptors\": {\n      \"get\": {\n        \"description\": \"List NotifierDescriptor\",\n        \"operationId\": \"listNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptorList\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptors\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create NotifierDescriptor\",\n        \"operationId\": \"createNotifierDescriptor\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n              }\n            }\n          },\n          \"description\": \"Fresh notifierDescriptor\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptors created just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/notifierDescriptors/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete NotifierDescriptor\",\n        \"operationId\": \"deleteNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response notifierDescriptor deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get NotifierDescriptor\",\n        \"operationId\": \"getNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response single notifierDescriptor\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch NotifierDescriptor\",\n        \"operationId\": \"patchNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptor patched just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update NotifierDescriptor\",\n        \"operationId\": \"updateNotifierDescriptor\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of notifierDescriptor\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n              }\n            }\n          },\n          \"description\": \"Updated notifierDescriptor\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n                }\n              }\n            },\n            \"description\": \"Response notifierDescriptors updated just now\"\n          }\n        },\n        \"tags\": [\n          \"NotifierDescriptorV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasons\": {\n      \"get\": {\n        \"description\": \"List Reason\",\n        \"operationId\": \"listReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonList\"\n                }\n              }\n            },\n            \"description\": \"Response reasons\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Reason\",\n        \"operationId\": \"createReason\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reason\"\n              }\n            }\n          },\n          \"description\": \"Fresh reason\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response reasons created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasons/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Reason\",\n        \"operationId\": \"deleteReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reason deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Reason\",\n        \"operationId\": \"getReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response single reason\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Reason\",\n        \"operationId\": \"patchReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response reason patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Reason\",\n        \"operationId\": \"updateReason\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reason\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Reason\"\n              }\n            }\n          },\n          \"description\": \"Updated reason\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reason\"\n                }\n              }\n            },\n            \"description\": \"Response reasons updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasontypes\": {\n      \"get\": {\n        \"description\": \"List ReasonType\",\n        \"operationId\": \"listReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonTypeList\"\n                }\n              }\n            },\n            \"description\": \"Response reasontypes\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ReasonType\",\n        \"operationId\": \"createReasonType\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReasonType\"\n              }\n            }\n          },\n          \"description\": \"Fresh reasontype\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response reasontypes created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/reasontypes/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ReasonType\",\n        \"operationId\": \"deleteReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reasontype deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ReasonType\",\n        \"operationId\": \"getReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response single reasontype\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ReasonType\",\n        \"operationId\": \"patchReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response reasontype patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ReasonType\",\n        \"operationId\": \"updateReasonType\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reasontype\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReasonType\"\n              }\n            }\n          },\n          \"description\": \"Updated reasontype\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonType\"\n                }\n              }\n            },\n            \"description\": \"Response reasontypes updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReasonTypeV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/subscriptions\": {\n      \"get\": {\n        \"description\": \"List Subscription\",\n        \"operationId\": \"listSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SubscriptionList\"\n                }\n              }\n            },\n            \"description\": \"Response subscriptions\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Subscription\",\n        \"operationId\": \"createSubscription\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Subscription\"\n              }\n            }\n          },\n          \"description\": \"Fresh subscription\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response subscriptions created just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/notification.halo.run/v1alpha1/subscriptions/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Subscription\",\n        \"operationId\": \"deleteSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response subscription deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Subscription\",\n        \"operationId\": \"getSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response single subscription\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Subscription\",\n        \"operationId\": \"patchSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response subscription patched just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Subscription\",\n        \"operationId\": \"updateSubscription\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of subscription\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Subscription\"\n              }\n            }\n          },\n          \"description\": \"Updated subscription\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Subscription\"\n                }\n              }\n            },\n            \"description\": \"Response subscriptions updated just now\"\n          }\n        },\n        \"tags\": [\n          \"SubscriptionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensiondefinitions\": {\n      \"get\": {\n        \"description\": \"List ExtensionDefinition\",\n        \"operationId\": \"listExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinitionList\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinitions\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ExtensionDefinition\",\n        \"operationId\": \"createExtensionDefinition\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n              }\n            }\n          },\n          \"description\": \"Fresh extensiondefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinitions created just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensiondefinitions/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ExtensionDefinition\",\n        \"operationId\": \"deleteExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response extensiondefinition deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ExtensionDefinition\",\n        \"operationId\": \"getExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response single extensiondefinition\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ExtensionDefinition\",\n        \"operationId\": \"patchExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinition patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ExtensionDefinition\",\n        \"operationId\": \"updateExtensionDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensiondefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n              }\n            }\n          },\n          \"description\": \"Updated extensiondefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensiondefinitions updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions\": {\n      \"get\": {\n        \"description\": \"List ExtensionPointDefinition\",\n        \"operationId\": \"listExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinitionList\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinitions\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ExtensionPointDefinition\",\n        \"operationId\": \"createExtensionPointDefinition\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n              }\n            }\n          },\n          \"description\": \"Fresh extensionpointdefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinitions created just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ExtensionPointDefinition\",\n        \"operationId\": \"deleteExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response extensionpointdefinition deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ExtensionPointDefinition\",\n        \"operationId\": \"getExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response single extensionpointdefinition\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ExtensionPointDefinition\",\n        \"operationId\": \"patchExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinition patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ExtensionPointDefinition\",\n        \"operationId\": \"updateExtensionPointDefinition\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of extensionpointdefinition\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n              }\n            }\n          },\n          \"description\": \"Updated extensionpointdefinition\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n                }\n              }\n            },\n            \"description\": \"Response extensionpointdefinitions updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ExtensionPointDefinitionV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/plugins\": {\n      \"get\": {\n        \"description\": \"List Plugin\",\n        \"operationId\": \"listPlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PluginList\"\n                }\n              }\n            },\n            \"description\": \"Response plugins\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Plugin\",\n        \"operationId\": \"createPlugin\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Plugin\"\n              }\n            }\n          },\n          \"description\": \"Fresh plugin\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response plugins created just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/plugins/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Plugin\",\n        \"operationId\": \"deletePlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response plugin deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Plugin\",\n        \"operationId\": \"getPlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response single plugin\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Plugin\",\n        \"operationId\": \"patchPlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response plugin patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Plugin\",\n        \"operationId\": \"updatePlugin\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of plugin\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Plugin\"\n              }\n            }\n          },\n          \"description\": \"Updated plugin\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plugin\"\n                }\n              }\n            },\n            \"description\": \"Response plugins updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/reverseproxies\": {\n      \"get\": {\n        \"description\": \"List ReverseProxy\",\n        \"operationId\": \"listReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxyList\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxies\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create ReverseProxy\",\n        \"operationId\": \"createReverseProxy\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReverseProxy\"\n              }\n            }\n          },\n          \"description\": \"Fresh reverseproxy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxies created just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/plugin.halo.run/v1alpha1/reverseproxies/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete ReverseProxy\",\n        \"operationId\": \"deleteReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response reverseproxy deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get ReverseProxy\",\n        \"operationId\": \"getReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response single reverseproxy\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch ReverseProxy\",\n        \"operationId\": \"patchReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxy patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update ReverseProxy\",\n        \"operationId\": \"updateReverseProxy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of reverseproxy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReverseProxy\"\n              }\n            }\n          },\n          \"description\": \"Updated reverseproxy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReverseProxy\"\n                }\n              }\n            },\n            \"description\": \"Response reverseproxies updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ReverseProxyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/devices\": {\n      \"get\": {\n        \"description\": \"List Device\",\n        \"operationId\": \"listDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/DeviceList\"\n                }\n              }\n            },\n            \"description\": \"Response devices\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Device\",\n        \"operationId\": \"createDevice\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Device\"\n              }\n            }\n          },\n          \"description\": \"Fresh device\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response devices created just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/devices/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Device\",\n        \"operationId\": \"deleteDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response device deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Device\",\n        \"operationId\": \"getDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response single device\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Device\",\n        \"operationId\": \"patchDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response device patched just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Device\",\n        \"operationId\": \"updateDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of device\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Device\"\n              }\n            }\n          },\n          \"description\": \"Updated device\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Device\"\n                }\n              }\n            },\n            \"description\": \"Response devices updated just now\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/personalaccesstokens\": {\n      \"get\": {\n        \"description\": \"List PersonalAccessToken\",\n        \"operationId\": \"listPersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessTokenList\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstokens\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create PersonalAccessToken\",\n        \"operationId\": \"createPersonalAccessToken\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n              }\n            }\n          },\n          \"description\": \"Fresh personalaccesstoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstokens created just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/personalaccesstokens/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete PersonalAccessToken\",\n        \"operationId\": \"deletePersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response personalaccesstoken deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get PersonalAccessToken\",\n        \"operationId\": \"getPersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response single personalaccesstoken\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch PersonalAccessToken\",\n        \"operationId\": \"patchPersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstoken patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update PersonalAccessToken\",\n        \"operationId\": \"updatePersonalAccessToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of personalaccesstoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n              }\n            }\n          },\n          \"description\": \"Updated personalaccesstoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"Response personalaccesstokens updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/remembermetokens\": {\n      \"get\": {\n        \"description\": \"List RememberMeToken\",\n        \"operationId\": \"listRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeTokenList\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetokens\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create RememberMeToken\",\n        \"operationId\": \"createRememberMeToken\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RememberMeToken\"\n              }\n            }\n          },\n          \"description\": \"Fresh remembermetoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetokens created just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/security.halo.run/v1alpha1/remembermetokens/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete RememberMeToken\",\n        \"operationId\": \"deleteRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response remembermetoken deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get RememberMeToken\",\n        \"operationId\": \"getRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response single remembermetoken\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch RememberMeToken\",\n        \"operationId\": \"patchRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetoken patched just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update RememberMeToken\",\n        \"operationId\": \"updateRememberMeToken\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of remembermetoken\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RememberMeToken\"\n              }\n            }\n          },\n          \"description\": \"Updated remembermetoken\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/RememberMeToken\"\n                }\n              }\n            },\n            \"description\": \"Response remembermetokens updated just now\"\n          }\n        },\n        \"tags\": [\n          \"RememberMeTokenV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/attachments\": {\n      \"get\": {\n        \"description\": \"List Attachment\",\n        \"operationId\": \"listAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AttachmentList\"\n                }\n              }\n            },\n            \"description\": \"Response attachments\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Attachment\",\n        \"operationId\": \"createAttachment\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Attachment\"\n              }\n            }\n          },\n          \"description\": \"Fresh attachment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response attachments created just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/attachments/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Attachment\",\n        \"operationId\": \"deleteAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response attachment deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Attachment\",\n        \"operationId\": \"getAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response single attachment\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Attachment\",\n        \"operationId\": \"patchAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response attachment patched just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Attachment\",\n        \"operationId\": \"updateAttachment\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of attachment\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Attachment\"\n              }\n            }\n          },\n          \"description\": \"Updated attachment\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"Response attachments updated just now\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/groups\": {\n      \"get\": {\n        \"description\": \"List Group\",\n        \"operationId\": \"listGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/GroupList\"\n                }\n              }\n            },\n            \"description\": \"Response groups\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Group\",\n        \"operationId\": \"createGroup\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Group\"\n              }\n            }\n          },\n          \"description\": \"Fresh group\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response groups created just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/groups/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Group\",\n        \"operationId\": \"deleteGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response group deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Group\",\n        \"operationId\": \"getGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response single group\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Group\",\n        \"operationId\": \"patchGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response group patched just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Group\",\n        \"operationId\": \"updateGroup\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of group\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Group\"\n              }\n            }\n          },\n          \"description\": \"Updated group\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Group\"\n                }\n              }\n            },\n            \"description\": \"Response groups updated just now\"\n          }\n        },\n        \"tags\": [\n          \"GroupV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/localthumbnails\": {\n      \"get\": {\n        \"description\": \"List LocalThumbnail\",\n        \"operationId\": \"listLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnailList\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnails\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create LocalThumbnail\",\n        \"operationId\": \"createLocalThumbnail\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/LocalThumbnail\"\n              }\n            }\n          },\n          \"description\": \"Fresh localthumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnails created just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/localthumbnails/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete LocalThumbnail\",\n        \"operationId\": \"deleteLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response localthumbnail deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get LocalThumbnail\",\n        \"operationId\": \"getLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response single localthumbnail\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch LocalThumbnail\",\n        \"operationId\": \"patchLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnail patched just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update LocalThumbnail\",\n        \"operationId\": \"updateLocalThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of localthumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/LocalThumbnail\"\n              }\n            }\n          },\n          \"description\": \"Updated localthumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/LocalThumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response localthumbnails updated just now\"\n          }\n        },\n        \"tags\": [\n          \"LocalThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policies\": {\n      \"get\": {\n        \"description\": \"List Policy\",\n        \"operationId\": \"listPolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyList\"\n                }\n              }\n            },\n            \"description\": \"Response policies\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Policy\",\n        \"operationId\": \"createPolicy\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Policy\"\n              }\n            }\n          },\n          \"description\": \"Fresh policy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response policies created just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policies/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Policy\",\n        \"operationId\": \"deletePolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response policy deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Policy\",\n        \"operationId\": \"getPolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response single policy\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Policy\",\n        \"operationId\": \"patchPolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response policy patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Policy\",\n        \"operationId\": \"updatePolicy\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policy\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Policy\"\n              }\n            }\n          },\n          \"description\": \"Updated policy\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Policy\"\n                }\n              }\n            },\n            \"description\": \"Response policies updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policytemplates\": {\n      \"get\": {\n        \"description\": \"List PolicyTemplate\",\n        \"operationId\": \"listPolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplateList\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplates\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create PolicyTemplate\",\n        \"operationId\": \"createPolicyTemplate\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PolicyTemplate\"\n              }\n            }\n          },\n          \"description\": \"Fresh policytemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplates created just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/policytemplates/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete PolicyTemplate\",\n        \"operationId\": \"deletePolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response policytemplate deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get PolicyTemplate\",\n        \"operationId\": \"getPolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response single policytemplate\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch PolicyTemplate\",\n        \"operationId\": \"patchPolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplate patched just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update PolicyTemplate\",\n        \"operationId\": \"updatePolicyTemplate\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of policytemplate\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PolicyTemplate\"\n              }\n            }\n          },\n          \"description\": \"Updated policytemplate\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PolicyTemplate\"\n                }\n              }\n            },\n            \"description\": \"Response policytemplates updated just now\"\n          }\n        },\n        \"tags\": [\n          \"PolicyTemplateV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/thumbnails\": {\n      \"get\": {\n        \"description\": \"List Thumbnail\",\n        \"operationId\": \"listThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ThumbnailList\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnails\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Thumbnail\",\n        \"operationId\": \"createThumbnail\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Thumbnail\"\n              }\n            }\n          },\n          \"description\": \"Fresh thumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnails created just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/storage.halo.run/v1alpha1/thumbnails/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Thumbnail\",\n        \"operationId\": \"deleteThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response thumbnail deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Thumbnail\",\n        \"operationId\": \"getThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response single thumbnail\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Thumbnail\",\n        \"operationId\": \"patchThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnail patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Thumbnail\",\n        \"operationId\": \"updateThumbnail\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of thumbnail\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Thumbnail\"\n              }\n            }\n          },\n          \"description\": \"Updated thumbnail\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Thumbnail\"\n                }\n              }\n            },\n            \"description\": \"Response thumbnails updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/theme.halo.run/v1alpha1/themes\": {\n      \"get\": {\n        \"description\": \"List Theme\",\n        \"operationId\": \"listTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ThemeList\"\n                }\n              }\n            },\n            \"description\": \"Response themes\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create Theme\",\n        \"operationId\": \"createTheme\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Theme\"\n              }\n            }\n          },\n          \"description\": \"Fresh theme\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response themes created just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      }\n    },\n    \"/apis/theme.halo.run/v1alpha1/themes/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete Theme\",\n        \"operationId\": \"deleteTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Response theme deleted just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Get Theme\",\n        \"operationId\": \"getTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response single theme\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"patch\": {\n        \"description\": \"Patch Theme\",\n        \"operationId\": \"patchTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json-patch+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/JsonPatch\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response theme patched just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update Theme\",\n        \"operationId\": \"updateTheme\",\n        \"parameters\": [\n          {\n            \"description\": \"Name of theme\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Theme\"\n              }\n            }\n          },\n          \"description\": \"Updated theme\"\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Theme\"\n                }\n              }\n            },\n            \"description\": \"Response themes updated just now\"\n          }\n        },\n        \"tags\": [\n          \"ThemeV1alpha1\"\n        ]\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"AddOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"add\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"AnnotationSetting\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AnnotationSettingSpec\"\n          }\n        }\n      },\n      \"AnnotationSettingList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/AnnotationSetting\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AnnotationSettingSpec\": {\n        \"required\": [\n          \"formSchema\",\n          \"targetRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"formSchema\": {\n            \"minLength\": 1,\n            \"type\": \"array\",\n            \"items\": {\n              \"minLength\": 1,\n              \"type\": \"object\"\n            }\n          },\n          \"targetRef\": {\n            \"$ref\": \"#/components/schemas/GroupKind\"\n          }\n        }\n      },\n      \"Attachment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AttachmentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/AttachmentStatus\"\n          }\n        }\n      },\n      \"AttachmentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Attachment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AttachmentSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of attachment\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"Group name\"\n          },\n          \"mediaType\": {\n            \"type\": \"string\",\n            \"description\": \"Media type of attachment\"\n          },\n          \"ownerName\": {\n            \"type\": \"string\",\n            \"description\": \"Name of User who uploads the attachment\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Policy name\"\n          },\n          \"size\": {\n            \"minimum\": 0,\n            \"type\": \"integer\",\n            \"description\": \"Size of attachment. Unit is Byte\",\n            \"format\": \"int64\"\n          },\n          \"tags\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Tags of attachment\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Tag name\"\n            }\n          }\n        }\n      },\n      \"AttachmentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\",\n            \"description\": \"Permalink of attachment.\\nIf it is in local storage, the public URL will be set.\\nIf it is in s3 storage, the Object URL will be set.\\n\"\n          },\n          \"thumbnails\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"AuthProvider\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AuthProviderSpec\"\n          }\n        },\n        \"description\": \"Auth provider extension.\"\n      },\n      \"AuthProviderList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/AuthProvider\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AuthProviderSpec\": {\n        \"required\": [\n          \"authType\",\n          \"authenticationUrl\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"authType\": {\n            \"type\": \"string\",\n            \"description\": \"Auth type: form or oauth2.\",\n            \"enum\": [\n              \"FORM\",\n              \"OAUTH2\"\n            ]\n          },\n          \"authenticationUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Authentication url of the auth provider\"\n          },\n          \"bindingUrl\": {\n            \"type\": \"string\"\n          },\n          \"configMapRef\": {\n            \"$ref\": \"#/components/schemas/ConfigMapRef\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of the auth provider\"\n          },\n          \"helpPage\": {\n            \"type\": \"string\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"method\": {\n            \"type\": \"string\"\n          },\n          \"rememberMeSupport\": {\n            \"type\": \"boolean\"\n          },\n          \"settingRef\": {\n            \"$ref\": \"#/components/schemas/SettingRef\"\n          },\n          \"unbindUrl\": {\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Author\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Backup\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/BackupSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/BackupStatus\"\n          }\n        }\n      },\n      \"BackupList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Backup\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"BackupSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"expiresAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"format\": {\n            \"type\": \"string\",\n            \"description\": \"Backup file format. Currently, only zip format is supported.\"\n          }\n        }\n      },\n      \"BackupStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"completionTimestamp\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"failureMessage\": {\n            \"type\": \"string\"\n          },\n          \"failureReason\": {\n            \"type\": \"string\"\n          },\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Name of backup file.\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PENDING\",\n              \"RUNNING\",\n              \"SUCCEEDED\",\n              \"FAILED\"\n            ]\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of backup file. Data unit: byte\",\n            \"format\": \"int64\"\n          },\n          \"startTimestamp\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          }\n        }\n      },\n      \"Category\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CategorySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CategoryStatus\"\n          }\n        }\n      },\n      \"CategoryList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Category\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CategorySpec\": {\n        \"required\": [\n          \"displayName\",\n          \"priority\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eWhether to hide the category from the category list.\\u003c/p\\u003e\\n \\u003cp\\u003eWhen set to true, the category including its subcategories and related posts will\\n not be displayed in the category list, but it can still be accessed by permalink.\\u003c/p\\u003e\\n \\u003cp\\u003eLimitation: It only takes effect on the theme-side categorized list and it only\\n allows to be set to true on the first level(root node) of categories.\\u003c/p\\u003e\"\n          },\n          \"postTemplate\": {\n            \"maxLength\": 255,\n            \"type\": \"string\",\n            \"description\": \"\\u003cp\\u003eUsed to specify the template for the posts associated with the category.\\u003c/p\\u003e\\n \\u003cp\\u003eThe priority is not as high as that of the post.\\u003c/p\\u003e\\n \\u003cp\\u003eIf the post also specifies a template, the post\\u0027s template will prevail.\\u003c/p\\u003e\"\n          },\n          \"preventParentPostCascadeQuery\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eif a category is queried for related posts, the default behavior is to\\n query all posts under the category including its subcategories, but if this field is\\n set to true, cascade query behavior will be terminated here.\\u003c/p\\u003e\\n \\u003cp\\u003eFor example, if a category has subcategories A and B, and A has subcategories C and\\n D and C marked this field as true, when querying posts under A category,all posts under A\\n and B will be queried, but C and D will not be queried.\\u003c/p\\u003e\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"maxLength\": 255,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CategoryStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的已发布且公开的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"Comment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        }\n      },\n      \"CommentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Comment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CommentOwner\": {\n        \"required\": [\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"maxLength\": 64,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentSpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"subjectRef\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"lastReadTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"hasNewReply\": {\n            \"type\": \"boolean\"\n          },\n          \"lastReplyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"replyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"unreadReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visibleReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"Condition\": {\n        \"required\": [\n          \"lastTransitionTime\",\n          \"status\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"lastTransitionTime\": {\n            \"type\": \"string\",\n            \"description\": \"Last time the condition transitioned from one status to another.\",\n            \"format\": \"date-time\"\n          },\n          \"message\": {\n            \"maxLength\": 32768,\n            \"type\": \"string\",\n            \"description\": \"Human-readable message indicating details about last transition.\\n This may be an empty string.\"\n          },\n          \"reason\": {\n            \"maxLength\": 1024,\n            \"pattern\": \"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$\",\n            \"type\": \"string\",\n            \"description\": \"Unique, one-word, CamelCase reason for the condition\\u0027s last transition.\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"description\": \"Status is the status of the condition. Can be True, False, Unknown.\",\n            \"enum\": [\n              \"TRUE\",\n              \"FALSE\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"type\": {\n            \"maxLength\": 316,\n            \"pattern\": \"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$\",\n            \"type\": \"string\",\n            \"description\": \"type of condition in CamelCase or in foo.example.com/CamelCase.\\n example: Ready, Initialized.\\n maxLength: 316.\"\n          }\n        },\n        \"description\": \"EqualsAndHashCode 排除了lastTransitionTime否则失败时，lastTransitionTime 会被更新\\n 导致 equals 为 false，一直被加入队列.\"\n      },\n      \"ConfigMap\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"data\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eConfigMap holds configuration data to consume.\\u003c/p\\u003e\"\n      },\n      \"ConfigMapList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ConfigMap\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ConfigMapRef\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CopyOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"copy\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"Counter\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"approvedComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"downvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"totalComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"A counter for number of requests by extension resource name.\"\n      },\n      \"CounterList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Counter\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CustomTemplates\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"category\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          },\n          \"page\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          },\n          \"post\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TemplateDescriptor\"\n            }\n          }\n        }\n      },\n      \"Device\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/DeviceSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/DeviceStatus\"\n          }\n        }\n      },\n      \"DeviceList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Device\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"DeviceSpec\": {\n        \"required\": [\n          \"ipAddress\",\n          \"principalName\",\n          \"sessionId\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"ipAddress\": {\n            \"maxLength\": 129,\n            \"type\": \"string\"\n          },\n          \"lastAccessedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"lastAuthenticatedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"principalName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"rememberMeSeriesId\": {\n            \"type\": \"string\"\n          },\n          \"sessionId\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"userAgent\": {\n            \"maxLength\": 500,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"DeviceStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"browser\": {\n            \"type\": \"string\"\n          },\n          \"os\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Excerpt\": {\n        \"required\": [\n          \"autoGenerate\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"autoGenerate\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ExtensionDefinition\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ExtensionSpec\"\n          }\n        },\n        \"description\": \"Extension definition.\\n An {@link ExtensionDefinition ExtensionDefinition} is a type of metadata that provides additional information about\\n an extension. An extension is a way to add new functionality to an existing class, structure,\\n enumeration, or protocol type without needing to subclass it.\"\n      },\n      \"ExtensionDefinitionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ExtensionDefinition\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ExtensionPointDefinition\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ExtensionPointSpec\"\n          }\n        },\n        \"description\": \"Extension point definition.\\n An {@link ExtensionPointDefinition ExtensionPointDefinition} is a concept used in \\u003ccode\\u003eHalo\\u003c/code\\u003e to allow for the\\n dynamic extension of system. It defines a location within \\u003ccode\\u003eHalo\\u003c/code\\u003e where\\n additional functionality can be added through the use of plugins or extensions.\"\n      },\n      \"ExtensionPointDefinitionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ExtensionPointDefinition\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ExtensionPointSpec\": {\n        \"required\": [\n          \"className\",\n          \"displayName\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"className\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"icon\": {\n            \"type\": \"string\"\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"SINGLETON\",\n              \"MULTI_INSTANCE\"\n            ]\n          }\n        }\n      },\n      \"ExtensionSpec\": {\n        \"required\": [\n          \"className\",\n          \"displayName\",\n          \"extensionPointName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"className\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"extensionPointName\": {\n            \"type\": \"string\"\n          },\n          \"icon\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"FileReverseProxyProvider\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"directory\": {\n            \"type\": \"string\"\n          },\n          \"filename\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Group\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/GroupSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/GroupStatus\"\n          }\n        }\n      },\n      \"GroupKind\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"is group name of Extension.\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"is kind name of Extension.\"\n          }\n        },\n        \"description\": \"GroupKind contains group and kind data only.\"\n      },\n      \"GroupList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Group\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"GroupSpec\": {\n        \"required\": [\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of group\"\n          }\n        }\n      },\n      \"GroupStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"totalAttachments\": {\n            \"minimum\": 0,\n            \"type\": \"integer\",\n            \"description\": \"Total of attachments under the current group\",\n            \"format\": \"int64\"\n          },\n          \"updateTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Update timestamp of the group\",\n            \"format\": \"date-time\"\n          }\n        }\n      },\n      \"InterestReason\": {\n        \"required\": [\n          \"reasonType\",\n          \"subject\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"expression\": {\n            \"type\": \"string\",\n            \"description\": \"The expression to be interested in\"\n          },\n          \"reasonType\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the reason definition to be interested in\"\n          },\n          \"subject\": {\n            \"$ref\": \"#/components/schemas/InterestReasonSubject\"\n          }\n        },\n        \"description\": \"The reason to be interested in\"\n      },\n      \"InterestReasonSubject\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"if name is not specified, it presents all subjects of the specified reason type and custom resources\"\n          }\n        },\n        \"description\": \"The subject name of reason type to be interested in\"\n      },\n      \"JsonPatch\": {\n        \"minItems\": 1,\n        \"uniqueItems\": true,\n        \"type\": \"array\",\n        \"description\": \"JSON schema for JSONPatch operations\",\n        \"items\": {\n          \"oneOf\": [\n            {\n              \"$ref\": \"#/components/schemas/AddOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/ReplaceOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/TestOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/RemoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/MoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/CopyOperation\"\n            }\n          ]\n        }\n      },\n      \"License\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"url\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Common data objects for license.\"\n      },\n      \"LocalThumbnail\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/LocalThumbnailSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/LocalThumbnailStatus\"\n          }\n        }\n      },\n      \"LocalThumbnailList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/LocalThumbnail\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"LocalThumbnailSpec\": {\n        \"required\": [\n          \"filePath\",\n          \"imageSignature\",\n          \"imageUri\",\n          \"size\",\n          \"thumbSignature\",\n          \"thumbnailUri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"filePath\": {\n            \"type\": \"string\",\n            \"description\": \"Consider the compatibility of the system and migration, use unix-style relative paths\\n here.\"\n          },\n          \"imageSignature\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"A hash signature for the image uri.\"\n          },\n          \"imageUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"size\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"S\",\n              \"M\",\n              \"L\",\n              \"XL\"\n            ]\n          },\n          \"thumbSignature\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"A hash signature for the thumbnail uri.\"\n          },\n          \"thumbnailUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"LocalThumbnailStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PENDING\",\n              \"SUCCEEDED\",\n              \"FAILED\"\n            ]\n          }\n        }\n      },\n      \"Menu\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuSpec\"\n          }\n        }\n      },\n      \"MenuItem\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuItemSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/MenuItemStatus\"\n          }\n        }\n      },\n      \"MenuItemList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/MenuItem\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"MenuItemSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Children of this menu item\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"The name of menu item child\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"The display name of menu item.\"\n          },\n          \"href\": {\n            \"type\": \"string\",\n            \"description\": \"The href of this menu item.\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"description\": \"The priority is for ordering.\",\n            \"format\": \"int32\"\n          },\n          \"target\": {\n            \"type\": \"string\",\n            \"description\": \"The \\u003ca\\u003e target attribute of this menu item.\",\n            \"enum\": [\n              \"_blank\",\n              \"_self\",\n              \"_parent\",\n              \"_top\"\n            ]\n          },\n          \"targetRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        },\n        \"description\": \"The spec of menu item.\"\n      },\n      \"MenuItemStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Calculated Display name of menu item.\"\n          },\n          \"href\": {\n            \"type\": \"string\",\n            \"description\": \"Calculated href of manu item.\"\n          }\n        },\n        \"description\": \"The status of menu item.\"\n      },\n      \"MenuList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Menu\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"MenuSpec\": {\n        \"required\": [\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"The display name of the menu.\"\n          },\n          \"menuItems\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Menu items of this menu.\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Name of menu item.\"\n            }\n          }\n        },\n        \"description\": \"The spec of menu.\"\n      },\n      \"Metadata\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Annotations are like key-value format.\"\n          },\n          \"creationTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Creation timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"deletionTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Deletion timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"finalizers\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"string\",\n              \"nullable\": true\n            }\n          },\n          \"generateName\": {\n            \"type\": \"string\",\n            \"description\": \"The name field will be generated automatically according to the given generateName field\"\n          },\n          \"labels\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Labels are like key-value format.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Metadata name\"\n          },\n          \"version\": {\n            \"type\": \"integer\",\n            \"description\": \"Current version of the Extension. It will be bumped up every update.\",\n            \"format\": \"int64\",\n            \"nullable\": true\n          }\n        },\n        \"description\": \"Metadata of Extension.\"\n      },\n      \"MoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"move\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"Notification\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/NotificationSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link Notification Notification} is a custom extension that used to store notification information for\\n inner use, it\\u0027s on-site notification.\\u003c/p\\u003e\\n\\n \\u003cp\\u003eSupports the following operations:\\u003c/p\\u003e\\n \\u003cul\\u003e\\n    \\u003cli\\u003eMarked as read: {@link NotificationSpec#setUnread(boolean) NotificationSpec#setUnread(boolean)}\\u003c/li\\u003e\\n    \\u003cli\\u003eGet the last read time: {@link NotificationSpec#getLastReadAt NotificationSpec#getLastReadAt()}\\u003c/li\\u003e\\n    \\u003cli\\u003eFilter by recipient: {@link NotificationSpec#getRecipient NotificationSpec#getRecipient()}\\u003c/li\\u003e\\n \\u003c/ul\\u003e\"\n      },\n      \"NotificationList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Notification\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"NotificationSpec\": {\n        \"required\": [\n          \"htmlContent\",\n          \"rawContent\",\n          \"reason\",\n          \"recipient\",\n          \"title\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"htmlContent\": {\n            \"type\": \"string\"\n          },\n          \"lastReadAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"rawContent\": {\n            \"type\": \"string\"\n          },\n          \"reason\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"The name of reason\"\n          },\n          \"recipient\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"The name of user\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"unread\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"NotificationTemplate\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/NotificationTemplateSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link NotificationTemplate NotificationTemplate} is a custom extension that defines a notification template.\\u003c/p\\u003e\\n \\u003cp\\u003eIt describes the notification template\\u0027s name, description, and the template content.\\u003c/p\\u003e\\n \\u003cp\\u003e{@link Spec#getReasonSelector Spec#getReasonSelector()} is used to select the template by reasonType and language,\\n if multiple templates are matched, the best match will be selected. This is useful when you\\n want to override the default template.\\u003c/p\\u003e\"\n      },\n      \"NotificationTemplateList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/NotificationTemplate\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"NotificationTemplateSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"reasonSelector\": {\n            \"$ref\": \"#/components/schemas/ReasonSelector\"\n          },\n          \"template\": {\n            \"$ref\": \"#/components/schemas/TemplateContent\"\n          }\n        }\n      },\n      \"NotifierDescriptor\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/NotifierDescriptorSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link NotifierDescriptor NotifierDescriptor} is a custom extension that defines a notifier.\\u003c/p\\u003e\\n \\u003cp\\u003eIt describes the notifier\\u0027s name, description, and the extension name of the notifier to\\n let the user know what the notifier is and what it can do in the UI and also let the\\n \\u003ccode\\u003eNotificationCenter\\u003c/code\\u003e know how to load the notifier and prepare the notifier\\u0027s settings.\\u003c/p\\u003e\"\n      },\n      \"NotifierDescriptorList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/NotifierDescriptor\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"NotifierDescriptorSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"notifierExtName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"notifierExtName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"receiverSettingRef\": {\n            \"$ref\": \"#/components/schemas/NotifierSettingRef\"\n          },\n          \"senderSettingRef\": {\n            \"$ref\": \"#/components/schemas/NotifierSettingRef\"\n          }\n        }\n      },\n      \"NotifierSettingRef\": {\n        \"required\": [\n          \"group\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PatSpec\": {\n        \"required\": [\n          \"name\",\n          \"tokenId\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"expiresAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"lastUsed\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"revoked\": {\n            \"type\": \"boolean\"\n          },\n          \"revokesAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"scopes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"tokenId\": {\n            \"type\": \"string\"\n          },\n          \"username\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PersonalAccessToken\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PatSpec\"\n          }\n        }\n      },\n      \"PersonalAccessTokenList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Plugin\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PluginSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PluginStatus\"\n          }\n        },\n        \"description\": \"A custom resource for Plugin.\"\n      },\n      \"PluginAuthor\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"website\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PluginList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Plugin\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PluginSpec\": {\n        \"required\": [\n          \"version\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"author\": {\n            \"$ref\": \"#/components/schemas/PluginAuthor\"\n          },\n          \"configMapName\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"enabled\": {\n            \"type\": \"boolean\"\n          },\n          \"homepage\": {\n            \"type\": \"string\"\n          },\n          \"issues\": {\n            \"type\": \"string\"\n          },\n          \"license\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/License\"\n            }\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"pluginDependencies\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"repo\": {\n            \"type\": \"string\"\n          },\n          \"requires\": {\n            \"type\": \"string\",\n            \"description\": \"SemVer format.\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"pattern\": \"^(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)(?:-((?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+([0-9a-zA-Z-]+(?:\\\\.[0-9a-zA-Z-]+)*))?$\",\n            \"type\": \"string\",\n            \"description\": \"plugin version.\"\n          }\n        }\n      },\n      \"PluginStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"entry\": {\n            \"type\": \"string\"\n          },\n          \"lastProbeState\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"CREATED\",\n              \"DISABLED\",\n              \"RESOLVED\",\n              \"STARTED\",\n              \"STOPPED\",\n              \"FAILED\",\n              \"UNLOADED\"\n            ]\n          },\n          \"lastStartTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"loadLocation\": {\n            \"type\": \"string\",\n            \"description\": \"Load location of the plugin, often a path.\",\n            \"format\": \"uri\"\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PENDING\",\n              \"STARTING\",\n              \"CREATED\",\n              \"DISABLING\",\n              \"DISABLED\",\n              \"RESOLVED\",\n              \"STARTED\",\n              \"STOPPED\",\n              \"FAILED\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"stylesheet\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Policy\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PolicySpec\"\n          }\n        }\n      },\n      \"PolicyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Policy\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PolicyRule\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiGroups\": {\n            \"type\": \"array\",\n            \"description\": \"APIGroups is the name of the APIGroup that contains the resources.\\n If multiple API groups are specified, any action requested against one of the enumerated\\n resources in any API group will be allowed.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"nonResourceURLs\": {\n            \"type\": \"array\",\n            \"description\": \"NonResourceURLs is a set of partial urls that a user should have access to.\\n *s are allowed, but only as the full, final step in the path\\n If an action is not a resource API request, then the URL is split on \\u0027/\\u0027 and is checked\\n against the NonResourceURLs to look for a match.\\n Since non-resource URLs are not namespaced, this field is only applicable for\\n ClusterRoles referenced from a ClusterRoleBinding.\\n Rules can either apply to API resources (such as \\\"pods\\\" or \\\"secrets\\\") or non-resource\\n URL paths (such as \\\"/api\\\"),  but not both.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"resourceNames\": {\n            \"type\": \"array\",\n            \"description\": \"ResourceNames is an optional white list of names that the rule applies to.  An empty set\\n means that everything is allowed.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"resources\": {\n            \"type\": \"array\",\n            \"description\": \"Resources is a list of resources this rule applies to.  \\u0027*\\u0027 represents all resources in\\n the specified apiGroups.\\n \\u0027*\\u0026#47;foo\\u0027 represents the subresource \\u0027foo\\u0027 for all resources in the specified\\n apiGroups.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"verbs\": {\n            \"type\": \"array\",\n            \"description\": \"about who the rule applies to or which namespace the rule applies to.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        \"description\": \"PolicyRule holds information that describes a policy rule, but does not contain information\\n about whom the rule applies to or which namespace the rule applies to.\"\n      },\n      \"PolicySpec\": {\n        \"required\": [\n          \"displayName\",\n          \"templateName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"configMapName\": {\n            \"type\": \"string\",\n            \"description\": \"Reference name of ConfigMap extension\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of policy\"\n          },\n          \"templateName\": {\n            \"type\": \"string\",\n            \"description\": \"Reference name of PolicyTemplate\"\n          }\n        }\n      },\n      \"PolicyTemplate\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PolicyTemplateSpec\"\n          }\n        }\n      },\n      \"PolicyTemplateList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/PolicyTemplate\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PolicyTemplateSpec\": {\n        \"required\": [\n          \"settingName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Post\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003ePost extension.\\u003c/p\\u003e\"\n      },\n      \"PostList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Post\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"PostSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"文章引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"PostStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Reason\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReasonSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link Reason Reason} is a custom extension that defines a reason for a notification, It represents\\n an instance of a {@link ReasonType ReasonType}.\\u003c/p\\u003e\\n \\u003cp\\u003eIt can be understood as an event that triggers a notification.\\u003c/p\\u003e\"\n      },\n      \"ReasonAttributes\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"empty\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"description\": \"Attributes used to transfer data\"\n      },\n      \"ReasonList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Reason\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReasonProperty\": {\n        \"required\": [\n          \"name\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"optional\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"type\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonSelector\": {\n        \"required\": [\n          \"language\",\n          \"reasonType\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"language\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"default\": \"default\"\n          },\n          \"reasonType\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonSpec\": {\n        \"required\": [\n          \"author\",\n          \"reasonType\",\n          \"subject\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"attributes\": {\n            \"$ref\": \"#/components/schemas/ReasonAttributes\"\n          },\n          \"author\": {\n            \"type\": \"string\"\n          },\n          \"reasonType\": {\n            \"type\": \"string\"\n          },\n          \"subject\": {\n            \"$ref\": \"#/components/schemas/ReasonSubject\"\n          }\n        }\n      },\n      \"ReasonSubject\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"name\",\n          \"title\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"type\": \"string\"\n          },\n          \"url\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonType\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReasonTypeSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link ReasonType ReasonType} is a custom extension that defines a type of reason.\\u003c/p\\u003e\\n \\u003cp\\u003eOne {@link ReasonType ReasonType} can have multiple {@link Reason Reason}s to notify.\\u003c/p\\u003e\"\n      },\n      \"ReasonTypeList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonType\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReasonTypeSpec\": {\n        \"required\": [\n          \"description\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"properties\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonProperty\"\n            }\n          }\n        }\n      },\n      \"Ref\": {\n        \"required\": [\n          \"group\",\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"Extension group\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Extension kind\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Extension name. This field is mandatory\"\n          },\n          \"version\": {\n            \"type\": \"string\",\n            \"description\": \"Extension version\"\n          }\n        },\n        \"description\": \"Extension reference object. The name is mandatory\"\n      },\n      \"RememberMeToken\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/RememberMeTokenSpec\"\n          }\n        }\n      },\n      \"RememberMeTokenList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/RememberMeToken\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RememberMeTokenSpec\": {\n        \"required\": [\n          \"series\",\n          \"tokenValue\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"lastUsed\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"series\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"tokenValue\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"username\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"RemoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"remove\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"ReplaceOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"replace\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Reply\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReplySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/ReplyStatus\"\n          }\n        }\n      },\n      \"ReplyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Reply\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReplySpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"commentName\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"commentName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"quoteReply\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReplyStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReverseProxy\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"rules\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReverseProxyRule\"\n            }\n          }\n        },\n        \"description\": \"\\u003cp\\u003eThe reverse proxy custom resource is used to configure a path to proxy it to a directory or\\n file.\\u003c/p\\u003e\\n \\u003cp\\u003eHTTP proxy may be added in the future.\\u003c/p\\u003e\"\n      },\n      \"ReverseProxyList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReverseProxy\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReverseProxyRule\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"$ref\": \"#/components/schemas/FileReverseProxyProvider\"\n          },\n          \"path\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Role\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"rules\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"rules\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/PolicyRule\"\n            }\n          }\n        }\n      },\n      \"RoleBinding\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"roleRef\": {\n            \"$ref\": \"#/components/schemas/RoleRef\"\n          },\n          \"subjects\": {\n            \"type\": \"array\",\n            \"description\": \"Subjects holds references to the objects the role applies to.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Subject\"\n            }\n          }\n        },\n        \"description\": \"RoleBinding references a role, but does not contain it.\\n It can reference a Role in the global.\\n It adds who information via Subjects.\"\n      },\n      \"RoleBindingList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/RoleBinding\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RoleList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Role\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"RoleRef\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiGroup\": {\n            \"type\": \"string\",\n            \"description\": \"APIGroup is the group for the resource being referenced.\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Kind is the type of resource being referenced.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Name is the name of resource being referenced.\"\n          }\n        },\n        \"description\": \"RoleRef contains information that points to the role being used.\"\n      },\n      \"Secret\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"data\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\",\n              \"format\": \"byte\"\n            },\n            \"description\": \"\\u003cp\\u003eThe total bytes of the values in\\n the Data field must be less than {@link run.halo.app.extension.Secret#MAX_SECRET_SIZE #MAX_SECRET_SIZE} bytes.\\u003c/p\\u003e\\n \\u003cp\\u003e\\u003ccode\\u003edata\\u003c/code\\u003e contains the secret data. Each key must consist of alphanumeric\\n characters, \\u0027-\\u0027, \\u0027_\\u0027 or \\u0027.\\u0027. The serialized form of the secret data is a\\n base64 encoded string, representing the arbitrary (possibly non-string)\\n data value here. Described in\\n \\u003ca href\\u003d\\\"https://tools.ietf.org/html/rfc4648#section-4\\\"\\u003erfc4648#section-4\\u003c/a\\u003e\\n \\u003c/p\\u003e\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"stringData\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"\\u003ccode\\u003estringData\\u003c/code\\u003e allows specifying non-binary secret data in string form.\\n It is provided as a write-only input field for convenience.\\n All keys and values are merged into the data field on write, overwriting any existing\\n values.\\n The stringData field is never output when reading from the API.\"\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"description\": \"Used to facilitate programmatic handling of secret data.\\n More info:\\n \\u003ca href\\u003d\\\"https://kubernetes.io/docs/concepts/configuration/secret/#secret-types\\\"\\u003esecret-types\\u003c/a\\u003e\"\n          }\n        },\n        \"description\": \"Secret is a small piece of sensitive data which should be kept secret, such as a password,\\n a token, or a key.\"\n      },\n      \"SecretList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Secret\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Setting\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SettingSpec\"\n          }\n        },\n        \"description\": \"{@link Setting Setting} is a custom extension to generate forms based on configuration.\"\n      },\n      \"SettingForm\": {\n        \"minLength\": 1,\n        \"required\": [\n          \"formSchema\",\n          \"group\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"formSchema\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\"\n            }\n          },\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"label\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SettingList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Setting\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SettingRef\": {\n        \"required\": [\n          \"group\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SettingSpec\": {\n        \"required\": [\n          \"forms\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"forms\": {\n            \"minLength\": 1,\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/SettingForm\"\n            }\n          }\n        }\n      },\n      \"SinglePage\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SinglePageSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/SinglePageStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eSingle page extension.\\u003c/p\\u003e\"\n      },\n      \"SinglePageList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/SinglePage\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SinglePageSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"SinglePageStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SnapShotSpec\": {\n        \"required\": [\n          \"owner\",\n          \"rawType\",\n          \"subjectRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"contentPatch\": {\n            \"type\": \"string\"\n          },\n          \"contributors\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"parentSnapshotName\": {\n            \"type\": \"string\"\n          },\n          \"rawPatch\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"maxLength\": 50,\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"such as: markdown | html | json | asciidoc | latex.\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        }\n      },\n      \"Snapshot\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SnapShotSpec\"\n          }\n        }\n      },\n      \"SnapshotList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Snapshot\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Subject\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiGroup\": {\n            \"type\": \"string\",\n            \"description\": \"APIGroup holds the API group of the referenced subject.\\n Defaults to \\\"\\\" for ServiceAccount subjects.\\n Defaults to \\\"rbac.authorization.halo.run\\\" for User and Group subjects.\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Kind of object being referenced. Values defined by this API group are \\\"User\\\", \\\"Group\\\",\\n and \\\"ServiceAccount\\\".\\n If the Authorizer does not recognize the kind value, the Authorizer should report\\n an error.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Name of the object being referenced.\"\n          }\n        }\n      },\n      \"Subscription\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SubscriptionSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link Subscription Subscription} is a custom extension that defines a subscriber to be notified when a\\n certain {@link Reason Reason} is triggered.\\u003c/p\\u003e\\n \\u003cp\\u003eIt holds a {@link Subscriber Subscriber} to the user to be notified, a {@link InterestReason InterestReason} to\\n subscribe to.\\u003c/p\\u003e\"\n      },\n      \"SubscriptionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Subscription\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SubscriptionSpec\": {\n        \"required\": [\n          \"reason\",\n          \"subscriber\",\n          \"unsubscribeToken\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"disabled\": {\n            \"type\": \"boolean\",\n            \"description\": \"Perhaps users need to unsubscribe and interact without receiving notifications again\"\n          },\n          \"reason\": {\n            \"$ref\": \"#/components/schemas/InterestReason\"\n          },\n          \"subscriber\": {\n            \"$ref\": \"#/components/schemas/SubscriptionSubscriber\"\n          },\n          \"unsubscribeToken\": {\n            \"type\": \"string\",\n            \"description\": \"The token to unsubscribe\"\n          }\n        }\n      },\n      \"SubscriptionSubscriber\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"The subscriber to be notified\"\n      },\n      \"Tag\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/TagSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/TagStatus\"\n          }\n        }\n      },\n      \"TagList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Tag\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"TagSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"color\": {\n            \"pattern\": \"^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$\",\n            \"type\": \"string\",\n            \"description\": \"Color regex explanation.\\n \\u003cpre\\u003e\\n ^                 # start of the line\\n #                 # start with a number sign `#`\\n (                 # start of (group 1)\\n   [a-fA-F0-9]{6}  # support z-f, A-F and 0-9, with a length of 6\\n   |               # or\\n   [a-fA-F0-9]{3}  # support z-f, A-F and 0-9, with a length of 3\\n )                 # end of (group 1)\\n $                 # end of the line\\n \\u003c/pre\\u003e\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TagStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"TemplateContent\": {\n        \"required\": [\n          \"title\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"htmlBody\": {\n            \"type\": \"string\"\n          },\n          \"rawBody\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TemplateDescriptor\": {\n        \"required\": [\n          \"file\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"file\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"screenshot\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Type used to describe custom template page.\"\n      },\n      \"TestOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"test\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Theme\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ThemeSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/ThemeStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eTheme extension.\\u003c/p\\u003e\"\n      },\n      \"ThemeList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Theme\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ThemeSpec\": {\n        \"required\": [\n          \"author\",\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"author\": {\n            \"$ref\": \"#/components/schemas/Author\"\n          },\n          \"configMapName\": {\n            \"type\": \"string\"\n          },\n          \"customTemplates\": {\n            \"$ref\": \"#/components/schemas/CustomTemplates\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"homepage\": {\n            \"type\": \"string\"\n          },\n          \"issues\": {\n            \"type\": \"string\"\n          },\n          \"license\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/License\"\n            }\n          },\n          \"logo\": {\n            \"type\": \"string\"\n          },\n          \"repo\": {\n            \"type\": \"string\"\n          },\n          \"requires\": {\n            \"type\": \"string\"\n          },\n          \"settingName\": {\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ThemeStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"location\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"READY\",\n              \"FAILED\",\n              \"UNKNOWN\"\n            ]\n          }\n        }\n      },\n      \"Thumbnail\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ThumbnailSpec\"\n          }\n        }\n      },\n      \"ThumbnailList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Thumbnail\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ThumbnailSpec\": {\n        \"required\": [\n          \"imageSignature\",\n          \"imageUri\",\n          \"size\",\n          \"thumbnailUri\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"imageSignature\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"imageUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"size\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"S\",\n              \"M\",\n              \"L\",\n              \"XL\"\n            ]\n          },\n          \"thumbnailUri\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"User\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/UserSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/UserStatus\"\n          }\n        },\n        \"description\": \"The extension represents user details of Halo.\"\n      },\n      \"UserConnection\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/UserConnectionSpec\"\n          }\n        },\n        \"description\": \"User connection extension.\"\n      },\n      \"UserConnectionList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/UserConnection\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"UserConnectionSpec\": {\n        \"required\": [\n          \"providerUserId\",\n          \"registrationId\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"providerUserId\": {\n            \"type\": \"string\",\n            \"description\": \"The unique identifier for the user\\u0027s connection to the OAuth provider.\\n for example, the user\\u0027s GitHub id.\"\n          },\n          \"registrationId\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the OAuth provider (e.g. Google, Facebook, Twitter).\"\n          },\n          \"updatedAt\": {\n            \"type\": \"string\",\n            \"description\": \"The time when the user connection was last updated.\",\n            \"format\": \"date-time\"\n          },\n          \"username\": {\n            \"type\": \"string\",\n            \"description\": \"The {@link Metadata#getName Metadata#getName()} of the user associated with the OAuth connection.\"\n          }\n        }\n      },\n      \"UserList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/User\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"UserSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"email\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"bio\": {\n            \"type\": \"string\"\n          },\n          \"disabled\": {\n            \"type\": \"boolean\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"emailVerified\": {\n            \"type\": \"boolean\"\n          },\n          \"loginHistoryLimit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"password\": {\n            \"type\": \"string\"\n          },\n          \"phone\": {\n            \"type\": \"string\"\n          },\n          \"registeredAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"totpEncryptedSecret\": {\n            \"type\": \"string\"\n          },\n          \"twoFactorAuthEnabled\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"UserStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"basicAuth\": {\n        \"scheme\": \"basic\",\n        \"type\": \"http\"\n      },\n      \"bearerAuth\": {\n        \"bearerFormat\": \"JWT\",\n        \"scheme\": \"bearer\",\n        \"type\": \"http\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "api-docs/openapi/v3_0/apis_public.api_v1alpha1.json",
    "content": "{\n  \"openapi\": \"3.0.1\",\n  \"info\": {\n    \"title\": \"Halo\",\n    \"version\": \"2.23.0-SNAPSHOT\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:8091\",\n      \"description\": \"Generated server url\"\n    }\n  ],\n  \"security\": [\n    {\n      \"basicAuth\": [],\n      \"bearerAuth\": []\n    }\n  ],\n  \"paths\": {\n    \"/apis/api.content.halo.run/v1alpha1/categories\": {\n      \"get\": {\n        \"description\": \"Lists categories.\",\n        \"operationId\": \"queryCategories\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CategoryVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/categories/{name}\": {\n      \"get\": {\n        \"description\": \"Gets category by name.\",\n        \"operationId\": \"queryCategoryByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Category name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CategoryVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/categories/{name}/posts\": {\n      \"get\": {\n        \"description\": \"Lists posts by category name.\",\n        \"operationId\": \"queryPostsByCategoryName\",\n        \"parameters\": [\n          {\n            \"description\": \"Category name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CategoryV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"Lists posts.\",\n        \"operationId\": \"queryPosts\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/posts/{name}\": {\n      \"get\": {\n        \"description\": \"Gets a post by name.\",\n        \"operationId\": \"queryPostByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PostVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/posts/{name}/navigation\": {\n      \"get\": {\n        \"description\": \"Gets a post navigation by name.\",\n        \"operationId\": \"queryPostNavigationByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NavigationPostVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/singlepages\": {\n      \"get\": {\n        \"description\": \"Lists single pages\",\n        \"operationId\": \"querySinglePages\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedSinglePageVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/singlepages/{name}\": {\n      \"get\": {\n        \"description\": \"Gets single page by name\",\n        \"operationId\": \"querySinglePageByName\",\n        \"parameters\": [\n          {\n            \"description\": \"SinglePage name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SinglePageVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SinglePageV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/tags\": {\n      \"get\": {\n        \"description\": \"Lists tags\",\n        \"operationId\": \"queryTags\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/tags/{name}\": {\n      \"get\": {\n        \"description\": \"Gets tag by name\",\n        \"operationId\": \"queryTagByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Tag name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TagVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.content.halo.run/v1alpha1/tags/{name}/posts\": {\n      \"get\": {\n        \"description\": \"Lists posts by tag name\",\n        \"operationId\": \"queryPostsByTagName\",\n        \"parameters\": [\n          {\n            \"description\": \"Tag name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TagV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/comments\": {\n      \"get\": {\n        \"description\": \"List comments.\",\n        \"operationId\": \"ListComments_1\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"The comment subject group.\",\n            \"in\": \"query\",\n            \"name\": \"group\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The comment subject version.\",\n            \"in\": \"query\",\n            \"name\": \"version\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The comment subject kind.\",\n            \"in\": \"query\",\n            \"name\": \"kind\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The comment subject name.\",\n            \"in\": \"query\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Whether to include replies. Default is false.\",\n            \"in\": \"query\",\n            \"name\": \"withReplies\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          },\n          {\n            \"description\": \"Reply size of the comment, default is 10, only works when withReplies is true.\",\n            \"in\": \"query\",\n            \"name\": \"replySize\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CommentWithReplyVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create a comment.\",\n        \"operationId\": \"CreateComment_1\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CommentRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Comment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/comments/{name}\": {\n      \"get\": {\n        \"description\": \"Get a comment.\",\n        \"operationId\": \"GetComment\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CommentVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/comments/{name}/reply\": {\n      \"get\": {\n        \"description\": \"List comment replies.\",\n        \"operationId\": \"ListCommentReplies\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReplyVoList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create a reply.\",\n        \"operationId\": \"CreateReply_1\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReplyRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Reply\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"CommentV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/indices/-/search\": {\n      \"post\": {\n        \"description\": \"Search indices.\",\n        \"operationId\": \"IndicesSearch\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/SearchOption\"\n              }\n            }\n          },\n          \"description\": \"Please note that the \\\"filterPublished\\\", \\\"filterExposed\\\" and \\\"filterRecycled\\\" fields are ignored in this endpoint.\"\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SearchResult\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"IndexV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/menus/-\": {\n      \"get\": {\n        \"description\": \"Gets primary menu.\",\n        \"operationId\": \"queryPrimaryMenu\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/menus/{name}\": {\n      \"get\": {\n        \"description\": \"Gets menu by name.\",\n        \"operationId\": \"queryMenuByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Menu name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MenuVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MenuV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/stats/-\": {\n      \"get\": {\n        \"description\": \"Gets site stats\",\n        \"operationId\": \"queryStats\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SiteStatsVo\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SystemV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/trackers/counter\": {\n      \"post\": {\n        \"description\": \"Count an extension resource visits.\",\n        \"operationId\": \"count\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CounterRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MetricsV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/trackers/downvote\": {\n      \"post\": {\n        \"description\": \"Downvote an extension resource.\",\n        \"operationId\": \"downvote\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VoteRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MetricsV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.halo.run/v1alpha1/trackers/upvote\": {\n      \"post\": {\n        \"description\": \"Upvote an extension resource.\",\n        \"operationId\": \"upvote\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VoteRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"MetricsV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/subscriptions/{name}/unsubscribe\": {\n      \"get\": {\n        \"description\": \"Unsubscribe a subscription\",\n        \"operationId\": \"Unsubscribe\",\n        \"parameters\": [\n          {\n            \"description\": \"Subscription name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Unsubscribe token\",\n            \"in\": \"query\",\n            \"name\": \"token\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.plugin.halo.run/v1alpha1/plugins/{name}/available\": {\n      \"get\": {\n        \"description\": \"Gets plugin available by name.\",\n        \"operationId\": \"queryPluginAvailableByName\",\n        \"parameters\": [\n          {\n            \"description\": \"Plugin name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"boolean\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PluginV1alpha1Public\"\n        ]\n      }\n    },\n    \"/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri\": {\n      \"get\": {\n        \"description\": \"Get thumbnail by URI\",\n        \"operationId\": \"GetThumbnailByUri\",\n        \"parameters\": [\n          {\n            \"description\": \"The URI of the image\",\n            \"in\": \"query\",\n            \"name\": \"uri\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"The size of the thumbnail\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"S\",\n                \"M\",\n                \"L\",\n                \"XL\"\n              ]\n            }\n          },\n          {\n            \"description\": \"The width of the thumbnail, if \\u0027size\\u0027 is not provided, this parameter will be used to determine the size\",\n            \"in\": \"query\",\n            \"name\": \"width\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"enum\": [\n                \"400\",\n                \"800\",\n                \"1200\",\n                \"1600\"\n              ]\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"binary\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"ThumbnailV1alpha1Public\"\n        ]\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"AddOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"add\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"CategorySpec\": {\n        \"required\": [\n          \"displayName\",\n          \"priority\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eWhether to hide the category from the category list.\\u003c/p\\u003e\\n \\u003cp\\u003eWhen set to true, the category including its subcategories and related posts will\\n not be displayed in the category list, but it can still be accessed by permalink.\\u003c/p\\u003e\\n \\u003cp\\u003eLimitation: It only takes effect on the theme-side categorized list and it only\\n allows to be set to true on the first level(root node) of categories.\\u003c/p\\u003e\"\n          },\n          \"postTemplate\": {\n            \"maxLength\": 255,\n            \"type\": \"string\",\n            \"description\": \"\\u003cp\\u003eUsed to specify the template for the posts associated with the category.\\u003c/p\\u003e\\n \\u003cp\\u003eThe priority is not as high as that of the post.\\u003c/p\\u003e\\n \\u003cp\\u003eIf the post also specifies a template, the post\\u0027s template will prevail.\\u003c/p\\u003e\"\n          },\n          \"preventParentPostCascadeQuery\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eif a category is queried for related posts, the default behavior is to\\n query all posts under the category including its subcategories, but if this field is\\n set to true, cascade query behavior will be terminated here.\\u003c/p\\u003e\\n \\u003cp\\u003eFor example, if a category has subcategories A and B, and A has subcategories C and\\n D and C marked this field as true, when querying posts under A category,all posts under A\\n and B will be queried, but C and D will not be queried.\\u003c/p\\u003e\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"maxLength\": 255,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CategoryStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的已发布且公开的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"CategoryVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CategorySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CategoryStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link Category Category}.\"\n      },\n      \"CategoryVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CategoryVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Comment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        }\n      },\n      \"CommentEmailOwner\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\",\n            \"description\": \"avatar for comment owner\"\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"display name for comment owner\"\n          },\n          \"email\": {\n            \"type\": \"string\",\n            \"description\": \"email for comment owner\"\n          },\n          \"website\": {\n            \"type\": \"string\",\n            \"description\": \"website for comment owner\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003eThe creator info of the comment.\\u003c/p\\u003e\\n This {@link CommentEmailOwner CommentEmailOwner} is only applicable to the user who is allowed to comment\\n without logging in.\"\n      },\n      \"CommentOwner\": {\n        \"required\": [\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"maxLength\": 64,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentRequest\": {\n        \"required\": [\n          \"content\",\n          \"raw\",\n          \"subjectRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentEmailOwner\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        },\n        \"description\": \"Request parameter object for {@link Comment Comment}.\"\n      },\n      \"CommentSpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"subjectRef\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"lastReadTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CommentStatsVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"comment stats value object.\"\n      },\n      \"CommentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"hasNewReply\": {\n            \"type\": \"boolean\"\n          },\n          \"lastReplyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"replyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"unreadReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visibleReplyCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"CommentVo\": {\n        \"required\": [\n          \"metadata\",\n          \"owner\",\n          \"spec\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"CommentVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CommentVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"CommentWithReplyVo\": {\n        \"required\": [\n          \"metadata\",\n          \"owner\",\n          \"spec\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"replies\": {\n            \"$ref\": \"#/components/schemas/ListResultReplyVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CommentSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CommentStatus\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"CommentWithReplyVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CommentWithReplyVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"Condition\": {\n        \"required\": [\n          \"lastTransitionTime\",\n          \"status\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"lastTransitionTime\": {\n            \"type\": \"string\",\n            \"description\": \"Last time the condition transitioned from one status to another.\",\n            \"format\": \"date-time\"\n          },\n          \"message\": {\n            \"maxLength\": 32768,\n            \"type\": \"string\",\n            \"description\": \"Human-readable message indicating details about last transition.\\n This may be an empty string.\"\n          },\n          \"reason\": {\n            \"maxLength\": 1024,\n            \"pattern\": \"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$\",\n            \"type\": \"string\",\n            \"description\": \"Unique, one-word, CamelCase reason for the condition\\u0027s last transition.\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"description\": \"Status is the status of the condition. Can be True, False, Unknown.\",\n            \"enum\": [\n              \"TRUE\",\n              \"FALSE\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"type\": {\n            \"maxLength\": 316,\n            \"pattern\": \"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$\",\n            \"type\": \"string\",\n            \"description\": \"type of condition in CamelCase or in foo.example.com/CamelCase.\\n example: Ready, Initialized.\\n maxLength: 316.\"\n          }\n        },\n        \"description\": \"EqualsAndHashCode 排除了lastTransitionTime否则失败时，lastTransitionTime 会被更新\\n 导致 equals 为 false，一直被加入队列.\"\n      },\n      \"ContentVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A value object for Content from {@link Snapshot Snapshot}.\"\n      },\n      \"ContributorVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"bio\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A value object for {@link run.halo.app.core.extension.User run.halo.app.core.extension.User}.\"\n      },\n      \"CopyOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"copy\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"CounterRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"hostname\": {\n            \"type\": \"string\"\n          },\n          \"language\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"plural\": {\n            \"type\": \"string\"\n          },\n          \"referrer\": {\n            \"type\": \"string\"\n          },\n          \"screen\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Excerpt\": {\n        \"required\": [\n          \"autoGenerate\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"autoGenerate\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"HaloDocument\": {\n        \"required\": [\n          \"content\",\n          \"id\",\n          \"metadataName\",\n          \"ownerName\",\n          \"permalink\",\n          \"title\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Custom metadata. Make sure the map is serializable.\"\n          },\n          \"categories\": {\n            \"type\": \"array\",\n            \"description\": \"Document categories. The item in the list is the category metadata name.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document content. Safety content, without HTML tag.\"\n          },\n          \"creationTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Document creation timestamp.\",\n            \"format\": \"date-time\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"description\": \"Document description.\"\n          },\n          \"exposed\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether the document is exposed to the public.\"\n          },\n          \"id\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document ID. It should be unique globally.\"\n          },\n          \"metadataName\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Metadata name of the corresponding extension.\"\n          },\n          \"ownerName\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document owner metadata name.\"\n          },\n          \"permalink\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document permalink.\"\n          },\n          \"published\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether the document is published.\"\n          },\n          \"recycled\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether the document is recycled.\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"description\": \"Document tags. The item in the list is the tag metadata name.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document title.\"\n          },\n          \"type\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Document type. e.g.: post.content.halo.run, singlepage.content.halo.run, moment.moment\\n .halo.run, doc.doc.halo.run.\"\n          },\n          \"updateTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Document update timestamp.\",\n            \"format\": \"date-time\"\n          }\n        },\n        \"description\": \"Document for search.\"\n      },\n      \"JsonPatch\": {\n        \"minItems\": 1,\n        \"uniqueItems\": true,\n        \"type\": \"array\",\n        \"description\": \"JSON schema for JSONPatch operations\",\n        \"items\": {\n          \"oneOf\": [\n            {\n              \"$ref\": \"#/components/schemas/AddOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/ReplaceOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/TestOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/RemoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/MoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/CopyOperation\"\n            }\n          ]\n        }\n      },\n      \"ListResultReplyVo\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReplyVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedPostVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CategoryVo\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TagVo\"\n            }\n          }\n        },\n        \"description\": \"A value object for {@link Post Post}.\"\n      },\n      \"ListedPostVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedPostVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ListedSinglePageVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SinglePageSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/SinglePageStatus\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedSinglePageVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedSinglePageVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"MenuItemSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Children of this menu item\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"The name of menu item child\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"The display name of menu item.\"\n          },\n          \"href\": {\n            \"type\": \"string\",\n            \"description\": \"The href of this menu item.\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"description\": \"The priority is for ordering.\",\n            \"format\": \"int32\"\n          },\n          \"target\": {\n            \"type\": \"string\",\n            \"description\": \"The \\u003ca\\u003e target attribute of this menu item.\",\n            \"enum\": [\n              \"_blank\",\n              \"_self\",\n              \"_parent\",\n              \"_top\"\n            ]\n          },\n          \"targetRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        }\n      },\n      \"MenuItemStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Calculated Display name of menu item.\"\n          },\n          \"href\": {\n            \"type\": \"string\",\n            \"description\": \"Calculated href of manu item.\"\n          }\n        }\n      },\n      \"MenuItemVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/MenuItemVo\"\n            }\n          },\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Gets menu item\\u0027s display name.\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"parentName\": {\n            \"type\": \"string\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuItemSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/MenuItemStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link MenuItem MenuItem}.\"\n      },\n      \"MenuSpec\": {\n        \"required\": [\n          \"displayName\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"The display name of the menu.\"\n          },\n          \"menuItems\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Menu items of this menu.\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Name of menu item.\"\n            }\n          }\n        }\n      },\n      \"MenuVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"menuItems\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/MenuItemVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/MenuSpec\"\n          }\n        },\n        \"description\": \"A value object for {@link Menu Menu}.\"\n      },\n      \"Metadata\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Annotations are like key-value format.\"\n          },\n          \"creationTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Creation timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"deletionTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Deletion timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"finalizers\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"string\",\n              \"nullable\": true\n            }\n          },\n          \"generateName\": {\n            \"type\": \"string\",\n            \"description\": \"The name field will be generated automatically according to the given generateName field\"\n          },\n          \"labels\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Labels are like key-value format.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Metadata name\"\n          },\n          \"version\": {\n            \"type\": \"integer\",\n            \"description\": \"Current version of the Extension. It will be bumped up every update.\",\n            \"format\": \"int64\",\n            \"nullable\": true\n          }\n        },\n        \"description\": \"Metadata of Extension.\"\n      },\n      \"MoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"move\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"NavigationPostVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"next\": {\n            \"$ref\": \"#/components/schemas/ListedPostVo\"\n          },\n          \"previous\": {\n            \"$ref\": \"#/components/schemas/ListedPostVo\"\n          }\n        },\n        \"description\": \"Post navigation vo to hold previous and next item.\"\n      },\n      \"OwnerInfo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Comment owner info.\"\n      },\n      \"PostSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"文章引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"PostStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PostVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CategoryVo\"\n            }\n          },\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentVo\"\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TagVo\"\n            }\n          }\n        },\n        \"description\": \"A value object for {@link Post Post}.\"\n      },\n      \"Ref\": {\n        \"required\": [\n          \"group\",\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"Extension group\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Extension kind\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Extension name. This field is mandatory\"\n          },\n          \"version\": {\n            \"type\": \"string\",\n            \"description\": \"Extension version\"\n          }\n        },\n        \"description\": \"Extension reference object. The name is mandatory\"\n      },\n      \"RemoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"remove\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"ReplaceOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"replace\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"Reply\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReplySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/ReplyStatus\"\n          }\n        }\n      },\n      \"ReplyRequest\": {\n        \"required\": [\n          \"content\",\n          \"raw\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentEmailOwner\"\n          },\n          \"quoteReply\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"A request parameter object for {@link Reply Reply}.\"\n      },\n      \"ReplySpec\": {\n        \"required\": [\n          \"allowNotification\",\n          \"approved\",\n          \"commentName\",\n          \"content\",\n          \"hidden\",\n          \"owner\",\n          \"priority\",\n          \"raw\",\n          \"top\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowNotification\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"approved\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"approvedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"commentName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"content\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"creationTime\": {\n            \"type\": \"string\",\n            \"description\": \"The user-defined creation time default is \\u003ccode\\u003emetadata.creationTimestamp\\u003c/code\\u003e.\",\n            \"format\": \"date-time\"\n          },\n          \"hidden\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"ipAddress\": {\n            \"type\": \"string\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/CommentOwner\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"quoteReply\": {\n            \"type\": \"string\"\n          },\n          \"raw\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"top\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReplyStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"ReplyVo\": {\n        \"required\": [\n          \"metadata\",\n          \"owner\",\n          \"spec\",\n          \"stats\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/OwnerInfo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/ReplySpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/CommentStatsVo\"\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ReplyVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReplyVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SearchOption\": {\n        \"required\": [\n          \"keyword\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Additional annotations for extending search option by other search engines.\"\n          },\n          \"filterExposed\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether to filter exposed content. If null, it will not filter.\"\n          },\n          \"filterPublished\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether to filter published content. If null, it will not filter.\"\n          },\n          \"filterRecycled\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether to filter recycled content. If null, it will not filter.\"\n          },\n          \"highlightPostTag\": {\n            \"type\": \"string\",\n            \"description\": \"Post HTML tag of highlighted fragment.\"\n          },\n          \"highlightPreTag\": {\n            \"type\": \"string\",\n            \"description\": \"Pre HTML tag of highlighted fragment.\"\n          },\n          \"includeCategoryNames\": {\n            \"type\": \"array\",\n            \"description\": \"Category names to include(and). If null, it will include all categories.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"includeOwnerNames\": {\n            \"type\": \"array\",\n            \"description\": \"Owner names to include(or). If null, it will include all owners.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"includeTagNames\": {\n            \"type\": \"array\",\n            \"description\": \"Tag names to include(and). If null, it will include all tags.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"includeTypes\": {\n            \"type\": \"array\",\n            \"description\": \"Types to include(or). If null, it will include all types.\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"keyword\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"Search keyword.\"\n          },\n          \"limit\": {\n            \"maximum\": 1000,\n            \"minimum\": 1,\n            \"type\": \"integer\",\n            \"description\": \"Limit of result.\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"Search option. It is used to control search behavior.\"\n      },\n      \"SearchResult\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"hits\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/HaloDocument\"\n            }\n          },\n          \"keyword\": {\n            \"type\": \"string\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"processingTimeMillis\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"SinglePageSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"SinglePageStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"SinglePageVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"$ref\": \"#/components/schemas/ContentVo\"\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ContributorVo\"\n            }\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/ContributorVo\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SinglePageSpec\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/StatsVo\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/SinglePageStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link SinglePage SinglePage}.\"\n      },\n      \"SiteStatsVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"category\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"comment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"post\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"A value object for site stats.\"\n      },\n      \"StatsVo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"comment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"Stats value object.\"\n      },\n      \"TagSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"color\": {\n            \"pattern\": \"^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$\",\n            \"type\": \"string\",\n            \"description\": \"Color regex explanation.\\n \\u003cpre\\u003e\\n ^                 # start of the line\\n #                 # start with a number sign `#`\\n (                 # start of (group 1)\\n   [a-fA-F0-9]{6}  # support z-f, A-F and 0-9, with a length of 6\\n   |               # or\\n   [a-fA-F0-9]{3}  # support z-f, A-F and 0-9, with a length of 3\\n )                 # end of (group 1)\\n $                 # end of the line\\n \\u003c/pre\\u003e\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TagStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"TagVo\": {\n        \"required\": [\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/TagSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/TagStatus\"\n          }\n        },\n        \"description\": \"A value object for {@link Tag Tag}.\"\n      },\n      \"TagVoList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/TagVo\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"TestOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"test\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"VoteRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"plural\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"basicAuth\": {\n        \"scheme\": \"basic\",\n        \"type\": \"http\"\n      },\n      \"bearerAuth\": {\n        \"bearerFormat\": \"JWT\",\n        \"scheme\": \"bearer\",\n        \"type\": \"http\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "api-docs/openapi/v3_0/apis_uc.api_v1alpha1.json",
    "content": "{\n  \"openapi\": \"3.0.1\",\n  \"info\": {\n    \"title\": \"Halo\",\n    \"version\": \"2.23.0-SNAPSHOT\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:8091\",\n      \"description\": \"Generated server url\"\n    }\n  ],\n  \"security\": [\n    {\n      \"basicAuth\": [],\n      \"bearerAuth\": []\n    }\n  ],\n  \"paths\": {\n    \"/apis/api.notification.halo.run/v1alpha1/notifiers/{name}/receiver-config\": {\n      \"get\": {\n        \"description\": \"Fetch receiver config of notifier\",\n        \"operationId\": \"FetchReceiverConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Save receiver config of notifier\",\n        \"operationId\": \"SaveReceiverConfig\",\n        \"parameters\": [\n          {\n            \"description\": \"Notifier name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {},\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotifierV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notification-preferences\": {\n      \"get\": {\n        \"description\": \"List notification preferences for the authenticated user.\",\n        \"operationId\": \"ListUserNotificationPreferences\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonTypeNotifierMatrix\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Save notification preferences for the authenticated user.\",\n        \"operationId\": \"SaveUserNotificationPreferences\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ReasonTypeNotifierCollectionRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReasonTypeNotifierMatrix\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications\": {\n      \"get\": {\n        \"description\": \"List notifications for the authenticated user.\",\n        \"operationId\": \"ListUserNotifications\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/NotificationList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/-/mark-specified-as-read\": {\n      \"put\": {\n        \"description\": \"Mark the specified notifications as read.\",\n        \"operationId\": \"MarkNotificationsAsRead\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/MarkSpecifiedRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete the specified notification.\",\n        \"operationId\": \"DeleteSpecifiedNotification\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Notification name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}/mark-as-read\": {\n      \"put\": {\n        \"description\": \"Mark the specified notification as read.\",\n        \"operationId\": \"MarkNotificationAsRead\",\n        \"parameters\": [\n          {\n            \"description\": \"Username\",\n            \"in\": \"path\",\n            \"name\": \"username\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Notification name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Notification\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"NotificationV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.auth.halo.run/v1alpha1/user-connections/{registerId}/disconnect\": {\n      \"put\": {\n        \"description\": \"Disconnect my connection from a third-party platform.\",\n        \"operationId\": \"DisconnectMyConnection\",\n        \"parameters\": [\n          {\n            \"description\": \"The registration ID of the third-party platform.\",\n            \"in\": \"path\",\n            \"name\": \"registerId\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/UserConnection\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserConnectionV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts\": {\n      \"get\": {\n        \"description\": \"List posts owned by the current user.\",\n        \"operationId\": \"ListMyPosts\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Posts filtered by publish phase.\",\n            \"in\": \"query\",\n            \"name\": \"publishPhase\",\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"DRAFT\",\n                \"PENDING_APPROVAL\",\n                \"PUBLISHED\",\n                \"FAILED\"\n              ]\n            }\n          },\n          {\n            \"description\": \"Posts filtered by keyword.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Posts filtered by category including sub-categories.\",\n            \"in\": \"query\",\n            \"name\": \"categoryWithChildren\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ListedPostList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Create my post. If you want to create a post with content, please set\\n annotation: \\\"content.halo.run/content-json\\\" into annotations and refer\\n to Content for corresponding data type.\\n\",\n        \"operationId\": \"CreateMyPost\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}\": {\n      \"get\": {\n        \"description\": \"Get post that belongs to the current user.\",\n        \"operationId\": \"GetMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update my post.\",\n        \"operationId\": \"UpdateMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Post\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/draft\": {\n      \"get\": {\n        \"description\": \"Get my post draft.\",\n        \"operationId\": \"GetMyPostDraft\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Should include patched content and raw or not.\",\n            \"in\": \"query\",\n            \"name\": \"patched\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Update draft of my post. Please make sure set annotation:\\n\\\"content.halo.run/content-json\\\" into annotations and refer to\\nContent for corresponding data type.\\n\",\n        \"operationId\": \"UpdateMyPostDraft\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Snapshot\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/publish\": {\n      \"put\": {\n        \"description\": \"Publish my post.\",\n        \"operationId\": \"PublishMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/recycle\": {\n      \"delete\": {\n        \"description\": \"Move my post to recycle bin.\",\n        \"operationId\": \"RecycleMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/unpublish\": {\n      \"put\": {\n        \"description\": \"Unpublish my post.\",\n        \"operationId\": \"UnpublishMyPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Post name\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Post\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PostV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.content.halo.run/v1alpha1/snapshots/{name}\": {\n      \"get\": {\n        \"description\": \"Get snapshot for one post.\",\n        \"operationId\": \"GetSnapshotForPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Snapshot name.\",\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Post name.\",\n            \"in\": \"query\",\n            \"name\": \"postName\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Should include patched content and raw or not.\",\n            \"in\": \"query\",\n            \"name\": \"patched\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Snapshot\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"SnapshotV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.halo.run/v1alpha1/annotationsettings\": {\n      \"get\": {\n        \"description\": \"List available AnnotationSettings for the given targetRef. The available AnnotationSettings are determined by the currently activated theme and started plugins.\",\n        \"operationId\": \"listAvailableAnnotationSettings\",\n        \"parameters\": [\n          {\n            \"description\": \"The targetRef of the AnnotationSetting. e.g.: \\u0027content.halo.run/Post\",\n            \"in\": \"query\",\n            \"name\": \"targetRef\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/AnnotationSetting\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AnnotationSettingV1AlphaUc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.halo.run/v1alpha1/user-preferences/{group}\": {\n      \"get\": {\n        \"description\": \"Get my preference by group.\",\n        \"operationId\": \"getMyPreference\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of user preference, e.g. `notification`.\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"UserPreferenceV1alpha1Uc\"\n        ]\n      },\n      \"put\": {\n        \"description\": \"Create or update my preference by group.\",\n        \"operationId\": \"updateMyPreference\",\n        \"parameters\": [\n          {\n            \"description\": \"Group of user preference, e.g. `notification`.\",\n            \"in\": \"path\",\n            \"name\": \"group\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"type\": \"object\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content, preference updated successfully.\"\n          }\n        },\n        \"tags\": [\n          \"UserPreferenceV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings\": {\n      \"get\": {\n        \"description\": \"Get Two-factor authentication settings.\",\n        \"operationId\": \"GetTwoFactorAuthenticationSettings\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings/disabled\": {\n      \"put\": {\n        \"description\": \"Disable Two-factor authentication\",\n        \"operationId\": \"DisableTwoFactor\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PasswordRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings/enabled\": {\n      \"put\": {\n        \"description\": \"Enable Two-factor authentication\",\n        \"operationId\": \"EnableTwoFactor\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PasswordRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp\": {\n      \"post\": {\n        \"description\": \"Configure a TOTP\",\n        \"operationId\": \"ConfigurerTotp\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/TotpRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp/-\": {\n      \"delete\": {\n        \"operationId\": \"DeleteTotp\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PasswordRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TwoFactorAuthSettings\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp/auth-link\": {\n      \"get\": {\n        \"description\": \"Get TOTP auth link, including secret\",\n        \"operationId\": \"GetTotpAuthLink\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TotpAuthLinkResponse\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"TwoFactorAuthV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/devices\": {\n      \"get\": {\n        \"description\": \"List all user devices\",\n        \"operationId\": \"ListDevices\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/UserDevice\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/devices/{deviceId}\": {\n      \"delete\": {\n        \"description\": \"Revoke a own device\",\n        \"operationId\": \"RevokeDevice\",\n        \"parameters\": [\n          {\n            \"description\": \"Device ID\",\n            \"in\": \"path\",\n            \"name\": \"deviceId\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204 NO_CONTENT\": {\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"DeviceV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens\": {\n      \"get\": {\n        \"description\": \"Obtain PAT list.\",\n        \"operationId\": \"ObtainPats\",\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                  }\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"description\": \"Generate a PAT.\",\n        \"operationId\": \"GeneratePat\",\n        \"requestBody\": {\n          \"content\": {\n            \"*/*\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PersonalAccessToken\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}\": {\n      \"delete\": {\n        \"description\": \"Delete a PAT\",\n        \"operationId\": \"DeletePat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      },\n      \"get\": {\n        \"description\": \"Obtain a PAT.\",\n        \"operationId\": \"ObtainPat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}/actions/restoration\": {\n      \"put\": {\n        \"description\": \"Restore a PAT.\",\n        \"operationId\": \"RestorePat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}/actions/revocation\": {\n      \"put\": {\n        \"description\": \"Revoke a PAT\",\n        \"operationId\": \"RevokePat\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {},\n        \"tags\": [\n          \"PersonalAccessTokenV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.storage.halo.run/v1alpha1/attachments\": {\n      \"get\": {\n        \"description\": \"List attachments of the current user uploaded.\",\n        \"operationId\": \"ListMyAttachments\",\n        \"parameters\": [\n          {\n            \"description\": \"Page number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"page\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Size number. Default is 0.\",\n            \"in\": \"query\",\n            \"name\": \"size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"description\": \"Label selector. e.g.: hidden!\\u003dtrue\",\n            \"in\": \"query\",\n            \"name\": \"labelSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Field selector. e.g.: metadata.name\\u003d\\u003dhalo\",\n            \"in\": \"query\",\n            \"name\": \"fieldSelector\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\",\n            \"in\": \"query\",\n            \"name\": \"sort\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          {\n            \"description\": \"Filter attachments without group. This parameter will ignore group parameter.\",\n            \"in\": \"query\",\n            \"name\": \"ungrouped\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          },\n          {\n            \"description\": \"Keyword for searching.\",\n            \"in\": \"query\",\n            \"name\": \"keyword\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"description\": \"Acceptable media types.\",\n            \"in\": \"query\",\n            \"name\": \"accepts\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/AttachmentList\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      },\n      \"post\": {\n        \"deprecated\": true,\n        \"description\": \"Create attachment for the given post. Deprecated in favor of /attachments/-/upload.\",\n        \"operationId\": \"CreateAttachmentForPost\",\n        \"parameters\": [\n          {\n            \"description\": \"Wait for permalink.\",\n            \"in\": \"query\",\n            \"name\": \"waitForPermalink\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PostAttachmentRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload\": {\n      \"post\": {\n        \"description\": \"Upload attachment to user center storage.\",\n        \"operationId\": \"UploadAttachmentForUc\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UploadForm\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      }\n    },\n    \"/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload-from-url\": {\n      \"post\": {\n        \"deprecated\": true,\n        \"description\": \"Upload attachment from the given URL.\\nDeprecated in favor of /attachments/-/upload.\",\n        \"operationId\": \"ExternalTransferAttachment_1\",\n        \"parameters\": [\n          {\n            \"description\": \"Wait for permalink.\",\n            \"in\": \"query\",\n            \"name\": \"waitForPermalink\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/UcUploadFromUrlRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"default\": {\n            \"content\": {\n              \"*/*\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Attachment\"\n                }\n              }\n            },\n            \"description\": \"default response\"\n          }\n        },\n        \"tags\": [\n          \"AttachmentV1alpha1Uc\"\n        ]\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"AddOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"add\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"AnnotationSetting\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AnnotationSettingSpec\"\n          }\n        }\n      },\n      \"AnnotationSettingSpec\": {\n        \"required\": [\n          \"formSchema\",\n          \"targetRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"formSchema\": {\n            \"minLength\": 1,\n            \"type\": \"array\",\n            \"items\": {\n              \"minLength\": 1,\n              \"type\": \"object\"\n            }\n          },\n          \"targetRef\": {\n            \"$ref\": \"#/components/schemas/GroupKind\"\n          }\n        }\n      },\n      \"Attachment\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/AttachmentSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/AttachmentStatus\"\n          }\n        }\n      },\n      \"AttachmentList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Attachment\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"AttachmentSpec\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"displayName\": {\n            \"type\": \"string\",\n            \"description\": \"Display name of attachment\"\n          },\n          \"groupName\": {\n            \"type\": \"string\",\n            \"description\": \"Group name\"\n          },\n          \"mediaType\": {\n            \"type\": \"string\",\n            \"description\": \"Media type of attachment\"\n          },\n          \"ownerName\": {\n            \"type\": \"string\",\n            \"description\": \"Name of User who uploads the attachment\"\n          },\n          \"policyName\": {\n            \"type\": \"string\",\n            \"description\": \"Policy name\"\n          },\n          \"size\": {\n            \"minimum\": 0,\n            \"type\": \"integer\",\n            \"description\": \"Size of attachment. Unit is Byte\",\n            \"format\": \"int64\"\n          },\n          \"tags\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"description\": \"Tags of attachment\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"Tag name\"\n            }\n          }\n        }\n      },\n      \"AttachmentStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\",\n            \"description\": \"Permalink of attachment.\\nIf it is in local storage, the public URL will be set.\\nIf it is in s3 storage, the Object URL will be set.\\n\"\n          },\n          \"thumbnails\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"Category\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/CategorySpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/CategoryStatus\"\n          }\n        }\n      },\n      \"CategorySpec\": {\n        \"required\": [\n          \"displayName\",\n          \"priority\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"children\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eWhether to hide the category from the category list.\\u003c/p\\u003e\\n \\u003cp\\u003eWhen set to true, the category including its subcategories and related posts will\\n not be displayed in the category list, but it can still be accessed by permalink.\\u003c/p\\u003e\\n \\u003cp\\u003eLimitation: It only takes effect on the theme-side categorized list and it only\\n allows to be set to true on the first level(root node) of categories.\\u003c/p\\u003e\"\n          },\n          \"postTemplate\": {\n            \"maxLength\": 255,\n            \"type\": \"string\",\n            \"description\": \"\\u003cp\\u003eUsed to specify the template for the posts associated with the category.\\u003c/p\\u003e\\n \\u003cp\\u003eThe priority is not as high as that of the post.\\u003c/p\\u003e\\n \\u003cp\\u003eIf the post also specifies a template, the post\\u0027s template will prevail.\\u003c/p\\u003e\"\n          },\n          \"preventParentPostCascadeQuery\": {\n            \"type\": \"boolean\",\n            \"description\": \"\\u003cp\\u003eif a category is queried for related posts, the default behavior is to\\n query all posts under the category including its subcategories, but if this field is\\n set to true, cascade query behavior will be terminated here.\\u003c/p\\u003e\\n \\u003cp\\u003eFor example, if a category has subcategories A and B, and A has subcategories C and\\n D and C marked this field as true, when querying posts under A category,all posts under A\\n and B will be queried, but C and D will not be queried.\\u003c/p\\u003e\"\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"template\": {\n            \"maxLength\": 255,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CategoryStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"description\": \"包括当前和其下所有层级的已发布且公开的文章数量 (depth\\u003dmax).\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"Condition\": {\n        \"required\": [\n          \"lastTransitionTime\",\n          \"status\",\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"lastTransitionTime\": {\n            \"type\": \"string\",\n            \"description\": \"Last time the condition transitioned from one status to another.\",\n            \"format\": \"date-time\"\n          },\n          \"message\": {\n            \"maxLength\": 32768,\n            \"type\": \"string\",\n            \"description\": \"Human-readable message indicating details about last transition.\\n This may be an empty string.\"\n          },\n          \"reason\": {\n            \"maxLength\": 1024,\n            \"pattern\": \"^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$\",\n            \"type\": \"string\",\n            \"description\": \"Unique, one-word, CamelCase reason for the condition\\u0027s last transition.\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"description\": \"Status is the status of the condition. Can be True, False, Unknown.\",\n            \"enum\": [\n              \"TRUE\",\n              \"FALSE\",\n              \"UNKNOWN\"\n            ]\n          },\n          \"type\": {\n            \"maxLength\": 316,\n            \"pattern\": \"^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$\",\n            \"type\": \"string\",\n            \"description\": \"type of condition in CamelCase or in foo.example.com/CamelCase.\\n example: Ready, Initialized.\\n maxLength: 316.\"\n          }\n        },\n        \"description\": \"EqualsAndHashCode 排除了lastTransitionTime否则失败时，lastTransitionTime 会被更新\\n 导致 equals 为 false，一直被加入队列.\"\n      },\n      \"Contributor\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatar\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"description\": \"Contributor from user.\"\n      },\n      \"CopyOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"copy\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"Device\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\",\n          \"status\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/DeviceSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/DeviceStatus\"\n          }\n        }\n      },\n      \"DeviceSpec\": {\n        \"required\": [\n          \"ipAddress\",\n          \"principalName\",\n          \"sessionId\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"ipAddress\": {\n            \"maxLength\": 129,\n            \"type\": \"string\"\n          },\n          \"lastAccessedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"lastAuthenticatedTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"principalName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"rememberMeSeriesId\": {\n            \"type\": \"string\"\n          },\n          \"sessionId\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"userAgent\": {\n            \"maxLength\": 500,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"DeviceStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"browser\": {\n            \"type\": \"string\"\n          },\n          \"os\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Excerpt\": {\n        \"required\": [\n          \"autoGenerate\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"autoGenerate\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"raw\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"GroupKind\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"is group name of Extension.\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"is kind name of Extension.\"\n          }\n        },\n        \"description\": \"GroupKind contains group and kind data only.\"\n      },\n      \"JsonPatch\": {\n        \"minItems\": 1,\n        \"uniqueItems\": true,\n        \"type\": \"array\",\n        \"description\": \"JSON schema for JSONPatch operations\",\n        \"items\": {\n          \"oneOf\": [\n            {\n              \"$ref\": \"#/components/schemas/AddOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/ReplaceOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/TestOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/RemoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/MoveOperation\"\n            },\n            {\n              \"$ref\": \"#/components/schemas/CopyOperation\"\n            }\n          ]\n        }\n      },\n      \"ListedPost\": {\n        \"required\": [\n          \"categories\",\n          \"contributors\",\n          \"owner\",\n          \"post\",\n          \"stats\",\n          \"tags\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Category\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Contributor\"\n            }\n          },\n          \"owner\": {\n            \"$ref\": \"#/components/schemas/Contributor\"\n          },\n          \"post\": {\n            \"$ref\": \"#/components/schemas/Post\"\n          },\n          \"stats\": {\n            \"$ref\": \"#/components/schemas/Stats\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Tag\"\n            }\n          }\n        },\n        \"description\": \"A chunk of items.\"\n      },\n      \"ListedPostList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ListedPost\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"MarkSpecifiedRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"names\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"Metadata\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"annotations\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Annotations are like key-value format.\"\n          },\n          \"creationTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Creation timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"deletionTimestamp\": {\n            \"type\": \"string\",\n            \"description\": \"Deletion timestamp of the Extension.\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"finalizers\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"nullable\": true,\n            \"items\": {\n              \"type\": \"string\",\n              \"nullable\": true\n            }\n          },\n          \"generateName\": {\n            \"type\": \"string\",\n            \"description\": \"The name field will be generated automatically according to the given generateName field\"\n          },\n          \"labels\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Labels are like key-value format.\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Metadata name\"\n          },\n          \"version\": {\n            \"type\": \"integer\",\n            \"description\": \"Current version of the Extension. It will be bumped up every update.\",\n            \"format\": \"int64\",\n            \"nullable\": true\n          }\n        },\n        \"description\": \"Metadata of Extension.\"\n      },\n      \"MoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"from\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"from\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"move\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"Notification\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/NotificationSpec\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003e{@link Notification Notification} is a custom extension that used to store notification information for\\n inner use, it\\u0027s on-site notification.\\u003c/p\\u003e\\n\\n \\u003cp\\u003eSupports the following operations:\\u003c/p\\u003e\\n \\u003cul\\u003e\\n    \\u003cli\\u003eMarked as read: {@link NotificationSpec#setUnread(boolean) NotificationSpec#setUnread(boolean)}\\u003c/li\\u003e\\n    \\u003cli\\u003eGet the last read time: {@link NotificationSpec#getLastReadAt NotificationSpec#getLastReadAt()}\\u003c/li\\u003e\\n    \\u003cli\\u003eFilter by recipient: {@link NotificationSpec#getRecipient NotificationSpec#getRecipient()}\\u003c/li\\u003e\\n \\u003c/ul\\u003e\"\n      },\n      \"NotificationList\": {\n        \"required\": [\n          \"first\",\n          \"hasNext\",\n          \"hasPrevious\",\n          \"items\",\n          \"last\",\n          \"page\",\n          \"size\",\n          \"total\",\n          \"totalPages\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"first\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the first page.\"\n          },\n          \"hasNext\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"hasPrevious\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page has previous page.\"\n          },\n          \"items\": {\n            \"type\": \"array\",\n            \"description\": \"A chunk of items.\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Notification\"\n            }\n          },\n          \"last\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates whether current page is the last page.\"\n          },\n          \"page\": {\n            \"type\": \"integer\",\n            \"description\": \"Page number, starts from 1. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"size\": {\n            \"type\": \"integer\",\n            \"description\": \"Size of each page. If not set or equal to 0, it means no pagination.\",\n            \"format\": \"int32\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"description\": \"Total elements.\",\n            \"format\": \"int64\"\n          },\n          \"totalPages\": {\n            \"type\": \"integer\",\n            \"description\": \"Indicates total pages.\",\n            \"format\": \"int64\"\n          }\n        }\n      },\n      \"NotificationSpec\": {\n        \"required\": [\n          \"htmlContent\",\n          \"rawContent\",\n          \"reason\",\n          \"recipient\",\n          \"title\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"htmlContent\": {\n            \"type\": \"string\"\n          },\n          \"lastReadAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"rawContent\": {\n            \"type\": \"string\"\n          },\n          \"reason\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"The name of reason\"\n          },\n          \"recipient\": {\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"The name of user\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"unread\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"NotifierInfo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PasswordRequest\": {\n        \"required\": [\n          \"password\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"password\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PatSpec\": {\n        \"required\": [\n          \"name\",\n          \"tokenId\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"expiresAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"lastUsed\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"revoked\": {\n            \"type\": \"boolean\"\n          },\n          \"revokesAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"scopes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"tokenId\": {\n            \"type\": \"string\"\n          },\n          \"username\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PersonalAccessToken\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PatSpec\"\n          }\n        }\n      },\n      \"Post\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/PostSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/PostStatus\"\n          }\n        },\n        \"description\": \"\\u003cp\\u003ePost extension.\\u003c/p\\u003e\"\n      },\n      \"PostAttachmentRequest\": {\n        \"required\": [\n          \"file\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\"\n          },\n          \"postName\": {\n            \"type\": \"string\",\n            \"description\": \"Post name.\"\n          },\n          \"singlePageName\": {\n            \"type\": \"string\",\n            \"description\": \"Single page name.\"\n          }\n        }\n      },\n      \"PostSpec\": {\n        \"required\": [\n          \"allowComment\",\n          \"deleted\",\n          \"excerpt\",\n          \"pinned\",\n          \"priority\",\n          \"publish\",\n          \"slug\",\n          \"title\",\n          \"visible\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"allowComment\": {\n            \"type\": \"boolean\",\n            \"default\": true\n          },\n          \"baseSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"categories\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"deleted\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"excerpt\": {\n            \"$ref\": \"#/components/schemas/Excerpt\"\n          },\n          \"headSnapshot\": {\n            \"type\": \"string\"\n          },\n          \"htmlMetas\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"owner\": {\n            \"type\": \"string\"\n          },\n          \"pinned\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"priority\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\",\n            \"default\": 0\n          },\n          \"publish\": {\n            \"type\": \"boolean\",\n            \"default\": false\n          },\n          \"publishTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"releaseSnapshot\": {\n            \"type\": \"string\",\n            \"description\": \"文章引用到的已发布的内容，用于主题端显示.\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"template\": {\n            \"type\": \"string\"\n          },\n          \"title\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"visible\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"PUBLIC\",\n              \"INTERNAL\",\n              \"PRIVATE\"\n            ],\n            \"default\": \"PUBLIC\"\n          }\n        }\n      },\n      \"PostStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"commentsCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"conditions\": {\n            \"type\": \"array\",\n            \"properties\": {\n              \"empty\": {\n                \"type\": \"boolean\"\n              }\n            },\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Condition\"\n            }\n          },\n          \"contributors\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"excerpt\": {\n            \"type\": \"string\"\n          },\n          \"hideFromList\": {\n            \"type\": \"boolean\",\n            \"description\": \"see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\"\n          },\n          \"inProgress\": {\n            \"type\": \"boolean\"\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"phase\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"ReasonTypeInfo\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"uiPermissions\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"ReasonTypeNotifierCollectionRequest\": {\n        \"required\": [\n          \"reasonTypeNotifiers\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"reasonTypeNotifiers\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonTypeNotifierRequest\"\n            }\n          }\n        }\n      },\n      \"ReasonTypeNotifierMatrix\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"notifiers\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/NotifierInfo\"\n            }\n          },\n          \"reasonTypes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ReasonTypeInfo\"\n            }\n          },\n          \"stateMatrix\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"boolean\"\n              }\n            }\n          }\n        }\n      },\n      \"ReasonTypeNotifierRequest\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"notifiers\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"reasonType\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"Ref\": {\n        \"required\": [\n          \"group\",\n          \"kind\",\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"group\": {\n            \"type\": \"string\",\n            \"description\": \"Extension group\"\n          },\n          \"kind\": {\n            \"type\": \"string\",\n            \"description\": \"Extension kind\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Extension name. This field is mandatory\"\n          },\n          \"version\": {\n            \"type\": \"string\",\n            \"description\": \"Extension version\"\n          }\n        },\n        \"description\": \"Extension reference object. The name is mandatory\"\n      },\n      \"RemoveOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"remove\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          }\n        }\n      },\n      \"ReplaceOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"replace\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"SnapShotSpec\": {\n        \"required\": [\n          \"owner\",\n          \"rawType\",\n          \"subjectRef\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"contentPatch\": {\n            \"type\": \"string\"\n          },\n          \"contributors\": {\n            \"uniqueItems\": true,\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"lastModifyTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"owner\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"parentSnapshotName\": {\n            \"type\": \"string\"\n          },\n          \"rawPatch\": {\n            \"type\": \"string\"\n          },\n          \"rawType\": {\n            \"maxLength\": 50,\n            \"minLength\": 1,\n            \"type\": \"string\",\n            \"description\": \"such as: markdown | html | json | asciidoc | latex.\"\n          },\n          \"subjectRef\": {\n            \"$ref\": \"#/components/schemas/Ref\"\n          }\n        }\n      },\n      \"Snapshot\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/SnapShotSpec\"\n          }\n        }\n      },\n      \"Stats\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"approvedComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"totalComment\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"upvote\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visit\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        },\n        \"description\": \"Stats value object.\"\n      },\n      \"Tag\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/TagSpec\"\n          },\n          \"status\": {\n            \"$ref\": \"#/components/schemas/TagStatus\"\n          }\n        }\n      },\n      \"TagSpec\": {\n        \"required\": [\n          \"displayName\",\n          \"slug\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"color\": {\n            \"pattern\": \"^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$\",\n            \"type\": \"string\",\n            \"description\": \"Color regex explanation.\\n \\u003cpre\\u003e\\n ^                 # start of the line\\n #                 # start with a number sign `#`\\n (                 # start of (group 1)\\n   [a-fA-F0-9]{6}  # support z-f, A-F and 0-9, with a length of 6\\n   |               # or\\n   [a-fA-F0-9]{3}  # support z-f, A-F and 0-9, with a length of 3\\n )                 # end of (group 1)\\n $                 # end of the line\\n \\u003c/pre\\u003e\"\n          },\n          \"cover\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"displayName\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"slug\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TagStatus\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"observedVersion\": {\n            \"type\": \"integer\",\n            \"format\": \"int64\"\n          },\n          \"permalink\": {\n            \"type\": \"string\"\n          },\n          \"postCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"visiblePostCount\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          }\n        }\n      },\n      \"TestOperation\": {\n        \"required\": [\n          \"op\",\n          \"path\",\n          \"value\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"op\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"test\"\n            ]\n          },\n          \"path\": {\n            \"pattern\": \"^(/[^/~]*(~[01][^/~]*)*)*$\",\n            \"type\": \"string\",\n            \"description\": \"A JSON Pointer path pointing to the location to move/copy from.\",\n            \"example\": \"/a/b/c\"\n          },\n          \"value\": {\n            \"description\": \"Value can be any JSON value\"\n          }\n        }\n      },\n      \"TotpAuthLinkResponse\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"authLink\": {\n            \"type\": \"string\",\n            \"description\": \"QR Code with base64 encoded.\",\n            \"format\": \"uri\"\n          },\n          \"rawSecret\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TotpRequest\": {\n        \"required\": [\n          \"code\",\n          \"password\",\n          \"secret\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\"\n          },\n          \"password\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          },\n          \"secret\": {\n            \"minLength\": 1,\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"TwoFactorAuthSettings\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"available\": {\n            \"type\": \"boolean\",\n            \"description\": \"Check if 2FA is available.\"\n          },\n          \"emailVerified\": {\n            \"type\": \"boolean\"\n          },\n          \"enabled\": {\n            \"type\": \"boolean\"\n          },\n          \"totpConfigured\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"UcUploadFromUrlRequest\": {\n        \"required\": [\n          \"url\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"Custom file name\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"format\": \"url\"\n          }\n        }\n      },\n      \"UploadForm\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"description\": \"The file to upload. If not provided, the url will be used.\",\n            \"format\": \"binary\"\n          },\n          \"filename\": {\n            \"type\": \"string\",\n            \"description\": \"The filename to use when uploading from url. If not provided, the filename will be\\n extracted from the url.\"\n          },\n          \"url\": {\n            \"type\": \"string\",\n            \"description\": \"The url to upload from. If not provided, the file will be used.\"\n          }\n        },\n        \"description\": \"Upload form from console. The file and url are mutually exclusive. If both are provided,\\n the file will be used.\"\n      },\n      \"UserConnection\": {\n        \"required\": [\n          \"apiVersion\",\n          \"kind\",\n          \"metadata\",\n          \"spec\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"apiVersion\": {\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"type\": \"string\"\n          },\n          \"metadata\": {\n            \"$ref\": \"#/components/schemas/Metadata\"\n          },\n          \"spec\": {\n            \"$ref\": \"#/components/schemas/UserConnectionSpec\"\n          }\n        },\n        \"description\": \"User connection extension.\"\n      },\n      \"UserConnectionSpec\": {\n        \"required\": [\n          \"providerUserId\",\n          \"registrationId\",\n          \"username\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"providerUserId\": {\n            \"type\": \"string\",\n            \"description\": \"The unique identifier for the user\\u0027s connection to the OAuth provider.\\n for example, the user\\u0027s GitHub id.\"\n          },\n          \"registrationId\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the OAuth provider (e.g. Google, Facebook, Twitter).\"\n          },\n          \"updatedAt\": {\n            \"type\": \"string\",\n            \"description\": \"The time when the user connection was last updated.\",\n            \"format\": \"date-time\"\n          },\n          \"username\": {\n            \"type\": \"string\",\n            \"description\": \"The {@link Metadata#getName Metadata#getName()} of the user associated with the OAuth connection.\"\n          }\n        }\n      },\n      \"UserDevice\": {\n        \"required\": [\n          \"active\",\n          \"currentDevice\",\n          \"device\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"active\": {\n            \"type\": \"boolean\"\n          },\n          \"currentDevice\": {\n            \"type\": \"boolean\"\n          },\n          \"device\": {\n            \"$ref\": \"#/components/schemas/Device\"\n          }\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"basicAuth\": {\n        \"scheme\": \"basic\",\n        \"type\": \"http\"\n      },\n      \"bearerAuth\": {\n        \"bearerFormat\": \"JWT\",\n        \"scheme\": \"bearer\",\n        \"type\": \"http\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "application/build.gradle",
    "content": "import de.undercouch.gradle.tasks.download.Download\nimport org.gradle.crypto.checksum.Checksum\nimport org.springframework.boot.gradle.tasks.bundling.BootBuildImage\nimport org.springframework.boot.gradle.tasks.bundling.BootJar\nimport org.springframework.util.StringUtils\n\nplugins {\n    id 'checkstyle'\n    id 'java'\n    id 'idea'\n    id 'jacoco'\n    alias(libs.plugins.spring.boot)\n    alias(libs.plugins.spring.dependency.management)\n    alias(libs.plugins.git.properties)\n    alias(libs.plugins.undercouch.download)\n    alias(libs.plugins.lombok)\n    alias(libs.plugins.checksum)\n    alias(libs.plugins.springdoc.openapi)\n    alias(libs.plugins.versions)\n}\n\ngroup = 'run.halo.app'\n\ntasks.withType(JavaCompile).configureEach {\n    options.release = 21\n    options.encoding = 'UTF-8'\n}\n\njava {\n    toolchain {\n        languageVersion = JavaLanguageVersion.of(21)\n    }\n}\n\nidea {\n    module {\n        resourceDirs += file(\"../ui/build/dist/\")\n    }\n}\n\ncheckstyle {\n    toolVersion = libs.versions.checkstyle.get()\n    showViolations = false\n    ignoreFailures = false\n}\n\nrepositories {\n    mavenCentral()\n\n    flatDir {\n        dir layout.projectDirectory.dir('libs')\n    }\n}\n\n\nconfigurations {\n    compileOnly {\n        extendsFrom annotationProcessor\n    }\n}\n\nspringBoot {\n    buildInfo {\n        properties {\n            artifact = 'halo'\n            name = 'halo'\n        }\n    }\n}\n\nbootJar {\n    archiveBaseName = 'halo'\n    manifest {\n        attributes 'Implementation-Title': 'Halo Application',\n                'Implementation-Vendor': 'Halo OSS Team'\n    }\n    duplicatesStrategy = DuplicatesStrategy.EXCLUDE\n}\n\ngitProperties {\n    dotGitDirectory = layout.settingsDirectory.dir('.git')\n}\n\ntasks.named('jar') {\n    enabled = false\n}\n\ndependencies {\n    implementation project(':api')\n    implementation libs.r2dbc.migrate.starter\n    // Fix https://github.com/halo-dev/halo/issues/7289\n    // Build from https://github.com/halo-dev/thymeleaf/commit/d23498ea297059deff04ba8c3578de59c73ccf03\n    runtimeOnly ':thymeleaf:3.1.3.RELEASE'\n    runtimeOnly ':thymeleaf-spring6:3.1.3.RELEASE'\n    annotationProcessor platform(project(':platform:application'))\n\n    annotationProcessor \"org.springframework.boot:spring-boot-configuration-processor\"\n    annotationProcessor \"org.springframework:spring-context-indexer\"\n\n    testImplementation 'org.springframework.boot:spring-boot-starter-test'\n    testImplementation 'org.springframework.boot:spring-boot-webtestclient'\n    testImplementation 'org.springframework.security:spring-security-test'\n    testImplementation 'io.projectreactor:reactor-test'\n\n    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'\n\n    annotationProcessor 'com.github.therapi:therapi-runtime-javadoc-scribe'\n\n    // webjars\n    runtimeOnly 'org.webjars.npm:jsencrypt:3.5.4'\n    runtimeOnly 'org.webjars.npm:normalize.css:8.0.1'\n}\n\ntasks.register('createChecksums', Checksum) {\n    dependsOn tasks.named('bootJar')\n    inputFiles.setFrom(layout.buildDirectory.files('libs'))\n    outputDirectory = layout.buildDirectory.dir(\"libs\")\n    checksumAlgorithm = Checksum.Algorithm.SHA256\n}\n\ntasks.register('copyUiDist', Copy) {\n    from project(':ui').layout.buildDirectory.dir('dist')\n    into layout.buildDirectory.dir('resources/main')\n    mustRunAfter ':ui:doBuild'\n}\n\ntasks.named('classes') {\n    dependsOn tasks.named('copyUiDist')\n    mustRunAfter tasks.named('downloadPluginPresets')\n}\n\ntasks.named('build') {\n    dependsOn tasks.named('createChecksums')\n}\n\ntasks.named('test', Test) {\n    useJUnitPlatform()\n    maxHeapSize = '1G'\n    finalizedBy jacocoTestReport\n}\n\ntasks.named('jacocoTestReport', JacocoReport) {\n    reports {\n        xml.required = true\n        html.required = false\n    }\n}\n\ntasks.register('downloadPluginPresets', Download) {\n    def presetPluginUrls = [\n            'https://github.com/halo-dev/plugin-comment-widget/releases/download/v3.0.0/plugin-comment-widget-3.0.0.jar'               : 'plugin-comment-widget.jar',\n            'https://github.com/halo-dev/plugin-search-widget/releases/download/v1.7.0/plugin-search-widget-1.7.0.jar'                 : 'plugin-search-widget.jar',\n            'https://github.com/halo-dev/plugin-sitemap/releases/download/v1.1.2/plugin-sitemap-1.1.2.jar'                             : 'plugin-sitemap.jar',\n            'https://github.com/halo-dev/plugin-feed/releases/download/v1.5.0/plugin-feed-1.5.0.jar'                                   : 'plugin-feed.jar',\n            'https://github.com/halo-sigs/plugin-shiki/releases/download/v1.0.11/plugin-shiki-1.0.11.jar'                              : 'plugin-shiki.jar',\n            'https://github.com/halo-sigs/plugin-editor-hyperlink-card/releases/download/v1.6.0/plugin-editor-hyperlink-card-1.6.0.jar': 'plugin-editor-hyperlink-card.jar',\n\n            // Currently, plugin-app-store is not open source, so we need to download it from the official website.\n            // Please see https://github.com/halo-dev/plugin-app-store/issues/55\n            // https://www.halo.run/store/apps/app-VYJbF/releases/app-release-qafgml3o\n            'https://www.halo.run/store/apps/app-VYJbF/releases/download/app-release-qafgml3o/assets/app-release-qafgml3o-wt4v7err'    : 'appstore.jar',\n    ]\n    src presetPluginUrls.keySet()\n    dest layout.buildDirectory.dir('resources/main/presets/plugins')\n    eachFile { f ->\n        f.name = presetPluginUrls[f.sourceURL.toString()]\n    }\n}\n\nopenApi {\n    outputDir = file(\"$rootDir/api-docs/openapi/v3_0\")\n    groupedApiMappings = [\n            'http://localhost:8091/v3/api-docs/apis_aggregated.api_v1alpha1': 'aggregated.json',\n            'http://localhost:8091/v3/api-docs/apis_public.api_v1alpha1'    : 'apis_public.api_v1alpha1.json',\n            'http://localhost:8091/v3/api-docs/apis_console.api_v1alpha1'   : 'apis_console.api_v1alpha1.json',\n            'http://localhost:8091/v3/api-docs/apis_uc.api_v1alpha1'        : 'apis_uc.api_v1alpha1.json',\n            'http://localhost:8091/v3/api-docs/apis_extension.api_v1alpha1' : 'apis_extension.api_v1alpha1.json',\n    ]\n    customBootRun {\n        args = ['--server.port=8091',\n                '--spring.profiles.active=doc',\n                \"--halo.work-dir=${layout.buildDirectory.get()}/tmp/workdir-for-generating-apidocs\"]\n    }\n}\n\ntasks.named('forkedSpringBootRun') {\n    dependsOn ':api:jar'\n}\n\ntasks.named('generateOpenApiDocs') {\n    outputs.upToDateWhen {\n        false\n    }\n}\n\ndef branchProvider = providers.exec { commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD' }\n        .standardOutput\n        .asText\n        .map { it.trim().replaceAll(\"/\", \"-\") }\ndef abbreviatedIdProvider = providers.exec { commandLine 'git', 'rev-parse', '--short', 'HEAD' }\n        .standardOutput\n        .asText\n        .map { it.trim() }\n\ndef isRelease = project.hasProperty('release') && Boolean.parseBoolean(project.property('release').toString())\ndef repoProvider = providers.provider { isRelease ? 'halo' : 'halo-dev' }\ndef tagProvider = providers.provider {\n    isRelease ? \"${project.version}-cnb\" : abbreviatedIdProvider.map { \"sha-${it}-cnb\" }.get()\n}\ndef branchTagProvider = branchProvider.map { \"${it}-cnb\" }\ndef archiveFileProvider = tasks.named('bootJar', BootJar).<RegularFile> flatMap { it.archiveFile }\n\ntasks.register('publishToGhcr', BootBuildImage) {\n    def ghcrUserProvider = providers.environmentVariable('GHCR_USERNAME')\n    def ghcrTokenProvider = providers.environmentVariable('GHCR_TOKEN')\n\n    group = 'publishing'\n    description = 'Build and publish the Docker image to GitHub Container Registry.'\n    imageName = \"ghcr.io/halo-dev/${repoProvider.get()}:${tagProvider.get()}\"\n    publish = ghcrTokenProvider.map { StringUtils.hasText(it) }.orElse(false)\n    if (!isRelease) {\n        tags.add(\"ghcr.io/halo-dev/${repoProvider.get()}:${branchTagProvider.get()}\")\n    }\n    docker {\n        publishRegistry {\n            username = ghcrUserProvider\n            password = ghcrTokenProvider\n        }\n    }\n    archiveFile.set(archiveFileProvider)\n}\n\ntasks.register('publishToDockerHub', BootBuildImage) {\n    def dockerUserProvider = providers.environmentVariable('DOCKER_USERNAME')\n    def dockerTokenProvider = providers.environmentVariable('DOCKER_TOKEN')\n    group = 'publishing'\n    description = 'Build and publish the Docker image to Docker Hub.'\n    imageName = \"halohub/${repoProvider.get()}:${tagProvider.get()}\"\n    publish = dockerTokenProvider.map { StringUtils.hasText(it) }.orElse(false)\n    if (!isRelease) {\n        tags.add(\"halohub/${repoProvider.get()}:${branchTagProvider.get()}\")\n    }\n    docker {\n        publishRegistry {\n            username = dockerUserProvider\n            password = dockerTokenProvider\n        }\n    }\n    archiveFile.set(archiveFileProvider)\n}\n\ntasks.register('publishToAllRegistries') {\n    group = 'publishing'\n    description = 'Build and publish the Docker image to all configured registries.'\n    dependsOn tasks.named('publishToGhcr'), tasks.named('publishToDockerHub')\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/Application.java",
    "content": "package run.halo.app;\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.builder.SpringApplicationBuilder;\nimport org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;\nimport org.springframework.boot.context.properties.ConfigurationPropertiesScan;\nimport org.springframework.boot.integration.autoconfigure.IntegrationAutoConfiguration;\nimport org.springframework.scheduling.annotation.EnableScheduling;\n\n/**\n * Halo main class.\n *\n * @author ryanwang\n * @author JohnNiang\n * @author guqing\n * @date 2017-11-14\n */\n@EnableScheduling\n@SpringBootApplication(scanBasePackages = \"run.halo.app\", exclude =\n    IntegrationAutoConfiguration.class)\n@ConfigurationPropertiesScan(basePackages = \"run.halo.app.infra.properties\")\npublic class Application {\n\n    public static void main(String[] args) {\n        new SpringApplicationBuilder(Application.class)\n            .applicationStartup(new BufferingApplicationStartup(1024))\n            .run(args);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/AbstractContentService.java",
    "content": "package run.halo.app.content;\n\nimport java.security.Principal;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.UUID;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.Nullable;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\n\n/**\n * Abstract Service for {@link Snapshot}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@AllArgsConstructor\npublic abstract class AbstractContentService {\n\n    private final ReactiveExtensionClient client;\n\n    public Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName) {\n        if (StringUtils.isBlank(snapshotName) || StringUtils.isBlank(baseSnapshotName)) {\n            return Mono.empty();\n        }\n        // TODO: refactor this method to use client.get instead of fetch but please be careful\n        return client.fetch(Snapshot.class, baseSnapshotName)\n            .doOnNext(this::checkBaseSnapshot)\n            .flatMap(baseSnapshot -> {\n                if (StringUtils.equals(snapshotName, baseSnapshotName)) {\n                    var contentWrapper = ContentWrapper.patchSnapshot(baseSnapshot, baseSnapshot);\n                    return Mono.just(contentWrapper);\n                }\n                return client.fetch(Snapshot.class, snapshotName)\n                    .map(snapshot -> ContentWrapper.patchSnapshot(snapshot, baseSnapshot));\n            })\n            .switchIfEmpty(Mono.defer(() -> {\n                log.error(\"The content snapshot [{}] or base snapshot [{}] not found.\",\n                    snapshotName, baseSnapshotName);\n                return Mono.empty();\n            }));\n    }\n\n    protected void checkBaseSnapshot(Snapshot snapshot) {\n        Assert.notNull(snapshot, \"The snapshot must not be null.\");\n        if (!Snapshot.isBaseSnapshot(snapshot)) {\n            throw new IllegalArgumentException(\n                String.format(\"The snapshot [%s] is not a base snapshot.\",\n                    snapshot.getMetadata().getName()));\n        }\n    }\n\n    protected Mono<ContentWrapper> draftContent(@Nullable String baseSnapshotName,\n        ContentRequest contentRequest,\n        @Nullable String parentSnapshotName) {\n        return create(baseSnapshotName, contentRequest, parentSnapshotName)\n            .flatMap(head -> {\n                String baseSnapshotNameToUse =\n                    StringUtils.defaultIfBlank(baseSnapshotName, head.getMetadata().getName());\n                return restoredContent(baseSnapshotNameToUse, head);\n            });\n    }\n\n    protected Mono<ContentWrapper> draftContent(String baseSnapshotName, ContentRequest content) {\n        return this.draftContent(baseSnapshotName, content, content.headSnapshotName());\n    }\n\n    private Mono<Snapshot> create(@Nullable String baseSnapshotName,\n        ContentRequest contentRequest,\n        @Nullable String parentSnapshotName) {\n        Snapshot snapshot = contentRequest.toSnapshot();\n        snapshot.getMetadata().setName(UUID.randomUUID().toString());\n        snapshot.getSpec().setParentSnapshotName(parentSnapshotName);\n\n        return client.fetch(Snapshot.class, baseSnapshotName)\n            .doOnNext(this::checkBaseSnapshot)\n            .defaultIfEmpty(snapshot)\n            .map(baseSnapshot -> determineRawAndContentPatch(snapshot, baseSnapshot,\n                contentRequest)\n            )\n            .flatMap(source -> getContextUsername()\n                .doOnNext(username -> {\n                    Snapshot.addContributor(source, username);\n                    source.getSpec().setOwner(username);\n                })\n                .thenReturn(source)\n            )\n            .flatMap(client::create);\n    }\n\n    protected Mono<ContentWrapper> updateContent(String baseSnapshotName,\n        ContentRequest contentRequest) {\n        Assert.notNull(contentRequest, \"The contentRequest must not be null\");\n        Assert.notNull(baseSnapshotName, \"The baseSnapshotName must not be null\");\n        Assert.notNull(contentRequest.headSnapshotName(), \"The headSnapshotName must not be null\");\n        return Mono.defer(() -> client.fetch(Snapshot.class, contentRequest.headSnapshotName())\n                .flatMap(headSnapshot -> {\n                    var oldVersion = contentRequest.version();\n                    var version = headSnapshot.getMetadata().getVersion();\n                    if (hasConflict(oldVersion, version)) {\n                        // draft a new snapshot as the head snapshot\n                        return create(baseSnapshotName, contentRequest,\n                            contentRequest.headSnapshotName());\n                    }\n                    return Mono.just(headSnapshot);\n                })\n                .flatMap(headSnapshot -> client.fetch(Snapshot.class, baseSnapshotName)\n                    .map(baseSnapshot -> determineRawAndContentPatch(headSnapshot,\n                        baseSnapshot, contentRequest))\n                )\n                .flatMap(headSnapshot -> getContextUsername()\n                    .doOnNext(username -> Snapshot.addContributor(headSnapshot, username))\n                    .thenReturn(headSnapshot)\n                )\n                .flatMap(client::update)\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(throwable -> throwable instanceof OptimisticLockingFailureException))\n            .flatMap(head -> restoredContent(baseSnapshotName, head));\n    }\n\n    protected Flux<Snapshot> listSnapshotsBy(Ref ref) {\n        var snapshotListOptions = new ListOptions();\n        var query = Queries.isNull(\"metadata.deletionTimestamp\")\n            .and(Queries.equal(\"spec.subjectRef\", Snapshot.toSubjectRefKey(ref)));\n        snapshotListOptions.setFieldSelector(FieldSelector.of(query));\n        var sort = Sort.by(\"metadata.creationTimestamp\", \"metadata.name\").descending();\n        return client.listAll(Snapshot.class, snapshotListOptions, sort);\n    }\n\n    boolean hasConflict(Long oldVersion, Long newVersion) {\n        return oldVersion != null && !newVersion.equals(oldVersion);\n    }\n\n    protected Mono<ContentWrapper> restoredContent(String baseSnapshotName, Snapshot headSnapshot) {\n        return client.fetch(Snapshot.class, baseSnapshotName)\n            .doOnNext(this::checkBaseSnapshot)\n            .map(baseSnapshot -> ContentWrapper.patchSnapshot(headSnapshot, baseSnapshot));\n    }\n\n    protected Snapshot determineRawAndContentPatch(Snapshot snapshotToUse,\n        Snapshot baseSnapshot,\n        ContentRequest contentRequest) {\n        Assert.notNull(baseSnapshot, \"The baseSnapshot must not be null.\");\n        Assert.notNull(contentRequest, \"The contentRequest must not be null.\");\n        Assert.notNull(snapshotToUse, \"The snapshotToUse not be null.\");\n        String originalRaw = baseSnapshot.getSpec().getRawPatch();\n        String originalContent = baseSnapshot.getSpec().getContentPatch();\n        String baseSnapshotName = baseSnapshot.getMetadata().getName();\n\n        snapshotToUse.getSpec().setLastModifyTime(Instant.now());\n        // it is the v1 snapshot, set the content directly\n        if (StringUtils.equals(baseSnapshotName,\n            snapshotToUse.getMetadata().getName())) {\n            snapshotToUse.getSpec().setRawPatch(contentRequest.raw());\n            snapshotToUse.getSpec().setContentPatch(contentRequest.content());\n            MetadataUtil.nullSafeAnnotations(snapshotToUse)\n                .put(Snapshot.KEEP_RAW_ANNO, Boolean.TRUE.toString());\n        } else {\n            // otherwise diff a patch based on the v1 snapshot\n            String revisedRaw = contentRequest.rawPatchFrom(originalRaw);\n            String revisedContent = contentRequest.contentPatchFrom(originalContent);\n            snapshotToUse.getSpec().setRawPatch(revisedRaw);\n            snapshotToUse.getSpec().setContentPatch(revisedContent);\n        }\n        return snapshotToUse;\n    }\n\n    protected Mono<String> getContextUsername() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Principal::getName);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/AbstractEventReconciler.java",
    "content": "package run.halo.app.content;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport org.springframework.context.SmartLifecycle;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.infra.InitializationPhase;\n\n/**\n * An abstract class for reconciling events.\n *\n * @author guqing\n * @since 2.15.0\n */\npublic abstract class AbstractEventReconciler<E> implements Reconciler<E>, SmartLifecycle {\n    protected final RequestQueue<E> queue;\n\n    protected final Controller controller;\n\n    protected volatile boolean running = false;\n\n    private final String controllerName;\n\n    protected AbstractEventReconciler(String controllerName) {\n        this.controllerName = controllerName;\n        this.queue = new DefaultQueue<>(Instant::now);\n        this.controller = this.setupWith(null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return new DefaultController<>(\n            controllerName,\n            this,\n            queue,\n            null,\n            Duration.ofMillis(100),\n            Duration.ofMinutes(10)\n        );\n    }\n\n    @Override\n    public void start() {\n        controller.start();\n        running = true;\n    }\n\n    @Override\n    public void stop() {\n        running = false;\n        controller.dispose();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.CONTROLLERS.getPhase();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/CategoryPostCountUpdater.java",
    "content": "package run.halo.app.content;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport com.google.common.collect.Sets;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.event.post.PostDeletedEvent;\nimport run.halo.app.event.post.PostUpdatedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * A class used to update the post count of the category when the post changes.\n *\n * @author guqing\n * @since 2.15.0\n */\n@Component\npublic class CategoryPostCountUpdater\n    extends AbstractEventReconciler<CategoryPostCountUpdater.PostRelatedCategories> {\n\n    protected final ExtensionClient client;\n    private final CategoryPostCountService categoryPostCountService;\n\n    public CategoryPostCountUpdater(ExtensionClient client) {\n        super(CategoryPostCountUpdater.class.getName());\n        this.client = client;\n        this.categoryPostCountService = new CategoryPostCountService(client);\n    }\n\n    @Override\n    public Result reconcile(PostRelatedCategories request) {\n        var categoryChanges = request.categoryChanges();\n\n        categoryPostCountService.recalculatePostCount(categoryChanges);\n\n        client.fetch(Post.class, request.postName()).ifPresent(post -> {\n            var categories = defaultIfNull(post.getSpec().getCategories(), List.<String>of());\n            var annotations = MetadataUtil.nullSafeAnnotations(post);\n            var categoryAnno = JsonUtils.objectToJson(categories);\n            var oldCategoryAnno = annotations.get(Post.LAST_ASSOCIATED_CATEGORIES_ANNO);\n\n            if (!categoryAnno.equals(oldCategoryAnno)) {\n                annotations.put(Post.LAST_ASSOCIATED_CATEGORIES_ANNO, categoryAnno);\n                client.update(post);\n            }\n        });\n        return Result.doNotRetry();\n    }\n\n    static class CategoryPostCountService {\n\n        private final ExtensionClient client;\n\n        public CategoryPostCountService(ExtensionClient client) {\n            this.client = client;\n        }\n\n        public void recalculatePostCount(Collection<String> categoryNames) {\n            for (String categoryName : categoryNames) {\n                recalculatePostCount(categoryName);\n            }\n        }\n\n        public void recalculatePostCount(String categoryName) {\n            var totalPostCount = countTotalPosts(categoryName);\n            var visiblePostCount = countVisiblePosts(categoryName);\n            client.fetch(Category.class, categoryName).ifPresent(category -> {\n                category.getStatusOrDefault().setPostCount(totalPostCount);\n                category.getStatusOrDefault().setVisiblePostCount(visiblePostCount);\n\n                client.update(category);\n            });\n        }\n\n        private int countTotalPosts(String categoryName) {\n            var postListOptions = new ListOptions();\n            postListOptions.setFieldSelector(FieldSelector.of(\n                basePostQuery(categoryName)\n            ));\n            return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))\n                .getTotal();\n        }\n\n        private int countVisiblePosts(String categoryName) {\n            var postListOptions = new ListOptions();\n            var fieldQuery = basePostQuery(categoryName)\n                .and(equal(\"spec.visible\", Post.VisibleEnum.PUBLIC.name()));\n            var labelSelector = LabelSelector.builder()\n                .eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE)\n                .build();\n            postListOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n            postListOptions.setLabelSelector(labelSelector);\n            return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))\n                .getTotal();\n        }\n\n        private static Condition basePostQuery(String categoryName) {\n            return Queries.isNull(\"metadata.deletionTimestamp\")\n                .and(equal(\"spec.deleted\", BooleanUtils.FALSE))\n                .and(equal(\"spec.categories\", categoryName));\n        }\n    }\n\n    public record PostRelatedCategories(String postName, Collection<String> categoryChanges) {\n    }\n\n    @EventListener(PostUpdatedEvent.class)\n    public void onPostUpdated(PostUpdatedEvent event) {\n        var postName = event.getName();\n        var changes = calcCategoriesToUpdate(event.getName());\n        queue.addImmediately(new PostRelatedCategories(postName, changes));\n    }\n\n    @EventListener(PostDeletedEvent.class)\n    public void onPostDeleted(PostDeletedEvent event) {\n        var postName = event.getName();\n        var categories = defaultIfNull(event.getPost().getSpec().getCategories(),\n            List.<String>of());\n        queue.addImmediately(new PostRelatedCategories(postName, categories));\n    }\n\n    private Set<String> calcCategoriesToUpdate(String postName) {\n        return client.fetch(Post.class, postName)\n            .map(post -> {\n                var annotations = MetadataUtil.nullSafeAnnotations(post);\n                var oldCategories =\n                    Optional.ofNullable(annotations.get(Post.LAST_ASSOCIATED_CATEGORIES_ANNO))\n                        .filter(StringUtils::isNotBlank)\n                        .map(categoriesJson -> JsonUtils.jsonToObject(categoriesJson,\n                            String[].class))\n                        .orElse(new String[0]);\n\n                Set<String> categoriesToUpdate = Sets.newHashSet(oldCategories);\n                var newCategories = post.getSpec().getCategories();\n                if (newCategories != null) {\n                    categoriesToUpdate.addAll(newCategories);\n                }\n                return categoriesToUpdate;\n            })\n            .orElse(Set.of());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/CategoryService.java",
    "content": "package run.halo.app.content;\n\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Category;\n\npublic interface CategoryService {\n\n    Flux<Category> listChildren(@NonNull String categoryName);\n\n    Mono<Category> getParentByName(@NonNull String categoryName);\n\n    Mono<Boolean> isCategoryHidden(@NonNull String categoryName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/Content.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\n\npublic record Content(@Schema(requiredMode = REQUIRED) String raw,\n                      @Schema(requiredMode = REQUIRED) String content,\n                      @Schema(requiredMode = REQUIRED) String rawType) {\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/ContentRequest.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.HashMap;\nimport lombok.Builder;\nimport org.apache.commons.lang3.StringUtils;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Ref;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Builder\npublic record ContentRequest(@Schema(requiredMode = REQUIRED) Ref subjectRef,\n                             String headSnapshotName,\n                             @Schema(requiredMode = NOT_REQUIRED) Long version,\n                             @Schema(requiredMode = REQUIRED) String raw,\n                             @Schema(requiredMode = REQUIRED) String content,\n                             @Schema(requiredMode = REQUIRED) String rawType) {\n\n    public Snapshot toSnapshot() {\n        final Snapshot snapshot = new Snapshot();\n\n        Metadata metadata = new Metadata();\n        metadata.setAnnotations(new HashMap<>());\n        snapshot.setMetadata(metadata);\n\n        Snapshot.SnapShotSpec snapShotSpec = new Snapshot.SnapShotSpec();\n        snapShotSpec.setSubjectRef(subjectRef);\n\n        snapShotSpec.setRawType(rawType);\n        snapShotSpec.setRawPatch(StringUtils.defaultString(raw()));\n        snapShotSpec.setContentPatch(StringUtils.defaultString(content()));\n\n        snapshot.setSpec(snapShotSpec);\n        return snapshot;\n    }\n\n    public String rawPatchFrom(String originalRaw) {\n        // originalRaw content from v1\n        return PatchUtils.diffToJsonPatch(originalRaw, this.raw);\n    }\n\n    public String contentPatchFrom(String originalContent) {\n        // originalContent from v1\n        return PatchUtils.diffToJsonPatch(originalContent, this.content);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/ContentUpdateParam.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\n\npublic record ContentUpdateParam(Long version, @Schema(requiredMode = REQUIRED) String raw,\n                                 @Schema(requiredMode = REQUIRED) String content,\n                                 @Schema(requiredMode = REQUIRED) String rawType) {\n\n    public static ContentUpdateParam from(Content content) {\n        return new ContentUpdateParam(null, content.raw(), content.content(),\n            content.rawType());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/Contributor.java",
    "content": "package run.halo.app.content;\n\nimport lombok.Data;\n\n/**\n * Contributor from user.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\npublic class Contributor {\n    private String displayName;\n    private String avatar;\n    private String name;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/ListedPost.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Tag;\n\n/**\n * An aggregate object of {@link Post} and {@link Category}\n * and {@link Tag} and more for post list.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Accessors(chain = true)\npublic class ListedPost {\n\n    @Schema(requiredMode = REQUIRED)\n    private Post post;\n\n    @Schema(requiredMode = REQUIRED)\n    private List<Category> categories;\n\n    @Schema(requiredMode = REQUIRED)\n    private List<Tag> tags;\n\n    @Schema(requiredMode = REQUIRED)\n    private List<Contributor> contributors;\n\n    @Schema(requiredMode = REQUIRED)\n    private Contributor owner;\n\n    @Schema(requiredMode = REQUIRED)\n    private Stats stats;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/ListedSinglePage.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport run.halo.app.core.extension.content.SinglePage;\n\n\n/**\n * An aggregate object of {@link SinglePage} and {@link Contributor} single page list.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Accessors(chain = true)\npublic class ListedSinglePage {\n\n    @Schema(requiredMode = REQUIRED)\n    private SinglePage page;\n\n    @Schema(requiredMode = REQUIRED)\n    private List<Contributor> contributors;\n\n    @Schema(requiredMode = REQUIRED)\n    private Contributor owner;\n\n    @Schema(requiredMode = REQUIRED)\n    private Stats stats;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/ListedSnapshotDto.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Instant;\nimport lombok.Data;\nimport lombok.experimental.Accessors;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.extension.MetadataOperator;\n\n@Data\n@Accessors(chain = true)\npublic class ListedSnapshotDto {\n    @Schema(requiredMode = REQUIRED)\n    private MetadataOperator metadata;\n\n    @Schema(requiredMode = REQUIRED)\n    private Spec spec;\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"ListedSnapshotSpec\")\n    public static class Spec {\n        @Schema(requiredMode = REQUIRED)\n        private String owner;\n\n        private Instant modifyTime;\n    }\n\n    /**\n     * Creates from snapshot.\n     */\n    public static ListedSnapshotDto from(Snapshot snapshot) {\n        return new ListedSnapshotDto()\n            .setMetadata(snapshot.getMetadata())\n            .setSpec(new Spec()\n                .setOwner(snapshot.getSpec().getOwner())\n                .setModifyTime(snapshot.getSpec().getLastModifyTime())\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/NotificationReasonConst.java",
    "content": "package run.halo.app.content;\n\n/**\n * Notification reason constants for content module.\n *\n * @author guqing\n * @since 2.9.0\n */\npublic enum NotificationReasonConst {\n    ;\n    public static final String NEW_COMMENT_ON_POST = \"new-comment-on-post\";\n    public static final String NEW_COMMENT_ON_PAGE = \"new-comment-on-single-page\";\n    public static final String SOMEONE_REPLIED_TO_YOU = \"someone-replied-to-you\";\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/PostContentServiceImpl.java",
    "content": "package run.halo.app.content;\n\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\n\n/**\n * Provides ability to get post content for the specified post.\n *\n * @author guqing\n * @since 2.16.0\n */\n@Component\npublic class PostContentServiceImpl extends AbstractContentService implements PostContentService {\n    private final ReactiveExtensionClient client;\n\n    public PostContentServiceImpl(ReactiveExtensionClient client) {\n        super(client);\n        this.client = client;\n    }\n\n    @Override\n    public Mono<ContentWrapper> getHeadContent(String postName) {\n        return client.get(Post.class, postName)\n            .flatMap(post -> {\n                var headSnapshot = post.getSpec().getHeadSnapshot();\n                return super.getContent(headSnapshot, post.getSpec().getBaseSnapshot());\n            });\n    }\n\n    @Override\n    public Mono<ContentWrapper> getReleaseContent(String postName) {\n        return client.get(Post.class, postName)\n            .flatMap(post -> {\n                var releaseSnapshot = post.getSpec().getReleaseSnapshot();\n                return super.getContent(releaseSnapshot, post.getSpec().getBaseSnapshot());\n            });\n    }\n\n    @Override\n    public Mono<ContentWrapper> getSpecifiedContent(String postName, String snapshotName) {\n        return client.get(Post.class, postName)\n            .flatMap(post -> {\n                var baseSnapshot = post.getSpec().getBaseSnapshot();\n                return super.getContent(snapshotName, baseSnapshot);\n            });\n    }\n\n    @Override\n    public Flux<String> listSnapshots(String postName) {\n        return client.get(Post.class, postName)\n            .flatMapMany(page -> listSnapshotsBy(Ref.of(page)))\n            .map(snapshot -> snapshot.getMetadata().getName());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/PostHideFromListStateUpdater.java",
    "content": "package run.halo.app.content;\n\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.time.Duration;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.event.post.CategoryHiddenStateChangeEvent;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.ReactiveExtensionPaginatedOperator;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * Synchronize the {@link Post.PostStatus#getHideFromList()} state of the post with the category.\n *\n * @author guqing\n * @since 2.17.0\n */\n@Component\npublic class PostHideFromListStateUpdater\n    extends AbstractEventReconciler<CategoryHiddenStateChangeEvent> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private final ReactiveExtensionPaginatedOperator reactiveExtensionPaginatedOperator;\n    private final ReactiveExtensionClient client;\n\n    protected PostHideFromListStateUpdater(ReactiveExtensionClient client,\n        ReactiveExtensionPaginatedOperator reactiveExtensionPaginatedOperator) {\n        super(PostHideFromListStateUpdater.class.getName());\n        this.reactiveExtensionPaginatedOperator = reactiveExtensionPaginatedOperator;\n        this.client = client;\n    }\n\n    @Override\n    public Result reconcile(CategoryHiddenStateChangeEvent request) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            equal(\"spec.categories\", request.getCategoryName())\n        ));\n\n        reactiveExtensionPaginatedOperator.list(Post.class, listOptions)\n            .flatMap(post -> {\n                post.getStatusOrDefault().setHideFromList(request.isHidden());\n                return client.update(post);\n            })\n            .then()\n            .block(BLOCKING_TIMEOUT);\n        return Result.doNotRetry();\n    }\n\n    @EventListener(CategoryHiddenStateChangeEvent.class)\n    public void onApplicationEvent(@NonNull CategoryHiddenStateChangeEvent event) {\n        this.queue.addImmediately(event);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/PostQuery.java",
    "content": "package run.halo.app.content;\n\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static run.halo.app.core.extension.content.Post.PUBLISHED_LABEL;\nimport static run.halo.app.core.extension.content.Post.PostPhase.PENDING_APPROVAL;\nimport static run.halo.app.extension.index.query.Queries.contains;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.or;\nimport static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Optional;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.extension.router.SortableRequest;\n\n/**\n * A query object for {@link Post} list.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class PostQuery extends SortableRequest {\n\n    private final String username;\n\n    public PostQuery(ServerRequest request) {\n        this(request, null);\n    }\n\n    public PostQuery(ServerRequest request, @Nullable String username) {\n        super(request.exchange());\n        this.username = username;\n    }\n\n    @Nullable\n    public String getPublishPhase() {\n        return queryParams.getFirst(\"publishPhase\");\n    }\n\n    @Nullable\n    public String getCategoryWithChildren() {\n        var value = queryParams.getFirst(\"categoryWithChildren\");\n        return StringUtils.defaultIfBlank(value, null);\n    }\n\n    @Nullable\n    public String getKeyword() {\n        return StringUtils.defaultIfBlank(queryParams.getFirst(\"keyword\"), null);\n    }\n\n    /**\n     * Build a list options from the query object.\n     *\n     * @return a list options\n     */\n    @Override\n    public ListOptions toListOptions() {\n        var builder = ListOptions.builder(super.toListOptions());\n\n        Optional.ofNullable(getKeyword())\n            .filter(StringUtils::isNotBlank)\n            .ifPresent(keyword -> builder.andQuery(or(\n                contains(\"status.excerpt\", keyword),\n                contains(\"spec.slug\", keyword),\n                contains(\"spec.title\", keyword)\n            )));\n\n        Optional.ofNullable(getPublishPhase())\n            .filter(StringUtils::isNotBlank)\n            .map(Post.PostPhase::from)\n            .ifPresent(phase -> {\n                if (PENDING_APPROVAL.equals(phase)) {\n                    builder.andQuery(equal(\"status.phase\", phase.name()));\n                }\n                var labelSelector = builder.labelSelector();\n                Optional.of(phase)\n                    .filter(Post.PostPhase.PUBLISHED::equals)\n                    .ifPresentOrElse(\n                        published -> labelSelector.eq(PUBLISHED_LABEL, Boolean.TRUE.toString()),\n                        () -> labelSelector.notEq(PUBLISHED_LABEL, Boolean.TRUE.toString())\n                    );\n            });\n\n        Optional.ofNullable(username)\n            .filter(StringUtils::isNotBlank)\n            .ifPresent(username -> builder.andQuery(equal(\"spec.owner\", username)));\n\n        return builder.build();\n    }\n\n    public static void buildParameters(Builder builder) {\n        IListRequest.buildParameters(builder);\n        builder.parameter(sortParameter())\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"publishPhase\")\n                .description(\"Posts filtered by publish phase.\")\n                .implementation(Post.PostPhase.class)\n                .required(false))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"keyword\")\n                .description(\"Posts filtered by keyword.\")\n                .implementation(String.class)\n                .required(false))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"categoryWithChildren\")\n                .description(\"Posts filtered by category including sub-categories.\")\n                .implementation(String.class)\n                .required(false));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/PostRequest.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.Ref;\n\n/**\n * Post and content data for creating and updating post.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic record PostRequest(@Schema(requiredMode = REQUIRED) @NonNull Post post,\n                          @Schema(requiredMode = REQUIRED) @NonNull ContentUpdateParam content) {\n\n    public ContentRequest contentRequest() {\n        Ref subjectRef = Ref.of(post);\n        return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.version(),\n            content.raw(), content.content(), content.rawType());\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/PostService.java",
    "content": "package run.halo.app.content;\n\nimport java.util.List;\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListResult;\n\n/**\n * Service for {@link Post}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface PostService {\n\n    Mono<ListResult<ListedPost>> listPost(PostQuery query);\n\n    Mono<Post> draftPost(PostRequest postRequest);\n\n    Mono<Post> updatePost(PostRequest postRequest);\n\n    Mono<Post> updateBy(@NonNull Post post);\n\n    Mono<ContentWrapper> getHeadContent(String postName);\n\n    Mono<ContentWrapper> getHeadContent(Post post);\n\n    Mono<ContentWrapper> getReleaseContent(String postName);\n\n    Mono<ContentWrapper> getReleaseContent(Post post);\n\n    Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName);\n\n    Flux<ListedSnapshotDto> listSnapshots(String name);\n\n    Mono<Post> publish(Post post);\n\n    Mono<Post> unpublish(Post post);\n\n    /**\n     * Get post by username.\n     *\n     * @param postName is post name.\n     * @param username is username.\n     * @return full post data or empty.\n     */\n    Mono<Post> getByUsername(String postName, String username);\n\n    Mono<Post> revertToSpecifiedSnapshot(String postName, String snapshotName);\n\n    Mono<ContentWrapper> deleteContent(String postName, String snapshotName);\n\n    Mono<Post> recycleBy(String postName, String username);\n\n    Flux<Category> listCategories(List<String> categories);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/PostSorter.java",
    "content": "package run.halo.app.content;\n\nimport java.time.Instant;\nimport java.util.Comparator;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport org.springframework.util.comparator.Comparators;\nimport run.halo.app.core.extension.content.Post;\n\n/**\n * A sorter for {@link Post}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic enum PostSorter {\n    PUBLISH_TIME,\n    CREATE_TIME;\n\n    static final Function<Post, String> name = post -> post.getMetadata().getName();\n\n    /**\n     * Converts {@link Comparator} from {@link PostSorter} and ascending.\n     *\n     * @param sorter a {@link PostSorter}\n     * @param ascending ascending if true, otherwise descending\n     * @return a {@link Comparator} of {@link Post}\n     */\n    public static Comparator<Post> from(PostSorter sorter, Boolean ascending) {\n        if (Objects.equals(true, ascending)) {\n            return from(sorter);\n        }\n        return from(sorter).reversed();\n    }\n\n    /**\n     * Converts {@link Comparator} from {@link PostSorter}.\n     *\n     * @param sorter a {@link PostSorter}\n     * @return a {@link Comparator} of {@link Post}\n     */\n    public static Comparator<Post> from(PostSorter sorter) {\n        if (sorter == null) {\n            return defaultComparator();\n        }\n        if (CREATE_TIME.equals(sorter)) {\n            Function<Post, Instant> comparatorFunc =\n                post -> post.getMetadata().getCreationTimestamp();\n            return Comparator.comparing(comparatorFunc)\n                .thenComparing(name);\n        }\n\n        if (PUBLISH_TIME.equals(sorter)) {\n            Function<Post, Instant> comparatorFunc =\n                post -> post.getSpec().getPublishTime();\n            return Comparator.comparing(comparatorFunc, Comparators.nullsLow())\n                .thenComparing(name);\n        }\n\n        throw new IllegalArgumentException(\"Unsupported sort value: \" + sorter);\n    }\n\n    static PostSorter convertFrom(String sort) {\n        for (PostSorter sorter : values()) {\n            if (sorter.name().equalsIgnoreCase(sort)) {\n                return sorter;\n            }\n        }\n        return null;\n    }\n\n    static Comparator<Post> defaultComparator() {\n        Function<Post, Instant> createTime =\n            post -> post.getMetadata().getCreationTimestamp();\n        return Comparator.comparing(createTime)\n            .thenComparing(name);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/SinglePageQuery.java",
    "content": "package run.halo.app.content;\n\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static run.halo.app.core.extension.content.Post.PostPhase.PENDING_APPROVAL;\nimport static run.halo.app.core.extension.content.SinglePage.PUBLISHED_LABEL;\nimport static run.halo.app.extension.index.query.Queries.contains;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.in;\nimport static run.halo.app.extension.index.query.Queries.or;\nimport static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Optional;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.extension.router.SortableRequest;\n\n/**\n * Query parameter for {@link SinglePage} list.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class SinglePageQuery extends SortableRequest {\n\n    public SinglePageQuery(ServerRequest request) {\n        super(request.exchange());\n    }\n\n    @Override\n    public ListOptions toListOptions() {\n        var builder = ListOptions.builder(super.toListOptions());\n\n        Optional.ofNullable(queryParams.getFirst(\"keyword\"))\n            .filter(StringUtils::isNotBlank)\n            .ifPresent(keyword -> builder.andQuery(or(\n                contains(\"spec.title\", keyword),\n                contains(\"spec.slug\", keyword),\n                contains(\"status.excerpt\", keyword)\n            )));\n\n        Optional.ofNullable(queryParams.getFirst(\"publishPhase\"))\n            .filter(StringUtils::isNotBlank)\n            .map(Post.PostPhase::from)\n            .ifPresent(phase -> {\n                if (PENDING_APPROVAL.equals(phase)) {\n                    builder.andQuery(equal(\"status.phase\", phase.name()));\n                }\n                var labelSelector = builder.labelSelector();\n                Optional.of(phase)\n                    .filter(Post.PostPhase.PUBLISHED::equals)\n                    .ifPresentOrElse(\n                        published -> labelSelector.eq(PUBLISHED_LABEL, Boolean.TRUE.toString()),\n                        () -> labelSelector.notEq(PUBLISHED_LABEL, Boolean.TRUE.toString())\n                    );\n            });\n\n        Optional.ofNullable(queryParams.getFirst(\"visible\"))\n            .filter(StringUtils::isNotBlank)\n            .map(Post.VisibleEnum::from)\n            .ifPresent(visible -> builder.andQuery(equal(\"spec.visible\", visible.name())));\n\n        Optional.ofNullable(queryParams.get(\"contributor\"))\n            .filter(contributors -> !contributors.isEmpty())\n            .ifPresent(contributors -> builder.andQuery(in(\"status.contributors\", contributors)));\n\n        return builder.build();\n    }\n\n    @Override\n    public Sort getSort() {\n        var sort = super.getSort();\n        var orders = sort.stream()\n            .map(order -> {\n                if (\"creationTimestamp\".equals(order.getProperty())) {\n                    return order.withProperty(\"metadata.creationTimestamp\");\n                }\n                if (\"publishTime\".equals(order.getProperty())) {\n                    return order.withProperty(\"spec.publishTime\");\n                }\n                return order;\n            })\n            .toList();\n        return Sort.by(orders);\n    }\n\n    public static void buildParameters(Builder builder) {\n        IListRequest.buildParameters(builder);\n        builder.parameter(sortParameter())\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"contributor\")\n                .description(\"SinglePages filtered by contributor.\")\n                .implementationArray(String.class)\n                .required(false))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"publishPhase\")\n                .description(\"SinglePages filtered by publish phase.\")\n                .implementation(Post.PostPhase.class)\n                .required(false))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"visible\")\n                .description(\"SinglePages filtered by visibility.\")\n                .implementation(Post.VisibleEnum.class)\n                .required(false))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"keyword\")\n                .description(\"SinglePages filtered by keyword.\")\n                .implementation(String.class)\n                .required(false));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/SinglePageRequest.java",
    "content": "package run.halo.app.content;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.Ref;\n\n/**\n * A request parameter for {@link SinglePage}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic record SinglePageRequest(@Schema(requiredMode = REQUIRED) SinglePage page,\n                                @Schema(requiredMode = REQUIRED) ContentUpdateParam content) {\n\n    public ContentRequest contentRequest() {\n        Ref subjectRef = Ref.of(page);\n        return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.version(),\n            content.raw(), content.content(), content.rawType());\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/SinglePageService.java",
    "content": "package run.halo.app.content;\n\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ListResult;\n\n/**\n * Single page service.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface SinglePageService {\n\n    Mono<ContentWrapper> getHeadContent(String singlePageName);\n\n    Mono<ContentWrapper> getReleaseContent(String singlePageName);\n\n    Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName);\n\n    Flux<ListedSnapshotDto> listSnapshots(String pageName);\n\n    Mono<ListResult<ListedSinglePage>> list(SinglePageQuery listRequest);\n\n    Mono<SinglePage> draft(SinglePageRequest pageRequest);\n\n    Mono<SinglePage> update(SinglePageRequest pageRequest);\n\n    Mono<SinglePage> revertToSpecifiedSnapshot(String pageName, String snapshotName);\n\n    Mono<ContentWrapper> deleteContent(String postName, String snapshotName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/SnapshotService.java",
    "content": "package run.halo.app.content;\n\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Snapshot;\n\npublic interface SnapshotService {\n\n    Mono<Snapshot> getBy(String snapshotName);\n\n    Mono<Snapshot> getPatchedBy(String snapshotName, String baseSnapshotName);\n\n    Mono<Snapshot> patchAndCreate(@NonNull Snapshot snapshot,\n        @Nullable Snapshot baseSnapshot,\n        @NonNull Content content);\n\n    Mono<Snapshot> patchAndUpdate(@NonNull Snapshot snapshot,\n        @NonNull Snapshot baseSnapshot,\n        @NonNull Content content);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/Stats.java",
    "content": "package run.halo.app.content;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n/**\n * Stats value object.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\npublic class Stats {\n\n    private Integer visit;\n\n    private Integer upvote;\n\n    private Integer totalComment;\n\n    private Integer approvedComment;\n\n    public Stats() {\n    }\n\n    @Builder\n    public Stats(Integer visit, Integer upvote, Integer totalComment, Integer approvedComment) {\n        this.visit = visit;\n        this.upvote = upvote;\n        this.totalComment = totalComment;\n        this.approvedComment = approvedComment;\n    }\n\n    public static Stats empty() {\n        return Stats.builder()\n            .visit(0)\n            .upvote(0)\n            .totalComment(0)\n            .approvedComment(0)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/AbstractCommentService.java",
    "content": "package run.halo.app.content.comment;\n\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport org.jsoup.Jsoup;\nimport org.jsoup.safety.Safelist;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\n@RequiredArgsConstructor\npublic abstract class AbstractCommentService {\n    protected final RoleService roleService;\n    protected final ReactiveExtensionClient client;\n    protected final UserService userService;\n    protected final CounterService counterService;\n    private final Safelist safelist = Safelist.relaxed()\n        // Allow <s> tag, which is used for strikethrough\n        .addTags(\"s\")\n        // Allow <code> tag's class attribute, for syntax highlighting\n        .addAttributes(\"code\", \"class\")\n        // Allow <a> tag's target attribute\n        .addAttributes(\"a\", \"target\")\n        .preserveRelativeLinks(true);\n\n    protected Mono<User> fetchCurrentUser() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(securityContext -> securityContext.getAuthentication().getName())\n            .flatMap(username -> client.fetch(User.class, username));\n    }\n\n    Mono<Boolean> hasCommentManagePermission() {\n        return ReactiveSecurityContextHolder.getContext()\n            .flatMap(securityContext -> {\n                var authentication = securityContext.getAuthentication();\n                var roles = AuthorityUtils.authoritiesToRoles(authentication.getAuthorities());\n                return roleService.contains(roles,\n                    Set.of(AuthorityUtils.COMMENT_MANAGEMENT_ROLE_NAME));\n            });\n    }\n\n    protected Comment.CommentOwner toCommentOwner(User user) {\n        Comment.CommentOwner owner = new Comment.CommentOwner();\n        owner.setKind(User.KIND);\n        owner.setName(user.getMetadata().getName());\n        owner.setDisplayName(user.getSpec().getDisplayName());\n        return owner;\n    }\n\n    protected Mono<OwnerInfo> getOwnerInfo(Comment.CommentOwner owner) {\n        if (User.KIND.equals(owner.getKind())) {\n            return userService.getUserOrGhost(owner.getName())\n                .map(OwnerInfo::from);\n        }\n        if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {\n            return Mono.just(OwnerInfo.from(owner));\n        }\n        return Mono.error(new IllegalStateException(\"Unsupported owner kind: \" + owner.getKind()));\n    }\n\n    protected Mono<CommentStats> fetchCommentStats(String commentName) {\n        return this.fetchStats(MeterUtils.nameOf(Comment.class, commentName));\n    }\n\n    protected Mono<CommentStats> fetchReplyStats(String replyName) {\n        return this.fetchStats(MeterUtils.nameOf(Reply.class, replyName));\n    }\n\n    private Mono<CommentStats> fetchStats(String meterName) {\n        Assert.notNull(meterName, \"The reply must not be null.\");\n        return counterService.getByName(meterName)\n            .map(counter -> CommentStats.builder()\n                .upvote(counter.getUpvote())\n                .build()\n            )\n            .switchIfEmpty(Mono.fromSupplier(CommentStats::empty));\n    }\n\n    /**\n     * Check if the given html is a safe HTML.\n     *\n     * @param html html content\n     * @return true if the html is safe, false otherwise\n     */\n    protected boolean isSafeHtml(@NonNull String html) {\n        return Jsoup.isValid(html, safelist);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/CommentEmailOwner.java",
    "content": "package run.halo.app.content.comment;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.Comment;\n\n/**\n * <p>The creator info of the comment.</p>\n * This {@link CommentEmailOwner} is only applicable to the user who is allowed to comment\n * without logging in.\n *\n * @param email email for comment owner\n * @param avatar avatar for comment owner\n * @param displayName display name for comment owner\n * @param website website for comment owner\n */\npublic record CommentEmailOwner(String email, String avatar, String displayName, String website) {\n\n    public CommentEmailOwner {\n        Assert.hasText(displayName, \"The 'displayName' must not be empty.\");\n    }\n\n    /**\n     * Converts {@link CommentEmailOwner} to {@link Comment.CommentOwner}.\n     *\n     * @return a comment owner\n     */\n    public Comment.CommentOwner toCommentOwner() {\n        Comment.CommentOwner commentOwner = new Comment.CommentOwner();\n        commentOwner.setKind(Comment.CommentOwner.KIND_EMAIL);\n        // email nullable\n        commentOwner.setName(StringUtils.defaultString(email));\n\n        commentOwner.setDisplayName(displayName);\n        Map<String, String> annotations = new LinkedHashMap<>();\n        commentOwner.setAnnotations(annotations);\n        annotations.put(Comment.CommentOwner.AVATAR_ANNO, avatar);\n        annotations.put(Comment.CommentOwner.WEBSITE_ANNO, website);\n        return commentOwner;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/CommentNotificationReasonPublisher.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.apache.commons.lang3.StringUtils.defaultIfBlank;\nimport static run.halo.app.content.comment.ReplyNotificationSubscriptionHelper.identityFrom;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.Builder;\nimport lombok.RequiredArgsConstructor;\nimport lombok.experimental.UtilityClass;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.event.post.CommentCreatedEvent;\nimport run.halo.app.event.post.ReplyCreatedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.ExternalLinkProcessor;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.notification.NotificationReasonEmitter;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Notification reason publisher for {@link Comment} and {@link Reply}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@Component\n@RequiredArgsConstructor\npublic class CommentNotificationReasonPublisher {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private static final GroupVersionKind POST_GVK = GroupVersionKind.fromExtension(Post.class);\n    private static final GroupVersionKind PAGE_GVK =\n        GroupVersionKind.fromExtension(SinglePage.class);\n\n    private final ExtensionClient client;\n    private final NewCommentOnPostReasonPublisher newCommentOnPostReasonPublisher;\n    private final NewCommentOnPageReasonPublisher newCommentOnPageReasonPublisher;\n    private final NewReplyReasonPublisher newReplyReasonPublisher;\n\n    /**\n     * On new comment.\n     */\n    @Async\n    @EventListener(CommentCreatedEvent.class)\n    public void onNewComment(CommentCreatedEvent event) {\n        Comment comment = event.getComment();\n        if (isPostComment(comment)) {\n            newCommentOnPostReasonPublisher.publishReasonBy(comment);\n        } else if (isPageComment(comment)) {\n            newCommentOnPageReasonPublisher.publishReasonBy(comment);\n        }\n    }\n\n    /**\n     * On new reply.\n     */\n    @Async\n    @EventListener(ReplyCreatedEvent.class)\n    public void onNewReply(ReplyCreatedEvent event) {\n        Reply reply = event.getReply();\n        var commentName = reply.getSpec().getCommentName();\n        client.fetch(Comment.class, commentName)\n            .ifPresent(comment -> newReplyReasonPublisher.publishReasonBy(reply, comment));\n    }\n\n    boolean isPostComment(Comment comment) {\n        return Ref.groupKindEquals(comment.getSpec().getSubjectRef(), POST_GVK);\n    }\n\n    boolean isPageComment(Comment comment) {\n        return Ref.groupKindEquals(comment.getSpec().getSubjectRef(), PAGE_GVK);\n    }\n\n    /**\n     * Comment content converter, convert relative links to absolute links.\n     */\n    @Component\n    @RequiredArgsConstructor\n    static class CommentContentConverter {\n        private final ExternalLinkProcessor externalLinkProcessor;\n\n        /**\n         * Convert relative links to absolute links.\n         *\n         * @param content the content to convert\n         * @return the converted content\n         */\n        public String convertRelativeLinks(String content) {\n            Document parse = Jsoup.parse(content);\n            parse.select(\"img\").forEach(element -> {\n                var src = element.attr(\"src\");\n                element.attr(\"src\",  externalLinkProcessor.processLink(src));\n            });\n            return parse.body().html();\n        }\n    }\n\n    @Component\n    @RequiredArgsConstructor\n    static class NewCommentOnPostReasonPublisher {\n\n        private final ExtensionClient client;\n        private final NotificationReasonEmitter notificationReasonEmitter;\n        private final ExternalLinkProcessor externalLinkProcessor;\n        private final CommentContentConverter commentContentConverter;\n\n        public void publishReasonBy(Comment comment) {\n            Ref subjectRef = comment.getSpec().getSubjectRef();\n            Post post = client.fetch(Post.class, subjectRef.getName()).orElseThrow();\n            if (doNotEmitReason(comment, post)) {\n                return;\n            }\n\n            String postUrl =\n                externalLinkProcessor.processLink(post.getStatusOrDefault().getPermalink());\n            var reasonSubject = Reason.Subject.builder()\n                .apiVersion(post.getApiVersion())\n                .kind(post.getKind())\n                .name(subjectRef.getName())\n                .title(post.getSpec().getTitle())\n                .url(postUrl)\n                .build();\n            Comment.CommentOwner owner = comment.getSpec().getOwner();\n            notificationReasonEmitter.emit(NotificationReasonConst.NEW_COMMENT_ON_POST,\n                builder -> {\n                    var attributes = CommentOnPostReasonData.builder()\n                        .postName(subjectRef.getName())\n                        .postOwner(post.getSpec().getOwner())\n                        .postTitle(post.getSpec().getTitle())\n                        .postUrl(postUrl)\n                        .commenter(owner.getDisplayName())\n                        .content(commentContentConverter.convertRelativeLinks(\n                            comment.getSpec().getContent()))\n                        .commentName(comment.getMetadata().getName())\n                        .build();\n                    builder.attributes(ReasonDataConverter.toAttributeMap(attributes))\n                        .author(identityFrom(owner))\n                        .subject(reasonSubject);\n                }).block(BLOCKING_TIMEOUT);\n        }\n\n        boolean doNotEmitReason(Comment comment, Post post) {\n            Comment.CommentOwner commentOwner = comment.getSpec().getOwner();\n            return isPostOwner(post, commentOwner);\n        }\n\n        boolean isPostOwner(Post post, Comment.CommentOwner commentOwner) {\n            String kind = commentOwner.getKind();\n            String name = commentOwner.getName();\n            var postOwner = post.getSpec().getOwner();\n            if (Comment.CommentOwner.KIND_EMAIL.equals(kind)) {\n                return client.fetch(User.class, postOwner)\n                    .filter(user -> name.equals(user.getSpec().getEmail()))\n                    .isPresent();\n            }\n            return name.equals(postOwner);\n        }\n\n        @Builder\n        record CommentOnPostReasonData(String postName, String postOwner, String postTitle,\n                                       String postUrl, String commenter, String content,\n                                       String commentName) {\n        }\n    }\n\n    @Component\n    @RequiredArgsConstructor\n    static class NewCommentOnPageReasonPublisher {\n        private final ExtensionClient client;\n        private final NotificationReasonEmitter notificationReasonEmitter;\n        private final ExternalLinkProcessor externalLinkProcessor;\n        private final CommentContentConverter commentContentConverter;\n\n        public void publishReasonBy(Comment comment) {\n            Ref subjectRef = comment.getSpec().getSubjectRef();\n            var singlePage = client.fetch(SinglePage.class, subjectRef.getName()).orElseThrow();\n\n            if (doNotEmitReason(comment, singlePage)) {\n                return;\n            }\n\n            var pageUrl = externalLinkProcessor\n                .processLink(singlePage.getStatusOrDefault().getPermalink());\n\n            var reasonSubject = Reason.Subject.builder()\n                .apiVersion(singlePage.getApiVersion())\n                .kind(singlePage.getKind())\n                .name(subjectRef.getName())\n                .title(singlePage.getSpec().getTitle())\n                .url(pageUrl)\n                .build();\n\n            Comment.CommentOwner owner = comment.getSpec().getOwner();\n            notificationReasonEmitter.emit(NotificationReasonConst.NEW_COMMENT_ON_PAGE,\n                builder -> {\n                    var attributes = CommentOnPageReasonData.builder()\n                        .pageName(subjectRef.getName())\n                        .pageOwner(singlePage.getSpec().getOwner())\n                        .pageTitle(singlePage.getSpec().getTitle())\n                        .pageUrl(pageUrl)\n                        .commenter(defaultIfBlank(owner.getDisplayName(), owner.getName()))\n                        .content(commentContentConverter.convertRelativeLinks(\n                            comment.getSpec().getContent()))\n                        .commentName(comment.getMetadata().getName())\n                        .build();\n                    builder.attributes(ReasonDataConverter.toAttributeMap(attributes))\n                        .author(identityFrom(owner))\n                        .subject(reasonSubject);\n                }).block(BLOCKING_TIMEOUT);\n        }\n\n        public boolean doNotEmitReason(Comment comment, SinglePage page) {\n            Comment.CommentOwner commentOwner = comment.getSpec().getOwner();\n            return isPageOwner(page, commentOwner);\n        }\n\n        boolean isPageOwner(SinglePage page, Comment.CommentOwner commentOwner) {\n            String kind = commentOwner.getKind();\n            String name = commentOwner.getName();\n            var pageOwner = page.getSpec().getOwner();\n            if (Comment.CommentOwner.KIND_EMAIL.equals(kind)) {\n                return client.fetch(User.class, pageOwner)\n                    .filter(user -> name.equals(user.getSpec().getEmail()))\n                    .isPresent();\n            }\n            return name.equals(pageOwner);\n        }\n\n        @Builder\n        record CommentOnPageReasonData(String pageName, String pageOwner, String pageTitle,\n                                       String pageUrl, String commenter, String content,\n                                       String commentName) {\n        }\n    }\n\n    @UtilityClass\n    static class ReasonDataConverter {\n        public static <T> Map<String, Object> toAttributeMap(T data) {\n            Assert.notNull(data, \"Reason attributes must not be null\");\n            return JsonUtils.mapper().convertValue(data, new TypeReference<>() {\n            });\n        }\n    }\n\n    @Component\n    @RequiredArgsConstructor\n    static class NewReplyReasonPublisher {\n        private final ExtensionClient client;\n        private final NotificationReasonEmitter notificationReasonEmitter;\n        private final ExtensionGetter extensionGetter;\n        private final CommentContentConverter commentContentConverter;\n\n        public void publishReasonBy(Reply reply, Comment comment) {\n            boolean isQuoteReply = StringUtils.isNotBlank(reply.getSpec().getQuoteReply());\n\n            Optional<Reply> quoteReplyOptional = Optional.of(isQuoteReply)\n                .filter(Boolean::booleanValue)\n                .flatMap(isQuote -> client.fetch(Reply.class, reply.getSpec().getQuoteReply()));\n\n            if (doNotEmitReason(reply, quoteReplyOptional.orElse(null), comment)) {\n                return;\n            }\n\n            var reasonSubject = quoteReplyOptional\n                .map(quoteReply -> Subscription.ReasonSubject.builder()\n                    .apiVersion(quoteReply.getApiVersion())\n                    .kind(quoteReply.getKind())\n                    .name(quoteReply.getMetadata().getName())\n                    .build()\n                )\n                .orElseGet(() -> Subscription.ReasonSubject.builder()\n                    .apiVersion(comment.getApiVersion())\n                    .kind(comment.getKind())\n                    .name(comment.getMetadata().getName())\n                    .build()\n                );\n\n            var reasonSubjectTitle = quoteReplyOptional\n                .map(quoteReply -> quoteReply.getSpec().getContent())\n                .orElse(comment.getSpec().getContent());\n\n            var quoteReplyContent = quoteReplyOptional\n                .map(quoteReply -> commentContentConverter\n                    .convertRelativeLinks(quoteReply.getSpec().getContent()))\n                .orElse(null);\n            var replyOwner = reply.getSpec().getOwner();\n\n            var repliedOwner = quoteReplyOptional\n                .map(quoteReply -> quoteReply.getSpec().getOwner())\n                .orElseGet(() -> comment.getSpec().getOwner());\n\n            var reasonAttributesBuilder = NewReplyReasonData.builder()\n                .commentContent(\n                    commentContentConverter.convertRelativeLinks(comment.getSpec().getContent())\n                )\n                .isQuoteReply(isQuoteReply)\n                .quoteContent(quoteReplyContent)\n                .commentName(comment.getMetadata().getName())\n                .replier(defaultIfBlank(replyOwner.getDisplayName(), replyOwner.getName()))\n                .content(commentContentConverter.convertRelativeLinks(reply.getSpec().getContent()))\n                .replyName(reply.getMetadata().getName())\n                .replyOwner(identityFrom(replyOwner).name())\n                .repliedOwner(identityFrom(repliedOwner).name());\n\n            getCommentSubjectDisplay(comment.getSpec().getSubjectRef())\n                .ifPresent(subject -> {\n                    reasonAttributesBuilder.commentSubjectTitle(subject.title());\n                    reasonAttributesBuilder.commentSubjectUrl(subject.url());\n                });\n\n            notificationReasonEmitter.emit(NotificationReasonConst.SOMEONE_REPLIED_TO_YOU,\n                builder -> {\n                    var data = ReasonDataConverter.toAttributeMap(reasonAttributesBuilder.build());\n                    builder.attributes(data)\n                        .author(identityFrom(replyOwner))\n                        .subject(Reason.Subject.builder()\n                            .apiVersion(reasonSubject.getApiVersion())\n                            .kind(reasonSubject.getKind())\n                            .name(reasonSubject.getName())\n                            .title(reasonSubjectTitle)\n                            .build());\n                }).block(BLOCKING_TIMEOUT);\n        }\n\n        /**\n         * To be compatible with older versions, it may be empty, so use optional.\n         */\n        @SuppressWarnings(\"unchecked\")\n        Optional<CommentSubject.SubjectDisplay> getCommentSubjectDisplay(Ref ref) {\n            return extensionGetter.getExtensions(CommentSubject.class)\n                .filter(commentSubject -> commentSubject.supports(ref))\n                .next()\n                .flatMap(subject -> subject.getSubjectDisplay(ref.getName()))\n                .blockOptional(BLOCKING_TIMEOUT);\n        }\n\n        boolean doNotEmitReason(Reply currentReply, Reply quoteReply, Comment comment) {\n            boolean isQuoteReply = StringUtils.isNotBlank(currentReply.getSpec().getQuoteReply());\n\n            if (isQuoteReply && quoteReply == null) {\n                throw new IllegalArgumentException(\n                    \"quoteReply can not be null when currentReply is reply to quote\");\n            }\n\n            Comment.CommentOwner commentOwner = isQuoteReply ? quoteReply.getSpec().getOwner()\n                : comment.getSpec().getOwner();\n\n            var currentReplyOwner = currentReply.getSpec().getOwner();\n            // reply to oneself do not emit reason\n            return currentReplyOwner.getKind().equals(commentOwner.getKind())\n                && currentReplyOwner.getName().equals(commentOwner.getName());\n        }\n\n        @Builder\n        record NewReplyReasonData(String commentContent, String commentSubjectTitle,\n                                  String commentSubjectUrl, boolean isQuoteReply,\n                                  String quoteContent,\n                                  String commentName, String replier, String content,\n                                  String replyName, String replyOwner, String repliedOwner) {\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/CommentQuery.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springframework.data.domain.Sort.Order.desc;\nimport static run.halo.app.extension.index.query.Queries.contains;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Optional;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.extension.router.QueryParamBuildUtil;\nimport run.halo.app.extension.router.SortableRequest;\n\n/**\n * Query criteria for comment list.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class CommentQuery extends SortableRequest {\n\n    public CommentQuery(ServerRequest request) {\n        super(request.exchange());\n    }\n\n    @Nullable\n    public String getKeyword() {\n        return queryParams.getFirst(\"keyword\");\n    }\n\n    @Nullable\n    public String getOwnerKind() {\n        return queryParams.getFirst(\"ownerKind\");\n    }\n\n    @Nullable\n    public String getOwnerName() {\n        return queryParams.getFirst(\"ownerName\");\n    }\n\n    @Override\n    public Sort getSort() {\n        // set default sort by last reply time\n        return super.getSort().and(Sort.by(desc(\"status.lastReplyTime\")));\n    }\n\n    /**\n     * Convert to list options.\n     */\n    @Override\n    public ListOptions toListOptions() {\n        var builder = ListOptions.builder(super.toListOptions());\n\n        Optional.ofNullable(getKeyword())\n            .filter(StringUtils::isNotBlank)\n            .ifPresent(keyword -> builder.andQuery(contains(\"spec.raw\", keyword)));\n\n        Optional.ofNullable(getOwnerName())\n            .filter(StringUtils::isNotBlank)\n            .ifPresent(ownerName -> {\n                var ownerKind = Optional.ofNullable(getOwnerKind())\n                    .filter(StringUtils::isNotBlank)\n                    .orElse(User.KIND);\n                builder.andQuery(\n                    equal(\"spec.owner\", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName))\n                );\n            });\n\n        return builder.build();\n    }\n\n    public static void buildParameters(Builder builder) {\n        IListRequest.buildParameters(builder);\n        builder.parameter(QueryParamBuildUtil.sortParameter())\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"keyword\")\n                .description(\"Comments filtered by keyword.\")\n                .implementation(String.class))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"ownerKind\")\n                .description(\"Commenter kind.\")\n                .implementation(String.class))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"ownerName\")\n                .description(\"Commenter name.\")\n                .implementation(String.class));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/CommentRequest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.UUID;\nimport lombok.Data;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Ref;\n\n/**\n * Request parameter object for {@link Comment}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\npublic class CommentRequest {\n\n    @Schema(requiredMode = REQUIRED)\n    private Ref subjectRef;\n\n    private CommentEmailOwner owner;\n\n    @Schema(requiredMode = REQUIRED, minLength = 1)\n    private String raw;\n\n    @Schema(requiredMode = REQUIRED, minLength = 1)\n    private String content;\n\n    @Schema(defaultValue = \"false\")\n    private Boolean allowNotification;\n\n    @Schema(defaultValue = \"false\")\n    private Boolean hidden;\n\n    /**\n     * Converts {@link CommentRequest} to {@link Comment}.\n     *\n     * @return a comment\n     */\n    public Comment toComment() {\n        Comment comment = new Comment();\n        comment.setMetadata(new Metadata());\n        comment.getMetadata().setName(UUID.randomUUID().toString());\n\n        Comment.CommentSpec spec = new Comment.CommentSpec();\n        comment.setSpec(spec);\n        spec.setSubjectRef(subjectRef);\n        spec.setRaw(raw);\n        spec.setContent(content);\n        spec.setAllowNotification(allowNotification);\n        spec.setHidden(hidden);\n\n        if (owner != null) {\n            spec.setOwner(owner.toCommentOwner());\n        }\n        return comment;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/CommentService.java",
    "content": "package run.halo.app.content.comment;\n\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Ref;\n\n/**\n * An application service for {@link Comment}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface CommentService {\n\n    Mono<ListResult<ListedComment>> listComment(CommentQuery query);\n\n    Mono<Comment> create(Comment comment);\n\n    Mono<Void> removeBySubject(@NonNull Ref subjectRef);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java",
    "content": "package run.halo.app.content.comment;\n\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.function.Function;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.exception.AccessDeniedException;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Comment service implementation.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class CommentServiceImpl extends AbstractCommentService implements CommentService {\n\n    private final ExtensionGetter extensionGetter;\n    private final SystemConfigFetcher environmentFetcher;\n\n    public CommentServiceImpl(RoleService roleService, ReactiveExtensionClient client,\n        UserService userService, CounterService counterService, ExtensionGetter extensionGetter,\n        SystemConfigFetcher environmentFetcher) {\n        super(roleService, client, userService, counterService);\n        this.extensionGetter = extensionGetter;\n        this.environmentFetcher = environmentFetcher;\n    }\n\n    @Override\n    public Mono<ListResult<ListedComment>> listComment(CommentQuery commentQuery) {\n        return this.client.listBy(Comment.class, commentQuery.toListOptions(),\n                commentQuery.toPageRequest())\n            .flatMap(comments -> Flux.fromStream(comments.get()\n                    .map(this::toListedComment))\n                .flatMapSequential(Function.identity())\n                .collectList()\n                .map(list -> new ListResult<>(comments.getPage(), comments.getSize(),\n                    comments.getTotal(), list)\n                )\n            );\n    }\n\n    @Override\n    public Mono<Comment> create(Comment comment) {\n        if (comment.getSpec() == null\n            || comment.getSpec().getContent() == null\n            || !isSafeHtml(comment.getSpec().getContent())) {\n            return Mono.error(new ServerWebInputException(\"\"\"\n                The content of comment must not be empty or contains unsafe HTML.\\\n                \"\"\"));\n        }\n        return environmentFetcher.fetchComment()\n            .flatMap(commentSetting -> {\n                if (Boolean.FALSE.equals(commentSetting.getEnable())) {\n                    return Mono.error(\n                        new AccessDeniedException(\"The comment function has been turned off.\",\n                            \"problemDetail.comment.turnedOff\", null));\n                }\n                if (checkCommentOwner(comment, commentSetting.getSystemUserOnly())) {\n                    return Mono.error(\n                        new AccessDeniedException(\"Allow only system users to comment.\",\n                            \"problemDetail.comment.systemUsersOnly\", null));\n                }\n\n                if (comment.getSpec().getTop() == null) {\n                    comment.getSpec().setTop(false);\n                }\n                if (comment.getSpec().getPriority() == null) {\n                    comment.getSpec().setPriority(0);\n                }\n                comment.getSpec()\n                    .setApproved(Boolean.FALSE.equals(commentSetting.getRequireReviewForNew()));\n\n                if (BooleanUtils.isTrue(comment.getSpec().getApproved())\n                    && comment.getSpec().getApprovedTime() == null) {\n                    comment.getSpec().setApprovedTime(Instant.now());\n                }\n\n                if (comment.getSpec().getCreationTime() == null) {\n                    comment.getSpec().setCreationTime(Instant.now());\n                }\n\n                if (comment.getSpec().getHidden() == null) {\n                    comment.getSpec().setHidden(false);\n                }\n\n                return Mono.just(comment);\n            })\n            .flatMap(populatedComment -> Mono.when(populateOwner(populatedComment),\n                    populateApproveState(populatedComment))\n                .thenReturn(populatedComment)\n            )\n            .flatMap(client::create);\n    }\n\n    private Mono<Void> populateApproveState(Comment comment) {\n        return hasCommentManagePermission()\n            .filter(Boolean::booleanValue)\n            .doOnNext(hasPermission -> {\n                comment.getSpec().setApproved(true);\n                comment.getSpec().setApprovedTime(Instant.now());\n            })\n            .then();\n    }\n\n    Mono<Void> populateOwner(Comment comment) {\n        if (comment.getSpec().getOwner() != null) {\n            return Mono.empty();\n        }\n        return fetchCurrentUser()\n            .switchIfEmpty(Mono.error(new IllegalStateException(\"The owner must not be null.\")))\n            .map(this::toCommentOwner)\n            .doOnNext(owner -> comment.getSpec().setOwner(owner))\n            .then();\n    }\n\n    @Override\n    public Mono<Void> removeBySubject(@NonNull Ref subjectRef) {\n        Assert.notNull(subjectRef, \"The subjectRef must not be null.\");\n        return cleanupComments(subjectRef, 200);\n    }\n\n    private Mono<Void> cleanupComments(Ref subjectRef, int batchSize) {\n        // ascending order by creation time and name\n        final var pageRequest = PageRequestImpl.of(1, batchSize,\n            Sort.by(\"metadata.creationTimestamp\", \"metadata.name\"));\n        // forever loop first page until no more to delete\n        return listCommentsByRef(subjectRef, pageRequest)\n            .flatMap(page -> Flux.fromIterable(page.getItems())\n                .flatMap(this::deleteWithRetry)\n                .then(page.hasNext() ? cleanupComments(subjectRef, batchSize) : Mono.empty())\n            );\n    }\n\n    private Mono<Comment> deleteWithRetry(Comment item) {\n        return client.delete(item)\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> attemptToDelete(item.getMetadata().getName()));\n    }\n\n    private Mono<Comment> attemptToDelete(String name) {\n        return Mono.defer(() -> client.fetch(Comment.class, name)\n                .flatMap(client::delete)\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    Mono<ListResult<Comment>> listCommentsByRef(Ref subjectRef, PageRequest pageRequest) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            and(\n                equal(\"spec.subjectRef\", Comment.toSubjectRefKey(subjectRef)),\n                isNull(\"metadata.deletionTimestamp\")\n            )\n        ));\n        return client.listBy(Comment.class, listOptions, pageRequest);\n    }\n\n    private boolean checkCommentOwner(Comment comment, Boolean onlySystemUser) {\n        Comment.CommentOwner owner = comment.getSpec().getOwner();\n        if (Boolean.TRUE.equals(onlySystemUser)) {\n            return owner != null && Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind());\n        }\n        return false;\n    }\n\n    private Mono<ListedComment> toListedComment(Comment comment) {\n        var builder = ListedComment.builder().comment(comment);\n        // not empty\n        var ownerInfoMono = getOwnerInfo(comment.getSpec().getOwner())\n            .doOnNext(builder::owner);\n        var subjectMono = getCommentSubject(comment.getSpec().getSubjectRef())\n            .doOnNext(builder::subject);\n        var statsMono = fetchCommentStats(comment.getMetadata().getName())\n            .doOnNext(builder::stats);\n        return Mono.when(ownerInfoMono, subjectMono, statsMono)\n            .then(Mono.fromSupplier(builder::build));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    Mono<Extension> getCommentSubject(Ref ref) {\n        return extensionGetter.getExtensions(CommentSubject.class)\n            .filter(subject -> subject.supports(ref))\n            .next()\n            .flatMap(subject -> subject.get(ref.getName()));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/CommentStats.java",
    "content": "package run.halo.app.content.comment;\n\nimport lombok.Builder;\nimport lombok.Value;\n\n/**\n * comment stats value object.\n *\n * @author LIlGG\n * @since 2.0.0\n */\n@Value\n@Builder\npublic class CommentStats {\n\n    Integer upvote;\n\n    public static CommentStats empty() {\n        return CommentStats.builder()\n            .upvote(0)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/ListedComment.java",
    "content": "package run.halo.app.content.comment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Builder;\nimport lombok.Data;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.Extension;\n\n/**\n * Listed comment.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Builder\npublic class ListedComment {\n\n    @Schema(requiredMode = REQUIRED)\n    private Comment comment;\n\n    @Schema(requiredMode = REQUIRED)\n    private OwnerInfo owner;\n\n    private Extension subject;\n\n    @Schema(requiredMode = REQUIRED)\n    private CommentStats stats;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/ListedReply.java",
    "content": "package run.halo.app.content.comment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Builder;\nimport lombok.Data;\nimport run.halo.app.core.extension.content.Reply;\n\n/**\n * Listed reply for {@link Reply}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Builder\npublic class ListedReply {\n\n    @Schema(requiredMode = REQUIRED)\n    private Reply reply;\n\n    @Schema(requiredMode = REQUIRED)\n    private OwnerInfo owner;\n\n    @Schema(requiredMode = REQUIRED)\n    private CommentStats stats;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/OwnerInfo.java",
    "content": "package run.halo.app.content.comment;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\n\n/**\n * Comment owner info.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@Builder\npublic class OwnerInfo {\n\n    String kind;\n\n    String name;\n\n    String displayName;\n\n    String avatar;\n\n    String email;\n\n    /**\n     * Convert user to owner info by owner that has an email kind .\n     *\n     * @param owner comment owner reference.\n     * @return owner info.\n     */\n    public static OwnerInfo from(Comment.CommentOwner owner) {\n        if (!Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {\n            throw new IllegalArgumentException(\"Only support 'email' owner kind.\");\n        }\n        return OwnerInfo.builder()\n            .kind(owner.getKind())\n            .name(owner.getName())\n            .email(owner.getName())\n            .displayName(owner.getDisplayName())\n            .avatar(owner.getAnnotation(Comment.CommentOwner.AVATAR_ANNO))\n            .build();\n    }\n\n    /**\n     * Convert user to owner info by {@link User}.\n     *\n     * @param user user extension.\n     * @return owner info.\n     */\n    public static OwnerInfo from(User user) {\n        return OwnerInfo.builder()\n            .kind(user.getKind())\n            .name(user.getMetadata().getName())\n            .email(user.getSpec().getEmail())\n            .avatar(user.getSpec().getAvatar())\n            .displayName(user.getSpec().getDisplayName())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/PostCommentSubject.java",
    "content": "package run.halo.app.content.comment;\n\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.ExternalLinkProcessor;\n\n/**\n * Comment subject for post.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class PostCommentSubject implements CommentSubject<Post> {\n\n    private final ReactiveExtensionClient client;\n    private final ExternalLinkProcessor externalLinkProcessor;\n\n    @Override\n    public Mono<Post> get(String name) {\n        return client.fetch(Post.class, name);\n    }\n\n    @Override\n    public Mono<SubjectDisplay> getSubjectDisplay(String name) {\n        return get(name)\n            .map(post -> {\n                var url = externalLinkProcessor\n                    .processLink(post.getStatusOrDefault().getPermalink());\n                return new SubjectDisplay(post.getSpec().getTitle(), url, \"文章\");\n            });\n    }\n\n    @Override\n    public boolean supports(Ref ref) {\n        Assert.notNull(ref, \"Subject ref must not be null.\");\n        var gvk = Post.GVK;\n        return Objects.equals(gvk.group(), ref.getGroup())\n            && Objects.equals(gvk.kind(), ref.getKind());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/ReplyNotificationSubscriptionHelper.java",
    "content": "package run.halo.app.content.comment;\n\nimport io.micrometer.common.util.StringUtils;\nimport java.time.Duration;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.lang.Nullable;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.notification.UserIdentity;\n\n/**\n * Reply notification subscription helper.\n *\n * @author guqing\n * @since 2.9.0\n */\n@Component\n@RequiredArgsConstructor\npublic class ReplyNotificationSubscriptionHelper {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private final NotificationCenter notificationCenter;\n\n    /**\n     * Subscribe new reply reason for comment.\n     *\n     * @param comment comment\n     */\n    public void subscribeNewReplyReasonForComment(Comment comment) {\n        subscribeReply(identityFrom(comment.getSpec().getOwner()));\n    }\n\n    /**\n     * Subscribe new reply reason for reply.\n     *\n     * @param reply reply\n     */\n    public void subscribeNewReplyReasonForReply(Reply reply) {\n        var subjectOwner = reply.getSpec().getOwner();\n        subscribeReply(identityFrom(subjectOwner));\n    }\n\n    void subscribeReply(UserIdentity identity) {\n        var subscriber = createSubscriber(identity);\n        if (subscriber == null) {\n            return;\n        }\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(NotificationReasonConst.SOMEONE_REPLIED_TO_YOU);\n        interestReason.setExpression(\"props.repliedOwner == '%s'\".formatted(identity.name()));\n        notificationCenter.subscribe(subscriber, interestReason).block(BLOCKING_TIMEOUT);\n    }\n\n    @Nullable\n    private Subscription.Subscriber createSubscriber(UserIdentity author) {\n        if (StringUtils.isBlank(author.name())) {\n            return null;\n        }\n\n        Subscription.Subscriber subscriber = new Subscription.Subscriber();\n        subscriber.setName(author.name());\n        return subscriber;\n    }\n\n    public static UserIdentity identityFrom(Comment.CommentOwner owner) {\n        if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {\n            return UserIdentity.anonymousWithEmail(owner.getName());\n        }\n        return UserIdentity.of(owner.getName());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/ReplyQuery.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.ServerWebInputException;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.router.SortableRequest;\n\n/**\n * Query criteria for {@link Reply} list.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class ReplyQuery extends SortableRequest {\n\n    public ReplyQuery(ServerWebExchange exchange) {\n        super(exchange);\n    }\n\n    @Schema(description = \"Replies filtered by commentName.\")\n    public String getCommentName() {\n        String commentName = queryParams.getFirst(\"commentName\");\n        if (StringUtils.isBlank(commentName)) {\n            throw new ServerWebInputException(\"The required parameter 'commentName' is missing.\");\n        }\n        return commentName;\n    }\n\n    /**\n     * Build list options from query criteria.\n     */\n    public ListOptions toListOptions() {\n        var listOptions =\n            labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());\n        var newFieldSelector = listOptions.getFieldSelector()\n            .andQuery(equal(\"spec.commentName\", getCommentName()));\n        listOptions.setFieldSelector(newFieldSelector);\n        return listOptions;\n    }\n\n    public PageRequest toPageRequest() {\n        var sort = getSort().and(Sort.by(\"spec.creationTime\").ascending());\n        return PageRequestImpl.of(getPage(), getSize(), sort);\n    }\n\n    public static void buildParameters(Builder builder) {\n        SortableRequest.buildParameters(builder);\n        builder.parameter(parameterBuilder()\n            .in(ParameterIn.QUERY)\n            .name(\"commentName\")\n            .description(\"Replies filtered by commentName.\")\n            .implementation(String.class)\n            .required(true));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/ReplyRequest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.UUID;\nimport lombok.Data;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.extension.Metadata;\n\n/**\n * A request parameter object for {@link Reply}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\npublic class ReplyRequest {\n\n    @Schema(requiredMode = REQUIRED, minLength = 1)\n    private String raw;\n\n    @Schema(requiredMode = REQUIRED, minLength = 1)\n    private String content;\n\n    @Schema(defaultValue = \"false\")\n    private Boolean allowNotification;\n\n    @Schema(defaultValue = \"false\")\n    private Boolean hidden;\n\n    private CommentEmailOwner owner;\n\n    private String quoteReply;\n\n    /**\n     * Converts {@link ReplyRequest} to {@link Reply}.\n     *\n     * @return a reply\n     */\n    public Reply toReply() {\n        Reply reply = new Reply();\n        reply.setMetadata(new Metadata());\n        reply.getMetadata().setName(UUID.randomUUID().toString());\n\n        Reply.ReplySpec spec = new Reply.ReplySpec();\n        reply.setSpec(spec);\n        spec.setRaw(raw);\n        spec.setContent(content);\n        spec.setAllowNotification(allowNotification);\n        spec.setHidden(hidden);\n        spec.setQuoteReply(quoteReply);\n\n        if (owner != null) {\n            spec.setOwner(owner.toCommentOwner());\n        }\n        return reply;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/ReplyService.java",
    "content": "package run.halo.app.content.comment;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.extension.ListResult;\n\n/**\n * An application service for {@link Reply}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface ReplyService {\n\n    Mono<Reply> create(String commentName, Reply reply);\n\n    Mono<ListResult<ListedReply>> list(ReplyQuery query);\n\n    Mono<Void> removeAllByComment(String commentName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.apache.commons.lang3.BooleanUtils.isTrue;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.function.UnaryOperator;\nimport org.apache.commons.lang3.StringUtils;\nimport org.reactivestreams.Publisher;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.exception.RequestRestrictedException;\n\n/**\n * A default implementation of {@link ReplyService}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Service\npublic class ReplyServiceImpl extends AbstractCommentService implements ReplyService {\n\n    private final Supplier<RequestRestrictedException> requestRestrictedExceptionSupplier =\n        () -> new RequestRestrictedException(\"problemDetail.comment.waitingForApproval\");\n\n    public ReplyServiceImpl(RoleService roleService, ReactiveExtensionClient client,\n        UserService userService, CounterService counterService) {\n        super(roleService, client, userService, counterService);\n    }\n\n    @Override\n    public Mono<Reply> create(String commentName, Reply reply) {\n        if (reply.getSpec() == null\n            || reply.getSpec().getContent() == null\n            || !isSafeHtml(reply.getSpec().getContent())) {\n            return Mono.error(new ServerWebInputException(\"\"\"\n                The content of reply must not be empty or contains unsafe HTML.\\\n                \"\"\"));\n        }\n        return client.get(Comment.class, commentName)\n            .flatMap(this::approveComment)\n            .filter(comment -> isTrue(comment.getSpec().getApproved()))\n            .switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))\n            .flatMap(comment -> prepareReply(comment, reply))\n            .flatMap(this::doCreateReply);\n    }\n\n    private Mono<Reply> doCreateReply(Reply prepared) {\n        var quotedReply = prepared.getSpec().getQuoteReply();\n        if (StringUtils.isBlank(quotedReply)) {\n            return client.create(prepared);\n        }\n        return approveReply(quotedReply)\n            .filter(reply -> isTrue(reply.getSpec().getApproved()))\n            .switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))\n            .doOnNext(approvedQuoteReply -> prepared.getSpec()\n                .setHidden(approvedQuoteReply.getSpec().getHidden())\n            )\n            .flatMap(approvedQuoteReply -> client.create(prepared));\n    }\n\n    private Mono<Comment> approveComment(Comment comment) {\n        return hasCommentManagePermission()\n            .flatMap(hasPermission -> {\n                if (hasPermission) {\n                    return doApproveComment(comment);\n                }\n                return Mono.just(comment);\n            });\n    }\n\n    private Mono<Comment> doApproveComment(Comment comment) {\n        UnaryOperator<Comment> updateFunc = commentToUpdate -> {\n            commentToUpdate.getSpec().setApproved(true);\n            commentToUpdate.getSpec().setApprovedTime(Instant.now());\n            return commentToUpdate;\n        };\n        return client.update(updateFunc.apply(comment))\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> updateCommentWithRetry(comment.getMetadata().getName(), updateFunc));\n    }\n\n    private Mono<Reply> approveReply(String replyName) {\n        return hasCommentManagePermission()\n            .flatMap(hasPermission -> {\n                if (hasPermission) {\n                    return doApproveReply(replyName);\n                }\n                return client.get(Reply.class, replyName);\n            });\n    }\n\n    private Mono<Reply> doApproveReply(String replyName) {\n        return Mono.defer(() -> client.get(Reply.class, replyName)\n                .flatMap(reply -> {\n                    reply.getSpec().setApproved(true);\n                    reply.getSpec().setApprovedTime(Instant.now());\n                    return client.update(reply);\n                })\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    private Mono<Comment> updateCommentWithRetry(String name, UnaryOperator<Comment> updateFunc) {\n        return Mono.defer(() -> client.get(Comment.class, name)\n                .map(updateFunc)\n                .flatMap(client::update)\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    private Mono<Reply> prepareReply(Comment comment, Reply reply) {\n        reply.getSpec().setCommentName(comment.getMetadata().getName());\n        reply.getSpec().setHidden(comment.getSpec().getHidden());\n        if (reply.getSpec().getTop() == null) {\n            reply.getSpec().setTop(false);\n        }\n        if (reply.getSpec().getPriority() == null) {\n            reply.getSpec().setPriority(0);\n        }\n        if (reply.getSpec().getCreationTime() == null) {\n            reply.getSpec().setCreationTime(Instant.now());\n        }\n        if (reply.getSpec().getApproved() == null) {\n            reply.getSpec().setApproved(false);\n        }\n        if (isTrue(reply.getSpec().getApproved())\n            && reply.getSpec().getApprovedTime() == null) {\n            reply.getSpec().setApprovedTime(Instant.now());\n        }\n\n        var steps = new ArrayList<Publisher<?>>();\n        var approveItMono = hasCommentManagePermission()\n            .filter(Boolean::booleanValue)\n            .doOnNext(hasPermission -> {\n                reply.getSpec().setApproved(true);\n                reply.getSpec().setApprovedTime(Instant.now());\n            });\n        steps.add(approveItMono);\n\n        var populateOwnerMono = fetchCurrentUser()\n            .switchIfEmpty(\n                Mono.error(new IllegalArgumentException(\"Reply owner must not be null.\")))\n            .doOnNext(user -> reply.getSpec().setOwner(toCommentOwner(user)));\n        if (reply.getSpec().getOwner() == null) {\n            steps.add(populateOwnerMono);\n        }\n        return Mono.when(steps).thenReturn(reply);\n    }\n\n    @Override\n    public Mono<ListResult<ListedReply>> list(ReplyQuery query) {\n        return client.listBy(Reply.class, query.toListOptions(), query.toPageRequest())\n            .flatMap(list -> Flux.fromStream(list.get()\n                    .map(this::toListedReply))\n                .flatMapSequential(Function.identity())\n                .collectList()\n                .map(listedReplies -> new ListResult<>(list.getPage(), list.getSize(),\n                    list.getTotal(), listedReplies))\n            );\n    }\n\n    @Override\n    public Mono<Void> removeAllByComment(String commentName) {\n        Assert.notNull(commentName, \"The commentName must not be null.\");\n        return cleanupComments(commentName, 200);\n    }\n\n    private Mono<Void> cleanupComments(String commentName, int batchSize) {\n        // ascending order by creation time and name\n        final var pageRequest = PageRequestImpl.of(1, batchSize,\n            Sort.by(\"metadata.creationTimestamp\", \"metadata.name\"));\n        // forever loop first page until no more to delete\n        return listRepliesByComment(commentName, pageRequest)\n            .flatMap(page -> Flux.fromIterable(page.getItems())\n                .flatMap(this::deleteWithRetry)\n                .then(page.hasNext() ? cleanupComments(commentName, batchSize) : Mono.empty())\n            );\n    }\n\n    private Mono<Reply> deleteWithRetry(Reply item) {\n        return client.delete(item)\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> attemptToDelete(item.getMetadata().getName()));\n    }\n\n    private Mono<Reply> attemptToDelete(String name) {\n        return Mono.defer(() -> client.fetch(Reply.class, name)\n                .flatMap(client::delete)\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    Mono<ListResult<Reply>> listRepliesByComment(String commentName, PageRequest pageRequest) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            and(\n                equal(\"spec.commentName\", commentName),\n                isNull(\"metadata.deletionTimestamp\")\n            )\n        ));\n        return client.listBy(Reply.class, listOptions, pageRequest);\n    }\n\n    private Mono<ListedReply> toListedReply(Reply reply) {\n        ListedReply.ListedReplyBuilder builder = ListedReply.builder()\n            .reply(reply);\n        return getOwnerInfo(reply.getSpec().getOwner())\n            .map(ownerInfo -> {\n                builder.owner(ownerInfo);\n                return builder;\n            })\n            .map(ListedReply.ListedReplyBuilder::build)\n            .flatMap(listedReply -> fetchReplyStats(reply.getMetadata().getName())\n                .doOnNext(listedReply::setStats)\n                .thenReturn(listedReply));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/comment/SinglePageCommentSubject.java",
    "content": "package run.halo.app.content.comment;\n\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.ExternalLinkProcessor;\n\n/**\n * Comment subject for {@link SinglePage}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class SinglePageCommentSubject implements CommentSubject<SinglePage> {\n\n    private final ReactiveExtensionClient client;\n\n    private final ExternalLinkProcessor externalLinkProcessor;\n\n    @Override\n    public Mono<SinglePage> get(String name) {\n        return client.fetch(SinglePage.class, name);\n    }\n\n    @Override\n    public Mono<SubjectDisplay> getSubjectDisplay(String name) {\n        return get(name)\n            .map(page -> {\n                var url = externalLinkProcessor\n                    .processLink(page.getStatusOrDefault().getPermalink());\n                return new SubjectDisplay(page.getSpec().getTitle(), url, \"页面\");\n            });\n    }\n\n    @Override\n    public boolean supports(Ref ref) {\n        Assert.notNull(ref, \"Subject ref must not be null.\");\n        var gvk = SinglePage.GVK;\n        return Objects.equals(gvk.group(), ref.getGroup())\n            && Objects.equals(gvk.kind(), ref.getKind());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/impl/CategoryServiceImpl.java",
    "content": "package run.halo.app.content.impl;\n\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.CategoryService;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\n\n@Component\n@RequiredArgsConstructor\npublic class CategoryServiceImpl implements CategoryService {\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Flux<Category> listChildren(@NonNull String categoryName) {\n        return client.fetch(Category.class, categoryName)\n            .expand(category -> {\n                var children = category.getSpec().getChildren();\n                if (children == null || children.isEmpty()) {\n                    return Mono.empty();\n                }\n                return Flux.fromIterable(children)\n                    .flatMap(name -> client.fetch(Category.class, name))\n                    .filter(this::isNotIndependent);\n            });\n    }\n\n    @Override\n    public Mono<Category> getParentByName(@NonNull String name) {\n        if (StringUtils.isBlank(name)) {\n            return Mono.empty();\n        }\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            equal(\"spec.children\", name)\n        ));\n        return client.listBy(Category.class, listOptions,\n                PageRequestImpl.of(1, 1, defaultSort())\n            )\n            .flatMap(result -> Mono.justOrEmpty(ListResult.first(result)));\n    }\n\n    @Override\n    public Mono<Boolean> isCategoryHidden(@NonNull String categoryName) {\n        return client.fetch(Category.class, categoryName)\n            .expand(category -> getParentByName(category.getMetadata().getName()))\n            .filter(category -> category.getSpec().isHideFromList())\n            .hasElements();\n    }\n\n    static Sort defaultSort() {\n        return Sort.by(Sort.Order.desc(\"spec.priority\"),\n            Sort.Order.desc(\"metadata.creationTimestamp\"),\n            Sort.Order.desc(\"metadata.name\"));\n    }\n\n    private boolean isNotIndependent(Category category) {\n        return !category.getSpec().isPreventParentPostCascadeQuery();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java",
    "content": "package run.halo.app.content.impl;\n\nimport static run.halo.app.extension.index.query.Queries.in;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport java.util.function.ToIntFunction;\nimport java.util.function.UnaryOperator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.content.AbstractContentService;\nimport run.halo.app.content.CategoryService;\nimport run.halo.app.content.ContentRequest;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.Contributor;\nimport run.halo.app.content.ListedPost;\nimport run.halo.app.content.ListedSnapshotDto;\nimport run.halo.app.content.PostQuery;\nimport run.halo.app.content.PostRequest;\nimport run.halo.app.content.PostService;\nimport run.halo.app.content.Stats;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.MetadataOperator;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.Condition;\nimport run.halo.app.infra.ConditionStatus;\n\n/**\n * A default implementation of {@link PostService}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\npublic class PostServiceImpl extends AbstractContentService implements PostService {\n    private final ReactiveExtensionClient client;\n    private final CounterService counterService;\n    private final UserService userService;\n    private final CategoryService categoryService;\n\n    public PostServiceImpl(ReactiveExtensionClient client, CounterService counterService,\n        UserService userService, CategoryService categoryService) {\n        super(client);\n        this.client = client;\n        this.counterService = counterService;\n        this.userService = userService;\n        this.categoryService = categoryService;\n    }\n\n    @Override\n    public Mono<ListResult<ListedPost>> listPost(PostQuery query) {\n        return buildListOptions(query)\n            .flatMap(listOptions ->\n                client.listBy(Post.class, listOptions, query.toPageRequest())\n            )\n            .flatMap(listResult -> Flux.fromStream(listResult.get())\n                .map(this::getListedPost)\n                .flatMapSequential(Function.identity())\n                .collectList()\n                .map(listedPosts -> new ListResult<>(listResult.getPage(), listResult.getSize(),\n                    listResult.getTotal(), listedPosts)\n                )\n                .defaultIfEmpty(ListResult.emptyResult())\n            );\n    }\n\n    Mono<ListOptions> buildListOptions(PostQuery query) {\n        var categoryName = query.getCategoryWithChildren();\n        if (categoryName == null) {\n            return Mono.just(query.toListOptions());\n        }\n        return categoryService.listChildren(categoryName)\n            .collectList()\n            .map(categories -> {\n                var categoryNames = categories.stream()\n                    .map(Category::getMetadata)\n                    .map(MetadataOperator::getName)\n                    .toList();\n                var listOptions = query.toListOptions();\n                var newFiledSelector = listOptions.getFieldSelector()\n                    .andQuery(in(\"spec.categories\", categoryNames));\n                listOptions.setFieldSelector(newFiledSelector);\n                return listOptions;\n            });\n    }\n\n    Mono<Stats> fetchStats(Post post) {\n        Assert.notNull(post, \"The post must not be null.\");\n        String name = post.getMetadata().getName();\n        return counterService.getByName(MeterUtils.nameOf(Post.class, name))\n            .map(counter -> Stats.builder()\n                .visit(counter.getVisit())\n                .upvote(counter.getUpvote())\n                .totalComment(counter.getTotalComment())\n                .approvedComment(counter.getApprovedComment())\n                .build()\n            )\n            .defaultIfEmpty(Stats.empty());\n    }\n\n    private Mono<ListedPost> getListedPost(Post post) {\n        Assert.notNull(post, \"The post must not be null.\");\n        var listedPost = new ListedPost().setPost(post);\n\n        var statsMono = fetchStats(post)\n            .doOnNext(listedPost::setStats);\n\n        var tagsMono = listTags(post.getSpec().getTags())\n            .collectList()\n            .doOnNext(listedPost::setTags);\n\n        var categoriesMono = listCategories(post.getSpec().getCategories())\n            .collectList()\n            .doOnNext(listedPost::setCategories);\n\n        var contributorsMono = listContributors(post.getStatusOrDefault().getContributors())\n            .collectList()\n            .doOnNext(listedPost::setContributors);\n\n        var ownerMono = userService.getUserOrGhost(post.getSpec().getOwner())\n            .map(user -> {\n                Contributor contributor = new Contributor();\n                contributor.setName(user.getMetadata().getName());\n                contributor.setDisplayName(user.getSpec().getDisplayName());\n                contributor.setAvatar(user.getSpec().getAvatar());\n                return contributor;\n            })\n            .doOnNext(listedPost::setOwner);\n        return Mono.when(statsMono, tagsMono, categoriesMono, contributorsMono, ownerMono)\n            .thenReturn(listedPost);\n    }\n\n    private Flux<Tag> listTags(List<String> tagNames) {\n        if (CollectionUtils.isEmpty(tagNames)) {\n            return Flux.empty();\n        }\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(in(\"metadata.name\", tagNames)));\n        return client.listAll(Tag.class, listOptions, Sort.by(\"metadata.creationTimestamp\"));\n    }\n\n    @Override\n    public Flux<Category> listCategories(List<String> categoryNames) {\n        if (CollectionUtils.isEmpty(categoryNames)) {\n            return Flux.empty();\n        }\n        ToIntFunction<Category> comparator =\n            category -> categoryNames.indexOf(category.getMetadata().getName());\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(in(\"metadata.name\", categoryNames)));\n        return client.listAll(Category.class, listOptions, Sort.unsorted())\n            .sort(Comparator.comparingInt(comparator));\n    }\n\n    private Flux<Contributor> listContributors(List<String> usernames) {\n        if (CollectionUtils.isEmpty(usernames)) {\n            return Flux.empty();\n        }\n        return Flux.fromIterable(usernames)\n            .flatMapSequential(userService::getUserOrGhost)\n            .map(user -> {\n                Contributor contributor = new Contributor();\n                contributor.setName(user.getMetadata().getName());\n                contributor.setDisplayName(user.getSpec().getDisplayName());\n                contributor.setAvatar(user.getSpec().getAvatar());\n                return contributor;\n            });\n    }\n\n    @Override\n    public Mono<Post> draftPost(PostRequest postRequest) {\n        return Mono.defer(\n                () -> {\n                    var post = postRequest.post();\n                    return getContextUsername()\n                        .doOnNext(username -> post.getSpec().setOwner(username))\n                        .thenReturn(post);\n                })\n            .flatMap(client::create)\n            .flatMap(post -> {\n                if (postRequest.content() == null) {\n                    return Mono.just(post);\n                }\n                var contentRequest =\n                    new ContentRequest(Ref.of(post), post.getSpec().getHeadSnapshot(),\n                        null,\n                        postRequest.content().raw(), postRequest.content().content(),\n                        postRequest.content().rawType());\n                return draftContent(post.getSpec().getBaseSnapshot(), contentRequest)\n                    .flatMap(contentWrapper -> waitForPostToDraftConcludingWork(\n                        post.getMetadata().getName(),\n                        contentWrapper)\n                    );\n            })\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    private Mono<Post> waitForPostToDraftConcludingWork(String postName,\n        ContentWrapper contentWrapper) {\n        return Mono.defer(() -> client.fetch(Post.class, postName)\n                .flatMap(post -> {\n                    post.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName());\n                    post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());\n                    if (Objects.equals(true, post.getSpec().getPublish())) {\n                        post.getSpec().setReleaseSnapshot(post.getSpec().getHeadSnapshot());\n                    }\n                    Condition condition = Condition.builder()\n                        .type(Post.PostPhase.DRAFT.name())\n                        .reason(\"DraftedSuccessfully\")\n                        .message(\"Drafted post successfully.\")\n                        .status(ConditionStatus.TRUE)\n                        .lastTransitionTime(Instant.now())\n                        .build();\n                    Post.PostStatus status = post.getStatusOrDefault();\n                    status.setPhase(Post.PostPhase.DRAFT.name());\n                    status.getConditionsOrDefault().addAndEvictFIFO(condition);\n                    return client.update(post);\n                }))\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    @Override\n    public Mono<Post> updatePost(PostRequest postRequest) {\n        Post post = postRequest.post();\n        String headSnapshot = post.getSpec().getHeadSnapshot();\n        String releaseSnapshot = post.getSpec().getReleaseSnapshot();\n        String baseSnapshot = post.getSpec().getBaseSnapshot();\n\n        if (StringUtils.equals(releaseSnapshot, headSnapshot)) {\n            // create new snapshot to update first\n            return draftContent(baseSnapshot, postRequest.contentRequest(), headSnapshot)\n                .flatMap(contentWrapper -> {\n                    post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());\n                    return client.update(post);\n                });\n        }\n        return updateContent(baseSnapshot, postRequest.contentRequest())\n            .flatMap(contentWrapper -> {\n                post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());\n                return client.update(post);\n            });\n    }\n\n    @Override\n    public Mono<Post> updateBy(@NonNull Post post) {\n        return client.update(post);\n    }\n\n    @Override\n    public Mono<ContentWrapper> getHeadContent(String postName) {\n        return client.get(Post.class, postName)\n            .flatMap(this::getHeadContent);\n    }\n\n    @Override\n    public Mono<ContentWrapper> getHeadContent(Post post) {\n        var headSnapshot = post.getSpec().getHeadSnapshot();\n        return getContent(headSnapshot, post.getSpec().getBaseSnapshot());\n    }\n\n    @Override\n    public Mono<ContentWrapper> getReleaseContent(String postName) {\n        return client.get(Post.class, postName)\n            .flatMap(this::getReleaseContent);\n    }\n\n    @Override\n    public Mono<ContentWrapper> getReleaseContent(Post post) {\n        var releaseSnapshot = post.getSpec().getReleaseSnapshot();\n        return getContent(releaseSnapshot, post.getSpec().getBaseSnapshot());\n    }\n\n    @Override\n    public Flux<ListedSnapshotDto> listSnapshots(String name) {\n        return client.fetch(Post.class, name)\n            .flatMapMany(page -> listSnapshotsBy(Ref.of(page)))\n            .map(ListedSnapshotDto::from);\n    }\n\n    @Override\n    public Mono<Post> publish(Post post) {\n        var spec = post.getSpec();\n        spec.setPublish(true);\n        if (spec.getHeadSnapshot() == null) {\n            spec.setHeadSnapshot(spec.getBaseSnapshot());\n        }\n        spec.setReleaseSnapshot(spec.getHeadSnapshot());\n        return client.update(post);\n    }\n\n    @Override\n    public Mono<Post> unpublish(Post post) {\n        post.getSpec().setPublish(false);\n        return client.update(post);\n    }\n\n    @Override\n    public Mono<Post> getByUsername(String postName, String username) {\n        return client.get(Post.class, postName)\n            .filter(post -> post.getSpec() != null)\n            .filter(post -> Objects.equals(username, post.getSpec().getOwner()));\n    }\n\n    @Override\n    public Mono<Post> revertToSpecifiedSnapshot(String postName, String snapshotName) {\n        return client.get(Post.class, postName)\n            .filter(post -> {\n                var head = post.getSpec().getHeadSnapshot();\n                return !StringUtils.equals(head, snapshotName);\n            })\n            .flatMap(post -> {\n                var baseSnapshot = post.getSpec().getBaseSnapshot();\n                return getContent(snapshotName, baseSnapshot)\n                    .map(content -> ContentRequest.builder()\n                        .subjectRef(Ref.of(post))\n                        .headSnapshotName(post.getSpec().getHeadSnapshot())\n                        .content(content.getContent())\n                        .raw(content.getRaw())\n                        .rawType(content.getRawType())\n                        .build()\n                    )\n                    .flatMap(contentRequest -> draftContent(baseSnapshot, contentRequest))\n                    .flatMap(content -> {\n                        post.getSpec().setHeadSnapshot(content.getSnapshotName());\n                        return publishPostWithRetry(post);\n                    });\n            });\n    }\n\n    @Override\n    public Mono<ContentWrapper> deleteContent(String postName, String snapshotName) {\n        return client.get(Post.class, postName)\n            .flatMap(post -> {\n                var headSnapshotName = post.getSpec().getHeadSnapshot();\n                if (StringUtils.equals(headSnapshotName, snapshotName)) {\n                    return updatePostWithRetry(post, record -> {\n                        // update head to release\n                        record.getSpec().setHeadSnapshot(record.getSpec().getReleaseSnapshot());\n                        return record;\n                    });\n                }\n                return Mono.just(post);\n            })\n            .flatMap(post -> {\n                var baseSnapshotName = post.getSpec().getBaseSnapshot();\n                var releaseSnapshotName = post.getSpec().getReleaseSnapshot();\n                if (StringUtils.equals(releaseSnapshotName, snapshotName)) {\n                    return Mono.error(new ServerWebInputException(\n                        \"The snapshot to delete is the release snapshot, please\"\n                            + \" revert to another snapshot first.\"));\n                }\n                if (StringUtils.equals(baseSnapshotName, snapshotName)) {\n                    return Mono.error(\n                        new ServerWebInputException(\"The first snapshot cannot be deleted.\"));\n                }\n                return client.fetch(Snapshot.class, snapshotName)\n                    .flatMap(client::delete)\n                    .flatMap(deleted -> restoredContent(baseSnapshotName, deleted));\n            });\n    }\n\n    @Override\n    public Mono<Post> recycleBy(String postName, String username) {\n        return getByUsername(postName, username)\n            .flatMap(post -> updatePostWithRetry(post, record -> {\n                record.getSpec().setDeleted(true);\n                return record;\n            }));\n    }\n\n    private Mono<Post> updatePostWithRetry(Post post, UnaryOperator<Post> func) {\n        return client.update(func.apply(post))\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> Mono.defer(() -> client.get(Post.class, post.getMetadata().getName())\n                        .map(func)\n                        .flatMap(client::update)\n                    )\n                    .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                        .filter(OptimisticLockingFailureException.class::isInstance))\n            );\n    }\n\n    Mono<Post> publishPostWithRetry(Post post) {\n        return publish(post)\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> Mono.defer(() -> client.get(Post.class, post.getMetadata().getName())\n                        .flatMap(this::publish))\n                    .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                        .filter(OptimisticLockingFailureException.class::isInstance))\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java",
    "content": "package run.halo.app.content.impl;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport java.util.function.UnaryOperator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.content.AbstractContentService;\nimport run.halo.app.content.ContentRequest;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.Contributor;\nimport run.halo.app.content.ListedSinglePage;\nimport run.halo.app.content.ListedSnapshotDto;\nimport run.halo.app.content.SinglePageQuery;\nimport run.halo.app.content.SinglePageRequest;\nimport run.halo.app.content.SinglePageService;\nimport run.halo.app.content.Stats;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.Condition;\nimport run.halo.app.infra.ConditionStatus;\n\n/**\n * Single page service implementation.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Service\npublic class SinglePageServiceImpl extends AbstractContentService implements SinglePageService {\n\n    private final ReactiveExtensionClient client;\n    private final CounterService counterService;\n    private final UserService userService;\n\n    public SinglePageServiceImpl(ReactiveExtensionClient client, CounterService counterService,\n        UserService userService) {\n        super(client);\n        this.client = client;\n        this.counterService = counterService;\n        this.userService = userService;\n    }\n\n    @Override\n    public Mono<ContentWrapper> getHeadContent(String singlePageName) {\n        return client.get(SinglePage.class, singlePageName)\n            .flatMap(singlePage -> {\n                String headSnapshot = singlePage.getSpec().getHeadSnapshot();\n                return getContent(headSnapshot, singlePage.getSpec().getBaseSnapshot());\n            });\n    }\n\n    @Override\n    public Mono<ContentWrapper> getReleaseContent(String singlePageName) {\n        return client.get(SinglePage.class, singlePageName)\n            .flatMap(singlePage -> {\n                String releaseSnapshot = singlePage.getSpec().getReleaseSnapshot();\n                return getContent(releaseSnapshot, singlePage.getSpec().getBaseSnapshot());\n            });\n    }\n\n    @Override\n    public Flux<ListedSnapshotDto> listSnapshots(String pageName) {\n        return client.fetch(SinglePage.class, pageName)\n            .flatMapMany(page -> listSnapshotsBy(Ref.of(page)))\n            .map(ListedSnapshotDto::from);\n    }\n\n    @Override\n    public Mono<ListResult<ListedSinglePage>> list(SinglePageQuery query) {\n        return client.listBy(SinglePage.class, query.toListOptions(), query.toPageRequest())\n            .flatMap(listResult -> Flux.fromStream(listResult.get().map(this::getListedSinglePage))\n                .flatMapSequential(Function.identity())\n                .collectList()\n                .map(listedSinglePages -> new ListResult<>(\n                    listResult.getPage(),\n                    listResult.getSize(),\n                    listResult.getTotal(),\n                    listedSinglePages)\n                )\n            );\n    }\n\n    @Override\n    public Mono<SinglePage> draft(SinglePageRequest pageRequest) {\n        return Mono.defer(\n                () -> {\n                    SinglePage page = pageRequest.page();\n                    return getContextUsername()\n                        .doOnNext(username -> page.getSpec().setOwner(username))\n                        .thenReturn(page);\n                }\n            )\n            .flatMap(client::create)\n            .flatMap(page -> {\n                var contentRequest =\n                    new ContentRequest(Ref.of(page), page.getSpec().getHeadSnapshot(),\n                        null,\n                        pageRequest.content().raw(), pageRequest.content().content(),\n                        pageRequest.content().rawType());\n                return draftContent(page.getSpec().getBaseSnapshot(), contentRequest)\n                    .flatMap(\n                        contentWrapper -> waitForPageToDraftConcludingWork(\n                            page.getMetadata().getName(),\n                            contentWrapper\n                        )\n                    );\n            });\n    }\n\n    private Mono<SinglePage> waitForPageToDraftConcludingWork(String pageName,\n        ContentWrapper contentWrapper) {\n        return Mono.defer(() -> client.fetch(SinglePage.class, pageName)\n                .flatMap(page -> {\n                    page.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName());\n                    page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());\n                    if (Objects.equals(true, page.getSpec().getPublish())) {\n                        page.getSpec().setReleaseSnapshot(page.getSpec().getHeadSnapshot());\n                    }\n                    Condition condition = Condition.builder()\n                        .type(Post.PostPhase.DRAFT.name())\n                        .reason(\"DraftedSuccessfully\")\n                        .message(\"Drafted page successfully\")\n                        .status(ConditionStatus.TRUE)\n                        .lastTransitionTime(Instant.now())\n                        .build();\n                    SinglePage.SinglePageStatus status = page.getStatusOrDefault();\n                    status.getConditionsOrDefault().addAndEvictFIFO(condition);\n                    status.setPhase(Post.PostPhase.DRAFT.name());\n                    return client.update(page);\n                }))\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance)\n            );\n    }\n\n    @Override\n    public Mono<SinglePage> update(SinglePageRequest pageRequest) {\n        SinglePage page = pageRequest.page();\n        String headSnapshot = page.getSpec().getHeadSnapshot();\n        String releaseSnapshot = page.getSpec().getReleaseSnapshot();\n        String baseSnapshot = page.getSpec().getBaseSnapshot();\n\n        // create new snapshot to update first\n        if (StringUtils.equals(headSnapshot, releaseSnapshot)) {\n            return draftContent(baseSnapshot, pageRequest.contentRequest(), headSnapshot)\n                .flatMap(contentWrapper -> {\n                    page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());\n                    return client.update(page);\n                });\n        }\n        return updateContent(baseSnapshot, pageRequest.contentRequest())\n            .flatMap(contentWrapper -> {\n                page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());\n                return client.update(page);\n            });\n    }\n\n    @Override\n    public Mono<SinglePage> revertToSpecifiedSnapshot(String pageName, String snapshotName) {\n        return client.get(SinglePage.class, pageName)\n            .filter(page -> {\n                var head = page.getSpec().getHeadSnapshot();\n                return !StringUtils.equals(head, snapshotName);\n            })\n            .flatMap(page -> {\n                var baseSnapshot = page.getSpec().getBaseSnapshot();\n                return getContent(snapshotName, baseSnapshot)\n                    .map(content -> ContentRequest.builder()\n                        .subjectRef(Ref.of(page))\n                        .headSnapshotName(page.getSpec().getHeadSnapshot())\n                        .content(content.getContent())\n                        .raw(content.getRaw())\n                        .rawType(content.getRawType())\n                        .build()\n                    )\n                    .flatMap(contentRequest -> draftContent(baseSnapshot, contentRequest))\n                    .flatMap(content -> {\n                        page.getSpec().setHeadSnapshot(content.getSnapshotName());\n                        return publishPageWithRetry(page);\n                    });\n            });\n    }\n\n    @Override\n    public Mono<ContentWrapper> deleteContent(String pageName, String snapshotName) {\n        return client.get(SinglePage.class, pageName)\n            .flatMap(page -> {\n                var headSnapshotName = page.getSpec().getHeadSnapshot();\n                if (StringUtils.equals(headSnapshotName, snapshotName)) {\n                    return updatePageWithRetry(page, record -> {\n                        // update head to release\n                        page.getSpec().setHeadSnapshot(page.getSpec().getReleaseSnapshot());\n                        return record;\n                    });\n                }\n                return Mono.just(page);\n            })\n            .flatMap(page -> {\n                var baseSnapshotName = page.getSpec().getBaseSnapshot();\n                var releaseSnapshotName = page.getSpec().getReleaseSnapshot();\n                if (StringUtils.equals(releaseSnapshotName, snapshotName)) {\n                    return Mono.error(new ServerWebInputException(\n                        \"The snapshot to delete is the release snapshot, please\"\n                            + \" revert to another snapshot first.\"));\n                }\n                if (StringUtils.equals(baseSnapshotName, snapshotName)) {\n                    return Mono.error(\n                        new ServerWebInputException(\"The first snapshot cannot be deleted.\"));\n                }\n                return client.fetch(Snapshot.class, snapshotName)\n                    .flatMap(client::delete)\n                    .flatMap(deleted -> restoredContent(baseSnapshotName, deleted));\n            });\n    }\n\n    private Mono<SinglePage> updatePageWithRetry(SinglePage page, UnaryOperator<SinglePage> func) {\n        return client.update(func.apply(page))\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> Mono.defer(() -> client.get(SinglePage.class, page.getMetadata().getName())\n                        .map(func)\n                        .flatMap(client::update)\n                    )\n                    .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                        .filter(OptimisticLockingFailureException.class::isInstance))\n            );\n    }\n\n    private Mono<SinglePage> publish(SinglePage singlePage) {\n        var spec = singlePage.getSpec();\n        spec.setPublish(true);\n        if (spec.getHeadSnapshot() == null) {\n            spec.setHeadSnapshot(spec.getBaseSnapshot());\n        }\n        spec.setReleaseSnapshot(spec.getHeadSnapshot());\n        return client.update(singlePage);\n    }\n\n    Mono<SinglePage> publishPageWithRetry(SinglePage page) {\n        return publish(page)\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> Mono.defer(() -> client.get(SinglePage.class, page.getMetadata().getName())\n                        .flatMap(this::publish))\n                    .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                        .filter(OptimisticLockingFailureException.class::isInstance))\n            );\n    }\n\n    private Mono<ListedSinglePage> getListedSinglePage(SinglePage singlePage) {\n        Assert.notNull(singlePage, \"The singlePage must not be null.\");\n        var listedSinglePage = new ListedSinglePage()\n            .setPage(singlePage);\n\n        var statsMono = fetchStats(singlePage)\n            .doOnNext(listedSinglePage::setStats);\n\n        var contributorsMono = listContributors(singlePage.getStatusOrDefault().getContributors())\n            .collectList()\n            .doOnNext(listedSinglePage::setContributors);\n\n        var ownerMono = userService.getUserOrGhost(singlePage.getSpec().getOwner())\n            .map(user -> {\n                Contributor contributor = new Contributor();\n                contributor.setName(user.getMetadata().getName());\n                contributor.setDisplayName(user.getSpec().getDisplayName());\n                contributor.setAvatar(user.getSpec().getAvatar());\n                return contributor;\n            })\n            .doOnNext(listedSinglePage::setOwner);\n        return Mono.when(statsMono, contributorsMono, ownerMono)\n            .thenReturn(listedSinglePage);\n    }\n\n    Mono<Stats> fetchStats(SinglePage singlePage) {\n        Assert.notNull(singlePage, \"The singlePage must not be null.\");\n        String name = singlePage.getMetadata().getName();\n        return counterService.getByName(MeterUtils.nameOf(SinglePage.class, name))\n            .map(counter -> Stats.builder()\n                .visit(counter.getVisit())\n                .upvote(counter.getUpvote())\n                .totalComment(counter.getTotalComment())\n                .approvedComment(counter.getApprovedComment())\n                .build()\n            )\n            .defaultIfEmpty(Stats.empty());\n    }\n\n    private Flux<Contributor> listContributors(List<String> usernames) {\n        if (usernames == null) {\n            return Flux.empty();\n        }\n        return Flux.fromIterable(usernames)\n            .flatMap(userService::getUserOrGhost)\n            .map(user -> {\n                Contributor contributor = new Contributor();\n                contributor.setName(user.getMetadata().getName());\n                contributor.setDisplayName(user.getSpec().getDisplayName());\n                contributor.setAvatar(user.getSpec().getAvatar());\n                return contributor;\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java",
    "content": "package run.halo.app.content.impl;\n\nimport java.time.Clock;\nimport java.util.HashMap;\nimport java.util.Objects;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.Content;\nimport run.halo.app.content.PatchUtils;\nimport run.halo.app.content.SnapshotService;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n@Service\npublic class SnapshotServiceImpl implements SnapshotService {\n\n    private final ReactiveExtensionClient client;\n\n    private final Clock clock;\n\n    public SnapshotServiceImpl(ReactiveExtensionClient client) {\n        this.client = client;\n        this.clock = Clock.systemDefaultZone();\n    }\n\n    @Override\n    public Mono<Snapshot> getBy(String snapshotName) {\n        return client.get(Snapshot.class, snapshotName);\n    }\n\n    @Override\n    public Mono<Snapshot> getPatchedBy(String snapshotName, String baseSnapshotName) {\n        if (StringUtils.isBlank(snapshotName) || StringUtils.isBlank(baseSnapshotName)) {\n            return Mono.empty();\n        }\n\n        return client.fetch(Snapshot.class, baseSnapshotName)\n            .filter(Snapshot::isBaseSnapshot)\n            .switchIfEmpty(Mono.error(() -> new IllegalArgumentException(\n                \"The snapshot \" + baseSnapshotName + \" is not a base snapshot.\")))\n            .flatMap(baseSnapshot ->\n                Mono.defer(() -> {\n                    if (Objects.equals(snapshotName, baseSnapshotName)) {\n                        return Mono.just(baseSnapshot);\n                    }\n                    return client.fetch(Snapshot.class, snapshotName);\n                }).doOnNext(snapshot -> {\n                    var baseRaw = baseSnapshot.getSpec().getRawPatch();\n                    var baseContent = baseSnapshot.getSpec().getContentPatch();\n\n                    var rawPatch = snapshot.getSpec().getRawPatch();\n                    var contentPatch = snapshot.getSpec().getContentPatch();\n\n                    var annotations = snapshot.getMetadata().getAnnotations();\n                    if (annotations == null) {\n                        annotations = new HashMap<>();\n                        snapshot.getMetadata().setAnnotations(annotations);\n                    }\n\n                    String patchedContent = baseContent;\n                    String patchedRaw = baseRaw;\n                    if (!Objects.equals(snapshot, baseSnapshot)) {\n                        patchedContent = PatchUtils.applyPatch(baseContent, contentPatch);\n                        patchedRaw = PatchUtils.applyPatch(baseRaw, rawPatch);\n                    }\n\n                    annotations.put(Snapshot.PATCHED_CONTENT_ANNO, patchedContent);\n                    annotations.put(Snapshot.PATCHED_RAW_ANNO, patchedRaw);\n                })\n            );\n    }\n\n    @Override\n    public Mono<Snapshot> patchAndCreate(@NonNull Snapshot snapshot,\n        @Nullable Snapshot baseSnapshot,\n        @NonNull Content content) {\n        return Mono.just(snapshot)\n            .doOnNext(s -> this.patch(s, baseSnapshot, content))\n            .flatMap(client::create);\n    }\n\n    @Override\n    public Mono<Snapshot> patchAndUpdate(@NonNull Snapshot snapshot,\n        @NonNull Snapshot baseSnapshot,\n        @NonNull Content content) {\n        return Mono.just(snapshot)\n            .doOnNext(s -> this.patch(s, baseSnapshot, content))\n            .flatMap(client::update);\n    }\n\n    private void patch(@NonNull Snapshot snapshot,\n        @Nullable Snapshot baseSnapshot,\n        @NonNull Content content) {\n        var annotations = snapshot.getMetadata().getAnnotations();\n        if (annotations != null) {\n            annotations.remove(Snapshot.PATCHED_CONTENT_ANNO);\n            annotations.remove(Snapshot.PATCHED_RAW_ANNO);\n        }\n        var spec = snapshot.getSpec();\n        if (spec == null) {\n            spec = new Snapshot.SnapShotSpec();\n        }\n        spec.setRawType(content.rawType());\n        if (baseSnapshot == null || Objects.equals(snapshot, baseSnapshot)) {\n            // indicate the snapshot is a base snapshot\n            // update raw and content directly\n            spec.setRawPatch(content.raw());\n            spec.setContentPatch(content.content());\n        } else {\n            // apply the patch and set the raw and content\n            var baseSpec = baseSnapshot.getSpec();\n            var baseContent = baseSpec.getContentPatch();\n            var baseRaw = baseSpec.getRawPatch();\n\n            var rawPatch = PatchUtils.diffToJsonPatch(baseRaw, content.raw());\n            var contentPatch = PatchUtils.diffToJsonPatch(baseContent, content.content());\n            spec.setRawPatch(rawPatch);\n            spec.setContentPatch(contentPatch);\n        }\n        spec.setLastModifyTime(clock.instant());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/permalinks/CategoryPermalinkPolicy.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport static org.springframework.web.util.UriUtils.encode;\n\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.theme.utils.PatternUtils;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class CategoryPermalinkPolicy implements PermalinkPolicy<Category> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private static final String DEFAULT_PERMALINK_PREFIX =\n        SystemSetting.ThemeRouteRules.empty().getCategories();\n\n    private final ExternalUrlSupplier externalUrlSupplier;\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    public String permalink(Category category) {\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(category);\n        String permalinkPrefix =\n            annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PREFIX);\n        String slug = encode(category.getSpec().getSlug(), StandardCharsets.UTF_8);\n        String path = PathUtils.combinePath(permalinkPrefix, slug);\n        return externalUrlSupplier.get()\n            .resolve(path)\n            .normalize().toString();\n    }\n\n    public String pattern() {\n        return environmentFetcher.fetchRouteRules()\n            .map(SystemSetting.ThemeRouteRules::getCategories)\n            .defaultIfEmpty(DEFAULT_PERMALINK_PREFIX)\n            .map(PatternUtils::normalizePattern)\n            .block(BLOCKING_TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/permalinks/ExtensionLocator.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport java.util.Objects;\nimport run.halo.app.extension.GroupVersionKind;\n\n/**\n * Slug can be modified, so it is not included in {@link #equals(Object)} and {@link #hashCode()}.\n *\n * @param gvk group version kind\n * @param name extension name\n * @param slug extension slug\n */\npublic record ExtensionLocator(GroupVersionKind gvk, String name, String slug) {\n\n    /**\n     * Create a new {@link ExtensionLocator} instance.\n     *\n     * @param gvk group version kind\n     * @param name extension name\n     * @param slug extension slug\n     */\n    public ExtensionLocator {\n        Objects.requireNonNull(gvk, \"Group version kind must not be null\");\n        Objects.requireNonNull(name, \"Extension name must not be null\");\n        Objects.requireNonNull(slug, \"Extension slug must not be null\");\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n        ExtensionLocator locator = (ExtensionLocator) o;\n        return gvk.equals(locator.gvk) && name.equals(locator.name);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(gvk, name);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/permalinks/PermalinkPolicy.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport org.springframework.util.PropertyPlaceholderHelper;\nimport run.halo.app.extension.AbstractExtension;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic interface PermalinkPolicy<T extends AbstractExtension> {\n\n    PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =\n        new PropertyPlaceholderHelper(\"{\", \"}\");\n\n    String permalink(T extension);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/permalinks/PostPermalinkPolicy.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport static org.springframework.web.util.UriUtils.encode;\n\nimport java.nio.charset.StandardCharsets;\nimport java.text.DecimalFormat;\nimport java.text.NumberFormat;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.Map;\nimport java.util.Properties;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.theme.utils.PatternUtils;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class PostPermalinkPolicy implements PermalinkPolicy<Post> {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    public static final String DEFAULT_CATEGORY = \"default\";\n    private static final String DEFAULT_PERMALINK_PATTERN =\n        SystemSetting.ThemeRouteRules.empty().getPost();\n    private static final NumberFormat NUMBER_FORMAT = new DecimalFormat(\"00\");\n\n    private final SystemConfigFetcher environmentFetcher;\n    private final ExternalUrlSupplier externalUrlSupplier;\n    private final PostService postService;\n\n    @Override\n    public String permalink(Post post) {\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(post);\n        String permalinkPattern =\n            annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PATTERN);\n        return createPermalink(post, permalinkPattern);\n    }\n\n    public String pattern() {\n        return environmentFetcher.fetchRouteRules()\n            .map(PatternUtils::normalizePostPattern)\n            .defaultIfEmpty(DEFAULT_PERMALINK_PATTERN)\n            .block(BLOCKING_TIMEOUT);\n    }\n\n    private String createPermalink(Post post, String pattern) {\n        Instant archiveTime = post.getSpec().getPublishTime();\n        if (archiveTime == null) {\n            archiveTime = post.getMetadata().getCreationTimestamp();\n        }\n        ZonedDateTime zonedDateTime = archiveTime.atZone(ZoneId.systemDefault());\n        Properties properties = new Properties();\n        properties.put(\"name\", post.getMetadata().getName());\n        properties.put(\"slug\", encode(post.getSpec().getSlug(), StandardCharsets.UTF_8));\n        properties.put(\"year\", String.valueOf(zonedDateTime.getYear()));\n        properties.put(\"month\", NUMBER_FORMAT.format(zonedDateTime.getMonthValue()));\n        properties.put(\"day\", NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth()));\n\n        var categorySlug = postService.listCategories(post.getSpec().getCategories())\n            .next()\n            .blockOptional(BLOCKING_TIMEOUT)\n            .map(category -> category.getSpec().getSlug())\n            .orElse(DEFAULT_CATEGORY);\n        properties.put(\"categorySlug\", categorySlug);\n\n        String simplifiedPattern = PathUtils.simplifyPathPattern(pattern);\n        String permalink =\n            PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(simplifiedPattern, properties);\n        return externalUrlSupplier.get()\n            .resolve(permalink)\n            .normalize()\n            .toString();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/permalinks/TagPermalinkPolicy.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.util.UriUtils;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.theme.utils.PatternUtils;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class TagPermalinkPolicy implements PermalinkPolicy<Tag> {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private static final String DEFAULT_PERMALINK_PREFIX =\n        SystemSetting.ThemeRouteRules.empty().getTags();\n    private final ExternalUrlSupplier externalUrlSupplier;\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    public String permalink(Tag tag) {\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(tag);\n        String permalinkPrefix =\n            annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PREFIX);\n\n        String slug = UriUtils.encode(tag.getSpec().getSlug(), StandardCharsets.UTF_8);\n        String path = PathUtils.combinePath(permalinkPrefix, slug);\n        return externalUrlSupplier.get()\n            .resolve(path)\n            .normalize().toString();\n    }\n\n    public String pattern() {\n        return environmentFetcher.fetchRouteRules()\n            .map(SystemSetting.ThemeRouteRules::getTags)\n            .defaultIfEmpty(DEFAULT_PERMALINK_PREFIX)\n            .map(PatternUtils::normalizePattern)\n            .block(BLOCKING_TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/stats/PostStatsUpdater.java",
    "content": "package run.halo.app.content.stats;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.Stats;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.event.post.PostStatsChangedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.infra.InitializationPhase;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@Component\npublic class PostStatsUpdater implements Reconciler<PostStatsUpdater.StatsRequest>,\n    SmartLifecycle {\n\n    private volatile boolean running = false;\n\n    private final ExtensionClient client;\n    private final RequestQueue<StatsRequest> queue;\n    private final Controller controller;\n\n    public PostStatsUpdater(ExtensionClient client) {\n        this.client = client;\n        queue = new DefaultQueue<>(Instant::now);\n        controller = this.setupWith(null);\n    }\n\n    @Override\n    public Result reconcile(StatsRequest request) {\n        client.fetch(Post.class, request.postName()).ifPresent(post -> {\n            var annotations = MetadataUtil.nullSafeAnnotations(post);\n            annotations.put(Post.STATS_ANNO, JsonUtils.objectToJson(request.stats()));\n            client.update(post);\n        });\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return new DefaultController<>(\n            this.getClass().getName(),\n            this,\n            queue,\n            null,\n            Duration.ofMillis(100),\n            Duration.ofMinutes(10));\n    }\n\n    @Override\n    public void start() {\n        this.controller.start();\n        this.running = true;\n    }\n\n    @Override\n    public void stop() {\n        this.running = false;\n        this.controller.dispose();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return this.running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.CONTROLLERS.getPhase();\n    }\n\n    @EventListener(PostStatsChangedEvent.class)\n    public void onReplyEvent(PostStatsChangedEvent event) {\n        var counter = event.getCounter();\n        var stats = Stats.builder()\n            .visit(counter.getVisit())\n            .upvote(counter.getUpvote())\n            .totalComment(counter.getTotalComment())\n            .approvedComment(counter.getApprovedComment())\n            .build();\n        var request = new StatsRequest(event.getPostName(), stats);\n        queue.addImmediately(request);\n    }\n\n    public record StatsRequest(String postName, Stats stats) {\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/stats/ReplyEventReconciler.java",
    "content": "package run.halo.app.content.stats;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.greaterThan;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Optional;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.event.post.CommentUnreadReplyCountChangedEvent;\nimport run.halo.app.event.post.ReplyEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.InitializationPhase;\n\n/**\n * Update the comment status after receiving the reply event.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\npublic class ReplyEventReconciler\n    implements Reconciler<ReplyEventReconciler.CommentName>, SmartLifecycle {\n    private volatile boolean running = false;\n\n    private final ExtensionClient client;\n    private final RequestQueue<CommentName> replyEventQueue;\n    private final Controller replyEventController;\n\n    public ReplyEventReconciler(ExtensionClient client) {\n        this.client = client;\n        replyEventQueue = new DefaultQueue<>(Instant::now);\n        replyEventController = this.setupWith(null);\n    }\n\n    @Override\n    public Result reconcile(CommentName request) {\n        String commentName = request.name();\n\n        client.fetch(Comment.class, commentName)\n            // if the comment has been deleted, then do nothing.\n            .filter(comment -> comment.getMetadata().getDeletionTimestamp() == null)\n            .ifPresent(comment -> {\n                // order by reply creation time desc to get first as last reply time\n                var baseQuery = and(\n                    equal(\"spec.commentName\", commentName),\n                    isNull(\"metadata.deletionTimestamp\")\n                );\n                var pageRequest = PageRequestImpl.ofSize(1).withSort(\n                    Sort.by(\"spec.creationTime\", \"metadata.name\").descending()\n                );\n                final Comment.CommentStatus status = comment.getStatusOrDefault();\n\n                var replyPageResult =\n                    client.listBy(Reply.class, listOptionsWithFieldQuery(baseQuery), pageRequest);\n                // total reply count\n                status.setReplyCount((int) replyPageResult.getTotal());\n\n                // calculate last reply time from total replies(top 1)\n                Instant lastReplyTime = replyPageResult.get()\n                    .map(reply -> reply.getSpec().getCreationTime())\n                    .findFirst()\n                    .orElse(null);\n                status.setLastReplyTime(lastReplyTime);\n\n                // calculate visible reply count(only approved and not hidden)\n                var visibleReplyPageResult =\n                    client.listBy(Reply.class, listOptionsWithFieldQuery(and(\n                        baseQuery,\n                        equal(\"spec.approved\", BooleanUtils.TRUE),\n                        equal(\"spec.hidden\", BooleanUtils.FALSE)\n                    )), pageRequest);\n                status.setVisibleReplyCount((int) visibleReplyPageResult.getTotal());\n\n                // calculate unread reply count(after last read time)\n                var unReadQuery = Optional.ofNullable(comment.getSpec().getLastReadTime())\n                    .map(lastReadTime -> and(\n                        baseQuery,\n                        greaterThan(\"spec.creationTime\", lastReadTime.toString())\n                    ))\n                    .orElse(baseQuery);\n                var unReadPageResult =\n                    client.listBy(Reply.class, listOptionsWithFieldQuery(unReadQuery), pageRequest);\n                status.setUnreadReplyCount((int) unReadPageResult.getTotal());\n\n                status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);\n\n                client.update(comment);\n            });\n        return new Result(false, null);\n    }\n\n    public record CommentName(String name) {\n        public static CommentName of(String name) {\n            return new CommentName(name);\n        }\n    }\n\n    static ListOptions listOptionsWithFieldQuery(Condition query) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(query));\n        return listOptions;\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return new DefaultController<>(\n            this.getClass().getName(),\n            this,\n            replyEventQueue,\n            null,\n            Duration.ofMillis(300),\n            Duration.ofMinutes(5));\n    }\n\n    @Override\n    public void start() {\n        this.replyEventController.start();\n        this.running = true;\n    }\n\n    @Override\n    public void stop() {\n        this.running = false;\n        this.replyEventController.dispose();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return this.running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.CONTROLLERS.getPhase();\n    }\n\n    @EventListener(ReplyEvent.class)\n    public void onReplyEvent(ReplyEvent replyEvent) {\n        var commentName = replyEvent.getReply().getSpec().getCommentName();\n        replyEventQueue.addImmediately(CommentName.of(commentName));\n    }\n\n    @EventListener(CommentUnreadReplyCountChangedEvent.class)\n    public void onUnreadReplyCountChangedEvent(CommentUnreadReplyCountChangedEvent event) {\n        replyEventQueue.addImmediately(CommentName.of(event.getCommentName()));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/stats/TagPostCountUpdater.java",
    "content": "package run.halo.app.content.stats;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport com.google.common.collect.Sets;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.AbstractEventReconciler;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.core.extension.content.Tag.TagStatus;\nimport run.halo.app.event.post.PostDeletedEvent;\nimport run.halo.app.event.post.PostEvent;\nimport run.halo.app.event.post.PostUpdatedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Update {@link TagStatus#postCount} when post related to tag is updated.\n *\n * @author guqing\n * @since 2.13.0\n */\n@Component\npublic class TagPostCountUpdater\n    extends AbstractEventReconciler<TagPostCountUpdater.PostRelatedTags> {\n    private final ExtensionClient client;\n\n    public TagPostCountUpdater(ExtensionClient client) {\n        super(TagPostCountUpdater.class.getName());\n        this.client = client;\n    }\n\n    @Override\n    public Result reconcile(PostRelatedTags postRelatedTags) {\n        for (var tag : postRelatedTags.tags()) {\n            updateTagRelatedPostCount(tag);\n        }\n\n        // Update last associated tags when handled\n        client.fetch(Post.class, postRelatedTags.postName()).ifPresent(post -> {\n            var tags = defaultIfNull(post.getSpec().getTags(), List.<String>of());\n            var annotations = MetadataUtil.nullSafeAnnotations(post);\n            var tagAnno = JsonUtils.objectToJson(tags);\n            var oldTagAnno = annotations.get(Post.LAST_ASSOCIATED_TAGS_ANNO);\n\n            if (!tagAnno.equals(oldTagAnno)) {\n                annotations.put(Post.LAST_ASSOCIATED_TAGS_ANNO, tagAnno);\n                client.update(post);\n            }\n        });\n        return Result.doNotRetry();\n    }\n\n    /**\n     * Listen to post event to calculate post related to tag for updating.\n     */\n    @EventListener(PostEvent.class)\n    public void onPostUpdated(PostEvent postEvent) {\n        var postName = postEvent.getName();\n        if (postEvent instanceof PostUpdatedEvent) {\n            var tagsToUpdate = calcTagsToUpdate(postEvent.getName());\n            queue.addImmediately(new PostRelatedTags(postName, tagsToUpdate));\n            return;\n        }\n\n        if (postEvent instanceof PostDeletedEvent deletedEvent) {\n            var tags = defaultIfNull(deletedEvent.getPost().getSpec().getTags(),\n                List.<String>of());\n            queue.addImmediately(new PostRelatedTags(postName, Sets.newHashSet(tags)));\n        }\n    }\n\n    private Set<String> calcTagsToUpdate(String postName) {\n        var post = client.fetch(Post.class, postName).orElseThrow();\n        var annotations = MetadataUtil.nullSafeAnnotations(post);\n        var oldTags = Optional.ofNullable(annotations.get(Post.LAST_ASSOCIATED_TAGS_ANNO))\n            .filter(StringUtils::isNotBlank)\n            .map(tagsJson -> JsonUtils.jsonToObject(tagsJson, String[].class))\n            .orElse(new String[0]);\n\n        var tagsToUpdate = Sets.newHashSet(oldTags);\n        var newTags = post.getSpec().getTags();\n        if (newTags != null) {\n            tagsToUpdate.addAll(newTags);\n        }\n        return tagsToUpdate;\n    }\n\n    public record PostRelatedTags(String postName, Set<String> tags) {\n    }\n\n    private void updateTagRelatedPostCount(String tagName) {\n        client.fetch(Tag.class, tagName).ifPresent(tag -> {\n            var commonFieldQuery = and(\n                equal(\"spec.tags\", tag.getMetadata().getName()),\n                isNull(\"metadata.deletionTimestamp\")\n            );\n            // Update post count\n            var allPostOptions = new ListOptions();\n            allPostOptions.setFieldSelector(FieldSelector.of(commonFieldQuery));\n            var result = client.listBy(Post.class, allPostOptions, PageRequestImpl.ofSize(1));\n            tag.getStatusOrDefault().setPostCount((int) result.getTotal());\n\n            // Update visible post count\n            var publicPostOptions = new ListOptions();\n            publicPostOptions.setLabelSelector(LabelSelector.builder()\n                .eq(Post.PUBLISHED_LABEL, \"true\")\n                .build());\n            publicPostOptions.setFieldSelector(FieldSelector.of(\n                and(\n                    commonFieldQuery,\n                    equal(\"spec.deleted\", \"false\"),\n                    equal(\"spec.visible\", Post.VisibleEnum.PUBLIC.name())\n                )\n            ));\n            var publicPosts =\n                client.listBy(Post.class, publicPostOptions, PageRequestImpl.ofSize(1));\n            tag.getStatusOrDefault().setVisiblePostCount((int) publicPosts.getTotal());\n\n            client.update(tag);\n        });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/stats/VisitedEventReconciler.java",
    "content": "package run.halo.app.content.stats;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.event.post.VisitedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.infra.InitializationPhase;\n\n/**\n * Update counters after receiving visit event.\n * It will cache the count in memory for one minute and then batch update to the database.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\npublic class VisitedEventReconciler\n    implements Reconciler<VisitedEventReconciler.VisitCountBucket>, SmartLifecycle {\n    private volatile boolean running = false;\n\n    private final ExtensionClient client;\n    private final RequestQueue<VisitCountBucket> visitedEventQueue;\n    private final Map<String, Integer> pooledVisitsMap = new ConcurrentHashMap<>();\n    private final Controller visitedEventController;\n\n    public VisitedEventReconciler(ExtensionClient client) {\n        this.client = client;\n        visitedEventQueue = new DefaultQueue<>(Instant::now);\n        visitedEventController = this.setupWith(null);\n    }\n\n    @Override\n    public Result reconcile(VisitCountBucket visitCountBucket) {\n        createOrUpdateVisits(visitCountBucket.name(), visitCountBucket.visits());\n        return new Result(false, null);\n    }\n\n    private void createOrUpdateVisits(String name, Integer visits) {\n        client.fetch(Counter.class, name)\n            .ifPresentOrElse(counter -> {\n                Integer existingVisit = ObjectUtils.defaultIfNull(counter.getVisit(), 0);\n                counter.setVisit(existingVisit + visits);\n                client.update(counter);\n            }, () -> {\n                Counter counter = Counter.emptyCounter(name);\n                counter.setVisit(visits);\n                client.create(counter);\n            });\n    }\n\n    /**\n     * Put the merged data into the queue every minute for updating to the database.\n     */\n    @Scheduled(cron = \"0 0/1 * * * ?\")\n    public void queuedVisitBucketTask() {\n        Iterator<Map.Entry<String, Integer>> iterator = pooledVisitsMap.entrySet().iterator();\n        while (iterator.hasNext()) {\n            Map.Entry<String, Integer> item = iterator.next();\n            visitedEventQueue.addImmediately(new VisitCountBucket(item.getKey(), item.getValue()));\n            iterator.remove();\n        }\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return new DefaultController<>(\n            this.getClass().getName(),\n            this,\n            visitedEventQueue,\n            null,\n            Duration.ofMillis(300),\n            Duration.ofMinutes(5));\n    }\n\n    @Override\n    public void start() {\n        this.visitedEventController.start();\n        this.running = true;\n    }\n\n    @Override\n    public void stop() {\n        log.debug(\"Persist visits to database before destroy...\");\n        try {\n            Iterator<Map.Entry<String, Integer>> iterator = pooledVisitsMap.entrySet().iterator();\n            while (iterator.hasNext()) {\n                Map.Entry<String, Integer> item = iterator.next();\n                createOrUpdateVisits(item.getKey(), item.getValue());\n                iterator.remove();\n            }\n        } catch (Exception e) {\n            log.error(\"Failed to persist visits to database.\", e);\n        }\n        this.running = false;\n        this.visitedEventController.dispose();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return this.running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.CONTROLLERS.getPhase();\n    }\n\n    public record VisitCountBucket(String name, int visits) {\n    }\n\n    @Component\n    @RequiredArgsConstructor\n    public class VisitedEventListener {\n        private final SchemeManager schemeManager;\n\n        @Async\n        @EventListener(VisitedEvent.class)\n        public void onVisited(VisitedEvent visitedEvent) {\n            mergeVisits(visitedEvent);\n        }\n\n        private void mergeVisits(VisitedEvent event) {\n            var gpn = new GroupPluralName(event.getGroup(), event.getPlural(), event.getName());\n            if (!checkVisitSubject(gpn)) {\n                log.debug(\"Skip visit event for: {}\", gpn);\n                return;\n            }\n            String counterName =\n                MeterUtils.nameOf(event.getGroup(), event.getPlural(), event.getName());\n            pooledVisitsMap.compute(counterName, (name, visits) -> {\n                if (visits == null) {\n                    return 1;\n                } else {\n                    return visits + 1;\n                }\n            });\n        }\n\n        private boolean checkVisitSubject(GroupPluralName groupPluralName) {\n            Optional<Scheme> schemeOptional = schemeManager.schemes().stream()\n                .filter(scheme -> {\n                    GroupVersionKind gvk = scheme.groupVersionKind();\n                    return scheme.plural().equals(groupPluralName.plural())\n                        && gvk.group().equals(groupPluralName.group());\n                })\n                .findFirst();\n            return schemeOptional.map(\n                    scheme -> client.fetch(scheme.groupVersionKind(), groupPluralName.name())\n                        .isPresent()\n                )\n                .orElse(false);\n        }\n\n        record GroupPluralName(String group, String plural, String name) {\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/content/stats/VotedEventReconciler.java",
    "content": "package run.halo.app.content.stats;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.event.post.DownvotedEvent;\nimport run.halo.app.event.post.UpvotedEvent;\nimport run.halo.app.event.post.VotedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.infra.InitializationPhase;\n\n/**\n * Update counters after receiving upvote or downvote event.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\npublic class VotedEventReconciler implements Reconciler<VotedEvent>, SmartLifecycle {\n    private volatile boolean running = false;\n\n    private final ExtensionClient client;\n    private final RequestQueue<VotedEvent> votedEventQueue;\n    private final Controller votedEventController;\n\n    public VotedEventReconciler(ExtensionClient client) {\n        this.client = client;\n        votedEventQueue = new DefaultQueue<>(Instant::now);\n        votedEventController = this.setupWith(null);\n    }\n\n    @Override\n    public Result reconcile(VotedEvent votedEvent) {\n        String counterName =\n            MeterUtils.nameOf(votedEvent.getGroup(), votedEvent.getPlural(), votedEvent.getName());\n        client.fetch(Counter.class, counterName)\n            .ifPresentOrElse(counter -> {\n                if (votedEvent instanceof UpvotedEvent) {\n                    Integer existingVote = ObjectUtils.defaultIfNull(counter.getUpvote(), 0);\n                    counter.setUpvote(existingVote + 1);\n                } else if (votedEvent instanceof DownvotedEvent) {\n                    Integer existingVote = ObjectUtils.defaultIfNull(counter.getDownvote(), 0);\n                    counter.setDownvote(existingVote + 1);\n                }\n                client.update(counter);\n            }, () -> {\n                Counter counter = Counter.emptyCounter(counterName);\n                if (votedEvent instanceof UpvotedEvent) {\n                    counter.setUpvote(1);\n                } else if (votedEvent instanceof DownvotedEvent) {\n                    counter.setDownvote(1);\n                }\n                client.create(counter);\n            });\n        return new Result(false, null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return new DefaultController<>(\n            this.getClass().getName(),\n            this,\n            votedEventQueue,\n            null,\n            Duration.ofMillis(300),\n            Duration.ofMinutes(5));\n    }\n\n    @Override\n    public void start() {\n        this.votedEventController.start();\n        this.running = true;\n    }\n\n    @Override\n    public void stop() {\n        this.running = false;\n        this.votedEventController.dispose();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return this.running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.CONTROLLERS.getPhase();\n    }\n\n    @Component\n    @RequiredArgsConstructor\n    public class VotedEventListener {\n        private final SchemeManager schemeManager;\n\n        /**\n         * Add up/down vote event to queue.\n         */\n        @Async\n        @EventListener(VotedEvent.class)\n        public void onVoted(VotedEvent event) {\n            var gpn = new GroupPluralName(event.getGroup(), event.getPlural(), event.getName());\n            if (!checkSubject(gpn)) {\n                log.debug(\"Skip voted event for: {}\", gpn);\n                return;\n            }\n            votedEventQueue.addImmediately(event);\n        }\n\n        private boolean checkSubject(\n            GroupPluralName groupPluralName) {\n            Optional<Scheme> schemeOptional = schemeManager.schemes().stream()\n                .filter(scheme -> {\n                    GroupVersionKind gvk = scheme.groupVersionKind();\n                    return scheme.plural().equals(groupPluralName.plural())\n                        && gvk.group().equals(groupPluralName.group());\n                })\n                .findFirst();\n            return schemeOptional.map(\n                    scheme -> client.fetch(scheme.groupVersionKind(), groupPluralName.name())\n                        .isPresent()\n                )\n                .orElse(false);\n        }\n\n        record GroupPluralName(String group, String plural, String name) {\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/AttachmentChangedEvent.java",
    "content": "package run.halo.app.core.attachment;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.attachment.Attachment;\n\n/**\n * Event triggered when an attachment is created, updated, or deleted.\n *\n * @author johnniang\n */\npublic class AttachmentChangedEvent extends ApplicationEvent {\n\n    @Getter\n    private final Attachment attachment;\n\n    public AttachmentChangedEvent(Object source, Attachment attachment) {\n        super(source);\n        this.attachment = attachment;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/AttachmentLister.java",
    "content": "package run.halo.app.core.attachment;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.extension.ListResult;\n\npublic interface AttachmentLister {\n\n    Mono<ListResult<Attachment>> listBy(SearchRequest searchRequest);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/AttachmentRootGetter.java",
    "content": "package run.halo.app.core.attachment;\n\nimport java.nio.file.Path;\nimport java.util.function.Supplier;\n\n/**\n * Gets the root path(work dir) of the local attachment.\n */\npublic interface AttachmentRootGetter extends Supplier<Path> {\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/PolicyConfigChangeDetector.java",
    "content": "package run.halo.app.core.attachment;\n\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionMatcher;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\n\n/**\n * <p>Detects changes to {@link ConfigMap} that are referenced by {@link Policy} and updates the\n * {@link Attachment} with the {@link Policy} reference to reflect the change.</p>\n * <p>Without this, the link to the attachment corresponding to the storage policy configuration\n * change may not be correctly updated and only the service can be restarted.</p>\n *\n * @author guqing\n * @since 2.20.0\n */\n@Component\n@RequiredArgsConstructor\npublic class PolicyConfigChangeDetector implements Reconciler<Reconciler.Request> {\n    static final String POLICY_UPDATED_AT = \"storage.halo.run/policy-updated-at\";\n    private final GroupVersionKind attachmentGvk = GroupVersionKind.fromExtension(Attachment.class);\n    private final ExtensionClient client;\n    private final AttachmentUpdateTrigger attachmentUpdateTrigger;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(ConfigMap.class, request.name())\n            .ifPresent(configMap -> {\n                var labels = configMap.getMetadata().getLabels();\n                if (labels == null) {\n                    return;\n                }\n                var policyName = labels.get(Policy.POLICY_OWNER_LABEL);\n                if (StringUtils.hasText(policyName)) {\n                    var options = ListOptions.builder()\n                        .andQuery(equal(\"spec.policyName\", policyName))\n                        .build();\n                    var attachmentNames =\n                        client.listAllNames(Attachment.class, options, Sort.unsorted());\n                    attachmentUpdateTrigger.addAll(attachmentNames);\n                }\n            });\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        ExtensionMatcher matcher = extension -> {\n            var configMap = (ConfigMap) extension;\n            var labels = configMap.getMetadata().getLabels();\n            return labels != null && labels.containsKey(Policy.POLICY_OWNER_LABEL);\n        };\n        return builder\n            .extension(new ConfigMap())\n            .syncAllOnStart(false)\n            .onAddMatcher(matcher)\n            .onUpdateMatcher(matcher)\n            .onDeleteMatcher(matcher)\n            .build();\n    }\n\n    @Component\n    static class AttachmentUpdateTrigger implements Reconciler<String>, SmartLifecycle {\n        private final RequestQueue<String> queue;\n\n        private final Controller controller;\n\n        private volatile boolean running = false;\n\n        private final ExtensionClient client;\n\n        public AttachmentUpdateTrigger(ExtensionClient client) {\n            this.client = client;\n            this.queue = new DefaultQueue<>(Instant::now);\n            this.controller = this.setupWith(null);\n        }\n\n        @Override\n        public Result reconcile(String name) {\n            client.fetch(Attachment.class, name).ifPresent(attachment -> {\n                var annotations = MetadataUtil.nullSafeAnnotations(attachment);\n                annotations.put(POLICY_UPDATED_AT, Instant.now().toString());\n                client.update(attachment);\n            });\n            return Result.doNotRetry();\n        }\n\n        void addAll(List<String> names) {\n            for (String name : names) {\n                queue.addImmediately(name);\n            }\n        }\n\n        @Override\n        public Controller setupWith(ControllerBuilder builder) {\n            return new DefaultController<>(\n                \"PolicyChangeAttachmentUpdater\",\n                this,\n                queue,\n                null,\n                Duration.ofMillis(100),\n                Duration.ofMinutes(10)\n            );\n        }\n\n        @Override\n        public void start() {\n            controller.start();\n            running = true;\n        }\n\n        @Override\n        public void stop() {\n            running = false;\n            controller.dispose();\n        }\n\n        @Override\n        public boolean isRunning() {\n            return running;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/SearchRequest.java",
    "content": "package run.halo.app.core.attachment;\n\nimport static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;\nimport static run.halo.app.extension.index.query.Queries.contains;\nimport static run.halo.app.extension.index.query.Queries.in;\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.extension.index.query.Queries.not;\nimport static run.halo.app.extension.index.query.Queries.or;\nimport static run.halo.app.extension.index.query.Queries.startsWith;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.List;\nimport java.util.Optional;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.extension.router.QueryParamBuildUtil;\nimport run.halo.app.extension.router.SortableRequest;\n\npublic class SearchRequest extends SortableRequest {\n\n    public SearchRequest(ServerRequest request) {\n        super(request.exchange());\n    }\n\n    public Optional<String> getKeyword() {\n        return Optional.ofNullable(queryParams.getFirst(\"keyword\"))\n            .filter(StringUtils::hasText);\n    }\n\n    public Optional<Boolean> getUngrouped() {\n        return Optional.ofNullable(queryParams.getFirst(\"ungrouped\"))\n            .map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class));\n    }\n\n    public Optional<List<String>> getAccepts() {\n        return Optional.ofNullable(queryParams.get(\"accepts\"))\n            .filter(accepts -> !accepts.isEmpty()\n                && !accepts.contains(\"*\")\n                && !accepts.contains(\"*/*\")\n            );\n    }\n\n    public ListOptions toListOptions(List<String> hiddenGroups) {\n        var builder = ListOptions.builder(super.toListOptions());\n\n        getKeyword().ifPresent(keyword -> {\n            builder.andQuery(contains(\"spec.displayName\", keyword));\n        });\n\n        getUngrouped()\n            .filter(ungrouped -> ungrouped)\n            .ifPresent(ungrouped -> builder.andQuery(isNull(\"spec.groupName\")));\n\n        if (!CollectionUtils.isEmpty(hiddenGroups)) {\n            builder.andQuery(or(isNull(\"spec.groupName\"), not(in(\"spec.groupName\", hiddenGroups))));\n        }\n\n        getAccepts().flatMap(accepts -> accepts.stream()\n                .filter(StringUtils::hasText)\n                .map(accept -> accept.replace(\"/*\", \"/\").toLowerCase())\n                .distinct()\n                .map(accept -> startsWith(\"spec.mediaType\", accept))\n                .reduce(Queries::or)\n            )\n            .ifPresent(builder::andQuery);\n\n        return builder.build();\n    }\n\n    public static void buildParameters(Builder builder) {\n        IListRequest.buildParameters(builder);\n        builder.parameter(QueryParamBuildUtil.sortParameter())\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"ungrouped\")\n                .required(false)\n                .description(\"\"\"\n                    Filter attachments without group. This parameter will ignore group \\\n                    parameter.\\\n                    \"\"\")\n                .implementation(Boolean.class))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"keyword\")\n                .required(false)\n                .description(\"Keyword for searching.\")\n                .implementation(String.class))\n            .parameter(parameterBuilder()\n                .in(ParameterIn.QUERY)\n                .name(\"accepts\")\n                .required(false)\n                .description(\"Acceptable media types.\")\n                .array(\n                    arraySchemaBuilder()\n                        .uniqueItems(true)\n                        .schema(schemaBuilder()\n                            .implementation(String.class)\n                            .example(\"image/*\"))\n                )\n                .implementationArray(String.class)\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/endpoint/AttachmentEndpoint.java",
    "content": "package run.halo.app.core.attachment.endpoint;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static run.halo.app.extension.ListResult.generateGenericClass;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.net.URL;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.http.codec.multipart.FormFieldPart;\nimport org.springframework.http.codec.multipart.Part;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.BodyExtractors;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.AttachmentLister;\nimport run.halo.app.core.attachment.SearchRequest;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.extension.service.AttachmentService;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class AttachmentEndpoint implements CustomEndpoint {\n\n    private final AttachmentService attachmentService;\n\n    private final AttachmentLister attachmentLister;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"AttachmentV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"/attachments/upload\", contentType(MediaType.MULTIPART_FORM_DATA),\n                request -> request.body(BodyExtractors.toMultipartData())\n                    .map(UploadRequest::new)\n                    .flatMap(uploadReq -> {\n                        var policyName = uploadReq.getPolicyName();\n                        var groupName = uploadReq.getGroupName();\n                        var filePart = uploadReq.getFile();\n                        return attachmentService.upload(policyName,\n                            groupName,\n                            filePart.filename(),\n                            filePart.content(),\n                            filePart.headers().getContentType());\n                    })\n                    .flatMap(attachment -> ServerResponse.ok().bodyValue(attachment)),\n                builder -> builder\n                    .operationId(\"UploadAttachment\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder().implementation(IUploadRequest.class))\n                        ))\n                    .response(responseBuilder().implementation(Attachment.class))\n                    .build())\n            .POST(\"/attachments/-/upload-from-url\", contentType(MediaType.APPLICATION_JSON),\n                request -> request.bodyToMono(UploadFromUrlRequest.class)\n                    .flatMap(uploadFromUrlRequest -> {\n                        var url = uploadFromUrlRequest.url();\n                        var policyName = uploadFromUrlRequest.policyName();\n                        var groupName = uploadFromUrlRequest.groupName();\n                        var fileName = uploadFromUrlRequest.filename();\n                        return attachmentService.uploadFromUrl(url, policyName,\n                            groupName, fileName);\n                    })\n                    .flatMap(attachment -> ServerResponse.ok().bodyValue(attachment)),\n                builder -> builder\n                    .operationId(\"ExternalTransferAttachment\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder().implementation(UploadFromUrlRequest.class))\n                        ))\n                    .response(responseBuilder().implementation(Attachment.class))\n                    .build()\n            )\n            .GET(\"/attachments\", this::search,\n                builder -> {\n                    builder\n                        .operationId(\"SearchAttachments\")\n                        .tag(tag)\n                        .response(\n                            responseBuilder().implementation(generateGenericClass(Attachment.class))\n                        );\n                    SearchRequest.buildParameters(builder);\n                }\n            )\n            .build();\n    }\n\n    Mono<ServerResponse> search(ServerRequest request) {\n        var searchRequest = new SearchRequest(request);\n        return attachmentLister.listBy(searchRequest)\n            .flatMap(listResult -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(listResult)\n            );\n    }\n\n    public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,\n                                       @Schema(requiredMode = REQUIRED, description = \"Storage \"\n                                           + \"policy name\") String policyName,\n                                       @Schema(description = \"The name of the group to which the \"\n                                           + \"attachment belongs\") String groupName,\n                                       @Schema(description = \"Custom file name\") String filename) {\n        public UploadFromUrlRequest {\n            if (Objects.isNull(url)) {\n                throw new ServerWebInputException(\"Required url is missing.\");\n            }\n\n            if (!StringUtils.hasText(policyName)) {\n                throw new ServerWebInputException(\"Policy name must not be blank\");\n            }\n        }\n    }\n\n    @Schema(types = \"object\")\n    public interface IUploadRequest {\n\n        @Schema(requiredMode = REQUIRED, description = \"Attachment file\")\n        FilePart getFile();\n\n        @Schema(requiredMode = REQUIRED, description = \"Storage policy name\")\n        String getPolicyName();\n\n        @Schema(description = \"The name of the group to which the attachment belongs\")\n        String getGroupName();\n\n    }\n\n    public record UploadRequest(MultiValueMap<String, Part> formData) implements IUploadRequest {\n\n        public FilePart getFile() {\n            if (formData.getFirst(\"file\") instanceof FilePart file) {\n                return file;\n            }\n            throw new ServerWebInputException(\"Invalid part of file\");\n        }\n\n        public String getPolicyName() {\n            if (formData.getFirst(\"policyName\") instanceof FormFieldPart form) {\n                return form.value();\n            }\n            throw new ServerWebInputException(\"Invalid part of policyName\");\n        }\n\n        @Override\n        public String getGroupName() {\n            if (formData.getFirst(\"groupName\") instanceof FormFieldPart form) {\n                return form.value();\n            }\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/endpoint/LocalAttachmentUploadHandler.java",
    "content": "package run.halo.app.core.attachment.endpoint;\n\nimport static java.nio.file.StandardOpenOption.CREATE_NEW;\nimport static run.halo.app.infra.utils.FileNameUtils.randomFileName;\nimport static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;\nimport static run.halo.app.infra.utils.FileUtils.deleteFileSilently;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.stream.Collectors;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.io.FilenameUtils;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.StringUtils;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.publisher.SynchronousSink;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.attachment.thumbnail.LocalThumbnailService;\nimport run.halo.app.core.attachment.thumbnail.ThumbnailUtils;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Attachment.AttachmentSpec;\nimport run.halo.app.core.extension.attachment.Constant;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.FileCategoryMatcher;\nimport run.halo.app.infra.exception.AttachmentAlreadyExistsException;\nimport run.halo.app.infra.exception.FileSizeExceededException;\nimport run.halo.app.infra.exception.FileTypeNotAllowedException;\nimport run.halo.app.infra.utils.FileNameUtils;\nimport run.halo.app.infra.utils.FileTypeDetectUtils;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass LocalAttachmentUploadHandler implements AttachmentHandler {\n\n    private static final String UPLOAD_PATH = \"upload\";\n\n    private final AttachmentRootGetter attachmentDirGetter;\n\n    private final LocalThumbnailService localThumbnailService;\n\n    private Clock clock = Clock.systemUTC();\n\n    /**\n     * Set clock for test.\n     *\n     * @param clock new clock\n     */\n    void setClock(Clock clock) {\n        this.clock = clock;\n    }\n\n    @Override\n    public Mono<Attachment> upload(UploadContext uploadOption) {\n        return Mono.just(uploadOption)\n            .filter(option -> this.shouldHandle(option.policy()))\n            .flatMap(option -> {\n                var configMap = option.configMap();\n                var setting = Optional.ofNullable(configMap)\n                    .map(ConfigMap::getData)\n                    .map(data -> data.get(\"default\"))\n                    .map(json -> JsonUtils.jsonToObject(json, PolicySetting.class))\n                    .orElseGet(PolicySetting::new);\n\n                final var attachmentsRoot = attachmentDirGetter.get();\n                final var uploadRoot = attachmentsRoot.resolve(UPLOAD_PATH);\n                final var file = option.file();\n                final Path attachmentPath;\n                final String filename = getFilename(file.filename(), setting);\n                if (StringUtils.hasText(setting.getLocation())) {\n                    attachmentPath =\n                        uploadRoot.resolve(setting.getLocation()).resolve(filename);\n                } else {\n                    attachmentPath = uploadRoot.resolve(filename);\n                }\n                checkDirectoryTraversal(uploadRoot, attachmentPath);\n\n                return validateFile(file, setting).then(Mono.fromRunnable(\n                        () -> {\n                            try {\n                                // init parent folders\n                                Files.createDirectories(attachmentPath.getParent());\n                            } catch (IOException e) {\n                                throw Exceptions.propagate(e);\n                            }\n                        })\n                    .subscribeOn(Schedulers.boundedElastic())\n                    .then(writeContent(file.content(), attachmentPath, true))\n                    .map(path -> {\n                        log.info(\"Wrote attachment {} into {}\", filename, path);\n                        // TODO check the file extension\n                        var metadata = new Metadata();\n                        metadata.setName(UUID.randomUUID().toString());\n                        var relativePath = attachmentsRoot.relativize(path).toString();\n\n                        var pathSegments = new ArrayList<String>();\n                        pathSegments.add(UPLOAD_PATH);\n                        for (Path p : uploadRoot.relativize(path)) {\n                            pathSegments.add(p.toString());\n                        }\n\n                        var uri = UriComponentsBuilder.newInstance()\n                            .pathSegment(pathSegments.toArray(String[]::new))\n                            .encode(StandardCharsets.UTF_8)\n                            .build()\n                            .toString();\n                        metadata.setAnnotations(Map.of(\n                            Constant.LOCAL_REL_PATH_ANNO_KEY, relativePath,\n                            Constant.URI_ANNO_KEY, uri));\n                        var spec = new AttachmentSpec();\n                        spec.setSize(path.toFile().length());\n                        spec.setMediaType(Optional.ofNullable(file.headers().getContentType())\n                            .map(MediaType::toString)\n                            .orElse(null));\n                        spec.setDisplayName(path.getFileName().toString());\n                        var attachment = new Attachment();\n                        attachment.setMetadata(metadata);\n                        attachment.setSpec(spec);\n\n                        attachment.setStatus(new Attachment.AttachmentStatus());\n                        doGetPermalink(attachment).ifPresent(permalink ->\n                            attachment.getStatus().setPermalink(permalink.toASCIIString())\n                        );\n                        var thumbnailLinks = doGetThumbnailLinks(attachment);\n                        var thumbnails = thumbnailLinks.keySet().stream()\n                            .collect(Collectors.toMap(\n                                ThumbnailSize::name,\n                                size -> thumbnailLinks.get(size).toASCIIString()\n                            ));\n                        if (!thumbnails.isEmpty()) {\n                            attachment.getStatus().setThumbnails(thumbnails);\n                        }\n                        return attachment;\n                    })\n                    .onErrorMap(FileAlreadyExistsException.class,\n                        e -> new AttachmentAlreadyExistsException(e.getFile()))\n                );\n            });\n    }\n\n    private Mono<Void> validateFile(FilePart file, PolicySetting setting) {\n        var validations = new ArrayList<Publisher<?>>(2);\n        var maxSize = setting.getMaxFileSize();\n        if (maxSize != null && maxSize.toBytes() > 0) {\n            validations.add(\n                file.content()\n                    .map(DataBuffer::readableByteCount)\n                    .reduce(0L, Long::sum)\n                    .filter(size -> size <= setting.getMaxFileSize().toBytes())\n                    .switchIfEmpty(Mono.error(new FileSizeExceededException(\n                        \"File size exceeds the maximum limit\",\n                        \"problemDetail.attachment.upload.fileSizeExceeded\",\n                        new Object[] {setting.getMaxFileSize().toKilobytes() + \"KB\"})\n                    ))\n            );\n        }\n        if (!CollectionUtils.isEmpty(setting.getAllowedFileTypes())) {\n            var typeValidator = file.content()\n                .next()\n                .handle((dataBuffer, sink) -> {\n                    var mimeType = detectMimeType(dataBuffer.asInputStream(), file.filename());\n                    if (!FileTypeDetectUtils.isValidExtensionForMime(mimeType, file.filename())) {\n                        handleFileTypeError(sink, \"fileTypeNotMatch\", mimeType);\n                        return;\n                    }\n                    var isAllow = setting.getAllowedFileTypes()\n                        .stream()\n                        .map(FileCategoryMatcher::of)\n                        .anyMatch(matcher -> matcher.match(mimeType));\n                    if (isAllow) {\n                        sink.next(dataBuffer);\n                        return;\n                    }\n                    handleFileTypeError(sink, \"fileTypeNotSupported\", mimeType);\n                });\n            validations.add(typeValidator);\n        }\n        return Mono.when(validations);\n    }\n\n    private static void handleFileTypeError(SynchronousSink<Object> sink, String detailCode,\n        String mimeType) {\n        sink.error(new FileTypeNotAllowedException(\"File type is not allowed\",\n            \"problemDetail.attachment.upload.\" + detailCode,\n            new Object[] {mimeType})\n        );\n    }\n\n    @NonNull\n    private String detectMimeType(InputStream inputStream, String name) {\n        try {\n            return FileTypeDetectUtils.detectMimeType(inputStream, name);\n        } catch (IOException e) {\n            log.warn(\"Failed to detect file type\", e);\n            return \"Unknown\";\n        }\n    }\n\n    @Override\n    public Mono<Attachment> delete(DeleteContext deleteContext) {\n        return Mono.just(deleteContext)\n            .filter(context -> this.shouldHandle(context.policy()))\n            .publishOn(Schedulers.boundedElastic())\n            .doOnNext(context -> {\n                var attachment = context.attachment();\n                log.info(\"Trying to delete {} from local\", attachment.getMetadata().getName());\n                var annotations = attachment.getMetadata().getAnnotations();\n                if (annotations != null) {\n                    var localRelativePath = annotations.get(Constant.LOCAL_REL_PATH_ANNO_KEY);\n                    if (StringUtils.hasText(localRelativePath)) {\n                        var attachmentsRoot = attachmentDirGetter.get();\n                        var attachmentPath = attachmentsRoot.resolve(localRelativePath);\n                        deleteAttachmentFile(attachmentPath);\n                        deleteThumbnails(attachmentPath);\n                    }\n                }\n            })\n            .map(DeleteContext::attachment);\n    }\n\n    @Override\n    public Mono<URI> getPermalink(Attachment attachment, Policy policy, ConfigMap configMap) {\n        if (!this.shouldHandle(policy)) {\n            return Mono.empty();\n        }\n        return Mono.justOrEmpty(doGetPermalink(attachment));\n    }\n\n    @Override\n    public Mono<URI> getSharedURL(Attachment attachment,\n        Policy policy,\n        ConfigMap configMap,\n        Duration ttl) {\n        return getPermalink(attachment, policy, configMap);\n    }\n\n    @Override\n    public Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment, Policy policy,\n        ConfigMap configMap) {\n        if (!this.shouldHandle(policy)) {\n            return Mono.empty();\n        }\n\n        return Mono.just(doGetThumbnailLinks(attachment));\n    }\n\n    protected Optional<URI> doGetPermalink(Attachment attachment) {\n        var annotations = attachment.getMetadata().getAnnotations();\n        if (annotations == null\n            || !annotations.containsKey(Constant.URI_ANNO_KEY)) {\n            return Optional.empty();\n        }\n        var uriStr = annotations.get(Constant.URI_ANNO_KEY);\n        return Optional.of(HaloUtils.safeToUri(uriStr));\n    }\n\n    private Map<ThumbnailSize, URI> doGetThumbnailLinks(Attachment attachment) {\n        if (attachment.getStatus() == null\n            || !StringUtils.hasText(attachment.getStatus().getPermalink())) {\n            return Map.of();\n        }\n        // Check media type first, then check file extension from permalink\n        var supported = Optional.ofNullable(attachment.getSpec().getMediaType())\n            .map(MediaType::parseMediaType)\n            .map(ThumbnailUtils::isSupportedImage)\n            .filter(Boolean::booleanValue)\n            .or(() -> Optional.ofNullable(attachment.getStatus().getPermalink())\n                .map(permalink -> {\n                    var path = URI.create(permalink).getPath();\n                    return FilenameUtils.getExtension(path);\n                })\n                .map(ThumbnailUtils::isSupportedImage)\n                .filter(Boolean::booleanValue)\n            )\n            .isPresent();\n        if (!supported) {\n            return Map.of();\n        }\n        var permalinkUri = URI.create(attachment.getStatus().getPermalink());\n        return ThumbnailUtils.buildSrcsetMap(permalinkUri);\n    }\n\n    private void deleteAttachmentFile(Path attachmentPath) {\n        var attachmentsRoot = attachmentDirGetter.get();\n        checkDirectoryTraversal(attachmentsRoot, attachmentPath);\n        try {\n            Files.deleteIfExists(attachmentPath);\n            log.info(\"Deleted attachment file {}\", attachmentPath);\n        } catch (IOException e) {\n            throw Exceptions.propagate(e);\n        }\n    }\n\n    private void deleteThumbnails(Path attachmentPath) {\n        this.localThumbnailService.delete(attachmentPath);\n    }\n\n    private boolean shouldHandle(Policy policy) {\n        if (policy == null\n            || policy.getSpec() == null\n            || !StringUtils.hasText(policy.getSpec().getTemplateName())) {\n            return false;\n        }\n        return \"local\".equals(policy.getSpec().getTemplateName());\n    }\n\n    /**\n     * Write content into file. We will detect duplicate filename and auto-rename it with 3 times\n     * retry.\n     *\n     * @param content is file content\n     * @param targetPath is target path\n     * @return file path\n     */\n    private Mono<Path> writeContent(Flux<DataBuffer> content,\n        Path targetPath,\n        boolean renameIfExists) {\n        return Mono.defer(() -> {\n            final var pathRef = new AtomicReference<>(targetPath);\n            return Mono.defer(\n                    // we have to use defer method to obtain a fresh path\n                    () -> DataBufferUtils.write(content, pathRef.get(), CREATE_NEW))\n                .retryWhen(Retry.max(3)\n                    .filter(t -> {\n                        if (renameIfExists) {\n                            return t instanceof FileAlreadyExistsException;\n                        }\n                        return false;\n                    })\n                    .doAfterRetry(signal -> {\n                        // rename the path\n                        var oldPath = pathRef.get();\n                        var fileName = randomFileName(oldPath.toString(), 4);\n                        pathRef.set(oldPath.resolveSibling(fileName));\n                    }))\n                // Delete file already wrote partially into attachment folder\n                // in case of content is terminated with an error\n                .onErrorResume(t -> deleteFileSilently(pathRef.get()).then(Mono.error(t)))\n                .then(Mono.fromSupplier(pathRef::get));\n        });\n    }\n\n    private String getFilename(String filename, PolicySetting setting) {\n        if (!setting.isAlwaysRenameFilename()) {\n            return filename;\n        }\n        var renameStrategy = setting.getRenameStrategy();\n        if (renameStrategy == null) {\n            return filename;\n        }\n        var renameMethod = renameStrategy.getMethod();\n        if (renameMethod == null) {\n            renameMethod = RenameMethod.RANDOM;\n        }\n        var excludeOriginalFilename = renameStrategy.isExcludeOriginalFilename();\n        switch (renameMethod) {\n            case TIMESTAMP -> {\n                return FileNameUtils.renameFilename(\n                    filename,\n                    () -> {\n                        var now = clock.instant();\n                        return now.toEpochMilli() + \"\";\n                    },\n                    excludeOriginalFilename);\n            }\n            case UUID -> {\n                return FileNameUtils.renameFilename(\n                    filename,\n                    () -> UUID.randomUUID().toString(),\n                    excludeOriginalFilename\n                );\n            }\n            default -> {\n                return FileNameUtils.renameFilename(\n                    filename,\n                    () -> {\n                        var length = renameStrategy.getRandomLength();\n                        if (length < 8) {\n                            length = 8;\n                        } else if (length > 64) {\n                            // The max filename length is 256, so we limit the random length to 64\n                            // for most cases.\n                            length = 64;\n                        }\n                        return RandomStringUtils.secure().nextAlphabetic(length);\n                    },\n                    excludeOriginalFilename);\n            }\n        }\n    }\n\n\n    @Data\n    public static class PolicySetting {\n\n        private String location;\n\n        private DataSize maxFileSize;\n\n        private Set<String> allowedFileTypes;\n\n        private boolean alwaysRenameFilename;\n\n        private RenameStrategy renameStrategy;\n\n        public void setMaxFileSize(String maxFileSize) {\n            if (!StringUtils.hasText(maxFileSize)) {\n                return;\n            }\n            this.maxFileSize = DataSize.parse(maxFileSize);\n        }\n    }\n\n    public enum RenameMethod {\n        RANDOM,\n        UUID,\n        TIMESTAMP\n    }\n\n    @Data\n    public static class RenameStrategy {\n\n        private RenameMethod method;\n\n        private int randomLength = 32;\n\n        private boolean excludeOriginalFilename;\n\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/endpoint/PolicyEndpoint.java",
    "content": "package run.halo.app.core.attachment.endpoint;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springframework.http.HttpStatus.NO_CONTENT;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.HashMap;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport org.springframework.transaction.reactive.TransactionalOperator;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport tools.jackson.databind.JsonNode;\nimport tools.jackson.databind.json.JsonMapper;\n\n@Component\n@RequiredArgsConstructor\nclass PolicyEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n\n    private final JsonMapper mapper;\n\n    private final ReactiveTransactionManager txManager;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"PolicyAlpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\n                \"/policies/{name}/configs/{group}\",\n                this::getPolicyConfigByGroup,\n                builder -> builder.operationId(\"getPolicyConfigByGroup\")\n                    .description(\"Get policy config by group\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Name of the policy\")\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"group\")\n                        .description(\"Name of the group\")\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder().implementation(Object.class))\n            )\n            .PUT(\n                \"/policies/{name}/configs/{group}\",\n                RequestPredicates.contentType(MediaType.APPLICATION_JSON),\n                this::updatePolicyConfigByGroup,\n                builder -> builder.operationId(\"updatePolicyConfigByGroup\")\n                    .description(\"Update policy config by group\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Name of the policy\")\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"group\")\n                        .description(\"Name of the group\")\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .requestBody(\n                        requestBodyBuilder().required(true).implementation(Object.class))\n                    .response(\n                        responseBuilder().responseCode(String.valueOf(NO_CONTENT.value()))\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> updatePolicyConfigByGroup(ServerRequest serverRequest) {\n        var policyName = serverRequest.pathVariable(\"name\");\n        var configGroup = serverRequest.pathVariable(\"group\");\n        return serverRequest.bodyToMono(JsonNode.class)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"Request body is required.\")\n            ))\n            .flatMap(jsonNode -> {\n                var tx = TransactionalOperator.create(txManager);\n                return client.get(Policy.class, policyName)\n                    .flatMap(policy -> Mono.justOrEmpty(policy.getSpec())\n                        .mapNotNull(Policy.PolicySpec::getConfigMapName)\n                        .filter(StringUtils::hasText)\n                        .flatMap(cmName -> client.fetch(ConfigMap.class, cmName))\n                        .switchIfEmpty(Mono.fromSupplier(() -> {\n                            // create a new configmap\n                            var cm = new ConfigMap();\n                            cm.setMetadata(new Metadata());\n                            cm.getMetadata().setGenerateName(policyName + \"-config-\");\n                            return cm;\n                        }))\n                        .flatMap(cm -> Mono.fromCallable(() -> {\n                            if (cm.getData() == null) {\n                                cm.setData(new HashMap<>());\n                            }\n                            var oldJson = cm.getData().get(configGroup);\n                            if (StringUtils.hasText(oldJson)\n                                && Objects.equals(jsonNode, mapper.readTree(oldJson))) {\n                                // skip if no change\n                                return null;\n                            }\n                            var newJson = mapper.writeValueAsString(jsonNode);\n                            cm.getData().put(configGroup, newJson);\n                            return cm;\n                        }))\n                        .flatMap(cm -> {\n                            if (cm.getMetadata().getVersion() != null) {\n                                return client.update(cm);\n                            }\n                            return client.create(cm);\n                        })\n                        .flatMap(cm -> {\n                            var cmName = cm.getMetadata().getName();\n                            if (policy.getSpec() != null\n                                && Objects.equals(policy.getSpec().getConfigMapName(), cmName)) {\n                                return Mono.just(cm);\n                            }\n                            if (policy.getSpec() == null) {\n                                policy.setSpec(new Policy.PolicySpec());\n                            }\n                            policy.getSpec().setConfigMapName(cmName);\n                            return client.update(policy);\n                        })\n                    )\n                    .as(tx::transactional);\n            })\n            .then(ServerResponse.noContent().build());\n    }\n\n    private Mono<ServerResponse> getPolicyConfigByGroup(ServerRequest serverRequest) {\n        var policyName = serverRequest.pathVariable(\"name\");\n        var configGroup = serverRequest.pathVariable(\"group\");\n\n        return client.get(Policy.class, policyName)\n            .filter(p -> p.getSpec() != null)\n            .map(p -> p.getSpec().getConfigMapName())\n            .filter(StringUtils::hasText)\n            .flatMap(cmName -> client.fetch(ConfigMap.class, cmName))\n            .filter(cm -> cm.getData() != null && cm.getData().containsKey(configGroup))\n            .map(cm -> cm.getData().get(configGroup))\n            .flatMap(json -> Mono.fromCallable(() -> mapper.readTree(json)))\n            .defaultIfEmpty(mapper.nullNode())\n            .flatMap(config -> ServerResponse.ok().bodyValue(config));\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"console.api.storage.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/extension/LocalThumbnail.java",
    "content": "package run.halo.app.core.attachment.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.experimental.Accessors;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"storage.halo.run\", version = \"v1alpha1\", kind = \"LocalThumbnail\",\n    plural = \"localthumbnails\", singular = \"localthumbnail\")\n@Deprecated(forRemoval = true, since = \"2.22.0\")\npublic class LocalThumbnail extends AbstractExtension {\n    public static final String UNIQUE_IMAGE_AND_SIZE_INDEX = \"uniqueImageAndSize\";\n    public static final String REQUEST_TO_GENERATE_ANNO = \"storage.halo.run/request-to-generate\";\n\n    @Schema(requiredMode = REQUIRED)\n    private Spec spec;\n\n    @Getter(onMethod_ = @NonNull)\n    @Schema(requiredMode = NOT_REQUIRED)\n    private Status status = new Status();\n\n    public void setStatus(Status status) {\n        this.status = (status == null ? new Status() : status);\n    }\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"LocalThumbnailSpec\")\n    public static class Spec {\n        /**\n         * A hash signature for the image uri.\n         *\n         * @see #getImageUri()\n         */\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String imageSignature;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String imageUri;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String thumbnailUri;\n\n        /**\n         * A hash signature for the thumbnail uri.\n         *\n         * @see #getThumbnailUri()\n         */\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String thumbSignature;\n\n        @Schema(requiredMode = REQUIRED)\n        private ThumbnailSize size;\n\n        /**\n         * Consider the compatibility of the system and migration, use unix-style relative paths\n         * here.\n         *\n         * @see AttachmentRootGetter\n         */\n        @Schema(requiredMode = REQUIRED)\n        private String filePath;\n    }\n\n    @Data\n    @Schema(name = \"LocalThumbnailStatus\")\n    public static class Status {\n        private Phase phase;\n    }\n\n    public enum Phase {\n        PENDING, SUCCEEDED, FAILED\n    }\n\n    public static String uniqueImageAndSize(LocalThumbnail localThumbnail) {\n        return uniqueImageAndSize(localThumbnail.getSpec().getImageSignature(),\n            localThumbnail.getSpec().getSize());\n    }\n\n    public static String uniqueImageAndSize(String imageSignature, ThumbnailSize size) {\n        return imageSignature + \"-\" + size.name();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/extension/Thumbnail.java",
    "content": "package run.halo.app.core.attachment.extension;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@Data\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"storage.halo.run\", version = \"v1alpha1\", kind = \"Thumbnail\",\n    plural = \"thumbnails\", singular = \"thumbnail\")\n@Deprecated(forRemoval = true, since = \"2.22.0\")\npublic class Thumbnail extends AbstractExtension {\n\n    public static final String ID_INDEX = \"thumbnail-id\";\n\n    @Schema(requiredMode = REQUIRED)\n    private Spec spec;\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"ThumbnailSpec\")\n    public static class Spec {\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String imageSignature;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String imageUri;\n\n        @Schema(requiredMode = REQUIRED)\n        private ThumbnailSize size;\n\n        @Schema(requiredMode = REQUIRED, minLength = 1)\n        private String thumbnailUri;\n    }\n\n    public static String idIndexFunc(Thumbnail thumbnail) {\n        return idIndexFunc(thumbnail.getSpec().getImageSignature(),\n            thumbnail.getSpec().getSize().name());\n    }\n\n    public static String idIndexFunc(String imageHash, String size) {\n        return imageHash + \"-\" + size;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/impl/AttachmentListerImpl.java",
    "content": "package run.halo.app.core.attachment.impl;\n\nimport java.util.List;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.AttachmentLister;\nimport run.halo.app.core.attachment.SearchRequest;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Group;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n@Component\n@RequiredArgsConstructor\npublic class AttachmentListerImpl implements AttachmentLister {\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<ListResult<Attachment>> listBy(SearchRequest searchRequest) {\n        var groupListOptions = ListOptions.builder()\n            .labelSelector()\n            .exists(Group.HIDDEN_LABEL)\n            .end()\n            .build();\n        return client.listAll(Group.class, groupListOptions, Sort.unsorted())\n            .map(group -> group.getMetadata().getName())\n            .collectList()\n            .defaultIfEmpty(List.of())\n            .flatMap(hiddenGroups -> client.listBy(Attachment.class,\n                searchRequest.toListOptions(hiddenGroups),\n                searchRequest.toPageRequest()\n            ));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/impl/AttachmentRootGetterImpl.java",
    "content": "package run.halo.app.core.attachment.impl;\n\nimport java.nio.file.Path;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@Component\n@RequiredArgsConstructor\npublic class AttachmentRootGetterImpl implements AttachmentRootGetter {\n    private final HaloProperties haloProp;\n\n    @Override\n    public Path get() {\n        return haloProp.getWorkDir().resolve(\"attachments\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/reconciler/AttachmentReconciler.java",
    "content": "package run.halo.app.core.attachment.reconciler;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\n\nimport java.net.URI;\nimport java.time.Duration;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.attachment.AttachmentChangedEvent;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Attachment.AttachmentStatus;\nimport run.halo.app.core.extension.attachment.Constant;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.extension.controller.RequeueException;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass AttachmentReconciler implements Reconciler<Request> {\n\n    private final ExtensionClient client;\n\n    private final AttachmentService attachmentService;\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    @Override\n    public Result reconcile(Request request) {\n        return client.fetch(Attachment.class, request.name()).map(attachment -> {\n            if (ExtensionUtil.isDeleted(attachment)) {\n                if (removeFinalizers(attachment.getMetadata(),\n                    Set.of(Constant.FINALIZER_NAME))) {\n                    cleanUpResources(attachment);\n                    client.update(attachment);\n                    this.eventPublisher.publishEvent(new AttachmentChangedEvent(this, attachment));\n                }\n                return null;\n            }\n            // add finalizer\n            addFinalizers(attachment.getMetadata(), Set.of(Constant.FINALIZER_NAME));\n\n            if (attachment.getStatus() == null) {\n                attachment.setStatus(new AttachmentStatus());\n            }\n            var permalink = attachmentService.getPermalink(attachment)\n                .map(URI::toASCIIString)\n                .blockOptional(Duration.ofSeconds(10))\n                .orElseThrow(() -> new RequeueException(new Result(true, null),\n                    \"Attachment handler is unavailable, requeue the request\"\n                ));\n            log.debug(\"Set attachment permalink: {} for {}\", permalink, request.name());\n            attachment.getStatus().setPermalink(permalink);\n            var thumbnails = attachmentService.getThumbnailLinks(attachment)\n                .map(map -> map.keySet()\n                    .stream()\n                    .collect(Collectors.toMap(Enum::name, k -> map.get(k).toString()))\n                )\n                .blockOptional(Duration.ofSeconds(10))\n                .orElse(null);\n            Result result = null;\n            if (thumbnails == null) {\n                log.warn(\"\"\"\n                    Failed to get thumbnails for attachment: {}, \\\n                    consider upgrading storage plugins\"\"\", request.name()\n                );\n                result = new Result(true, Duration.ofSeconds(10));\n            }\n            attachment.getStatus().setThumbnails(thumbnails);\n            log.debug(\"Set attachment thumbnails: {} for {}\", thumbnails, request.name());\n            client.update(attachment);\n            this.eventPublisher.publishEvent(new AttachmentChangedEvent(this, attachment));\n            return result;\n        }).orElse(null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Attachment())\n            .build();\n    }\n\n    void cleanUpResources(Attachment attachment) {\n        attachmentService.delete(attachment).block(Duration.ofSeconds(20));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/reconciler/LocalThumbnailsReconciler.java",
    "content": "package run.halo.app.core.attachment.reconciler;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.core.attachment.extension.LocalThumbnail;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\n@Deprecated(forRemoval = true, since = \"2.22.0\")\nclass LocalThumbnailsReconciler implements Reconciler<Reconciler.Request> {\n\n    private static final String CLEAN_UP_FINALIZER = \"thumbnail-cleaner\";\n\n    private final ExtensionClient client;\n\n    private final AttachmentRootGetter attachmentRoot;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(LocalThumbnail.class, request.name())\n            .ifPresent(thumbnail -> {\n                if (ExtensionUtil.isDeleted(thumbnail)) {\n                    if (removeFinalizers(thumbnail.getMetadata(), Set.of(CLEAN_UP_FINALIZER))) {\n                        // clean up thumbnail file\n                        cleanUpThumbnailFile(thumbnail);\n                        client.update(thumbnail);\n                    }\n                    return;\n                }\n                // Cleanup all existing local thumbnails\n                addFinalizers(thumbnail.getMetadata(), Set.of(CLEAN_UP_FINALIZER));\n                log.info(\"Cleaning up local thumbnail: {}\", thumbnail.getMetadata().getName());\n                client.delete(thumbnail);\n            });\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new LocalThumbnail())\n            .build();\n    }\n\n    private void cleanUpThumbnailFile(LocalThumbnail thumbnail) {\n        var filePath = thumbnail.getSpec().getFilePath();\n        if (StringUtils.hasText(filePath)) {\n            var thumbnailFile = attachmentRoot.get().resolve(filePath);\n            try {\n                if (Files.deleteIfExists(thumbnailFile)) {\n                    log.info(\"Deleted thumbnail file: {} for {}\",\n                        thumbnailFile, thumbnail.getMetadata().getName());\n                }\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/reconciler/PolicyReconciler.java",
    "content": "package run.halo.app.core.attachment.reconciler;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n@Component\n@RequiredArgsConstructor\npublic class PolicyReconciler implements Reconciler<Reconciler.Request> {\n    private final ExtensionClient client;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Policy.class, request.name())\n            .ifPresent(this::checkOwnerLabel);\n        return Result.doNotRetry();\n    }\n\n    private void checkOwnerLabel(Policy policy) {\n        var policyName = policy.getMetadata().getName();\n        var configMapName = policy.getSpec().getConfigMapName();\n        client.fetch(ConfigMap.class, configMapName)\n            .ifPresent(configMap -> {\n                var labels = MetadataUtil.nullSafeLabels(configMap);\n                labels.put(Policy.POLICY_OWNER_LABEL, policyName);\n                client.update(configMap);\n            });\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Policy())\n            // sync on start for compatible with previous data\n            .syncAllOnStart(true)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/reconciler/ThumbnailReconciler.java",
    "content": "package run.halo.app.core.attachment.reconciler;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.attachment.extension.Thumbnail;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n@Slf4j\n@Component\n@Deprecated(forRemoval = true, since = \"2.22.0\")\nclass ThumbnailReconciler implements Reconciler<Reconciler.Request> {\n\n    private final ExtensionClient client;\n\n    ThumbnailReconciler(ExtensionClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Thumbnail.class, request.name())\n            .ifPresent(thumbnail -> {\n                if (ExtensionUtil.isDeleted(thumbnail)) {\n                    return;\n                }\n                log.info(\"Clean up thumbnail: {}\", thumbnail.getMetadata().getName());\n                client.delete(thumbnail);\n            });\n        return null;\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Thumbnail())\n            .build();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/thumbnail/DefaultLocalThumbnailService.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\nimport static java.nio.file.StandardOpenOption.READ;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Optional;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport lombok.extern.slf4j.Slf4j;\nimport net.coobird.thumbnailator.ThumbnailParameter;\nimport net.coobird.thumbnailator.Thumbnails;\nimport net.coobird.thumbnailator.resizers.configurations.Rendering;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.core.io.PathResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.infra.properties.AttachmentProperties;\nimport run.halo.app.infra.properties.HaloProperties;\n\n/**\n * Default implementation of {@link LocalThumbnailService} that generates thumbnails using\n * Thumbnailator library and deletes them when no longer needed.\n *\n * @author johnniang\n * @since 2.22.0\n */\n@Slf4j\n@Service\nclass DefaultLocalThumbnailService implements LocalThumbnailService, DisposableBean {\n\n    private static final String THUMBNAIL_ROOT = \"thumbnails\";\n\n    private static final int DEFAULT_GENERATION_TIMEOUT_SECONDS = 60;\n\n    private static final int DEFAULT_GENERATION_CONCURRENT_THREADS = 5;\n\n    private ExecutorService executorService;\n\n    private final AttachmentRootGetter attachmentRootGetter;\n\n    /**\n     * Map to track in-progress thumbnail generation tasks. The key is the filename, and the\n     * value\n     * is a CompletableFuture representing the generation task.\n     */\n    private final ConcurrentMap<Path, CompletableFuture<Path>> inProgress;\n\n    private final AttachmentProperties.ThumbnailProperties thumbnailProperties;\n\n    public DefaultLocalThumbnailService(AttachmentRootGetter attachmentRootGetter,\n        HaloProperties haloProperties) {\n        this.attachmentRootGetter = attachmentRootGetter;\n        this.thumbnailProperties = haloProperties.getAttachment().getThumbnail();\n        var concurrentThreads = this.thumbnailProperties.getConcurrentThreads();\n        if (concurrentThreads == null || concurrentThreads < 1) {\n            concurrentThreads = DEFAULT_GENERATION_CONCURRENT_THREADS;\n        }\n        this.executorService =\n            Executors.newFixedThreadPool(concurrentThreads, Thread.ofPlatform()\n                .daemon()\n                .name(\"thumbnail-generator-\", 0)\n                .factory());\n        this.inProgress = new ConcurrentHashMap<>();\n    }\n\n    void setExecutorService(ExecutorService executorService) {\n        this.executorService = executorService;\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        this.executorService.close();\n    }\n\n    @Override\n    public Mono<Resource> generate(Path source, ThumbnailSize size) {\n        if (thumbnailProperties.isDisabled()) {\n            return Mono.empty();\n        }\n        var optionalThumbnailPath = resolveThumbnailPath(source, size);\n        if (optionalThumbnailPath.isEmpty()) {\n            log.warn(\"Failed to resolve thumbnail path for source: {}, size: {}\", source, size);\n            return Mono.empty();\n        }\n        var thumbnailPath = optionalThumbnailPath.get();\n        var thumbnailResource = new PathResource(thumbnailPath);\n        if (thumbnailResource.isReadable()) {\n            log.trace(\"Thumbnail already exists: {}\", thumbnailPath);\n            return Mono.just(thumbnailResource);\n        }\n        return Mono.fromFuture(() -> inProgress.computeIfAbsent(thumbnailPath, f ->\n                        CompletableFuture.supplyAsync(() -> generateThumbnail(\n                                    source, thumbnailPath, size\n                                ),\n                                this.executorService\n                            )\n                            .orTimeout(\n                                DEFAULT_GENERATION_TIMEOUT_SECONDS,\n                                TimeUnit.SECONDS\n                            )\n                    )\n                    .whenComplete((p, t) -> inProgress.remove(thumbnailPath)),\n                // We don't want to cancel the thumbnail generation task\n                // when some requests are cancelled\n                true\n            )\n            .map(PathResource::new);\n    }\n\n    @Override\n    public void delete(Path source) {\n        Arrays.stream(ThumbnailSize.values()).forEach(size -> {\n            var thumbnailPath = resolveThumbnailPath(source, size);\n            if (thumbnailPath.isEmpty()) {\n                log.warn(\"Failed to resolve thumbnail path for source: {}, size: {}\", source, size);\n                return;\n            }\n            try {\n                var deleted = Files.deleteIfExists(thumbnailPath.get());\n                if (deleted) {\n                    log.info(\"Deleted thumbnail: {} for {}\", thumbnailPath.get(), source);\n                }\n            } catch (IOException e) {\n                // Ignore the error\n                log.error(\"Failed to delete thumbnail: {}\", thumbnailPath.get(), e);\n            }\n        });\n    }\n\n    Optional<Path> resolveThumbnailPath(Path source, ThumbnailSize size) {\n        var attachmentRoot = this.attachmentRootGetter.get();\n        Path relativize;\n        try {\n            relativize = attachmentRoot.relativize(source);\n        } catch (IllegalArgumentException e) {\n            // The source path is not under the attachment root\n            if (log.isDebugEnabled()) {\n                log.warn(\"Failed to resolve thumbnail path for source: {}, size: {}\",\n                    source, size, e);\n            }\n            return Optional.empty();\n        }\n        var thumbnailPath = attachmentRoot.resolve(THUMBNAIL_ROOT)\n            .resolve(\"w\" + size.getWidth())\n            .resolve(relativize);\n        return Optional.of(thumbnailPath);\n    }\n\n    private Path generateThumbnail(Path sourcePath, Path thumbnailPath, ThumbnailSize size) {\n        if (!Files.exists(sourcePath)) {\n            log.trace(\"Attachment path does not exist: {}\", sourcePath);\n            return null;\n        }\n\n        // Double check if the thumbnail already exists\n        if (Files.exists(thumbnailPath)) {\n            return thumbnailPath;\n        }\n        if (log.isDebugEnabled()) {\n            log.debug(\n                \"Generating thumbnail for path: {}, target: {}, size: {}\",\n                sourcePath, thumbnailPath, size);\n        }\n        boolean shouldCleanup = true;\n        try (var inputStream = Files.newInputStream(sourcePath, READ)) {\n            Files.createDirectories(thumbnailPath.getParent());\n            // Pass InputStream or File here.\n            // See https://github.com/coobird/thumbnailator/issues/159#issuecomment-694978197\n            // for more.\n            var builder = Thumbnails.of(inputStream)\n                .width(size.getWidth())\n                .imageType(ThumbnailParameter.DEFAULT_IMAGE_TYPE)\n                .rendering(Rendering.SPEED)\n                .useExifOrientation(true);\n            if (thumbnailProperties.getQuality() != null) {\n                builder.outputQuality(thumbnailProperties.getQuality());\n            }\n            builder.toFile(thumbnailPath.toFile());\n            log.info(\"Generated thumbnail for path: {}, target: {}, size: {}\",\n                sourcePath, thumbnailPath, size);\n            // check size of thumbnails\n            var attachmentFileSize = Files.size(sourcePath);\n            var thumbnailFileSize = Files.size(thumbnailPath);\n            if (attachmentFileSize < thumbnailFileSize) {\n                Files.copy(sourcePath, thumbnailPath, REPLACE_EXISTING);\n                log.info(\"\"\"\n                        Replaced thumbnail with original file since it's smaller, \\\n                        path: {}, size: {} < {}\\\n                        \"\"\",\n                    thumbnailPath, attachmentFileSize, thumbnailFileSize);\n            }\n            shouldCleanup = false;\n            return thumbnailPath;\n        } catch (IOException e) {\n            log.warn(\"Failed to generate thumbnail for path: {}\",\n                sourcePath, e);\n            // return the original attachment path\n            return null;\n        } finally {\n            if (shouldCleanup) {\n                // delete the possibly created file\n                try {\n                    Files.deleteIfExists(thumbnailPath);\n                } catch (IOException ex) {\n                    // ignore this error\n                    log.warn(\"Failed to delete possibly created thumbnail file: {}\",\n                        thumbnailPath, ex);\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/thumbnail/DefaultThumbnailService.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.AttachmentChangedEvent;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.infra.ExternalUrlSupplier;\n\n/**\n * Implementation of {@link ThumbnailService}.\n *\n * <p>\n * Caches thumbnail links in memory for better performance.\n *\n * @author johnniang\n * @since 2.22.0\n */\n@Slf4j\n@Component\nclass DefaultThumbnailService implements ThumbnailService {\n\n    private static final Map<ThumbnailSize, URI> EMPTY_THUMBNAILS = Map.of();\n\n    private final Cache<String, Map<ThumbnailSize, URI>> thumbnailCache;\n\n    private final ReactiveExtensionClient client;\n\n    private final ExternalUrlSupplier externalUrlSupplier;\n\n    public DefaultThumbnailService(ReactiveExtensionClient client,\n        ExternalUrlSupplier externalUrlSupplier) {\n        this.client = client;\n        this.externalUrlSupplier = externalUrlSupplier;\n        this.thumbnailCache = Caffeine.newBuilder()\n            // TODO make it configurable\n            .maximumSize(10_000)\n            .build();\n    }\n\n    @EventListener\n    void handleAttachmentChangedEvent(AttachmentChangedEvent event) {\n        updateCache(event.getAttachment());\n    }\n\n    void updateCache(Attachment attachment) {\n        if (attachment.getStatus() == null) {\n            return;\n        }\n        var permalink = attachment.getStatus().getPermalink();\n        if (!StringUtils.hasText(permalink)) {\n            return;\n        }\n        if (ExtensionUtil.isDeleted(attachment)) {\n            thumbnailCache.invalidate(permalink);\n            return;\n        }\n        var thumbnails = attachment.getStatus().getThumbnails();\n        if (CollectionUtils.isEmpty(thumbnails)) {\n            thumbnailCache.put(permalink, EMPTY_THUMBNAILS);\n            return;\n        }\n        Map<ThumbnailSize, URI> validThumbnails = new HashMap<>();\n        thumbnails.forEach((key, value) -> {\n            var size = ThumbnailSize.optionalValueOf(key);\n            if (size.isPresent() && StringUtils.hasText(value)) {\n                validThumbnails.put(size.get(), URI.create(value));\n            }\n        });\n        if (validThumbnails.isEmpty()) {\n            thumbnailCache.put(permalink, EMPTY_THUMBNAILS);\n        } else {\n            thumbnailCache.put(permalink, Collections.unmodifiableMap(validThumbnails));\n        }\n    }\n\n    @Override\n    public Mono<URI> get(URI permalink, ThumbnailSize size) {\n        return get(permalink).mapNotNull(thumbnails -> thumbnails.get(size));\n    }\n\n    @Override\n    public Mono<Map<ThumbnailSize, URI>> get(URI permalink) {\n        var encodedPermalink = URI.create(permalink.toASCIIString());\n        if (!encodedPermalink.isAbsolute()) {\n            // build permalinks\n            return Mono.just(ThumbnailUtils.buildSrcsetMap(encodedPermalink));\n        }\n        // TODO Optimize concurrent requests for the same permalink\n        return Mono.deferContextual(contextView -> {\n            var externalUrl = ServerWebExchangeContextFilter.getExchange(contextView)\n                .map(exchange -> externalUrlSupplier.getURL(exchange.getRequest()))\n                .orElseGet(externalUrlSupplier::getRaw);\n            // check if the permalink is from local site\n            if (externalUrl != null\n                && Objects.equals(externalUrl.getAuthority(), encodedPermalink.getAuthority())) {\n                return Mono.just(ThumbnailUtils.buildSrcsetMap(encodedPermalink));\n            }\n            var permalinkString = encodedPermalink.toASCIIString();\n            var thumbnails = thumbnailCache.getIfPresent(permalinkString);\n            if (thumbnails != null) {\n                return Mono.just(thumbnails);\n            }\n            // query from attachments\n            var listOptions = ListOptions.builder()\n                .andQuery(Queries.equal(\"status.permalink\", permalinkString))\n                .build();\n            return client.listAll(Attachment.class, listOptions, ExtensionUtil.defaultSort())\n                .next()\n                // Here we allow concurrent updates\n                .doOnNext(this::updateCache)\n                .mapNotNull(attachment -> this.thumbnailCache.getIfPresent(permalinkString))\n                .switchIfEmpty(Mono.fromSupplier(() -> {\n                    // No attachment or no thumbnails, cache empty map to avoid cache miss again and\n                    // again.\n                    this.thumbnailCache.put(permalinkString, EMPTY_THUMBNAILS);\n                    return EMPTY_THUMBNAILS;\n                }));\n        });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/thumbnail/LocalThumbnailService.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport java.nio.file.Path;\nimport org.springframework.core.io.Resource;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\n\n/**\n * Service for generating and deleting local image thumbnails.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface LocalThumbnailService {\n\n    /**\n     * Generates thumbnail for the source image. If the thumbnail already exists, it will return the\n     * existing one.\n     *\n     * @param source the source image path\n     * @param size the thumbnail size\n     * @return the generated thumbnail resource\n     */\n    Mono<Resource> generate(Path source, ThumbnailSize size);\n\n    /**\n     * Deletes thumbnails associated with the source image.\n     *\n     * @param source the source image path\n     */\n    void delete(Path source);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/thumbnail/ThumbnailImgTagPostProcessor.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport static org.thymeleaf.templatemode.TemplateMode.HTML;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.ElementNames;\nimport org.thymeleaf.model.IAttribute;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.MatchingElementName;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.theme.dialect.ElementTagPostProcessor;\n\n@Slf4j\n@Component\nclass ThumbnailImgTagPostProcessor implements ElementTagPostProcessor {\n\n    private static final String DEFAULT_SIZES = \"\"\"\n        (max-width: 640px) 94vw, \\\n        (max-width: 768px) 92vw, \\\n        (max-width: 1024px) 88vw, \\\n        min(800px, 85vw)\\\n        \"\"\";\n\n    private final MatchingElementName matchingElementName;\n\n    private final ThumbnailService thumbnailService;\n\n    public ThumbnailImgTagPostProcessor(ThumbnailService thumbnailService) {\n        this.thumbnailService = thumbnailService;\n        this.matchingElementName =\n            MatchingElementName.forElementName(HTML, ElementNames.forHTMLName(\"img\"));\n    }\n\n\n    @Override\n    public Mono<IProcessableElementTag> process(ITemplateContext context,\n        IProcessableElementTag tag) {\n        if (!matchingElementName.matches(tag.getElementDefinition().getElementName())) {\n            return Mono.empty();\n        }\n        if (tag.hasAttribute(\"srcset\")) {\n            return Mono.empty();\n        }\n        var srcValue = Optional.ofNullable(tag.getAttribute(\"src\"))\n            .map(IAttribute::getValue)\n            .filter(StringUtils::hasText)\n            .map(String::trim)\n            .map(HaloUtils::safeToUri);\n        if (srcValue.isEmpty()) {\n            log.debug(\"Skip processing img tag without src attribute\");\n            return Mono.empty();\n        }\n        // get img tag\n        var imageUri = srcValue.get();\n\n        return thumbnailService.get(imageUri)\n            .filter(Predicate.not(Map::isEmpty))\n            .map(thumbnails -> {\n                var modelFactory = context.getModelFactory();\n                var newTag = tag;\n                if (!newTag.hasAttribute(\"sizes\")) {\n                    newTag = modelFactory.setAttribute(newTag, \"sizes\", DEFAULT_SIZES);\n                }\n                var srcset = thumbnails.keySet().stream()\n                    .map(size -> {\n                        var uri = thumbnails.get(size);\n                        return uri + \" \" + size.getWidth() + \"w\";\n                    })\n                    .collect(Collectors.joining(\", \"));\n                newTag = modelFactory.setAttribute(newTag, \"srcset\", srcset);\n                return newTag;\n            });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/thumbnail/ThumbnailResourceTransformer.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport java.io.IOException;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.io.PathResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.resource.ResourceTransformer;\nimport org.springframework.web.reactive.resource.ResourceTransformerChain;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\n\n/**\n * A {@link ResourceTransformer} to generate and serve image thumbnails on the fly.\n *\n * @author johnniang\n * @since 2.22.0\n */\n@Slf4j\npublic class ThumbnailResourceTransformer implements ResourceTransformer {\n\n    private final LocalThumbnailService localThumbnailService;\n\n    public ThumbnailResourceTransformer(LocalThumbnailService localThumbnailService) {\n        this.localThumbnailService = localThumbnailService;\n    }\n\n    @Override\n    public Mono<Resource> transform(ServerWebExchange exchange, Resource resource,\n        ResourceTransformerChain transformerChain) {\n        var width = exchange.getRequest().getQueryParams().getFirst(\"width\");\n        if (!StringUtils.hasText(width) || !resource.isFile()) {\n            return transformerChain.transform(exchange, resource);\n        }\n        var extension = StringUtils.getFilenameExtension(resource.getFilename());\n        if (!ThumbnailUtils.isSupportedImage(extension)) {\n            log.trace(\"Not a supported image type: {}\", extension);\n            return transformerChain.transform(exchange, resource);\n        }\n        var size = ThumbnailSize.fromWidth(width);\n        try {\n            var source = resource.getFile().toPath();\n            return localThumbnailService.generate(source, size)\n                .switchIfEmpty(Mono.fromSupplier(() -> new PathResource(source)))\n                .flatMap(transformed -> transformerChain.transform(exchange, transformed));\n        } catch (IOException e) {\n            return Mono.error(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/thumbnail/ThumbnailService.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport java.net.URI;\nimport java.util.Map;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\n\n/**\n * Service for managing thumbnails.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface ThumbnailService {\n\n    /**\n     * Get the thumbnail link for the given image URI and size.\n     *\n     * @param permalink the permalink of the image\n     * @param size the size of the thumbnail\n     * @return the thumbnail link\n     */\n    Mono<URI> get(URI permalink, ThumbnailSize size);\n\n    /**\n     * Get all thumbnail links for the given image URI.\n     *\n     * @param permalink the permalink of the image\n     * @return the map of thumbnail size to thumbnail link\n     */\n    Mono<Map<ThumbnailSize, URI>> get(URI permalink);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/attachment/thumbnail/ThumbnailUtils.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport java.net.URI;\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport org.apache.commons.io.FilenameUtils;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.http.MediaType;\nimport org.springframework.util.MimeType;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport run.halo.app.core.attachment.ThumbnailSize;\n\npublic enum ThumbnailUtils {\n    ;\n\n    private static final Set<String> SUPPORTED_IMAGE_SUFFIXES = Set.of(\n        \"jpg\", \"jpeg\", \"png\", \"bmp\", \"wbmp\"\n    );\n\n    private static final Set<MimeType> SUPPORTED_IMAGE_MIME_TYPES = Set.of(\n            \"image/jpg\", \"image/jpeg\", \"image/png\", \"image/bmp\", \"image/vnd.wap.wbmp\"\n        )\n        .stream()\n        .map(MediaType::parseMediaType)\n        .collect(Collectors.toSet());\n\n    /**\n     * Check if the given file suffix is a supported image format for thumbnail generation.\n     *\n     * @param fileSuffix the file suffix to check (without the dot)\n     * @return true if the file suffix is supported, false otherwise\n     */\n    public static boolean isSupportedImage(@Nullable String fileSuffix) {\n        if (!StringUtils.hasText(fileSuffix)) {\n            return false;\n        }\n        return SUPPORTED_IMAGE_SUFFIXES.contains(fileSuffix.toLowerCase());\n    }\n\n    public static boolean isSupportedImage(@Nullable MimeType mimeType) {\n        return SUPPORTED_IMAGE_MIME_TYPES.stream()\n            .anyMatch(supported -> supported.isCompatibleWith(mimeType));\n    }\n\n    /**\n     * Build a map of thumbnail size to its corresponding URI based on the given permalink.\n     *\n     * @param permalink permalink of the attachment in local storage. Make sure it's encoded.\n     * @return a map where the key is the thumbnail size and the value is the URI of the thumbnail\n     */\n    public static Map<ThumbnailSize, URI> buildSrcsetMap(URI permalink) {\n        var fileSuffix = FilenameUtils.getExtension(permalink.getPath());\n        if (!isSupportedImage(fileSuffix)) {\n            return Map.of();\n        }\n        return Arrays.stream(ThumbnailSize.values())\n            .collect(Collectors.toMap(t -> t, t ->\n                UriComponentsBuilder.fromUri(permalink)\n                    .queryParam(\"width\", t.getWidth())\n                    .build(true)\n                    .toUri()\n            ));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/counter/CounterService.java",
    "content": "package run.halo.app.core.counter;\n\nimport java.util.Collection;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Counter;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic interface CounterService {\n\n    Mono<Counter> getByName(String counterName);\n\n    Flux<Counter> getByNames(Collection<String> names);\n\n    Mono<Counter> deleteByName(String counterName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/counter/CounterServiceImpl.java",
    "content": "package run.halo.app.core.counter;\n\nimport java.util.Collection;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\n\n/**\n * Counter service implementation.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Service\npublic class CounterServiceImpl implements CounterService {\n\n    private final ReactiveExtensionClient client;\n\n    public CounterServiceImpl(ReactiveExtensionClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public Mono<Counter> getByName(String counterName) {\n        return client.fetch(Counter.class, counterName);\n    }\n\n    @Override\n    public Flux<Counter> getByNames(Collection<String> names) {\n        if (CollectionUtils.isEmpty(names)) {\n            return Flux.empty();\n        }\n        var options = ListOptions.builder()\n            .andQuery(Queries.in(\"metadata.name\", names))\n            .build();\n        return client.listAll(Counter.class, options, ExtensionUtil.defaultSort());\n    }\n\n    @Override\n    public Mono<Counter> deleteByName(String counterName) {\n        return client.fetch(Counter.class, counterName)\n            .flatMap(client::delete);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/counter/MeterUtils.java",
    "content": "package run.halo.app.core.counter;\n\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.Tag;\nimport io.micrometer.core.instrument.Tags;\nimport org.apache.commons.lang3.StringUtils;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * Meter utils.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class MeterUtils {\n\n    public static final Tag METRICS_COMMON_TAG = Tag.of(\"metrics.halo.run\", \"true\");\n    public static final String SCENE = \"scene\";\n    public static final String VISIT_SCENE = \"visit\";\n    public static final String UPVOTE_SCENE = \"upvote\";\n    public static final String DOWNVOTE_SCENE = \"downvote\";\n    public static final String TOTAL_COMMENT_SCENE = \"total_comment\";\n    public static final String APPROVED_COMMENT_SCENE = \"approved_comment\";\n\n    /**\n     * Build a counter name.\n     *\n     * @param group extension group\n     * @param plural extension plural\n     * @param name extension name\n     * @return counter name\n     */\n    public static String nameOf(String group, String plural, String name) {\n        if (StringUtils.isBlank(group)) {\n            return String.join(\"/\", plural, name);\n        }\n        return String.join(\".\", plural, group) + \"/\" + name;\n    }\n\n    public static <T extends AbstractExtension> String nameOf(Class<T> clazz, String name) {\n        GVK annotation = clazz.getAnnotation(GVK.class);\n        return nameOf(annotation.group(), annotation.plural(), name);\n    }\n\n    public static Counter visitCounter(MeterRegistry registry, String name) {\n        return counter(registry, name, Tag.of(SCENE, VISIT_SCENE));\n    }\n\n    public static Counter upvoteCounter(MeterRegistry registry, String name) {\n        return counter(registry, name, Tag.of(SCENE, UPVOTE_SCENE));\n    }\n\n    public static Counter downvoteCounter(MeterRegistry registry, String name) {\n        return counter(registry, name, Tag.of(SCENE, DOWNVOTE_SCENE));\n    }\n\n    public static Counter totalCommentCounter(MeterRegistry registry, String name) {\n        return counter(registry, name, Tag.of(SCENE, TOTAL_COMMENT_SCENE));\n    }\n\n    public static Counter approvedCommentCounter(MeterRegistry registry, String name) {\n        return counter(registry, name, Tag.of(SCENE, APPROVED_COMMENT_SCENE));\n    }\n\n    public static boolean isVisitCounter(Counter counter) {\n        String sceneValue = counter.getId().getTag(SCENE);\n        if (StringUtils.isBlank(sceneValue)) {\n            return false;\n        }\n        return VISIT_SCENE.equals(sceneValue);\n    }\n\n    public static boolean isUpvoteCounter(Counter counter) {\n        String sceneValue = counter.getId().getTag(SCENE);\n        if (StringUtils.isBlank(sceneValue)) {\n            return false;\n        }\n        return UPVOTE_SCENE.equals(sceneValue);\n    }\n\n    public static boolean isDownvoteCounter(Counter counter) {\n        String sceneValue = counter.getId().getTag(SCENE);\n        if (StringUtils.isBlank(sceneValue)) {\n            return false;\n        }\n        return DOWNVOTE_SCENE.equals(sceneValue);\n    }\n\n    public static boolean isTotalCommentCounter(Counter counter) {\n        String sceneValue = counter.getId().getTag(SCENE);\n        if (StringUtils.isBlank(sceneValue)) {\n            return false;\n        }\n        return TOTAL_COMMENT_SCENE.equals(sceneValue);\n    }\n\n    public static boolean isApprovedCommentCounter(Counter counter) {\n        String sceneValue = counter.getId().getTag(SCENE);\n        if (StringUtils.isBlank(sceneValue)) {\n            return false;\n        }\n        return APPROVED_COMMENT_SCENE.equals(sceneValue);\n    }\n\n    /**\n     * Build a {@link Counter} for halo extension.\n     *\n     * @param registry meter registry\n     * @param name counter name,build by {@link #nameOf(String, String, String)}\n     * @return counter find by name from registry if exists, otherwise create a new one.\n     */\n    private static Counter counter(MeterRegistry registry, String name, Tag... tags) {\n        Tags withTags = Tags.of(METRICS_COMMON_TAG).and(tags);\n        Counter counter = registry.find(name)\n            .tags(withTags)\n            .counter();\n        if (counter == null) {\n            return registry.counter(name, withTags);\n        }\n        return counter;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/AttachmentHandler.java",
    "content": "package run.halo.app.core.endpoint;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.util.Optional;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jspecify.annotations.Nullable;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.infra.SystemSetting.Attachment.UploadOptions;\n\n@Slf4j\n@RequiredArgsConstructor\n@Component\npublic class AttachmentHandler {\n\n    private final AttachmentService attachmentService;\n\n    /**\n     * Build OpenAPI doc of request and response for upload attachment endpoint.\n     *\n     * @param builder the operation builder\n     */\n    public void buildDoc(Builder builder) {\n        builder.requestBody(requestBodyBuilder()\n                .content(contentBuilder()\n                    .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                    .schema(schemaBuilder().implementation(UploadForm.class))\n                )\n            )\n            .response(responseBuilder().implementation(Attachment.class));\n    }\n\n    /**\n     * Handle upload attachment request.\n     *\n     * @param serverRequest the server request\n     * @param getConfig the upload options fetcher\n     * @return the server response\n     */\n    public Mono<ServerResponse> handleUpload(ServerRequest serverRequest,\n        Mono<UploadOptions> getConfig) {\n        var getForm = serverRequest.bind(UploadForm.class);\n        var uploadAttachment = Mono.zip(getForm, getConfig)\n            .flatMap(tuple2 -> {\n                var form = tuple2.getT1();\n                var config = tuple2.getT2();\n                var file = form.getFile();\n                var upload = Mono.defer(() -> {\n                    if (file != null) {\n                        var mediaType = Optional.ofNullable(file.headers().getContentType())\n                            .orElse(MediaType.APPLICATION_OCTET_STREAM);\n                        if (log.isDebugEnabled()) {\n                            log.debug(\"Preparing to upload attachment [filename={} mediaType={}]\",\n                                file.name(), mediaType);\n                        }\n                        return attachmentService.upload(\n                            config.policyName(),\n                            config.groupName(),\n                            file.filename(),\n                            file.content(),\n                            mediaType\n                        );\n                    }\n                    if (log.isDebugEnabled()) {\n                        log.debug(\"Preparing to upload attachment from url [{}], filename: {}\",\n                            form.getUrl(), form.getFilename());\n                    }\n                    var url = Optional.ofNullable(form.getUrl())\n                        .filter(StringUtils::hasText)\n                        .map(URI::create)\n                        .map(uri -> {\n                            try {\n                                return uri.toURL();\n                            } catch (MalformedURLException e) {\n                                throw new RuntimeException(e);\n                            }\n                        })\n                        .orElse(null);\n                    if (url == null) {\n                        return Mono.error(new ServerWebInputException(\n                            \"Invalid url provided: \" + form.getUrl()\n                        ));\n                    }\n                    return attachmentService.uploadFromUrl(\n                        url, config.policyName(), config.groupName(), form.getFilename()\n                    );\n                });\n                return upload.flatMap(a -> attachmentService.getPermalink(a)\n                    .doOnNext(permalink -> a.getStatus().setPermalink(permalink.toString()))\n                    .thenReturn(a)\n                );\n            });\n        return ServerResponse.ok().body(uploadAttachment, Attachment.class);\n    }\n\n    /**\n     * Upload form from console. The file and url are mutually exclusive. If both are provided,\n     * the file will be used.\n     *\n     */\n    @Data\n    @NoArgsConstructor\n    @AllArgsConstructor\n    public static class UploadForm {\n\n        /**\n         * The file to upload. If not provided, the url will be used.\n         */\n        @Nullable\n        private FilePart file;\n\n        /**\n         * The filename to use when uploading from url. If not provided, the filename will be\n         * extracted from the url.\n         */\n        @Nullable\n        private String filename;\n\n        /**\n         * The url to upload from. If not provided, the file will be used.\n         */\n        @Nullable\n        private String url;\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/WebSocketEndpointManager.java",
    "content": "package run.halo.app.core.endpoint;\n\nimport java.util.Collection;\n\n/**\n * Interface for managing WebSocket endpoints, including registering and unregistering.\n *\n * @author johnniang\n */\npublic interface WebSocketEndpointManager {\n\n    void register(Collection<WebSocketEndpoint> endpoints);\n\n    void unregister(Collection<WebSocketEndpoint> endpoints);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/WebSocketHandlerMapping.java",
    "content": "package run.halo.app.core.endpoint;\n\nimport com.google.common.collect.BiMap;\nimport com.google.common.collect.HashBiMap;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.server.reactive.observation.ServerRequestObservationContext;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.web.reactive.handler.AbstractHandlerMapping;\nimport org.springframework.web.reactive.socket.WebSocketHandler;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.util.pattern.PathPattern;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.ui.WebSocketUtils;\n\npublic class WebSocketHandlerMapping extends AbstractHandlerMapping\n    implements WebSocketEndpointManager, InitializingBean {\n\n    private final BiMap<PathPattern, WebSocketEndpoint> endpointMap;\n\n    private final ReadWriteLock rwLock;\n\n    public WebSocketHandlerMapping() {\n        this.endpointMap = HashBiMap.create();\n        this.rwLock = new ReentrantReadWriteLock();\n    }\n\n    @Override\n    @NonNull\n    public Mono<WebSocketHandler> getHandlerInternal(ServerWebExchange exchange) {\n        var request = exchange.getRequest();\n        if (!HttpMethod.GET.equals(request.getMethod())\n            || !WebSocketUtils.isWebSocketUpgrade(request.getHeaders())) {\n            // skip getting handler if the request is not a WebSocket.\n            return Mono.empty();\n        }\n\n        var lock = rwLock.readLock();\n        lock.lock();\n        try {\n            // Refer to org.springframework.web.reactive.handler.AbstractUrlHandlerMapping\n            // .lookupHandler\n            var pathContainer = request.getPath().pathWithinApplication();\n            List<PathPattern> matches = null;\n            for (var pattern : this.endpointMap.keySet()) {\n                if (pattern.matches(pathContainer)) {\n                    if (matches == null) {\n                        matches = new ArrayList<>();\n                    }\n                    matches.add(pattern);\n                }\n            }\n            if (matches == null) {\n                return Mono.empty();\n            }\n\n            if (matches.size() > 1) {\n                matches.sort(PathPattern.SPECIFICITY_COMPARATOR);\n            }\n\n            var pattern = matches.get(0);\n            exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, pattern);\n\n            var handler = endpointMap.get(pattern).handler();\n            exchange.getAttributes().put(BEST_MATCHING_HANDLER_ATTRIBUTE, handler);\n\n            ServerRequestObservationContext.findCurrent(exchange.getAttributes())\n                .ifPresent(context -> context.setPathPattern(pattern.toString()));\n\n            var pathWithinMapping = pattern.extractPathWithinPattern(pathContainer);\n            exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping);\n\n            var matchInfo = pattern.matchAndExtract(pathContainer);\n            Assert.notNull(matchInfo, \"Expect a match\");\n            exchange.getAttributes()\n                .put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, matchInfo.getUriVariables());\n            return Mono.just(handler);\n        } catch (Exception e) {\n            return Mono.error(e);\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public void register(Collection<WebSocketEndpoint> endpoints) {\n        if (CollectionUtils.isEmpty(endpoints)) {\n            return;\n        }\n        var lock = rwLock.writeLock();\n        lock.lock();\n        try {\n            endpoints.forEach(endpoint -> {\n                var urlPath = endpoint.urlPath();\n                urlPath = StringUtils.prependIfMissing(urlPath, \"/\");\n                var groupVersion = endpoint.groupVersion();\n                var parser = getPathPatternParser();\n                var pattern = parser.parse(\"/apis/\" + groupVersion + urlPath);\n                endpointMap.put(pattern, endpoint);\n            });\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public void unregister(Collection<WebSocketEndpoint> endpoints) {\n        if (CollectionUtils.isEmpty(endpoints)) {\n            return;\n        }\n        var lock = rwLock.writeLock();\n        lock.lock();\n        try {\n            BiMap<WebSocketEndpoint, PathPattern> inverseMap = endpointMap.inverse();\n            endpoints.forEach(inverseMap::remove);\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        var endpoints = obtainApplicationContext().getBeanProvider(WebSocketEndpoint.class)\n            .orderedStream()\n            .toList();\n        register(endpoints);\n    }\n\n    BiMap<PathPattern, WebSocketEndpoint> getEndpointMap() {\n        return endpointMap;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/AttachmentConsoleEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\n\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.endpoint.AttachmentHandler;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass AttachmentConsoleEndpoint implements CustomEndpoint {\n\n    private final SystemConfigFetcher systemConfigFetcher;\n\n    private final AttachmentHandler attachmentHandler;\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"console.api.storage.halo.run/v1alpha1\");\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"AttachmentV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .POST(\n                path(\"/attachments/-/upload\")\n                    .and(contentType(MediaType.MULTIPART_FORM_DATA)),\n                this::handleUpload,\n                builder -> {\n                    builder.operationId(\"uploadAttachmentForConsole\")\n                        .tag(tag)\n                        .description(\"Upload attachment endpoint for console.\");\n                    this.attachmentHandler.buildDoc(builder);\n                }\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> handleUpload(ServerRequest serverRequest) {\n        var getConfig = systemConfigFetcher.fetch(\n                SystemSetting.Attachment.GROUP,\n                SystemSetting.Attachment.class\n            )\n            .mapNotNull(SystemSetting.Attachment::console)\n            .filter(ac -> StringUtils.hasText(ac.policyName()))\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"Attachment system setting is not configured for console\"\n            )));\n        return attachmentHandler.handleUpload(serverRequest, getConfig);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/AuthProviderEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.AuthProvider;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.security.AuthProviderService;\nimport run.halo.app.security.ListedAuthProvider;\n\n/**\n * Auth provider endpoint.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class AuthProviderEndpoint implements CustomEndpoint {\n\n    private final AuthProviderService authProviderService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"AuthProviderV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"auth-providers\", this::listAuthProviders,\n                builder -> builder.operationId(\"listAuthProviders\")\n                    .description(\"Lists all auth providers\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .implementationArray(ListedAuthProvider.class))\n            )\n            .PUT(\"auth-providers/{name}/enable\", this::enableAuthProvider,\n                builder -> builder.operationId(\"enableAuthProvider\")\n                    .description(\"Enables an auth provider\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(AuthProvider.class))\n            )\n            .PUT(\"auth-providers/{name}/disable\", this::disableAuthProvider,\n                builder -> builder.operationId(\"disableAuthProvider\")\n                    .description(\"Disables an auth provider\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(AuthProvider.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> enableAuthProvider(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return authProviderService.enable(name)\n            .flatMap(authProvider -> ServerResponse.ok().bodyValue(authProvider));\n    }\n\n    private Mono<ServerResponse> disableAuthProvider(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return authProviderService.disable(name)\n            .flatMap(authProvider -> ServerResponse.ok().bodyValue(authProvider));\n    }\n\n    Mono<ServerResponse> listAuthProviders(ServerRequest request) {\n        return authProviderService.listAll()\n            .flatMap(providers -> ServerResponse.ok().bodyValue(providers));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/CommentEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.time.Instant;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.comment.CommentQuery;\nimport run.halo.app.content.comment.CommentRequest;\nimport run.halo.app.content.comment.CommentService;\nimport run.halo.app.content.comment.ListedComment;\nimport run.halo.app.content.comment.ReplyRequest;\nimport run.halo.app.content.comment.ReplyService;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.IpAddressUtils;\n\n\n/**\n * Endpoint for managing comment.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class CommentEndpoint implements CustomEndpoint {\n\n    private final CommentService commentService;\n    private final ReplyService replyService;\n\n    public CommentEndpoint(CommentService commentService, ReplyService replyService) {\n        this.commentService = commentService;\n        this.replyService = replyService;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"CommentV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"comments\", this::listComments, builder -> {\n                    builder.operationId(\"ListComments\")\n                        .description(\"List comments.\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(ListedComment.class))\n                        );\n                    CommentQuery.buildParameters(builder);\n                }\n            )\n            .POST(\"comments\", this::createComment,\n                builder -> builder.operationId(\"CreateComment\")\n                    .description(\"Create a comment.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(CommentRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Comment.class))\n            )\n            .POST(\"comments/{name}/reply\", this::createReply,\n                builder -> builder.operationId(\"CreateReply\")\n                    .description(\"Create a reply.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(ReplyRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Reply.class))\n            )\n            .build();\n    }\n\n    Mono<ServerResponse> listComments(ServerRequest request) {\n        CommentQuery commentQuery = new CommentQuery(request);\n        return commentService.listComment(commentQuery)\n            .flatMap(listedComments -> ServerResponse.ok().bodyValue(listedComments));\n    }\n\n    Mono<ServerResponse> createComment(ServerRequest request) {\n        return request.bodyToMono(CommentRequest.class)\n            .flatMap(commentRequest -> {\n                Comment comment = commentRequest.toComment();\n                comment.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));\n                comment.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));\n                return commentService.create(comment);\n            })\n            .flatMap(comment -> ServerResponse.ok().bodyValue(comment));\n    }\n\n    Mono<ServerResponse> createReply(ServerRequest request) {\n        String commentName = request.pathVariable(\"name\");\n        return request.bodyToMono(ReplyRequest.class)\n            .flatMap(replyRequest -> {\n                Reply reply = replyRequest.toReply();\n                // Create via console without audit\n                reply.getSpec().setApproved(true);\n                reply.getSpec().setApprovedTime(Instant.now());\n                reply.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));\n                reply.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));\n                // fix gh-2951\n                if (reply.getSpec().getHidden() == null) {\n                    reply.getSpec().setHidden(false);\n                }\n                return replyService.create(commentName, reply);\n            })\n            .flatMap(comment -> ServerResponse.ok().bodyValue(comment));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/ConsoleUserEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Objects;\nimport org.springdoc.core.fn.builders.parameter.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.GroupVersion;\n\n/**\n * User endpoint for console.\n *\n * @author johnniang\n */\n@Component\nclass ConsoleUserEndpoint implements CustomEndpoint {\n\n    private final UserService userService;\n\n    ConsoleUserEndpoint(UserService userService) {\n        this.userService = userService;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"UserV1alpha1Console\";\n        return RouterFunctions.nest(RequestPredicates.path(\"/users\"), SpringdocRouteBuilder.route()\n            .POST(\"/{username}/disable\", this::handleDisableUser, ops -> {\n                ops.operationId(\"DisableUser\")\n                    .tag(tag)\n                    .description(\"Disable user by username\")\n                    .parameter(Builder.parameterBuilder()\n                        .name(\"username\")\n                        .in(ParameterIn.PATH)\n                        .description(\"Username\")\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(User.class)\n                        .description(\"The user has been disabled.\")\n                    );\n            })\n            .POST(\"/{username}/enable\", this::handleEnableUser, ops -> {\n                ops.operationId(\"EnableUser\")\n                    .tag(tag)\n                    .description(\"Enable user by username\")\n                    .parameter(Builder.parameterBuilder()\n                        .name(\"username\")\n                        .in(ParameterIn.PATH)\n                        .description(\"Username\")\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(User.class)\n                        .description(\"The user has been enabled.\")\n                    );\n            })\n            .build());\n    }\n\n    private Mono<ServerResponse> handleEnableUser(ServerRequest request) {\n        return userService.enable(request.pathVariable(\"username\"))\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"The user was not found or has been enabled.\"))\n            )\n            .flatMap(user -> ServerResponse.ok().bodyValue(user));\n    }\n\n    private Mono<ServerResponse> handleDisableUser(ServerRequest request) {\n        var username = request.pathVariable(\"username\");\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Authentication::getName)\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"The current user is not authenticated.\"))\n            )\n            .filter(currentUsername -> !Objects.equals(currentUsername, username))\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"The user is the current user, can't disable it.\"\n            )))\n            .then(Mono.defer(() -> userService.disable(username)))\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"The user was not found or has been disabled.\"))\n            )\n            .flatMap(user -> ServerResponse.ok().bodyValue(user));\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return new GroupVersion(\"console.api.security.halo.run\", \"v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/CustomEndpointsBuilder.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\n\npublic class CustomEndpointsBuilder {\n\n    private final Map<GroupVersion, List<RouterFunction<ServerResponse>>> routerFunctionsMap;\n\n    public CustomEndpointsBuilder() {\n        routerFunctionsMap = new HashMap<>();\n    }\n\n    public CustomEndpointsBuilder add(CustomEndpoint customEndpoint) {\n        routerFunctionsMap\n            .computeIfAbsent(customEndpoint.groupVersion(), gv -> new LinkedList<>())\n            .add(customEndpoint.endpoint());\n        return this;\n    }\n\n    public RouterFunction<ServerResponse> build() {\n        SpringdocRouteBuilder routeBuilder = SpringdocRouteBuilder.route();\n        routerFunctionsMap.forEach((gv, routerFunctions) ->\n            routeBuilder.nest(RequestPredicates.path(\"/apis/\" + gv),\n                () -> routerFunctions.stream().reduce(RouterFunction::and).orElse(null)\n            )\n        );\n        if (routerFunctionsMap.isEmpty()) {\n            // return empty route.\n            return request -> Mono.empty();\n        }\n        routerFunctionsMap.clear();\n        return routeBuilder.build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/PluginEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;\nimport static org.springframework.core.io.buffer.DataBufferUtils.write;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static run.halo.app.extension.ListResult.generateGenericClass;\nimport static run.halo.app.extension.index.query.Queries.contains;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.or;\nimport static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;\nimport static run.halo.app.infra.utils.FileUtils.deleteFileSilently;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport lombok.Data;\nimport lombok.extern.slf4j.Slf4j;\nimport org.reactivestreams.Publisher;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.http.CacheControl;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.http.codec.multipart.FormFieldPart;\nimport org.springframework.http.codec.multipart.Part;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.reactive.resource.NoResourceFoundException;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.user.service.SettingConfigService;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.extension.router.SortableRequest;\nimport run.halo.app.infra.ReactiveUrlDataBufferFetcher;\nimport run.halo.app.infra.utils.SettingUtils;\nimport run.halo.app.plugin.PluginService;\nimport tools.jackson.databind.node.ObjectNode;\n\n@Slf4j\n@Component\npublic class PluginEndpoint implements CustomEndpoint, InitializingBean {\n\n    private final ReactiveExtensionClient client;\n\n    private final PluginService pluginService;\n\n    private final ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher;\n\n    private final SettingConfigService settingConfigService;\n\n    private final WebProperties webProperties;\n\n    private final Scheduler scheduler = Schedulers.boundedElastic();\n\n    private boolean useLastModified;\n\n    private CacheControl bundleCacheControl = CacheControl.empty();\n\n    public PluginEndpoint(ReactiveExtensionClient client,\n        PluginService pluginService,\n        ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher,\n        SettingConfigService settingConfigService,\n        WebProperties webProperties) {\n        this.client = client;\n        this.pluginService = pluginService;\n        this.reactiveUrlDataBufferFetcher = reactiveUrlDataBufferFetcher;\n        this.settingConfigService = settingConfigService;\n        this.webProperties = webProperties;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"PluginV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"plugins/install\", contentType(MediaType.MULTIPART_FORM_DATA),\n                this::install, builder -> builder.operationId(\"InstallPlugin\")\n                    .description(\"Install a plugin by uploading a Jar file.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder().implementation(InstallRequest.class))\n                        ))\n                    .response(responseBuilder().implementation(Plugin.class))\n            )\n            .POST(\"plugins/-/install-from-uri\", this::installFromUri,\n                builder -> builder.operationId(\"InstallPluginFromUri\")\n                    .description(\"Install a plugin from uri.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(InstallFromUriRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Plugin.class))\n            )\n            .POST(\"plugins/{name}/upgrade-from-uri\", this::upgradeFromUri,\n                builder -> builder.operationId(\"UpgradePluginFromUri\")\n                    .description(\"Upgrade a plugin from uri.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .required(true)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(UpgradeFromUriRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Plugin.class))\n            )\n            .POST(\"plugins/{name}/upgrade\", contentType(MediaType.MULTIPART_FORM_DATA),\n                this::upgrade, builder -> builder.operationId(\"UpgradePlugin\")\n                    .description(\"Upgrade a plugin by uploading a Jar file\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\").in(ParameterIn.PATH).required(true))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder().mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder().implementation(InstallRequest.class))))\n            )\n            .PUT(\"plugins/{name}/json-config\", this::updatePluginJsonConfig,\n                builder -> builder.operationId(\"updatePluginJsonConfig\")\n                    .description(\"Update the config of plugin setting.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder().mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder().implementation(Object.class))))\n                    .response(responseBuilder()\n                        .responseCode(String.valueOf(HttpStatus.NO_CONTENT.value()))\n                        .implementation(Void.class))\n            )\n            .PUT(\"plugins/{name}/reset-config\", this::resetSettingConfig,\n                builder -> builder.operationId(\"ResetPluginConfig\")\n                    .description(\"Reset the configMap of plugin setting.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(ConfigMap.class))\n            )\n            .PUT(\"plugins/{name}/reload\", this::reload,\n                builder -> builder.operationId(\"reloadPlugin\")\n                    .description(\"Reload a plugin by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(Plugin.class))\n            )\n            .PUT(\"plugins/{name}/plugin-state\", this::changePluginRunningState,\n                builder -> builder.operationId(\"ChangePluginRunningState\")\n                    .description(\"Change the running state of a plugin by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(RunningStateRequest.class))\n                        )\n                    )\n                    .response(responseBuilder()\n                        .implementation(Plugin.class))\n            )\n            .GET(\"plugins\", this::list, builder -> {\n                builder.operationId(\"ListPlugins\")\n                    .tag(tag)\n                    .description(\"List plugins using query criteria and sort params\")\n                    .response(responseBuilder().implementation(generateGenericClass(Plugin.class)));\n                ListRequest.buildParameters(builder);\n            })\n            .GET(\"plugins/{name}/setting\", this::fetchPluginSetting,\n                builder -> builder.operationId(\"fetchPluginSetting\")\n                    .description(\"Fetch setting of plugin.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(Setting.class))\n            )\n            .GET(\"plugins/{name}/json-config\", this::fetchPluginJsonConfig,\n                builder -> builder.operationId(\"fetchPluginJsonConfig\")\n                    .description(\n                        \"Fetch converted json config of plugin by configured configMapName.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(Object.class))\n            )\n            .GET(\"plugins/-/bundle.js\", this::fetchJsBundle,\n                builder -> builder.operationId(\"fetchJsBundle\")\n                    .description(\"Merge all JS bundles of enabled plugins into one.\")\n                    .tag(tag)\n                    .response(responseBuilder().implementation(String.class))\n            )\n            .GET(\"plugins/-/bundle.css\", this::fetchCssBundle,\n                builder -> builder.operationId(\"fetchCssBundle\")\n                    .description(\"Merge all CSS bundles of enabled plugins into one.\")\n                    .tag(tag)\n                    .response(responseBuilder().implementation(String.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> fetchPluginJsonConfig(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return client.fetch(Plugin.class, name)\n            .mapNotNull(plugin -> plugin.getSpec().getConfigMapName())\n            .flatMap(settingConfigService::fetchConfig)\n            .flatMap(json -> ServerResponse.ok().bodyValue(json));\n    }\n\n    private Mono<ServerResponse> updatePluginJsonConfig(ServerRequest request) {\n        final var pluginName = request.pathVariable(\"name\");\n        return client.fetch(Plugin.class, pluginName)\n            .doOnNext(plugin -> {\n                String configMapName = plugin.getSpec().getConfigMapName();\n                if (!StringUtils.hasText(configMapName)) {\n                    throw new ServerWebInputException(\n                        \"Unable to complete the request because the plugin configMapName is blank\");\n                }\n            })\n            .flatMap(plugin -> {\n                final String configMapName = plugin.getSpec().getConfigMapName();\n                return request.bodyToMono(ObjectNode.class)\n                    .switchIfEmpty(\n                        Mono.error(new ServerWebInputException(\"Required request body is missing\")))\n                    .flatMap(configMapJsonData ->\n                        settingConfigService.upsertConfig(configMapName, configMapJsonData));\n            })\n            .then(ServerResponse.noContent().build());\n    }\n\n    Mono<ServerResponse> changePluginRunningState(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return request.bodyToMono(RunningStateRequest.class)\n            .flatMap(runningState -> {\n                var enable = runningState.isEnable();\n                var async = runningState.isAsync();\n                return pluginService.changeState(name, enable, !async);\n            })\n            .flatMap(plugin -> ServerResponse.ok().bodyValue(plugin));\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        var cache = this.webProperties.getResources().getCache();\n        this.useLastModified = cache.isUseLastModified();\n        var cacheControl = cache.getCachecontrol().toHttpCacheControl();\n        if (cacheControl != null) {\n            this.bundleCacheControl = cacheControl;\n        }\n    }\n\n    @Data\n    @Schema(name = \"PluginRunningStateRequest\")\n    static class RunningStateRequest {\n        private boolean enable;\n        private boolean async;\n    }\n\n    private Mono<ServerResponse> fetchJsBundle(ServerRequest request) {\n        var versionOption = request.queryParam(\"v\");\n        return versionOption.map(s -> pluginService.getJsBundle(s).flatMap(\n                jsRes -> {\n                    var bodyBuilder = ServerResponse.ok()\n                        .cacheControl(bundleCacheControl)\n                        .contentType(MediaType.valueOf(\"text/javascript\"));\n                    if (useLastModified) {\n                        try {\n                            var lastModified = Instant.ofEpochMilli(jsRes.lastModified());\n                            bodyBuilder = bodyBuilder.lastModified(lastModified);\n                        } catch (IOException e) {\n                            if (e instanceof FileNotFoundException) {\n                                return Mono.error(\n                                    new NoResourceFoundException(request.uri(), \"bundle.js\")\n                                );\n                            }\n                            return Mono.error(e);\n                        }\n                    }\n                    return bodyBuilder.body(BodyInserters.fromResource(jsRes));\n                }))\n            .orElseGet(() -> pluginService.generateBundleVersion()\n                .flatMap(version -> ServerResponse\n                    .temporaryRedirect(buildJsBundleUri(\"js\", version))\n                    .cacheControl(CacheControl.noStore())\n                    .build()));\n    }\n\n    private Mono<ServerResponse> fetchCssBundle(ServerRequest request) {\n        return request.queryParam(\"v\")\n            .map(s -> pluginService.getCssBundle(s).flatMap(cssRes -> {\n                var bodyBuilder = ServerResponse.ok()\n                    .cacheControl(bundleCacheControl)\n                    .contentType(MediaType.valueOf(\"text/css\"));\n                if (useLastModified) {\n                    try {\n                        var lastModified = Instant.ofEpochMilli(cssRes.lastModified());\n                        bodyBuilder = bodyBuilder.lastModified(lastModified);\n                    } catch (IOException e) {\n                        if (e instanceof FileNotFoundException) {\n                            return Mono.error(\n                                new NoResourceFoundException(request.uri(), \"bundle.css\")\n                            );\n                        }\n                        return Mono.error(e);\n                    }\n                }\n                return bodyBuilder.body(BodyInserters.fromResource(cssRes));\n            }))\n            .orElseGet(() -> pluginService.generateBundleVersion()\n                .flatMap(version -> ServerResponse\n                    .temporaryRedirect(buildJsBundleUri(\"css\", version))\n                    .cacheControl(CacheControl.noStore())\n                    .build()));\n\n    }\n\n    URI buildJsBundleUri(String type, String version) {\n        return URI.create(\n            \"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.\" + type + \"?v=\" + version);\n    }\n\n    private Mono<ServerResponse> upgradeFromUri(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var content = request.bodyToMono(UpgradeFromUriRequest.class)\n            .map(UpgradeFromUriRequest::uri)\n            .flatMapMany(reactiveUrlDataBufferFetcher::fetch);\n\n        return Mono.usingWhen(\n                writeToTempFile(content),\n                path -> pluginService.upgrade(name, path),\n                this::deleteFileIfExists)\n            .flatMap(upgradedPlugin -> ServerResponse.ok().bodyValue(upgradedPlugin));\n    }\n\n    private Mono<ServerResponse> installFromUri(ServerRequest request) {\n        var content = request.bodyToMono(InstallFromUriRequest.class)\n            .map(InstallFromUriRequest::uri)\n            .flatMapMany(reactiveUrlDataBufferFetcher::fetch);\n\n        return Mono.usingWhen(\n                writeToTempFile(content),\n                pluginService::install,\n                this::deleteFileIfExists\n            )\n            .flatMap(newPlugin -> ServerResponse.ok().bodyValue(newPlugin));\n    }\n\n    public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {\n    }\n\n    public record UpgradeFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {\n    }\n\n    private Mono<ServerResponse> reload(ServerRequest serverRequest) {\n        var name = serverRequest.pathVariable(\"name\");\n        return ServerResponse.ok().body(pluginService.reload(name), Plugin.class);\n    }\n\n    private Mono<ServerResponse> fetchPluginConfig(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return client.fetch(Plugin.class, name)\n            .mapNotNull(plugin -> plugin.getSpec().getConfigMapName())\n            .flatMap(configMapName -> client.fetch(ConfigMap.class, configMapName))\n            .flatMap(configMap -> ServerResponse.ok().bodyValue(configMap));\n    }\n\n    private Mono<ServerResponse> fetchPluginSetting(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return client.fetch(Plugin.class, name)\n            .mapNotNull(plugin -> plugin.getSpec().getSettingName())\n            .flatMap(settingName -> client.fetch(Setting.class, settingName))\n            .flatMap(setting -> ServerResponse.ok().bodyValue(setting));\n    }\n\n    private Mono<ServerResponse> updatePluginConfig(ServerRequest request) {\n        final var pluginName = request.pathVariable(\"name\");\n        return client.fetch(Plugin.class, pluginName)\n            .doOnNext(plugin -> {\n                String configMapName = plugin.getSpec().getConfigMapName();\n                if (!StringUtils.hasText(configMapName)) {\n                    throw new ServerWebInputException(\n                        \"Unable to complete the request because the plugin configMapName is blank\");\n                }\n            })\n            .flatMap(plugin -> {\n                final String configMapName = plugin.getSpec().getConfigMapName();\n                return request.bodyToMono(ConfigMap.class)\n                    .doOnNext(configMapToUpdate -> {\n                        var configMapNameToUpdate = configMapToUpdate.getMetadata().getName();\n                        if (!configMapName.equals(configMapNameToUpdate)) {\n                            throw new ServerWebInputException(\n                                \"The name from the request body does not match the plugin \"\n                                    + \"configMapName name.\");\n                        }\n                    })\n                    .flatMap(configMapToUpdate -> client.fetch(ConfigMap.class, configMapName)\n                        .map(persisted -> {\n                            configMapToUpdate.getMetadata()\n                                .setVersion(persisted.getMetadata().getVersion());\n                            return configMapToUpdate;\n                        })\n                        .switchIfEmpty(client.create(configMapToUpdate))\n                    )\n                    .flatMap(client::update)\n                    .retryWhen(Retry.backoff(5, Duration.ofMillis(300))\n                        .filter(OptimisticLockingFailureException.class::isInstance)\n                    );\n            })\n            .flatMap(configMap -> ServerResponse.ok().bodyValue(configMap));\n    }\n\n    private Mono<ServerResponse> resetSettingConfig(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return client.fetch(Plugin.class, name)\n            .filter(plugin -> StringUtils.hasText(plugin.getSpec().getSettingName()))\n            .flatMap(plugin -> {\n                String configMapName = plugin.getSpec().getConfigMapName();\n                String settingName = plugin.getSpec().getSettingName();\n                return client.fetch(Setting.class, settingName)\n                    .map(SettingUtils::settingDefinedDefaultValueMap)\n                    .flatMap(data -> updateConfigMapData(configMapName, data));\n            })\n            .flatMap(configMap -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(configMap));\n    }\n\n    private Mono<ConfigMap> updateConfigMapData(String configMapName, Map<String, String> data) {\n        return client.fetch(ConfigMap.class, configMapName)\n            .flatMap(configMap -> {\n                configMap.setData(data);\n                return client.update(configMap);\n            })\n            .retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))\n                .filter(t -> t instanceof OptimisticLockingFailureException));\n    }\n\n\n    private Mono<ServerResponse> install(ServerRequest request) {\n        return request.multipartData()\n            .map(InstallRequest::new)\n            .flatMap(installRequest -> installRequest.getSource()\n                .flatMap(source -> {\n                    if (InstallSource.FILE.equals(source)) {\n                        return installFromFile(installRequest.getFile(), pluginService::install);\n                    }\n                    return Mono.error(\n                        new UnsupportedOperationException(\"Unsupported install source \" + source));\n                }))\n            .flatMap(plugin -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(plugin));\n    }\n\n    private Mono<ServerResponse> upgrade(ServerRequest request) {\n        var pluginName = request.pathVariable(\"name\");\n        return request.multipartData()\n            .map(InstallRequest::new)\n            .flatMap(installRequest -> installRequest.getSource()\n                .flatMap(source -> {\n                    if (InstallSource.FILE.equals(source)) {\n                        return installFromFile(installRequest.getFile(),\n                            path -> pluginService.upgrade(pluginName, path));\n                    }\n                    return Mono.error(\n                        new UnsupportedOperationException(\"Unsupported install source \" + source));\n                }))\n            .flatMap(upgradedPlugin -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(upgradedPlugin));\n    }\n\n    private Mono<Plugin> installFromFile(FilePart filePart,\n        Function<Path, Mono<Plugin>> resourceClosure) {\n        return Mono.usingWhen(\n            writeToTempFile(filePart.content()),\n            resourceClosure,\n            this::deleteFileIfExists);\n    }\n\n    public static class ListRequest extends SortableRequest {\n\n        public ListRequest(ServerRequest request) {\n            super(request.exchange());\n        }\n\n        @Schema(name = \"keyword\", description = \"Keyword of plugin name or description\")\n        public String getKeyword() {\n            return queryParams.getFirst(\"keyword\");\n        }\n\n        @Schema(name = \"enabled\", description = \"Whether the plugin is enabled\")\n        public Boolean getEnabled() {\n            var enabled = queryParams.getFirst(\"enabled\");\n            return enabled == null ? null : getSharedInstance().convert(enabled, Boolean.class);\n        }\n\n        @Override\n        public Sort getSort() {\n            var orders = super.getSort().stream()\n                .map(order -> {\n                    if (\"creationTimestamp\".equals(order.getProperty())) {\n                        return order.withProperty(\"metadata.creationTimestamp\");\n                    }\n                    return order;\n                })\n                .toList();\n            return Sort.by(orders);\n        }\n\n        @Override\n        public ListOptions toListOptions() {\n            var builder = ListOptions.builder(super.toListOptions());\n\n            Optional.ofNullable(queryParams.getFirst(\"keyword\"))\n                .filter(StringUtils::hasText)\n                .ifPresent(keyword -> builder.andQuery(or(\n                    contains(\"spec.displayName\", keyword),\n                    contains(\"spec.description\", keyword)\n                )));\n\n            Optional.ofNullable(queryParams.getFirst(\"enabled\"))\n                .map(Boolean::parseBoolean)\n                .ifPresent(enabled -> builder.andQuery(equal(\"spec.enabled\", enabled.toString())));\n\n            return builder.build();\n        }\n\n        public static void buildParameters(Builder builder) {\n            IListRequest.buildParameters(builder);\n            builder.parameter(sortParameter());\n            builder.parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"keyword\")\n                    .description(\"Keyword of plugin name or description\")\n                    .implementation(String.class)\n                    .required(false))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"enabled\")\n                    .description(\"Whether the plugin is enabled\")\n                    .implementation(Boolean.class)\n                    .required(false));\n        }\n    }\n\n    Mono<ServerResponse> list(ServerRequest request) {\n        return Mono.just(request)\n            .map(ListRequest::new)\n            .flatMap(listRequest -> client.listBy(\n                Plugin.class,\n                listRequest.toListOptions(),\n                listRequest.toPageRequest()\n            ))\n            .flatMap(listResult -> ServerResponse.ok().bodyValue(listResult));\n    }\n\n    @Schema(name = \"PluginInstallRequest\", types = \"object\")\n    public static class InstallRequest {\n\n        private final MultiValueMap<String, Part> multipartData;\n\n        public InstallRequest(MultiValueMap<String, Part> multipartData) {\n            this.multipartData = multipartData;\n        }\n\n        @Schema(requiredMode = NOT_REQUIRED, description = \"Plugin Jar file.\")\n        public FilePart getFile() {\n            var part = multipartData.getFirst(\"file\");\n            if (part == null) {\n                throw new ServerWebInputException(\"Form field file is required\");\n            }\n            if (!(part instanceof FilePart file)) {\n                throw new ServerWebInputException(\"Invalid parameter of file\");\n            }\n            if (!Paths.get(file.filename()).toString().endsWith(\".jar\")) {\n                throw new ServerWebInputException(\"Invalid file type, only jar is supported\");\n            }\n            return file;\n        }\n\n        @Schema(requiredMode = NOT_REQUIRED,\n            description = \"Plugin preset name. We will find the plugin from plugin presets\")\n        public Mono<String> getPresetName() {\n            var part = multipartData.getFirst(\"presetName\");\n            if (part == null) {\n                return Mono.error(new ServerWebInputException(\n                    \"Form field presetName is required.\"));\n            }\n            if (!(part instanceof FormFieldPart presetName)) {\n                return Mono.error(new ServerWebInputException(\n                    \"Invalid format of presetName field, string required\"));\n            }\n            if (!StringUtils.hasText(presetName.value())) {\n                return Mono.error(new ServerWebInputException(\"presetName must not be blank\"));\n            }\n            return Mono.just(presetName.value());\n        }\n\n        @Schema(requiredMode = NOT_REQUIRED, description = \"Install source. Default is file.\")\n        public Mono<InstallSource> getSource() {\n            var part = multipartData.getFirst(\"source\");\n            if (part == null) {\n                return Mono.just(InstallSource.FILE);\n            }\n            if (!(part instanceof FormFieldPart sourcePart)) {\n                return Mono.error(new ServerWebInputException(\n                    \"Invalid format of source field, string required.\"));\n            }\n            var installSource = InstallSource.valueOf(sourcePart.value().toUpperCase());\n            return Mono.just(installSource);\n        }\n    }\n\n    public enum InstallSource {\n        FILE,\n        PRESET,\n        URL\n    }\n\n    Mono<Void> deleteFileIfExists(Path path) {\n        return deleteFileSilently(path, this.scheduler).then();\n    }\n\n    private Mono<Path> writeToTempFile(Publisher<DataBuffer> content) {\n        return Mono.fromCallable(() -> Files.createTempFile(\"halo-plugin-\", \".jar\"))\n            .flatMap(path -> write(content, path).thenReturn(path))\n            .subscribeOn(this.scheduler);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/PostEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static run.halo.app.extension.MetadataUtil.nullSafeLabels;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Duration;\nimport java.util.Objects;\nimport java.util.function.Predicate;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerErrorException;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.content.Content;\nimport run.halo.app.content.ContentUpdateParam;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.ListedPost;\nimport run.halo.app.content.ListedSnapshotDto;\nimport run.halo.app.content.PostQuery;\nimport run.halo.app.content.PostRequest;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Endpoint for managing posts.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class PostEndpoint implements CustomEndpoint {\n\n    private int maxAttemptsWaitForPublish = 10;\n    private final PostService postService;\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"PostV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"posts\", this::listPost, builder -> {\n                    builder.operationId(\"ListPosts\")\n                        .description(\"List posts.\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(ListedPost.class))\n                        );\n                    PostQuery.buildParameters(builder);\n                }\n            )\n            .GET(\"posts/{name}/head-content\", this::fetchHeadContent,\n                builder -> builder.operationId(\"fetchPostHeadContent\")\n                    .description(\"Fetch head content of post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .GET(\"posts/{name}/content\", this::fetchContent,\n                builder -> builder.operationId(\"fetchPostContent\")\n                    .description(\"Fetch content of post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .parameter(parameterBuilder()\n                        .name(\"snapshotName\")\n                        .in(ParameterIn.QUERY)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .GET(\"posts/{name}/release-content\", this::fetchReleaseContent,\n                builder -> builder.operationId(\"fetchPostReleaseContent\")\n                    .description(\"Fetch release content of post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .GET(\"posts/{name}/snapshot\", this::listSnapshots,\n                builder -> builder.operationId(\"listPostSnapshots\")\n                    .description(\"List all snapshots for post content.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementationArray(ListedSnapshotDto.class))\n            )\n            .POST(\"posts\", this::draftPost,\n                builder -> builder.operationId(\"DraftPost\")\n                    .description(\"Draft a post.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(PostRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Post.class))\n            )\n            .PUT(\"posts/{name}\", this::updatePost,\n                builder -> builder.operationId(\"UpdateDraftPost\")\n                    .description(\"Update a post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(PostRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Post.class))\n            )\n            .PUT(\"posts/{name}/content\", this::updateContent,\n                builder -> builder.operationId(\"UpdatePostContent\")\n                    .description(\"Update a post's content.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(Content.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Post.class))\n            )\n            .PUT(\"posts/{name}/revert-content\", this::revertToSpecifiedSnapshot,\n                builder -> builder.operationId(\"revertToSpecifiedSnapshotForPost\")\n                    .description(\"Revert to specified snapshot for post content.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(RevertSnapshotParam.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Post.class))\n            )\n            .PUT(\"posts/{name}/publish\", this::publishPost,\n                builder -> builder.operationId(\"PublishPost\")\n                    .description(\"Publish a post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .parameter(parameterBuilder().name(\"headSnapshot\")\n                        .description(\"Head snapshot name of content.\")\n                        .in(ParameterIn.QUERY)\n                        .required(false))\n                    .parameter(parameterBuilder()\n                        .name(\"async\")\n                        .in(ParameterIn.QUERY)\n                        .implementation(Boolean.class)\n                        .required(false))\n                    .response(responseBuilder()\n                        .implementation(Post.class))\n            )\n            .PUT(\"posts/{name}/unpublish\", this::unpublishPost,\n                builder -> builder.operationId(\"UnpublishPost\")\n                    .description(\"UnPublish a post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true))\n                    .response(responseBuilder()\n                        .implementation(Post.class)))\n            .PUT(\"posts/{name}/recycle\", this::recyclePost,\n                builder -> builder.operationId(\"RecyclePost\")\n                    .description(\"Recycle a post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)))\n            .DELETE(\"posts/{name}/content\", this::deleteContent,\n                builder -> builder.operationId(\"deletePostContent\")\n                    .description(\"Delete a content for post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .parameter(parameterBuilder()\n                        .name(\"snapshotName\")\n                        .in(ParameterIn.QUERY)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> deleteContent(ServerRequest request) {\n        final var postName = request.pathVariable(\"name\");\n        final var snapshotName = request.queryParam(\"snapshotName\").orElseThrow();\n        return postService.deleteContent(postName, snapshotName)\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    private Mono<ServerResponse> revertToSpecifiedSnapshot(ServerRequest request) {\n        final var postName = request.pathVariable(\"name\");\n        return request.bodyToMono(RevertSnapshotParam.class)\n            .switchIfEmpty(\n                Mono.error(new ServerWebInputException(\"Required request body is missing.\")))\n            .flatMap(param -> postService.revertToSpecifiedSnapshot(postName, param.snapshotName))\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    @Schema(name = \"RevertSnapshotForPostParam\")\n    record RevertSnapshotParam(\n        @Schema(requiredMode = Schema.RequiredMode.REQUIRED, minLength = 1) String snapshotName) {\n    }\n\n    private Mono<ServerResponse> fetchContent(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        final var snapshotName = request.queryParam(\"snapshotName\").orElseThrow();\n        return client.fetch(Post.class, name)\n            .flatMap(post -> {\n                var baseSnapshot = post.getSpec().getBaseSnapshot();\n                return postService.getContent(snapshotName, baseSnapshot);\n            })\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    private Mono<ServerResponse> listSnapshots(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        var resultFlux = postService.listSnapshots(name);\n        return ServerResponse.ok().body(resultFlux, ListedSnapshotDto.class);\n    }\n\n    private Mono<ServerResponse> fetchReleaseContent(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return postService.getReleaseContent(name)\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    private Mono<ServerResponse> fetchHeadContent(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return postService.getHeadContent(name)\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    Mono<ServerResponse> draftPost(ServerRequest request) {\n        return request.bodyToMono(PostRequest.class)\n            .flatMap(postService::draftPost)\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    Mono<ServerResponse> updateContent(ServerRequest request) {\n        String postName = request.pathVariable(\"name\");\n        return request.bodyToMono(ContentUpdateParam.class)\n            .flatMap(content -> Mono.defer(() -> client.fetch(Post.class, postName)\n                    .flatMap(post -> {\n                        PostRequest postRequest = new PostRequest(post, content);\n                        return postService.updatePost(postRequest);\n                    }))\n                .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                    .filter(throwable -> throwable instanceof OptimisticLockingFailureException))\n            )\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    Mono<ServerResponse> updatePost(ServerRequest request) {\n        return request.bodyToMono(PostRequest.class)\n            .flatMap(postService::updatePost)\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    Mono<ServerResponse> publishPost(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        boolean asyncPublish = request.queryParam(\"async\")\n            .map(Boolean::parseBoolean)\n            .orElse(false);\n\n        return Mono.defer(() -> client.get(Post.class, name)\n                .doOnNext(post -> {\n                    var spec = post.getSpec();\n                    request.queryParam(\"headSnapshot\").ifPresent(spec::setHeadSnapshot);\n                    spec.setPublish(true);\n                    if (spec.getHeadSnapshot() == null) {\n                        spec.setHeadSnapshot(spec.getBaseSnapshot());\n                    }\n                    spec.setReleaseSnapshot(spec.getHeadSnapshot());\n                })\n                .flatMap(client::update)\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(t -> t instanceof OptimisticLockingFailureException))\n            .filter(post -> asyncPublish)\n            .switchIfEmpty(Mono.defer(() -> awaitPostPublished(name)))\n            .onErrorMap(Exceptions::isRetryExhausted, err -> new ServerErrorException(\n                \"Post publishing failed, please try again later.\", err))\n            .flatMap(publishResult -> ServerResponse.ok().bodyValue(publishResult));\n    }\n\n    private Mono<Post> awaitPostPublished(String postName) {\n        Predicate<Post> schedulePublish = post -> {\n            var labels = nullSafeLabels(post);\n            return BooleanUtils.TRUE.equals(labels.get(Post.SCHEDULING_PUBLISH_LABEL));\n        };\n        return Mono.defer(() -> client.get(Post.class, postName)\n                .filter(post -> {\n                    var releasedSnapshot = MetadataUtil.nullSafeAnnotations(post)\n                        .get(Post.LAST_RELEASED_SNAPSHOT_ANNO);\n                    var expectReleaseSnapshot = post.getSpec().getReleaseSnapshot();\n                    return Objects.equals(releasedSnapshot, expectReleaseSnapshot)\n                        || schedulePublish.test(post);\n                })\n                .switchIfEmpty(Mono.error(\n                    () -> new IllegalStateException(\"Retry to check post publish status\"))\n                ))\n            .retryWhen(Retry.backoff(maxAttemptsWaitForPublish, Duration.ofMillis(100))\n                .filter(IllegalStateException.class::isInstance)\n            );\n    }\n\n    private Mono<ServerResponse> unpublishPost(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return Mono.defer(() -> client.get(Post.class, name)\n                .doOnNext(post -> {\n                    var spec = post.getSpec();\n                    spec.setPublish(false);\n                })\n                .flatMap(client::update))\n            .retryWhen(Retry.backoff(3, Duration.ofMillis(100))\n                .filter(t -> t instanceof OptimisticLockingFailureException))\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    private Mono<ServerResponse> recyclePost(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return Mono.defer(() -> client.get(Post.class, name)\n                .doOnNext(post -> {\n                    var spec = post.getSpec();\n                    spec.setDeleted(true);\n                })\n                .flatMap(client::update))\n            .retryWhen(Retry.backoff(3, Duration.ofMillis(100))\n                .filter(t -> t instanceof OptimisticLockingFailureException))\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    Mono<ServerResponse> listPost(ServerRequest request) {\n        PostQuery postQuery = new PostQuery(request);\n        return postService.listPost(postQuery)\n            .flatMap(listedPosts -> ServerResponse.ok().bodyValue(listedPosts));\n    }\n\n    /**\n     * Convenient for testing, to avoid waiting too long for post published when testing.\n     */\n    void setMaxAttemptsWaitForPublish(int maxAttempts) {\n        this.maxAttemptsWaitForPublish = maxAttempts;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/ReplyEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\n\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.comment.ListedReply;\nimport run.halo.app.content.comment.ReplyQuery;\nimport run.halo.app.content.comment.ReplyService;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ListResult;\n\n/**\n * Endpoint for managing {@link Reply}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class ReplyEndpoint implements CustomEndpoint {\n\n    private final ReplyService replyService;\n\n    public ReplyEndpoint(ReplyService replyService) {\n        this.replyService = replyService;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"ReplyV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"replies\", this::listReplies, builder -> {\n                    builder.operationId(\"ListReplies\")\n                        .description(\"List replies.\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(ListedReply.class))\n                        );\n                    ReplyQuery.buildParameters(builder);\n                }\n            )\n            .build();\n    }\n\n    Mono<ServerResponse> listReplies(ServerRequest request) {\n        ReplyQuery replyQuery = new ReplyQuery(request.exchange());\n        return replyService.list(replyQuery)\n            .flatMap(listedReplies -> ServerResponse.ok().bodyValue(listedReplies));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/SinglePageEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.time.Duration;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport org.thymeleaf.util.StringUtils;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.content.Content;\nimport run.halo.app.content.ContentUpdateParam;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.ListedSinglePage;\nimport run.halo.app.content.ListedSnapshotDto;\nimport run.halo.app.content.SinglePageQuery;\nimport run.halo.app.content.SinglePageRequest;\nimport run.halo.app.content.SinglePageService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Endpoint for managing {@link SinglePage}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\n@AllArgsConstructor\npublic class SinglePageEndpoint implements CustomEndpoint {\n\n    private final SinglePageService singlePageService;\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"SinglePageV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"singlepages\", this::listSinglePage, builder -> {\n                    builder.operationId(\"ListSinglePages\")\n                        .description(\"List single pages.\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(ListedSinglePage.class))\n                        );\n                    SinglePageQuery.buildParameters(builder);\n                }\n            )\n            .GET(\"singlepages/{name}/head-content\", this::fetchHeadContent,\n                builder -> builder.operationId(\"fetchSinglePageHeadContent\")\n                    .description(\"Fetch head content of single page.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .GET(\"singlepages/{name}/release-content\", this::fetchReleaseContent,\n                builder -> builder.operationId(\"fetchSinglePageReleaseContent\")\n                    .description(\"Fetch release content of single page.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .GET(\"singlepages/{name}/content\", this::fetchContent,\n                builder -> builder.operationId(\"fetchSinglePageContent\")\n                    .description(\"Fetch content of single page.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .parameter(parameterBuilder().name(\"snapshotName\")\n                        .in(ParameterIn.QUERY)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .GET(\"singlepages/{name}/snapshot\", this::listSnapshots,\n                builder -> builder.operationId(\"listSinglePageSnapshots\")\n                    .description(\"List all snapshots for single page content.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementationArray(ListedSnapshotDto.class))\n            )\n            .POST(\"singlepages\", this::draftSinglePage,\n                builder -> builder.operationId(\"DraftSinglePage\")\n                    .description(\"Draft a single page.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(SinglePageRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(SinglePage.class))\n            )\n            .PUT(\"singlepages/{name}\", this::updateSinglePage,\n                builder -> builder.operationId(\"UpdateDraftSinglePage\")\n                    .description(\"Update a single page.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(SinglePageRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(SinglePage.class))\n            )\n            .PUT(\"singlepages/{name}/content\", this::updateContent,\n                builder -> builder.operationId(\"UpdateSinglePageContent\")\n                    .description(\"Update a single page's content.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(Content.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Post.class))\n            )\n            .PUT(\"singlepages/{name}/revert-content\", this::revertToSpecifiedSnapshot,\n                builder -> builder.operationId(\"revertToSpecifiedSnapshotForSinglePage\")\n                    .description(\"Revert to specified snapshot for single page content.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(RevertSnapshotParam.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Post.class))\n            )\n            .PUT(\"singlepages/{name}/publish\", this::publishSinglePage,\n                builder -> builder.operationId(\"PublishSinglePage\")\n                    .description(\"Publish a single page.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementation(SinglePage.class))\n            )\n            .DELETE(\"singlepages/{name}/content\", this::deleteContent,\n                builder -> builder.operationId(\"deleteSinglePageContent\")\n                    .description(\"Delete a content for post.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .parameter(parameterBuilder()\n                        .name(\"snapshotName\")\n                        .in(ParameterIn.QUERY)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementation(ContentWrapper.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> deleteContent(ServerRequest request) {\n        final var postName = request.pathVariable(\"name\");\n        final var snapshotName = request.queryParam(\"snapshotName\").orElseThrow();\n        return singlePageService.deleteContent(postName, snapshotName)\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    private Mono<ServerResponse> revertToSpecifiedSnapshot(ServerRequest request) {\n        final var postName = request.pathVariable(\"name\");\n        return request.bodyToMono(RevertSnapshotParam.class)\n            .switchIfEmpty(\n                Mono.error(new ServerWebInputException(\"Required request body is missing.\")))\n            .flatMap(\n                param -> singlePageService.revertToSpecifiedSnapshot(postName, param.snapshotName))\n            .flatMap(page -> ServerResponse.ok().bodyValue(page));\n    }\n\n    @Schema(name = \"RevertSnapshotForSingleParam\")\n    record RevertSnapshotParam(\n        @Schema(requiredMode = Schema.RequiredMode.REQUIRED, minLength = 1) String snapshotName) {\n    }\n\n    private Mono<ServerResponse> fetchContent(ServerRequest request) {\n        final var snapshotName = request.queryParam(\"snapshotName\").orElseThrow();\n        return client.fetch(SinglePage.class, request.pathVariable(\"name\"))\n            .flatMap(page -> {\n                var baseSnapshot = page.getSpec().getBaseSnapshot();\n                return singlePageService.getContent(snapshotName, baseSnapshot);\n            })\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    private Mono<ServerResponse> listSnapshots(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        var resultFlux = singlePageService.listSnapshots(name);\n        return ServerResponse.ok().body(resultFlux, ListedSnapshotDto.class);\n    }\n\n    private Mono<ServerResponse> fetchReleaseContent(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return singlePageService.getReleaseContent(name)\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    private Mono<ServerResponse> fetchHeadContent(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return singlePageService.getHeadContent(name)\n            .flatMap(content -> ServerResponse.ok().bodyValue(content));\n    }\n\n    Mono<ServerResponse> draftSinglePage(ServerRequest request) {\n        return request.bodyToMono(SinglePageRequest.class)\n            .flatMap(singlePageService::draft)\n            .flatMap(singlePage -> ServerResponse.ok().bodyValue(singlePage));\n    }\n\n    Mono<ServerResponse> updateContent(ServerRequest request) {\n        String pageName = request.pathVariable(\"name\");\n        return request.bodyToMono(ContentUpdateParam.class)\n            .flatMap(content -> Mono.defer(() -> client.fetch(SinglePage.class, pageName)\n                    .flatMap(page -> {\n                        SinglePageRequest pageRequest = new SinglePageRequest(page, content);\n                        return singlePageService.update(pageRequest);\n                    }))\n                .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                    .filter(throwable -> throwable instanceof OptimisticLockingFailureException))\n            )\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    Mono<ServerResponse> updateSinglePage(ServerRequest request) {\n        return request.bodyToMono(SinglePageRequest.class)\n            .flatMap(singlePageService::update)\n            .flatMap(page -> ServerResponse.ok().bodyValue(page));\n    }\n\n    Mono<ServerResponse> publishSinglePage(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        boolean asyncPublish = request.queryParam(\"async\")\n            .map(Boolean::parseBoolean)\n            .orElse(false);\n        return Mono.defer(() -> client.get(SinglePage.class, name)\n                .flatMap(singlePage -> {\n                    SinglePage.SinglePageSpec spec = singlePage.getSpec();\n                    spec.setPublish(true);\n                    if (spec.getHeadSnapshot() == null) {\n                        spec.setHeadSnapshot(spec.getBaseSnapshot());\n                    }\n                    spec.setReleaseSnapshot(spec.getHeadSnapshot());\n                    return client.update(singlePage);\n                })\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(t -> t instanceof OptimisticLockingFailureException))\n            .flatMap(post -> {\n                if (asyncPublish) {\n                    return Mono.just(post);\n                }\n                return client.fetch(SinglePage.class, name)\n                    .flatMap(latest -> {\n                        var latestReleasedSnapshotName = MetadataUtil.nullSafeAnnotations(latest)\n                            .get(Post.LAST_RELEASED_SNAPSHOT_ANNO);\n                        if (!StringUtils.equals(latestReleasedSnapshotName,\n                            latest.getSpec().getReleaseSnapshot())) {\n                            return Mono.error(new IllegalStateException(\n                                \"SinglePage publishing status is not as expected\"\n                            ));\n                        }\n                        return Mono.just(latest);\n                    })\n                    .retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))\n                        .filter(IllegalStateException.class::isInstance)\n                    )\n                    .doOnError(IllegalStateException.class, err -> {\n                        log.error(\"Failed to publish single page [{}]\", name, err);\n                        throw new IllegalStateException(\"Publishing wait timeout.\");\n                    });\n            })\n            .flatMap(page -> ServerResponse.ok().bodyValue(page));\n    }\n\n    Mono<ServerResponse> listSinglePage(ServerRequest request) {\n        var listRequest = new SinglePageQuery(request);\n        return singlePageService.list(listRequest)\n            .flatMap(listedPages -> ServerResponse.ok().bodyValue(listedPages));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/StatsEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport lombok.Data;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Stats endpoint.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class StatsEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n\n    public StatsEndpoint(ReactiveExtensionClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"SystemV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"stats\", this::getStats, builder -> builder.operationId(\"getStats\")\n                .description(\"Get stats.\")\n                .tag(tag)\n                .response(responseBuilder()\n                    .implementation(DashboardStats.class)\n                )\n            )\n            .build();\n    }\n\n    Mono<ServerResponse> getStats(ServerRequest request) {\n        var stats = DashboardStats.emptyStats();\n        Mono<Void> setFromCounters = client.listAll(\n                Counter.class, ListOptions.builder().build(), Sort.unsorted()\n            )\n            .doOnNext(counter -> {\n                var visit = counter.getVisit();\n                if (visit != null) {\n                    stats.setVisits(stats.getVisits() + visit);\n                }\n                var totalComment = counter.getTotalComment();\n                if (totalComment != null) {\n                    stats.setComments(stats.getComments() + totalComment);\n                }\n                var approvedComment = counter.getApprovedComment();\n                if (approvedComment != null) {\n                    stats.setApprovedComments(\n                        stats.getApprovedComments() + approvedComment\n                    );\n                }\n                var upvote = counter.getUpvote();\n                if (upvote != null) {\n                    stats.setUpvotes(stats.getUpvotes() + upvote);\n                }\n            })\n            .then();\n\n        Mono<Void> setUsers = client.countBy(User.class, ListOptions.builder()\n                .labelSelector()\n                .notEq(User.HIDDEN_USER_LABEL, \"true\")\n                .end()\n                .andQuery(isNull(\"metadata.deletionTimestamp\"))\n                .build()\n            )\n            .doOnNext(stats::setUsers)\n            .then();\n        Mono<Void> setPosts = client.countBy(Post.class, ListOptions.builder()\n                .andQuery(and(\n                    isNull(\"metadata.deletionTimestamp\"),\n                    equal(\"spec.deleted\", \"false\")\n                ))\n                .build()\n            )\n            .doOnNext(stats::setPosts)\n            .then();\n\n        return Mono.when(setFromCounters, setUsers, setPosts)\n            .thenReturn(stats)\n            .flatMap(body -> ServerResponse.ok().bodyValue(body));\n    }\n\n    @Data\n    public static class DashboardStats {\n        private long visits;\n        private long comments;\n        private long approvedComments;\n        private long upvotes;\n        private long users;\n        private long posts;\n\n        /**\n         * Creates an empty stats that populated initialize value.\n         *\n         * @return stats with initialize value.\n         */\n        public static DashboardStats emptyStats() {\n            DashboardStats stats = new DashboardStats();\n            stats.setVisits(0L);\n            stats.setComments(0L);\n            stats.setApprovedComments(0L);\n            stats.setUpvotes(0L);\n            stats.setUsers(0L);\n            stats.setPosts(0L);\n            return stats;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/SystemConfigEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport tools.jackson.databind.json.JsonMapper;\nimport tools.jackson.databind.node.ObjectNode;\n\n@Component\n@RequiredArgsConstructor\npublic class SystemConfigEndpoint implements CustomEndpoint {\n    private final SystemConfigFetcher configurableEnvironmentFetcher;\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"SystemConfigV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"/systemconfigs/{group}\", this::getConfigByGroup,\n                builder -> builder.operationId(\"getSystemConfigByGroup\")\n                    .description(\"Get system config by group\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                        )\n                        .implementation(Object.class))\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"group\")\n                        .required(true)\n                        .description(\"Group of the system config\")\n                    )\n            )\n            .PUT(\"/systemconfigs/{group}\", this::updateConfigByGroup,\n                builder -> builder.operationId(\"updateSystemConfigByGroup\")\n                    .description(\"Update system config by group\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"group\")\n                        .required(true)\n                        .description(\"Group of the system config\")\n                    )\n                    .requestBody(requestBodyBuilder().implementation(Object.class))\n                    .response(responseBuilder()\n                        .responseCode(String.valueOf(HttpStatus.NO_CONTENT))\n                        .implementation(Void.class)\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> updateConfigByGroup(ServerRequest request) {\n        final var group = request.pathVariable(\"group\");\n        return request.bodyToMono(ObjectNode.class)\n            .flatMap(objectNode -> configurableEnvironmentFetcher.getConfigMap()\n                .flatMap(cm -> {\n                    if (cm.getData() == null) {\n                        cm.setData(new HashMap<>());\n                    }\n                    cm.getData().put(group, objectNode.toString());\n                    return client.update(cm);\n                })\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance))\n            .then(ServerResponse.noContent().build());\n    }\n\n    private Mono<ServerResponse> getConfigByGroup(ServerRequest request) {\n        final var group = request.pathVariable(\"group\");\n        return configurableEnvironmentFetcher.fetch(group, ObjectNode.class)\n            .switchIfEmpty(Mono.fromSupplier(JsonMapper.shared()::createObjectNode))\n            .flatMap(json -> ServerResponse.ok().bodyValue(json));\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return new GroupVersion(\"console.api.halo.run\", \"v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/TagEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static run.halo.app.extension.index.query.Queries.contains;\nimport static run.halo.app.extension.index.query.Queries.or;\nimport static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.extension.router.SortableRequest;\n\n/**\n * post tag endpoint.\n *\n * @author LIlGG\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class TagEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"TagV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"tags\", this::listTag, builder -> {\n                    builder.operationId(\"ListPostTags\")\n                        .description(\"List Post Tags.\")\n                        .tag(tag)\n                        .response(\n                            responseBuilder()\n                                .implementation(ListResult.generateGenericClass(Tag.class))\n                        );\n                    TagQuery.buildParameters(builder);\n                }\n            )\n            .build();\n    }\n\n    Mono<ServerResponse> listTag(ServerRequest request) {\n        var tagQuery = new TagQuery(request);\n        return client.listBy(Tag.class, tagQuery.toListOptions(),\n                PageRequestImpl.of(tagQuery.getPage(), tagQuery.getSize(), tagQuery.getSort())\n            )\n            .flatMap(tags -> ServerResponse.ok().bodyValue(tags));\n    }\n\n    public static class TagQuery extends SortableRequest {\n\n        public TagQuery(ServerRequest request) {\n            super(request.exchange());\n        }\n\n        public Optional<String> getKeyword() {\n            return Optional.ofNullable(queryParams.getFirst(\"keyword\"))\n                .filter(StringUtils::hasText);\n        }\n\n        @Override\n        public ListOptions toListOptions() {\n            var builder = ListOptions.builder(super.toListOptions());\n            getKeyword().ifPresent(keyword -> builder.andQuery(\n                or(\n                    contains(\"spec.displayName\", keyword),\n                    contains(\"spec.slug\", keyword)\n                )\n            ));\n            return builder.build();\n        }\n\n        public static void buildParameters(Builder builder) {\n            IListRequest.buildParameters(builder);\n            builder.parameter(sortParameter())\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"keyword\")\n                    .description(\"Post tags filtered by keyword.\")\n                    .implementation(String.class)\n                    .required(false));\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/TrackerEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.event.post.DownvotedEvent;\nimport run.halo.app.event.post.UpvotedEvent;\nimport run.halo.app.event.post.VisitedEvent;\nimport run.halo.app.extension.GroupVersion;\n\n/**\n * Metrics counter endpoint.\n *\n * @author guqing\n * @since 2.0.0\n */\n@AllArgsConstructor\n@Component\npublic class TrackerEndpoint implements CustomEndpoint {\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"MetricsV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"trackers/counter\", this::increaseVisit,\n                builder -> builder.operationId(\"count\")\n                    .description(\"Count an extension resource visits.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(CounterRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Void.class))\n            )\n            .POST(\"trackers/upvote\", this::upvote,\n                builder -> builder.operationId(\"upvote\")\n                    .description(\"Upvote an extension resource.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(VoteRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Void.class))\n            )\n            .POST(\"trackers/downvote\", this::downvote,\n                builder -> builder.operationId(\"downvote\")\n                    .description(\"Downvote an extension resource.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(VoteRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Void.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> increaseVisit(ServerRequest request) {\n        return request.bodyToMono(CounterRequest.class)\n            .switchIfEmpty(\n                Mono.error(new IllegalArgumentException(\"Counter request body must not be empty\")))\n            .doOnNext(counterRequest -> {\n                eventPublisher.publishEvent(new VisitedEvent(this, counterRequest.group(),\n                    counterRequest.name(), counterRequest.plural()));\n            })\n            .then(ServerResponse.ok().build());\n    }\n\n    private Mono<ServerResponse> upvote(ServerRequest request) {\n        return request.bodyToMono(VoteRequest.class)\n            .switchIfEmpty(\n                Mono.error(new IllegalArgumentException(\"Upvote request body must not be empty\")))\n            .doOnNext(voteRequest -> {\n                eventPublisher.publishEvent(new UpvotedEvent(this, voteRequest.group(),\n                    voteRequest.name(), voteRequest.plural()));\n            })\n            .then(ServerResponse.ok().build());\n    }\n\n    private Mono<ServerResponse> downvote(ServerRequest request) {\n        return request.bodyToMono(VoteRequest.class)\n            .switchIfEmpty(\n                Mono.error(new IllegalArgumentException(\"Downvote request body must not be empty\")))\n            .doOnNext(voteRequest -> {\n                eventPublisher.publishEvent(new DownvotedEvent(this, voteRequest.group(),\n                    voteRequest.name(), voteRequest.plural()));\n            })\n            .then(ServerResponse.ok().build());\n    }\n\n    public record VoteRequest(String group, String plural, String name) {\n    }\n\n    public record CounterRequest(String group, String plural, String name, String hostname,\n                                 String screen, String language, String referrer) {\n        /**\n         * Construct counter request.\n         * group and session uid can be empty.\n         */\n        public CounterRequest {\n            Assert.notNull(plural, \"The plural must not be null.\");\n            Assert.notNull(name, \"The name must not be null.\");\n            group = StringUtils.defaultString(group);\n        }\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return new GroupVersion(\"api.halo.run\", \"v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/console/UserEndpoint.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static org.apache.commons.lang3.StringUtils.defaultIfBlank;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static run.halo.app.extension.ListResult.generateGenericClass;\nimport static run.halo.app.extension.index.query.Queries.contains;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.in;\nimport static run.halo.app.extension.index.query.Queries.or;\nimport static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;\nimport static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.google.common.io.Files;\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport io.github.resilience4j.ratelimiter.RequestNotPermitted;\nimport io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.Email;\nimport jakarta.validation.constraints.NotBlank;\nimport java.security.Principal;\nimport java.time.Duration;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.LinkedHashSet;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.http.codec.multipart.Part;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.validation.Validator;\nimport org.springframework.web.reactive.function.BodyExtractors;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.core.user.service.EmailVerificationService;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.SortableRequest;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemSetting.Attachment.UploadOptions;\nimport run.halo.app.infra.ValidationUtils;\nimport run.halo.app.infra.exception.RateLimitExceededException;\nimport run.halo.app.infra.exception.RestrictedNameException;\nimport run.halo.app.infra.exception.UnsatisfiedAttributeValueException;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@Component\n@RequiredArgsConstructor\npublic class UserEndpoint implements CustomEndpoint {\n\n    private static final String SELF_USER = \"-\";\n    private static final String USER_AVATAR_GROUP_NAME = \"user-avatar-group\";\n    private static final String DEFAULT_USER_AVATAR_ATTACHMENT_POLICY_NAME = \"default-policy\";\n    private static final DataSize MAX_AVATAR_FILE_SIZE = DataSize.ofMegabytes(2L);\n    private final ReactiveExtensionClient client;\n    private final UserService userService;\n    private final RoleService roleService;\n    private final AttachmentService attachmentService;\n    private final EmailVerificationService emailVerificationService;\n    private final RateLimiterRegistry rateLimiterRegistry;\n    private final SystemConfigFetcher environmentFetcher;\n    private final Validator validator;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"UserV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"/users/-\", this::me, builder -> builder.operationId(\"GetCurrentUserDetail\")\n                .description(\"Get current user detail\")\n                .tag(tag)\n                .response(responseBuilder().implementation(DetailedUser.class)))\n            .GET(\"/users/{name}\", this::getUserByName,\n                builder -> builder.operationId(\"GetUserDetail\")\n                    .description(\"Get user detail by name\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"User name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder().implementation(DetailedUser.class)))\n            .PUT(\"/users/-\", this::updateProfile,\n                builder -> builder.operationId(\"UpdateCurrentUser\")\n                    .description(\"Update current user profile, but password.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder().required(true).implementation(User.class))\n                    .response(responseBuilder().implementation(User.class)))\n            .POST(\"/users/{name}/permissions\", this::grantPermission,\n                builder -> builder.operationId(\"GrantPermission\")\n                    .description(\"Grant permissions to user\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH).name(\"name\")\n                        .description(\"User name\")\n                        .required(true))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .implementation(GrantRequest.class))\n                    .response(responseBuilder().implementation(User.class)))\n            .POST(\"/users\", this::createUser,\n                builder -> builder.operationId(\"CreateUser\")\n                    .description(\"Creates a new user.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .implementation(CreateUserRequest.class))\n                    .response(responseBuilder().implementation(User.class)))\n            .GET(\"/users/{name}/permissions\", this::getUserPermission,\n                builder -> builder.operationId(\"GetPermissions\")\n                    .description(\"Get permissions of user\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH).name(\"name\")\n                        .description(\"User name\")\n                        .required(true))\n                    .response(responseBuilder().implementation(UserPermission.class)))\n            .PUT(\"/users/-/password\", this::changeOwnPassword,\n                builder -> builder.operationId(\"ChangeOwnPassword\")\n                    .description(\"Change own password of user.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .implementation(ChangeOwnPasswordRequest.class))\n                    .response(responseBuilder()\n                        .implementation(User.class))\n            )\n            .PUT(\"/users/{name}/password\", this::changeAnyonePasswordForAdmin,\n                builder -> builder.operationId(\"ChangeAnyonePassword\")\n                    .description(\"Change anyone password of user for admin.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH).name(\"name\")\n                        .description(\n                            \"Name of user. If the name is equal to '-', it will change the \"\n                                + \"password of current user.\")\n                        .required(true))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .implementation(ChangePasswordRequest.class))\n                    .response(responseBuilder()\n                        .implementation(User.class))\n            )\n            .GET(\"users\", this::list, builder -> {\n                builder.operationId(\"ListUsers\")\n                    .tag(tag)\n                    .description(\"List users\")\n                    .response(responseBuilder()\n                        .implementation(generateGenericClass(ListedUser.class)));\n                ListRequest.buildParameters(builder);\n            })\n            .POST(\"users/{name}/avatar\", contentType(MediaType.MULTIPART_FORM_DATA),\n                this::uploadUserAvatar,\n                builder -> builder\n                    .operationId(\"UploadUserAvatar\")\n                    .description(\"upload user avatar\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"User name\")\n                        .required(true)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder().implementation(IAvatarUploadRequest.class))\n                        ))\n                    .response(responseBuilder().implementation(User.class))\n            )\n            .DELETE(\"users/{name}/avatar\", this::deleteUserAvatar, builder -> builder\n                .tag(tag)\n                .operationId(\"DeleteUserAvatar\")\n                .description(\"delete user avatar\")\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.PATH)\n                    .name(\"name\")\n                    .description(\"User name\")\n                    .required(true)\n                )\n                .response(responseBuilder().implementation(User.class))\n                .build())\n            .POST(\"users/-/send-email-verification-code\",\n                this::sendEmailVerificationCode,\n                builder -> builder\n                    .tag(tag)\n                    .operationId(\"SendEmailVerificationCode\")\n                    .requestBody(requestBodyBuilder()\n                        .implementation(EmailVerifyRequest.class)\n                        .required(true)\n                    )\n                    .description(\"Send email verification code for user\")\n                    .response(responseBuilder().implementation(Void.class))\n                    .build()\n            )\n            .POST(\"users/-/verify-email\", this::verifyEmail,\n                builder -> builder\n                    .tag(tag)\n                    .operationId(\"VerifyEmail\")\n                    .description(\"Verify email for user by code.\")\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .implementation(VerifyCodeRequest.class))\n                    .response(responseBuilder().implementation(Void.class))\n                    .build()\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> verifyEmail(ServerRequest request) {\n        return request.bodyToMono(VerifyCodeRequest.class)\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"Request body is required.\"))\n            )\n            .flatMap(this::doVerifyCode)\n            .then(ServerResponse.ok().build());\n    }\n\n    private Mono<Void> doVerifyCode(VerifyCodeRequest verifyCodeRequest) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Principal::getName)\n            .flatMap(username -> verifyPasswordAndCode(username, verifyCodeRequest));\n    }\n\n    private Mono<Void> verifyPasswordAndCode(String username, VerifyCodeRequest verifyCodeRequest) {\n        return userService.confirmPassword(username, verifyCodeRequest.password())\n            .filter(Boolean::booleanValue)\n            .switchIfEmpty(Mono.error(new UnsatisfiedAttributeValueException(\n                \"Password is incorrect.\", \"problemDetail.user.password.notMatch\", null)))\n            .flatMap(verified -> verifyEmailCode(username, verifyCodeRequest.code()));\n    }\n\n    private Mono<Void> verifyEmailCode(String username, String code) {\n        return Mono.just(username)\n            .transformDeferred(verificationEmailRateLimiter(username))\n            .flatMap(name -> emailVerificationService.verify(username, code))\n            .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);\n    }\n\n    public record EmailVerifyRequest(\n        @Schema(requiredMode = REQUIRED)\n        @Email\n        @NotBlank\n        String email) {\n    }\n\n    public record VerifyCodeRequest(\n        @Schema(requiredMode = REQUIRED) String password,\n        @Schema(requiredMode = REQUIRED, minLength = 1) String code) {\n    }\n\n    private Mono<ServerResponse> sendEmailVerificationCode(ServerRequest request) {\n        var emailMono = request.bodyToMono(EmailVerifyRequest.class)\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"Request body is required.\"))\n            )\n            .doOnNext(emailReq -> {\n                var bindingResult =\n                    ValidationUtils.validate(emailReq, validator, request.exchange());\n                if (bindingResult.hasErrors()) {\n                    // only email field is validated\n                    throw new ServerWebInputException(\"validation.error.email.pattern\");\n                }\n            })\n            .map(EmailVerifyRequest::email)\n            .map(String::toLowerCase);\n        return Mono.zip(emailMono, getAuthenticatedUserName())\n            .flatMap(tuple -> {\n                var email = tuple.getT1();\n                var username = tuple.getT2();\n                return Mono.just(username)\n                    .transformDeferred(sendEmailVerificationCodeRateLimiter(username, email))\n                    .flatMap(u -> emailVerificationService.sendVerificationCode(username, email))\n                    .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);\n            })\n            .then(ServerResponse.ok().build());\n    }\n\n    <T> RateLimiterOperator<T> verificationEmailRateLimiter(String username) {\n        String rateLimiterKey = \"verify-email-\" + username;\n        var rateLimiter =\n            rateLimiterRegistry.rateLimiter(rateLimiterKey, \"verify-email\");\n        return RateLimiterOperator.of(rateLimiter);\n    }\n\n    <T> RateLimiterOperator<T> sendEmailVerificationCodeRateLimiter(String username, String email) {\n        String rateLimiterKey = \"send-email-verification-code-\" + username + \":\" + email;\n        var rateLimiter =\n            rateLimiterRegistry.rateLimiter(rateLimiterKey, \"send-email-verification-code\");\n        return RateLimiterOperator.of(rateLimiter);\n    }\n\n    private Mono<ServerResponse> deleteUserAvatar(ServerRequest request) {\n        final var nameInPath = request.pathVariable(\"name\");\n        return getUserOrSelf(nameInPath)\n            .flatMap(user -> {\n                MetadataUtil.nullSafeAnnotations(user)\n                    .remove(User.AVATAR_ATTACHMENT_NAME_ANNO);\n                user.getSpec().setAvatar(null);\n                return client.update(user);\n            })\n            .flatMap(user -> ServerResponse.ok().bodyValue(user));\n    }\n\n    private Mono<User> getUserOrSelf(String name) {\n        if (!SELF_USER.equals(name)) {\n            return client.get(User.class, name);\n        }\n        return getAuthenticatedUserName()\n            .flatMap(currentUserName -> client.get(User.class, currentUserName));\n    }\n\n    private Mono<ServerResponse> uploadUserAvatar(ServerRequest request) {\n        final var username = request.pathVariable(\"name\");\n        return request.body(BodyExtractors.toMultipartData())\n            .map(AvatarUploadRequest::new)\n            .flatMap(this::uploadAvatar)\n            .flatMap(attachment -> getUserOrSelf(username)\n                .flatMap(user -> {\n                    MetadataUtil.nullSafeAnnotations(user)\n                        .put(User.AVATAR_ATTACHMENT_NAME_ANNO,\n                            attachment.getMetadata().getName());\n                    return client.update(user);\n                })\n                .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                    .filter(OptimisticLockingFailureException.class::isInstance))\n            )\n            .flatMap(user -> ServerResponse.ok().bodyValue(user));\n    }\n\n    @Schema(types = \"object\")\n    public interface IAvatarUploadRequest {\n        @Schema(requiredMode = REQUIRED, description = \"Avatar file\")\n        FilePart getFile();\n    }\n\n    public record AvatarUploadRequest(MultiValueMap<String, Part> formData) {\n        public FilePart getFile() {\n            Part file = formData.getFirst(\"file\");\n            if (file == null) {\n                throw new ServerWebInputException(\"No file part found in the request\");\n            }\n\n            if (!(file instanceof FilePart filePart)) {\n                throw new ServerWebInputException(\"Invalid part of file\");\n            }\n\n            if (!filePart.filename().endsWith(\".png\")) {\n                throw new ServerWebInputException(\"Only support avatar in PNG format\");\n            }\n            return filePart;\n        }\n    }\n\n    private Mono<Attachment> uploadAvatar(AvatarUploadRequest uploadRequest) {\n        var fallbackSetting =\n            environmentFetcher.fetch(SystemSetting.User.GROUP, SystemSetting.User.class)\n                .mapNotNull(SystemSetting.User::getAvatarPolicy)\n                .filter(StringUtils::isNotBlank);\n        var getAvatarPolicy = environmentFetcher.fetch(\n                SystemSetting.Attachment.GROUP, SystemSetting.Attachment.class\n            )\n            .mapNotNull(SystemSetting.Attachment::avatar)\n            .mapNotNull(UploadOptions::policyName)\n            .filter(StringUtils::isNotBlank)\n            .switchIfEmpty(fallbackSetting)\n            .defaultIfEmpty(DEFAULT_USER_AVATAR_ATTACHMENT_POLICY_NAME);\n        return getAvatarPolicy.flatMap(avatarPolicy -> {\n            FilePart filePart = uploadRequest.getFile();\n            var ext = Files.getFileExtension(filePart.filename());\n            return attachmentService.upload(avatarPolicy,\n                USER_AVATAR_GROUP_NAME,\n                UUID.randomUUID() + \".\" + ext,\n                maxSizeCheck(filePart.content()),\n                filePart.headers().getContentType()\n            );\n        });\n    }\n\n    private Flux<DataBuffer> maxSizeCheck(Flux<DataBuffer> content) {\n        var lenRef = new AtomicInteger(0);\n        return content.doOnNext(dataBuffer -> {\n            int len = lenRef.accumulateAndGet(dataBuffer.readableByteCount(), Integer::sum);\n            if (len > MAX_AVATAR_FILE_SIZE.toBytes()) {\n                throw new ServerWebInputException(\"The avatar file needs to be smaller than \"\n                    + MAX_AVATAR_FILE_SIZE.toMegabytes() + \" MB.\");\n            }\n        });\n    }\n\n    private Mono<ServerResponse> createUser(ServerRequest request) {\n        return request.bodyToMono(CreateUserRequest.class)\n            .doOnNext(createUserRequest -> {\n                if (StringUtils.isBlank(createUserRequest.name())) {\n                    throw new ServerWebInputException(\"Name is required\");\n                }\n                if (StringUtils.isBlank(createUserRequest.email())) {\n                    throw new ServerWebInputException(\"Email is required\");\n                }\n            })\n            .flatMap(userRequest -> {\n                User newUser = CreateUserRequest.from(userRequest);\n                var encryptedPwd = userService.encryptPassword(userRequest.password());\n                newUser.getSpec().setPassword(encryptedPwd);\n                return userService.createUser(newUser, userRequest.roles());\n            })\n            .flatMap(user -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(user)\n            );\n    }\n\n    private Mono<ServerResponse> getUserByName(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return userService.getUser(name)\n            .flatMap(user -> roleService.getRolesByUsername(name)\n                .collectList()\n                .flatMap(roleNames -> roleService.list(new HashSet<>(roleNames), true)\n                    .collectList()\n                    .map(roles -> new DetailedUser(user, roles))\n                )\n            )\n            .flatMap(detailedUser -> ServerResponse.ok().bodyValue(detailedUser));\n    }\n\n    record CreateUserRequest(@Schema(requiredMode = REQUIRED) String name,\n                             @Schema(requiredMode = REQUIRED) String email,\n                             String displayName,\n                             String avatar,\n                             String phone,\n                             String password,\n                             String bio,\n                             Map<String, String> annotations,\n                             Set<String> roles) {\n\n        /**\n         * <p>Creates a new user from {@link CreateUserRequest}.</p>\n         * Note: this method will not set password.\n         *\n         * @param userRequest user request\n         * @return user from request\n         */\n        public static User from(CreateUserRequest userRequest) {\n            var user = new User();\n            user.setMetadata(new Metadata());\n            user.getMetadata().setName(userRequest.name());\n            user.getMetadata().setAnnotations(new HashMap<>());\n            Map<String, String> annotations =\n                defaultIfNull(userRequest.annotations(), Map.of());\n            user.getMetadata().getAnnotations().putAll(annotations);\n\n            var spec = new User.UserSpec();\n            user.setSpec(spec);\n            spec.setEmail(userRequest.email());\n            spec.setDisplayName(defaultIfBlank(userRequest.displayName(), userRequest.name()));\n            spec.setAvatar(userRequest.avatar());\n            spec.setPhone(userRequest.phone());\n            spec.setBio(userRequest.bio());\n            return user;\n        }\n    }\n\n    private Mono<ServerResponse> updateProfile(ServerRequest request) {\n        return getAuthenticatedUserName()\n            .flatMap(currentUserName -> client.get(User.class, currentUserName))\n            .flatMap(currentUser -> request.bodyToMono(User.class)\n                .filter(user -> user.getMetadata() != null\n                    && Objects.equals(user.getMetadata().getName(),\n                    currentUser.getMetadata().getName())\n                )\n                .switchIfEmpty(\n                    Mono.error(() -> new ServerWebInputException(\"Username didn't match.\")))\n                .flatMap(user -> {\n                    var newDisplayName = user.getSpec().getDisplayName();\n                    var oldDisplayName = currentUser.getSpec().getDisplayName();\n                    return Mono.just(user)\n                        .filterWhen(u -> {\n                            if (Objects.equals(oldDisplayName, newDisplayName)) {\n                                return Mono.just(true);\n                            }\n                            return environmentFetcher.fetch(SystemSetting.User.GROUP,\n                                    SystemSetting.User.class)\n                                .map(setting -> isDisplayNameAllowed(setting, newDisplayName))\n                                .defaultIfEmpty(false);\n                        })\n                        .switchIfEmpty(Mono.defer(() -> Mono.error(new RestrictedNameException(\n                            \"The display name is restricted.\",\n                            \"problemDetail.user.displayName.restricted\",\n                            new Object[] {newDisplayName}\n                        ))));\n                })\n                .map(user -> {\n                    Map<String, String> oldAnnotations =\n                        MetadataUtil.nullSafeAnnotations(currentUser);\n                    Map<String, String> newAnnotations = user.getMetadata().getAnnotations();\n                    if (!CollectionUtils.isEmpty(newAnnotations)) {\n                        newAnnotations.put(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO,\n                            oldAnnotations.get(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO));\n                        newAnnotations.put(User.AVATAR_ATTACHMENT_NAME_ANNO,\n                            oldAnnotations.get(User.AVATAR_ATTACHMENT_NAME_ANNO));\n                        newAnnotations.put(User.EMAIL_TO_VERIFY,\n                            oldAnnotations.get(User.EMAIL_TO_VERIFY));\n                        currentUser.getMetadata().setAnnotations(newAnnotations);\n                    }\n                    var spec = currentUser.getSpec();\n                    var newSpec = user.getSpec();\n                    spec.setBio(newSpec.getBio());\n                    spec.setDisplayName(newSpec.getDisplayName());\n                    spec.setTwoFactorAuthEnabled(newSpec.getTwoFactorAuthEnabled());\n                    spec.setPhone(newSpec.getPhone());\n                    return currentUser;\n                })\n            )\n            .flatMap(client::update)\n            .flatMap(updatedUser -> ServerResponse.ok().bodyValue(updatedUser));\n    }\n\n    private static Mono<String> getAuthenticatedUserName() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Authentication::getName);\n    }\n\n    Mono<ServerResponse> changeAnyonePasswordForAdmin(ServerRequest request) {\n        final var nameInPath = request.pathVariable(\"name\");\n        return ReactiveSecurityContextHolder.getContext()\n            .map(ctx -> SELF_USER.equals(nameInPath) ? ctx.getAuthentication().getName()\n                : nameInPath)\n            .flatMap(username -> request.bodyToMono(ChangePasswordRequest.class)\n                .switchIfEmpty(Mono.defer(() ->\n                    Mono.error(new ServerWebInputException(\"Request body is empty\"))))\n                .flatMap(changePasswordRequest -> {\n                    var password = changePasswordRequest.password();\n                    // encode password\n                    return userService.updateWithRawPassword(username, password);\n                }))\n            .flatMap(updatedUser -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(updatedUser));\n    }\n\n    Mono<ServerResponse> changeOwnPassword(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(ctx -> ctx.getAuthentication().getName())\n            .flatMap(username -> request.bodyToMono(ChangeOwnPasswordRequest.class)\n                .switchIfEmpty(Mono.defer(() ->\n                    Mono.error(new ServerWebInputException(\"Request body is empty\"))))\n                .flatMap(changePasswordRequest -> {\n                    var rawOldPassword = changePasswordRequest.oldPassword();\n                    return userService.confirmPassword(username, rawOldPassword)\n                        .filter(Boolean::booleanValue)\n                        .switchIfEmpty(Mono.error(new UnsatisfiedAttributeValueException(\n                            \"Old password is incorrect.\",\n                            \"problemDetail.user.oldPassword.notMatch\",\n                            null))\n                        )\n                        .thenReturn(changePasswordRequest);\n                })\n                .flatMap(changePasswordRequest -> {\n                    var password = changePasswordRequest.password();\n                    // encode password\n                    return userService.updateWithRawPassword(username, password);\n                }))\n            .flatMap(updatedUser -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(updatedUser));\n    }\n\n    record ChangeOwnPasswordRequest(\n        @Schema(description = \"Old password.\", requiredMode = REQUIRED)\n        String oldPassword,\n        @Schema(description = \"New password.\", requiredMode = REQUIRED, minLength = 5)\n        String password) {\n\n        public ChangeOwnPasswordRequest {\n            if (password == null || password.length() < 5 || password.length() > 257) {\n                throw new UnsatisfiedAttributeValueException(\n                    \"password is required.\",\n                    \"validation.error.password.size\",\n                    new Object[] {5, 257});\n            }\n        }\n    }\n\n    record ChangePasswordRequest(\n        @Schema(description = \"New password.\", requiredMode = REQUIRED, minLength = 5)\n        String password) {\n    }\n\n    @NonNull\n    Mono<ServerResponse> me(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .filter(Authentication::isAuthenticated)\n            .flatMap(auth -> userService.getUser(auth.getName())\n                .flatMap(user -> {\n                    var roleNames = authoritiesToRoles(auth.getAuthorities());\n                    return roleService.list(roleNames, true)\n                        .collectList()\n                        .map(roles -> new DetailedUser(user, roles));\n                })\n            )\n            .flatMap(detailedUser -> ServerResponse.ok().bodyValue(detailedUser));\n    }\n\n    record DetailedUser(@Schema(requiredMode = REQUIRED) User user,\n                        @Schema(requiredMode = REQUIRED) List<Role> roles) {\n\n    }\n\n    @NonNull\n    Mono<ServerResponse> grantPermission(ServerRequest request) {\n        var username = request.pathVariable(\"name\");\n        return request.bodyToMono(GrantRequest.class)\n            .switchIfEmpty(\n                Mono.error(() -> new ServerWebInputException(\"Request body is empty\")))\n            .flatMap(grantRequest -> userService.grantRoles(username, grantRequest.roles())\n                .then(ServerResponse.ok().build()));\n    }\n\n    record GrantRequest(Set<String> roles) {\n    }\n\n    @NonNull\n    private Mono<ServerResponse> getUserPermission(ServerRequest request) {\n        var username = request.pathVariable(\"name\");\n        return Mono.defer(() -> {\n            if (SELF_USER.equals(username)) {\n                return ReactiveSecurityContextHolder.getContext()\n                    .map(SecurityContext::getAuthentication)\n                    .map(auth -> authoritiesToRoles(auth.getAuthorities()));\n            }\n            return roleService.getRolesByUsername(username)\n                .collect(Collectors.toCollection(LinkedHashSet::new));\n        }).flatMap(roleNames -> {\n            var up = new UserPermission();\n            var setRoles = roleService.list(roleNames, true)\n                .distinct()\n                .collectSortedList()\n                .doOnNext(up::setRoles);\n            var setPerms = roleService.listPermissions(roleNames)\n                .distinct()\n                .collectSortedList()\n                .doOnNext(permissions -> {\n                    up.setPermissions(permissions);\n                    up.setUiPermissions(uiPermissions(permissions));\n                });\n            return Mono.when(setRoles, setPerms).thenReturn(up);\n        }).flatMap(userPermission -> ServerResponse.ok().bodyValue(userPermission));\n    }\n\n    private List<String> uiPermissions(Collection<Role> roles) {\n        if (CollectionUtils.isEmpty(roles)) {\n            return List.of();\n        }\n        var uiPerms = new LinkedList<String>();\n        roles.forEach(role -> Optional.ofNullable(role.getMetadata().getAnnotations())\n            .map(annotations -> annotations.get(Role.UI_PERMISSIONS_ANNO))\n            .filter(StringUtils::isNotBlank)\n            .map(json -> JsonUtils.jsonToObject(json, new TypeReference<Set<String>>() {\n            }))\n            .ifPresent(uiPerms::addAll)\n        );\n        return uiPerms.stream().distinct().sorted().toList();\n    }\n\n    @Data\n    public static class UserPermission {\n        @Schema(requiredMode = REQUIRED)\n        private List<Role> roles;\n\n        @Schema(requiredMode = REQUIRED)\n        private List<Role> permissions;\n\n        @Schema(requiredMode = REQUIRED)\n        private List<String> uiPermissions;\n\n    }\n\n    public static class ListRequest extends SortableRequest {\n\n        public ListRequest(ServerRequest request) {\n            super(request.exchange());\n        }\n\n        @Schema(name = \"keyword\")\n        public String getKeyword() {\n            return queryParams.getFirst(\"keyword\");\n        }\n\n        @Schema(name = \"role\")\n        public String getRole() {\n            return queryParams.getFirst(\"role\");\n        }\n\n        /**\n         * Converts query parameters to list options.\n         */\n        public ListOptions toListOptions() {\n            var defaultListOptions =\n                labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());\n\n            var builder = ListOptions.builder(defaultListOptions);\n\n            Optional.ofNullable(getKeyword())\n                .filter(StringUtils::isNotBlank)\n                .ifPresent(keyword -> builder.andQuery(or(\n                    equal(\"spec.email\", keyword),\n                    contains(\"spec.displayName\", keyword),\n                    equal(\"metadata.name\", keyword)\n                )));\n\n            Optional.ofNullable(getRole())\n                .filter(StringUtils::isNotBlank)\n                .ifPresent(role -> builder.andQuery(in(User.USER_RELATED_ROLES_INDEX, role)));\n\n            return builder.build();\n        }\n\n        public static void buildParameters(Builder builder) {\n            SortableRequest.buildParameters(builder);\n            builder.parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"keyword\")\n                    .description(\"Keyword to search\")\n                    .implementation(String.class)\n                    .required(false))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"role\")\n                    .description(\"Role name\")\n                    .implementation(String.class)\n                    .required(false));\n        }\n\n    }\n\n    record ListedUser(@Schema(requiredMode = REQUIRED) User user,\n                      @Schema(requiredMode = REQUIRED) List<Role> roles) {\n    }\n\n    Mono<ServerResponse> list(ServerRequest request) {\n        return Mono.just(request)\n            .map(UserEndpoint.ListRequest::new)\n            .flatMap(listRequest -> client.listBy(User.class, listRequest.toListOptions(),\n                PageRequestImpl.of(\n                    listRequest.getPage(), listRequest.getSize(), listRequest.getSort()\n                )\n            ))\n            .flatMap(this::toListedUser)\n            .flatMap(listResult -> ServerResponse.ok().bodyValue(listResult));\n    }\n\n    private Mono<ListResult<ListedUser>> toListedUser(ListResult<User> listResult) {\n        var usernames = listResult.getItems().stream()\n            .map(user -> user.getMetadata().getName())\n            .collect(Collectors.toList());\n        return roleService.getRolesByUsernames(usernames)\n            .flatMap(usernameRolesMap -> {\n                var allRoleNames = new HashSet<String>();\n                usernameRolesMap.values().forEach(allRoleNames::addAll);\n                return roleService.list(allRoleNames)\n                    .collectMap(role -> role.getMetadata().getName())\n                    .map(roleMap -> {\n                        var listedUsers = listResult.getItems().stream()\n                            .map(user -> {\n                                var username = user.getMetadata().getName();\n                                var roles = Optional.ofNullable(usernameRolesMap.get(username))\n                                    .map(roleNames -> roleNames.stream()\n                                        .map(roleMap::get)\n                                        .filter(Objects::nonNull)\n                                        .toList()\n                                    )\n                                    .orElseGet(List::of);\n                                return new ListedUser(user, roles);\n                            })\n                            .toList();\n                        return convertFrom(listResult, listedUsers);\n                    });\n            });\n    }\n\n    <T> ListResult<T> convertFrom(ListResult<?> listResult, List<T> items) {\n        Assert.notNull(listResult, \"listResult must not be null\");\n        Assert.notNull(items, \"items must not be null\");\n        return new ListResult<>(listResult.getPage(), listResult.getSize(),\n            listResult.getTotal(), items);\n    }\n\n    private boolean isDisplayNameAllowed(SystemSetting.User setting, String displayName) {\n        String protectedUsernamesStr = setting.getProtectedUsernames();\n        if (protectedUsernamesStr == null || protectedUsernamesStr.trim().isEmpty()) {\n            return true;\n        }\n        Set<String> protectedLowerSet = Arrays.stream(protectedUsernamesStr.split(\",\"))\n            .map(String::trim)\n            .filter(n -> !n.isEmpty())\n            .map(String::toLowerCase)\n            .collect(Collectors.toUnmodifiableSet());\n        return !protectedLowerSet.contains(displayName.trim().toLowerCase());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/CategoryQueryEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static run.halo.app.core.endpoint.theme.PublicApiUtils.toAnotherListResult;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.SortableRequest;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.vo.CategoryVo;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\n\n/**\n * Endpoint for category query APIs.\n *\n * @author guqing\n * @since 2.5.0\n */\n@Component\n@RequiredArgsConstructor\npublic class CategoryQueryEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n    private final PostPublicQueryService postPublicQueryService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"CategoryV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"categories\", this::listCategories,\n                builder -> {\n                    builder.operationId(\"queryCategories\")\n                        .description(\"Lists categories.\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(CategoryVo.class))\n                        );\n                    CategoryPublicQuery.buildParameters(builder);\n                }\n            )\n            .GET(\"categories/{name}\", this::getByName,\n                builder -> builder.operationId(\"queryCategoryByName\")\n                    .description(\"Gets category by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Category name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(CategoryVo.class)\n                    )\n            )\n            .GET(\"categories/{name}/posts\", this::listPostsByCategoryName,\n                builder -> {\n                    builder.operationId(\"queryPostsByCategoryName\")\n                        .description(\"Lists posts by category name.\")\n                        .tag(tag)\n                        .parameter(parameterBuilder()\n                            .in(ParameterIn.PATH)\n                            .name(\"name\")\n                            .description(\"Category name\")\n                            .required(true)\n                        )\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(ListedPostVo.class))\n                        );\n                    PostPublicQuery.buildParameters(builder);\n                }\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> listPostsByCategoryName(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        final var query = new PostPublicQuery(request.exchange());\n        var listOptions = query.toListOptions();\n        var newFieldSelector = listOptions.getFieldSelector()\n            .andQuery(Queries.equal(\"spec.categories\", name));\n        listOptions.setFieldSelector(newFieldSelector);\n        return postPublicQueryService.list(listOptions, query.toPageRequest())\n            .flatMap(result -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(result)\n            );\n    }\n\n    private Mono<ServerResponse> getByName(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return client.get(Category.class, name)\n            .map(CategoryVo::from)\n            .flatMap(categoryVo -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(categoryVo)\n            );\n    }\n\n    private Mono<ServerResponse> listCategories(ServerRequest request) {\n        CategoryPublicQuery query = new CategoryPublicQuery(request.exchange());\n        return client.listBy(Category.class, query.toListOptions(), query.toPageRequest())\n            .map(listResult -> toAnotherListResult(listResult, CategoryVo::from))\n            .flatMap(result -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(result)\n            );\n    }\n\n    public static class CategoryPublicQuery extends SortableRequest {\n        public CategoryPublicQuery(ServerWebExchange exchange) {\n            super(exchange);\n        }\n\n        public static void buildParameters(Builder builder) {\n            SortableRequest.buildParameters(builder);\n        }\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return PublicApiUtils.groupVersion(new Category());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/CommentFinderEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.apache.commons.lang3.BooleanUtils.isFalse;\nimport static org.apache.commons.lang3.BooleanUtils.isTrue;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport io.github.resilience4j.ratelimiter.RequestNotPermitted;\nimport io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.ArraySchema;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.List;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.content.comment.CommentRequest;\nimport run.halo.app.content.comment.CommentService;\nimport run.halo.app.content.comment.ReplyRequest;\nimport run.halo.app.content.comment.ReplyService;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.extension.endpoint.SortResolver;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.exception.AccessDeniedException;\nimport run.halo.app.infra.exception.RateLimitExceededException;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.IpAddressUtils;\nimport run.halo.app.theme.finders.CommentFinder;\nimport run.halo.app.theme.finders.CommentPublicQueryService;\nimport run.halo.app.theme.finders.vo.CommentVo;\nimport run.halo.app.theme.finders.vo.CommentWithReplyVo;\nimport run.halo.app.theme.finders.vo.ReplyVo;\n\n/**\n * Endpoint for {@link CommentFinder}.\n */\n@Component\n@RequiredArgsConstructor\npublic class CommentFinderEndpoint implements CustomEndpoint {\n\n    private final CommentPublicQueryService commentPublicQueryService;\n    private final CommentService commentService;\n    private final ReplyService replyService;\n    private final SystemConfigFetcher environmentFetcher;\n    private final RateLimiterRegistry rateLimiterRegistry;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"CommentV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"comments\", this::createComment,\n                builder -> builder.operationId(\"CreateComment_1\")\n                    .description(\"Create a comment.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(CommentRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Comment.class))\n            )\n            .POST(\"comments/{name}/reply\", this::createReply,\n                builder -> builder.operationId(\"CreateReply_1\")\n                    .description(\"Create a reply.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(ReplyRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Reply.class))\n            )\n            .GET(\"comments\", this::listComments, builder -> {\n                builder.operationId(\"ListComments_1\")\n                    .description(\"List comments.\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .implementation(ListResult.generateGenericClass(CommentWithReplyVo.class))\n                    );\n                CommentQuery.buildParameters(builder);\n            })\n            .GET(\"comments/{name}\", this::getComment, builder -> {\n                builder.operationId(\"GetComment\")\n                    .description(\"Get a comment.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementation(ListResult.generateGenericClass(CommentVo.class))\n                    );\n            })\n            .GET(\"comments/{name}/reply\", this::listCommentReplies, builder -> {\n                builder.operationId(\"ListCommentReplies\")\n                    .description(\"List comment replies.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class))\n                    .response(responseBuilder()\n                        .implementation(ListResult.generateGenericClass(ReplyVo.class))\n                    );\n                PageableRequest.buildParameters(builder);\n            })\n            .build();\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"api.halo.run/v1alpha1\");\n    }\n\n    Mono<ServerResponse> createComment(ServerRequest request) {\n        return request.bodyToMono(CommentRequest.class)\n            .flatMap(commentRequest -> {\n                Comment comment = commentRequest.toComment();\n                comment.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));\n                comment.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));\n                return commentService.create(comment);\n            })\n            .flatMap(comment -> ServerResponse.ok().bodyValue(comment))\n            .transformDeferred(createIpBasedRateLimiter(request))\n            .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);\n    }\n\n    private <T> RateLimiterOperator<T> createIpBasedRateLimiter(ServerRequest request) {\n        var clientIp = IpAddressUtils.getIpAddress(request);\n        var rateLimiter = rateLimiterRegistry.rateLimiter(\"comment-creation-from-ip-\" + clientIp,\n            \"comment-creation\");\n        return RateLimiterOperator.of(rateLimiter);\n    }\n\n    Mono<ServerResponse> createReply(ServerRequest request) {\n        String commentName = request.pathVariable(\"name\");\n        return request.bodyToMono(ReplyRequest.class)\n            .flatMap(replyRequest -> {\n                Reply reply = replyRequest.toReply();\n                reply.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));\n                reply.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));\n                return environmentFetcher.fetchComment()\n                    .map(commentSetting -> {\n                        if (isFalse(commentSetting.getEnable())) {\n                            throw new AccessDeniedException(\n                                \"The comment function has been turned off.\",\n                                \"problemDetail.comment.turnedOff\", null);\n                        }\n                        if (checkReplyOwner(reply, commentSetting.getSystemUserOnly())) {\n                            throw new AccessDeniedException(\"Allow only system users to comment.\",\n                                \"problemDetail.comment.systemUsersOnly\", null);\n                        }\n                        reply.getSpec()\n                            .setApproved(isFalse(commentSetting.getRequireReviewForNew()));\n\n                        if (reply.getSpec().getHidden() == null) {\n                            reply.getSpec().setHidden(false);\n                        }\n\n                        return reply;\n                    })\n                    .defaultIfEmpty(reply);\n            })\n            .flatMap(reply -> replyService.create(commentName, reply))\n            .flatMap(comment -> ServerResponse.ok().bodyValue(comment))\n            .transformDeferred(createIpBasedRateLimiter(request))\n            .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);\n    }\n\n    private boolean checkReplyOwner(Reply reply, Boolean onlySystemUser) {\n        Comment.CommentOwner owner = reply.getSpec().getOwner();\n        if (isTrue(onlySystemUser)) {\n            return owner != null && Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind());\n        }\n        return false;\n    }\n\n    Mono<ServerResponse> listComments(ServerRequest request) {\n        CommentQuery commentQuery = new CommentQuery(request);\n        return commentPublicQueryService.list(commentQuery.toRef(), commentQuery.toPageRequest())\n            .flatMap(result -> {\n                if (commentQuery.getWithReplies()) {\n                    return commentPublicQueryService.convertToWithReplyVo(result,\n                        commentQuery.getReplySize());\n                }\n                return Mono.just(result);\n            })\n            .flatMap(list -> ServerResponse.ok().bodyValue(list));\n    }\n\n    Mono<ServerResponse> getComment(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return Mono.defer(() -> Mono.justOrEmpty(commentPublicQueryService.getByName(name)))\n            .subscribeOn(Schedulers.boundedElastic())\n            .flatMap(comment -> ServerResponse.ok().bodyValue(comment));\n    }\n\n    Mono<ServerResponse> listCommentReplies(ServerRequest request) {\n        String commentName = request.pathVariable(\"name\");\n        IListRequest.QueryListRequest queryParams =\n            new IListRequest.QueryListRequest(request.queryParams());\n        return commentPublicQueryService.listReply(commentName, queryParams.getPage(),\n                queryParams.getSize())\n            .flatMap(list -> ServerResponse.ok().bodyValue(list));\n    }\n\n    public static class CommentQuery extends PageableRequest {\n\n        private final ServerWebExchange exchange;\n\n        public CommentQuery(ServerRequest request) {\n            super(request.queryParams());\n            this.exchange = request.exchange();\n        }\n\n        @Schema(description = \"The comment subject group.\")\n        public String getGroup() {\n            return queryParams.getFirst(\"group\");\n        }\n\n        @Schema(requiredMode = REQUIRED, description = \"The comment subject version.\")\n        public String getVersion() {\n            return emptyToNull(queryParams.getFirst(\"version\"));\n        }\n\n        /**\n         * Gets the {@link Ref}s kind.\n         *\n         * @return comment subject ref kind\n         */\n        @Schema(requiredMode = REQUIRED, description = \"The comment subject kind.\")\n        public String getKind() {\n            String kind = emptyToNull(queryParams.getFirst(\"kind\"));\n            if (kind == null) {\n                throw new ServerWebInputException(\"The kind must not be null.\");\n            }\n            return kind;\n        }\n\n        /**\n         * Gets the {@link Ref}s name.\n         *\n         * @return comment subject ref name\n         */\n        @Schema(requiredMode = REQUIRED, description = \"The comment subject name.\")\n        public String getName() {\n            String name = emptyToNull(queryParams.getFirst(\"name\"));\n            if (name == null) {\n                throw new ServerWebInputException(\"The name must not be null.\");\n            }\n            return name;\n        }\n\n        @Schema(description = \"Whether to include replies. Default is false.\",\n            defaultValue = \"false\")\n        public Boolean getWithReplies() {\n            var withReplies = queryParams.getFirst(\"withReplies\");\n            return StringUtils.isNotBlank(withReplies) && Boolean.parseBoolean(withReplies);\n        }\n\n        @Schema(description = \"Reply size of the comment, default is 10, only works when \"\n            + \"withReplies is true.\", defaultValue = \"10\")\n        public int getReplySize() {\n            var replySize = queryParams.getFirst(\"replySize\");\n            return StringUtils.isNotBlank(replySize) ? Integer.parseInt(replySize) : 10;\n        }\n\n        @ArraySchema(uniqueItems = true,\n            arraySchema = @Schema(name = \"sort\",\n                description = \"Sort property and direction of the list result. Supported fields: \"\n                    + \"creationTimestamp\"),\n            schema = @Schema(description = \"like field,asc or field,desc\",\n                implementation = String.class,\n                example = \"creationTimestamp,desc\"))\n        public Sort getSort() {\n            return SortResolver.defaultInstance.resolve(exchange);\n        }\n\n        Ref toRef() {\n            Ref ref = new Ref();\n            ref.setGroup(getGroup());\n            ref.setKind(getKind());\n            ref.setVersion(getVersion());\n            ref.setName(getName());\n            return ref;\n        }\n\n        public PageRequest toPageRequest() {\n            return PageRequestImpl.of(getPage(), getSize(), getSort());\n        }\n\n        String emptyToNull(String str) {\n            return StringUtils.isBlank(str) ? null : str;\n        }\n\n        public static void buildParameters(Builder builder) {\n            PageableRequest.buildParameters(builder);\n            builder.parameter(sortParameter())\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"group\")\n                    .description(\"The comment subject group.\")\n                    .required(false)\n                    .implementation(String.class))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"version\")\n                    .description(\"The comment subject version.\")\n                    .required(true)\n                    .implementation(String.class))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"kind\")\n                    .description(\"The comment subject kind.\")\n                    .required(true))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"name\")\n                    .description(\"The comment subject name.\")\n                    .required(true)\n                    .implementation(String.class))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"withReplies\")\n                    .description(\"Whether to include replies. Default is false.\")\n                    .required(false)\n                    .implementation(Boolean.class))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"replySize\")\n                    .description(\"Reply size of the comment, default is 10, only works when \"\n                        + \"withReplies is true.\")\n                    .required(false)\n                    .schema(schemaBuilder()\n                        .implementation(Integer.class)\n                        .defaultValue(\"10\")));\n        }\n    }\n\n    public static class PageableRequest extends IListRequest.QueryListRequest {\n\n        public PageableRequest(MultiValueMap<String, String> queryParams) {\n            super(queryParams);\n        }\n\n        @Override\n        @JsonIgnore\n        public List<String> getLabelSelector() {\n            throw new UnsupportedOperationException(\"Unsupported this parameter\");\n        }\n\n        @Override\n        @JsonIgnore\n        public List<String> getFieldSelector() {\n            throw new UnsupportedOperationException(\"Unsupported this parameter\");\n        }\n\n        public static void buildParameters(Builder builder) {\n            builder.parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"page\")\n                    .implementation(Integer.class)\n                    .required(false)\n                    .description(\"Page number. Default is 0.\"))\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.QUERY)\n                    .name(\"size\")\n                    .implementation(Integer.class)\n                    .required(false)\n                    .description(\"Size number. Default is 0.\"));\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/MenuQueryEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Menu;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.theme.finders.MenuFinder;\nimport run.halo.app.theme.finders.vo.MenuVo;\n\n/**\n * Endpoint for menu query APIs.\n *\n * @author guqing\n * @since 2.5.0\n */\n@Component\n@RequiredArgsConstructor\npublic class MenuQueryEndpoint implements CustomEndpoint {\n\n    private final MenuFinder menuFinder;\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"MenuV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"menus/-\", this::getByName,\n                builder -> builder.operationId(\"queryPrimaryMenu\")\n                    .description(\"Gets primary menu.\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .implementation(MenuVo.class)\n                    )\n            )\n            .GET(\"menus/{name}\", this::getByName,\n                builder -> builder.operationId(\"queryMenuByName\")\n                    .description(\"Gets menu by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Menu name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(MenuVo.class)\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> getByName(ServerRequest request) {\n        return determineMenuName(request)\n            .flatMap(menuFinder::getByName)\n            .flatMap(menuVo -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(menuVo)\n            );\n    }\n\n    private Mono<String> determineMenuName(ServerRequest request) {\n        String name = request.pathVariables().getOrDefault(\"name\", \"-\");\n        if (!\"-\".equals(name)) {\n            return Mono.just(name);\n        }\n        // If name is \"-\", then get primary menu.\n        return environmentFetcher.fetch(SystemSetting.Menu.GROUP, SystemSetting.Menu.class)\n            .mapNotNull(SystemSetting.Menu::getPrimary)\n            .switchIfEmpty(\n                Mono.error(() -> new ServerWebInputException(\"Primary menu is not configured.\"))\n            );\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return PublicApiUtils.groupVersion(new Menu());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/PluginQueryEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.theme.finders.PluginFinder;\n\n/**\n * Endpoint for plugin query APIs.\n *\n * @author guqing\n * @since 2.5.0\n */\n@Component\n@RequiredArgsConstructor\npublic class PluginQueryEndpoint implements CustomEndpoint {\n\n    private final PluginFinder pluginFinder;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"PluginV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"plugins/{name}/available\", this::availableByName,\n                builder -> builder.operationId(\"queryPluginAvailableByName\")\n                    .description(\"Gets plugin available by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Plugin name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(Boolean.class)\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> availableByName(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        boolean available = pluginFinder.available(name);\n        return ServerResponse.ok().bodyValue(available);\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return PublicApiUtils.groupVersion(new Plugin());\n    }\n}"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/PostPublicQuery.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springframework.web.server.ServerWebExchange;\nimport run.halo.app.extension.router.SortableRequest;\n\n/**\n * Query parameters for post public APIs.\n *\n * @author guqing\n * @since 2.5.0\n */\npublic class PostPublicQuery extends SortableRequest {\n\n    public PostPublicQuery(ServerWebExchange exchange) {\n        super(exchange);\n    }\n\n    public static void buildParameters(Builder builder) {\n        SortableRequest.buildParameters(builder);\n    }\n}"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/PostQueryEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.NavigationPostVo;\nimport run.halo.app.theme.finders.vo.PostVo;\n\n/**\n * Endpoint for post query.\n *\n * @author guqing\n * @since 2.5.0\n */\n@Component\n@RequiredArgsConstructor\npublic class PostQueryEndpoint implements CustomEndpoint {\n\n    private final PostFinder postFinder;\n    private final PostPublicQueryService postPublicQueryService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"PostV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"posts\", this::listPosts,\n                builder -> {\n                    builder.operationId(\"queryPosts\")\n                        .description(\"Lists posts.\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(ListedPostVo.class))\n                        );\n                    PostPublicQuery.buildParameters(builder);\n                }\n            )\n            .GET(\"posts/{name}\", this::getPostByName,\n                builder -> builder.operationId(\"queryPostByName\")\n                    .description(\"Gets a post by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Post name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(PostVo.class)\n                    )\n            )\n            .GET(\"posts/{name}/navigation\", this::getPostNavigationByName,\n                builder -> builder.operationId(\"queryPostNavigationByName\")\n                    .description(\"Gets a post navigation by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Post name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(NavigationPostVo.class)\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> getPostNavigationByName(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return postFinder.cursor(name)\n            .flatMap(result -> ServerResponse.ok().bodyValue(result));\n    }\n\n    private Mono<ServerResponse> getPostByName(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return postFinder.getByName(name)\n            .switchIfEmpty(Mono.error(() -> new NotFoundException(\"Post not found\")))\n            .flatMap(post -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(post)\n            );\n    }\n\n    private Mono<ServerResponse> listPosts(ServerRequest request) {\n        PostPublicQuery query = new PostPublicQuery(request.exchange());\n        return postPublicQueryService.list(query.toListOptions(), query.toPageRequest())\n            .flatMap(result -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(result)\n            );\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return PublicApiUtils.groupVersion(new Post());\n    }\n}"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/PublicApiUtils.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.function.Function;\nimport lombok.experimental.UtilityClass;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.ListResult;\n\n/**\n * Utility class for public api.\n *\n * @author guqing\n * @since 2.5.0\n */\n@UtilityClass\npublic class PublicApiUtils {\n\n    /**\n     * Get group version from extension for public api.\n     *\n     * @param extension extension\n     * @return <code>api.{group}/{version}</code> if group is not empty,\n     * otherwise <code>api.halo.run/{version}</code>.\n     */\n    public static GroupVersion groupVersion(Extension extension) {\n        GroupVersionKind groupVersionKind = extension.groupVersionKind();\n        String group = StringUtils.defaultIfBlank(groupVersionKind.group(), \"halo.run\");\n        return new GroupVersion(\"api.\" + group, groupVersionKind.version());\n    }\n\n    /**\n     * Converts list result to another list result.\n     *\n     * @param listResult list result to be converted\n     * @param mapper mapper function to convert item\n     * @param <T> item type\n     * @param <R> converted item type\n     * @return converted list result\n     */\n    public static <T, R> ListResult<R> toAnotherListResult(ListResult<T> listResult,\n        Function<T, R> mapper) {\n        Assert.notNull(listResult, \"List result must not be null\");\n        Assert.notNull(mapper, \"The mapper must not be null\");\n        List<R> mappedItems = listResult.get()\n            .map(mapper)\n            .toList();\n        return new ListResult<>(listResult.getPage(), listResult.getSize(), listResult.getTotal(),\n            mappedItems);\n    }\n\n    /**\n     * Checks whether collection contains element.\n     *\n     * @param <T> element type\n     * @return true if collection contains element, otherwise false.\n     */\n    public static <T> boolean containsElement(@Nullable Collection<T> collection,\n        @Nullable T element) {\n        if (collection != null && element != null) {\n            return collection.contains(element);\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/SinglePageQueryEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.router.SortableRequest;\nimport run.halo.app.theme.finders.SinglePageConversionService;\nimport run.halo.app.theme.finders.SinglePageFinder;\nimport run.halo.app.theme.finders.vo.ListedSinglePageVo;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * Endpoint for single page query.\n *\n * @author guqing\n * @since 2.5.0\n */\n@Component\n@RequiredArgsConstructor\npublic class SinglePageQueryEndpoint implements CustomEndpoint {\n\n    private final SinglePageFinder singlePageFinder;\n\n    private final SinglePageConversionService singlePageConversionService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"SinglePageV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"singlepages\", this::listSinglePages,\n                builder -> {\n                    builder.operationId(\"querySinglePages\")\n                        .description(\"Lists single pages\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(\n                                ListResult.generateGenericClass(ListedSinglePageVo.class))\n                        );\n                    SinglePagePublicQuery.buildParameters(builder);\n                }\n            )\n            .GET(\"singlepages/{name}\", this::getByName,\n                builder -> builder.operationId(\"querySinglePageByName\")\n                    .description(\"Gets single page by name\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"SinglePage name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(SinglePageVo.class)\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> getByName(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return singlePageFinder.getByName(name)\n            .flatMap(result -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(result)\n            );\n    }\n\n    private Mono<ServerResponse> listSinglePages(ServerRequest request) {\n        var query = new SinglePagePublicQuery(request.exchange());\n        return singlePageConversionService.listBy(query.toListOptions(), query.toPageRequest())\n            .flatMap(result -> ServerResponse.ok().bodyValue(result));\n    }\n\n    static class SinglePagePublicQuery extends SortableRequest {\n\n        public SinglePagePublicQuery(ServerWebExchange exchange) {\n            super(exchange);\n        }\n\n        public static void buildParameters(Builder builder) {\n            SortableRequest.buildParameters(builder);\n        }\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return PublicApiUtils.groupVersion(new SinglePage());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/SiteStatsQueryEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.theme.finders.SiteStatsFinder;\nimport run.halo.app.theme.finders.vo.SiteStatsVo;\n\n/**\n * Endpoint for site stats query APIs.\n *\n * @author guqing\n * @since 2.5.0\n */\n@Component\n@RequiredArgsConstructor\npublic class SiteStatsQueryEndpoint implements CustomEndpoint {\n\n    private final SiteStatsFinder siteStatsFinder;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"SystemV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"stats/-\", this::getStats,\n                builder -> builder.operationId(\"queryStats\")\n                    .description(\"Gets site stats\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .implementation(SiteStatsVo.class)\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> getStats(ServerRequest request) {\n        return siteStatsFinder.getStats()\n            .flatMap(result -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(result)\n            );\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return new GroupVersion(\"api.halo.run\", \"v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/TagQueryEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.SortableRequest;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.TagFinder;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.TagVo;\n\n/**\n * Endpoint for tag query APIs.\n *\n * @author guqing\n * @since 2.5.0\n */\n@Component\n@RequiredArgsConstructor\npublic class TagQueryEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n    private final TagFinder tagFinder;\n    private final PostPublicQueryService postPublicQueryService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"TagV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"tags\", this::listTags,\n                builder -> {\n                    builder.operationId(\"queryTags\")\n                        .description(\"Lists tags\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(\n                                ListResult.generateGenericClass(TagVo.class))\n                        );\n                    TagPublicQuery.buildParameters(builder);\n                }\n            )\n            .GET(\"tags/{name}\", this::getTagByName,\n                builder -> builder.operationId(\"queryTagByName\")\n                    .description(\"Gets tag by name\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Tag name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(TagVo.class)\n                    )\n            )\n            .GET(\"tags/{name}/posts\", this::listPostsByTagName,\n                builder -> {\n                    builder.operationId(\"queryPostsByTagName\")\n                        .description(\"Lists posts by tag name\")\n                        .tag(tag)\n                        .parameter(parameterBuilder()\n                            .in(ParameterIn.PATH)\n                            .name(\"name\")\n                            .description(\"Tag name\")\n                            .required(true)\n                        )\n                        .response(responseBuilder()\n                            .implementation(ListedPostVo.class)\n                        );\n                    PostPublicQuery.buildParameters(builder);\n                }\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> getTagByName(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return tagFinder.getByName(name)\n            .flatMap(tag -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(tag)\n            );\n    }\n\n    private Mono<ServerResponse> listPostsByTagName(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        final var query = new PostPublicQuery(request.exchange());\n        var listOptions = query.toListOptions();\n        var newFieldSelector = listOptions.getFieldSelector()\n            .andQuery(Queries.equal(\"spec.tags\", name));\n        listOptions.setFieldSelector(newFieldSelector);\n        return postPublicQueryService.list(listOptions, query.toPageRequest())\n            .flatMap(result -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(result)\n            );\n    }\n\n    private Mono<ServerResponse> listTags(ServerRequest request) {\n        var query = new TagPublicQuery(request.exchange());\n        return client.listBy(Tag.class, query.toListOptions(), query.toPageRequest())\n            .map(result -> {\n                var tagVos = tagFinder.convertToVo(result.getItems());\n                return new ListResult<>(result.getPage(), result.getSize(),\n                    result.getTotal(), tagVos);\n            })\n            .flatMap(result -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(result)\n            );\n    }\n\n    static class TagPublicQuery extends SortableRequest {\n        public TagPublicQuery(ServerWebExchange exchange) {\n            super(exchange);\n        }\n\n        public static void buildParameters(Builder builder) {\n            SortableRequest.buildParameters(builder);\n        }\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return PublicApiUtils.groupVersion(new Tag());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/theme/ThumbnailEndpoint.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Arrays;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.core.io.Resource;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.attachment.thumbnail.ThumbnailService;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.infra.utils.HaloUtils;\n\n/**\n * Thumbnail endpoint for thumbnail resource access.\n *\n * @author guqing\n * @author johnniang\n * @since 2.19.0\n */\n@Component\n@RequiredArgsConstructor\npublic class ThumbnailEndpoint implements CustomEndpoint {\n\n    private final ThumbnailService thumbnailService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"ThumbnailV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"/thumbnails/-/via-uri\", this::getThumbnailByUri, builder -> {\n                builder.operationId(\"GetThumbnailByUri\")\n                    .description(\"Get thumbnail by URI\")\n                    .tag(tag)\n                    .response(responseBuilder().implementation(Resource.class))\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.QUERY)\n                        .name(\"uri\")\n                        .description(\"The URI of the image\")\n                        .required(true)\n                    )\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.QUERY)\n                        .name(\"size\")\n\n                        .implementation(ThumbnailSize.class)\n                        .description(\"The size of the thumbnail\")\n                        .required(true)\n                    )\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.QUERY)\n                        .name(\"width\")\n                        .schema(Builder.schemaBuilder()\n                            .type(\"integer\")\n                            .allowableValues(Arrays.stream(ThumbnailSize.allowedWidths())\n                                .map(String::valueOf)\n                                .toArray(String[]::new)\n                            )\n                        )\n                        .description(\"\"\"\n                            The width of the thumbnail, if 'size' is not provided, this \\\n                            parameter will be used to determine the size\\\n                            \"\"\")\n                        .required(false)\n                    );\n            })\n            .build();\n    }\n\n    private Mono<ServerResponse> getThumbnailByUri(ServerRequest request) {\n        var uri = request.queryParam(\"uri\")\n            .filter(StringUtils::isNotBlank)\n            .map(HaloUtils::safeToUri);\n        if (uri.isEmpty()) {\n            return Mono.error(\n                new ServerWebInputException(\"Required parameter 'uri' is missing or invalid\")\n            );\n        }\n        var size = request.queryParam(\"size\")\n            .filter(StringUtils::isNotBlank)\n            .flatMap(ThumbnailSize::optionalValueOf)\n            .or(() -> request.queryParam(\"width\")\n                .filter(StringUtils::isNotBlank)\n                .map(ThumbnailSize::fromWidth)\n            );\n        if (size.isEmpty()) {\n            return Mono.error(new ServerWebInputException(\n                \"Required parameter 'size' or 'width' is missing or invalid\"\n            ));\n        }\n        return thumbnailService.get(uri.get(), size.get())\n            .defaultIfEmpty(uri.get())\n            .flatMap(thumbnailLink -> ServerResponse.status(HttpStatus.FOUND)\n                .location(thumbnailLink)\n                .build()\n            );\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return new GroupVersion(\"api.storage.halo.run\", \"v1alpha1\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/uc/AnnotationSettingEndpoint.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static run.halo.app.extension.ExtensionUtil.defaultSort;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.apiresponse.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.plugin.PluginConst;\nimport run.halo.app.plugin.PluginService;\nimport run.halo.app.theme.service.ThemeService;\n\n/**\n * The endpoint for managing AnnotationSettings.\n *\n * @author johnniang\n * @since 2.22.3\n */\n@Component\n@RequiredArgsConstructor\nclass AnnotationSettingEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n\n    private final ThemeService themeService;\n\n    private final PluginService pluginService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"AnnotationSettingV1AlphaUc\";\n        return SpringdocRouteBuilder.route()\n            .GET(\n                \"/annotationsettings\",\n                this::listAvailableAnnotationSettings,\n                builder -> builder\n                    .operationId(\"listAvailableAnnotationSettings\")\n                    .description(\"\"\"\n                        List available AnnotationSettings for the given targetRef. \\\n                        The available AnnotationSettings are determined by \\\n                        the currently activated theme and started plugins.\"\"\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"targetRef\")\n                        .in(ParameterIn.QUERY)\n                        .description(\n                            \"The targetRef of the AnnotationSetting. e.g.: 'content.halo.run/Post\"\n                        )\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(Builder.responseBuilder()\n                        .implementationArray(AnnotationSetting.class)\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> listAvailableAnnotationSettings(ServerRequest serverRequest) {\n        var targetRef = serverRequest.queryParam(\"targetRef\")\n            .filter(StringUtils::hasText)\n            .orElse(null);\n        if (targetRef == null) {\n            return Mono.error(new ServerWebInputException(\"Query param 'targetRef' is required\"));\n        }\n        var getActivatedTheme = themeService.fetchActivatedThemeName()\n            .map(Optional::of)\n            .defaultIfEmpty(Optional.empty());\n        var getStartedPlugins = pluginService.getStartedPluginNames().collectList();\n        var annotationSettings = Mono.zip(getActivatedTheme, getStartedPlugins,\n                (themeName, pluginNames) -> {\n                    Condition labelConditions = null;\n                    if (themeName.isPresent()) {\n                        labelConditions = Queries.labelEqual(Theme.THEME_NAME_LABEL,\n                            themeName.get());\n                    }\n                    if (!CollectionUtils.isEmpty(pluginNames)) {\n                        var pluginLabelCondition =\n                            Queries.labelIn(PluginConst.PLUGIN_NAME_LABEL_NAME, pluginNames);\n                        if (labelConditions == null) {\n                            labelConditions = pluginLabelCondition;\n                        } else {\n                            labelConditions = labelConditions.or(pluginLabelCondition);\n                        }\n                    }\n                    if (labelConditions == null) {\n                        labelConditions = Queries.empty();\n                    }\n                    var builder = ListOptions.builder()\n                        .andQuery(labelConditions)\n                        .andQuery(Queries.equal(\"spec.targetRef\", targetRef));\n                    return builder.build();\n                })\n            .flatMapMany(\n                listOptions -> client.listAll(AnnotationSetting.class, listOptions, defaultSort())\n            );\n        return ServerResponse.ok().body(annotationSettings, AnnotationSetting.class);\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.halo.run/v1alpha1\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/uc/AttachmentUcEndpoint.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.Consumer;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.http.codec.multipart.FormFieldPart;\nimport org.springframework.http.codec.multipart.Part;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.web.reactive.function.BodyExtractors;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.attachment.AttachmentLister;\nimport run.halo.app.core.attachment.SearchRequest;\nimport run.halo.app.core.endpoint.AttachmentHandler;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.exception.NotFoundException;\n\n@Component\n@RequiredArgsConstructor\npublic class AttachmentUcEndpoint implements CustomEndpoint {\n\n    public static final String POST_NAME_LABEL = \"content.halo.run/post-name\";\n    public static final String SINGLE_PAGE_NAME_LABEL = \"content.halo.run/single-page-name\";\n\n    private final AttachmentService attachmentService;\n\n    private final AttachmentLister attachmentLister;\n\n    private final PostService postService;\n\n    private final SystemConfigFetcher systemSettingFetcher;\n\n    private final AttachmentHandler attachmentHandler;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"AttachmentV1alpha1Uc\";\n        return route()\n            .POST(\"/attachments\",\n                this::createAttachmentForPost,\n                builder -> builder.operationId(\"CreateAttachmentForPost\").tag(tag)\n                    .description(\"\"\"\n                        Create attachment for the given post. \\\n                        Deprecated in favor of /attachments/-/upload.\"\"\"\n                    )\n                    .deprecated(true)\n                    .parameter(parameterBuilder()\n                        .name(\"waitForPermalink\")\n                        .description(\"Wait for permalink.\")\n                        .in(ParameterIn.QUERY)\n                        .required(false)\n                        .implementation(boolean.class))\n                    .requestBody(requestBodyBuilder()\n                        .content(contentBuilder()\n                            .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder().implementation(PostAttachmentRequest.class)))\n                    )\n                    .response(responseBuilder().implementation(Attachment.class))\n            )\n            .POST(\n                \"/attachments/-/upload\",\n                contentType(MediaType.MULTIPART_FORM_DATA),\n                this::uploadAttachment,\n                builder -> {\n                    builder.operationId(\"UploadAttachmentForUc\")\n                        .description(\"Upload attachment to user center storage.\")\n                        .tag(tag);\n                    this.attachmentHandler.buildDoc(builder);\n                }\n            )\n            .POST(\"/attachments/-/upload-from-url\", contentType(MediaType.APPLICATION_JSON),\n                this::uploadFromUrlForPost,\n                builder -> builder\n                    .operationId(\"ExternalTransferAttachment_1\")\n                    .description(\"\"\"\n                        Upload attachment from the given URL.\n                        Deprecated in favor of /attachments/-/upload.\"\"\")\n                    .tag(tag)\n                    .deprecated(true)\n                    .parameter(parameterBuilder()\n                        .name(\"waitForPermalink\")\n                        .description(\"Wait for permalink.\")\n                        .in(ParameterIn.QUERY)\n                        .required(false)\n                        .implementation(boolean.class))\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder().implementation(UploadFromUrlRequest.class))\n                        ))\n                    .response(responseBuilder().implementation(Attachment.class))\n                    .build()\n            )\n            .GET(\"/attachments\", this::listMyAttachments, builder -> {\n                builder.operationId(\"ListMyAttachments\")\n                    .description(\"List attachments of the current user uploaded.\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .implementation(ListResult.generateGenericClass(Attachment.class))\n                    );\n                SearchRequest.buildParameters(builder);\n            })\n            .build();\n    }\n\n    private Mono<ServerResponse> uploadAttachment(ServerRequest request) {\n        var getConfigFromUser = systemSettingFetcher.fetch(\n                SystemSetting.User.GROUP,\n                SystemSetting.User.class\n            )\n            .mapNotNull(SystemSetting.User::getUcAttachmentPolicy)\n            .filter(StringUtils::isNotBlank)\n            .map(policyName -> SystemSetting.Attachment.UploadOptions.builder()\n                .policyName(policyName)\n                .build()\n            );\n        var getConfig = systemSettingFetcher.fetch(\n                SystemSetting.Attachment.GROUP,\n                SystemSetting.Attachment.class\n            )\n            .mapNotNull(SystemSetting.Attachment::uc)\n            .filter(uo -> StringUtils.isNotBlank(uo.policyName()))\n            .switchIfEmpty(Mono.defer(() -> getConfigFromUser))\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"Attachment system setting is not configured for console\"\n            )));\n        return attachmentHandler.handleUpload(request, getConfig);\n    }\n\n    private Mono<ServerResponse> listMyAttachments(ServerRequest request) {\n        return getCurrentUser()\n            .flatMap(username -> {\n                var searchRequest = new UcSearchRequest(request, username);\n                return attachmentLister.listBy(searchRequest)\n                    .flatMap(listResult -> ServerResponse.ok()\n                        .contentType(MediaType.APPLICATION_JSON)\n                        .bodyValue(listResult)\n                    );\n            });\n    }\n\n    @Getter\n    public static class UcSearchRequest extends SearchRequest {\n        private final String owner;\n\n        public UcSearchRequest(ServerRequest request, String owner) {\n            super(request);\n            Assert.state(StringUtils.isNotBlank(owner), \"Owner must not be blank.\");\n            this.owner = owner;\n        }\n\n        @Override\n        public ListOptions toListOptions(List<String> hiddenGroups) {\n            var listOptions = super.toListOptions(hiddenGroups);\n            return ListOptions.builder(listOptions)\n                .andQuery((equal(\"spec.ownerName\", owner)))\n                .build();\n        }\n    }\n\n    private Mono<ServerResponse> uploadFromUrlForPost(ServerRequest request) {\n        var uploadFromUrlRequestMono = request.bodyToMono(UploadFromUrlRequest.class);\n\n        var uploadAttachment = getPostSettingMono()\n            .flatMap(postSetting -> uploadFromUrlRequestMono.flatMap(\n                uploadFromUrlRequest -> {\n                    var url = uploadFromUrlRequest.url();\n                    var fileName = uploadFromUrlRequest.filename();\n                    return attachmentService.uploadFromUrl(url,\n                        postSetting.getAttachmentPolicyName(),\n                        postSetting.getAttachmentGroupName(),\n                        fileName\n                    );\n                })\n            );\n\n        var waitForPermalink = request.queryParam(\"waitForPermalink\")\n            .map(Boolean::valueOf)\n            .orElse(false);\n        if (waitForPermalink) {\n            uploadAttachment = waitForPermalink(uploadAttachment);\n        }\n        return ServerResponse.ok().body(uploadAttachment, Attachment.class);\n    }\n\n    private Mono<ServerResponse> createAttachmentForPost(ServerRequest request) {\n        var postAttachmentRequestMono = request.body(BodyExtractors.toMultipartData())\n            .map(PostAttachmentRequest::from)\n            .cache();\n\n        // get settings\n        var createdAttachment =\n            getPostSettingMono().flatMap(postSetting -> postAttachmentRequestMono\n                .flatMap(postAttachmentRequest -> getCurrentUser().flatMap(\n                    username -> attachmentService.upload(username,\n                        postSetting.getAttachmentPolicyName(),\n                        postSetting.getAttachmentGroupName(),\n                        postAttachmentRequest.file(),\n                        linkWith(postAttachmentRequest)))));\n\n        var waitForPermalink = request.queryParam(\"waitForPermalink\")\n            .map(Boolean::valueOf)\n            .orElse(false);\n        if (waitForPermalink) {\n            createdAttachment = waitForPermalink(createdAttachment);\n        }\n        return ServerResponse.ok().body(createdAttachment, Attachment.class);\n    }\n\n    private Mono<Attachment> waitForPermalink(Mono<Attachment> createdAttachment) {\n        createdAttachment = createdAttachment.flatMap(attachment ->\n            attachmentService.getPermalink(attachment)\n                .doOnNext(permalink -> {\n                    var status = attachment.getStatus();\n                    if (status == null) {\n                        status = new Attachment.AttachmentStatus();\n                        attachment.setStatus(status);\n                    }\n                    status.setPermalink(permalink.toString());\n                })\n                .thenReturn(attachment));\n        return createdAttachment;\n    }\n\n    private Mono<SystemSetting.Post> getPostSettingMono() {\n        return systemSettingFetcher.fetchPost().handle((postSetting, sink) -> {\n            var attachmentPolicyName = postSetting.getAttachmentPolicyName();\n            if (StringUtils.isBlank(attachmentPolicyName)) {\n                sink.error(new ServerWebInputException(\n                    \"Please configure storage policy for post attachment first.\"));\n                return;\n            }\n            sink.next(postSetting);\n        });\n    }\n\n    private Consumer<Attachment> linkWith(PostAttachmentRequest request) {\n        return attachment -> {\n            var labels = attachment.getMetadata().getLabels();\n            if (labels == null) {\n                labels = new HashMap<>();\n                attachment.getMetadata().setLabels(labels);\n            }\n            if (StringUtils.isNotBlank(request.postName())) {\n                labels.put(POST_NAME_LABEL, request.postName());\n            }\n            if (StringUtils.isNotBlank(request.singlePageName())) {\n                labels.put(SINGLE_PAGE_NAME_LABEL, request.singlePageName());\n            }\n        };\n    }\n\n    private Mono<Void> checkPostOwnership(Mono<PostAttachmentRequest> postAttachmentRequest) {\n        // check the post\n        var postNotFoundError = Mono.<Post>error(\n            () -> new NotFoundException(\"The post was not found or deleted.\")\n        );\n        return postAttachmentRequest.map(PostAttachmentRequest::postName)\n            .flatMap(postName -> getCurrentUser()\n                .flatMap(username -> postService.getByUsername(postName, username)\n                    .switchIfEmpty(postNotFoundError)))\n            .then();\n    }\n\n    private Mono<String> getCurrentUser() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Authentication::getName);\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.storage.halo.run/v1alpha1\");\n    }\n\n    @Schema(name = \"UcUploadFromUrlRequest\")\n    public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,\n                                       @Schema(description = \"Custom file name\") String filename) {\n        public UploadFromUrlRequest {\n            if (Objects.isNull(url)) {\n                throw new ServerWebInputException(\"Required url is missing.\");\n            }\n        }\n    }\n\n    @Schema(types = \"object\")\n    public record PostAttachmentRequest(\n        @Schema(requiredMode = REQUIRED, description = \"Attachment data.\")\n        FilePart file,\n\n        @Schema(requiredMode = NOT_REQUIRED, description = \"Post name.\")\n        String postName,\n\n        @Schema(requiredMode = NOT_REQUIRED, description = \"Single page name.\")\n        String singlePageName\n    ) {\n\n        /**\n         * Convert multipart data into PostAttachmentRequest.\n         *\n         * @param multipartData is multipart data from request.\n         * @return post attachment request data.\n         */\n        public static PostAttachmentRequest from(MultiValueMap<String, Part> multipartData) {\n            var part = multipartData.getFirst(\"postName\");\n            String postName = null;\n            if (part instanceof FormFieldPart formFieldPart) {\n                postName = formFieldPart.value();\n            }\n\n            part = multipartData.getFirst(\"singlePageName\");\n            String singlePageName = null;\n            if (part instanceof FormFieldPart formFieldPart) {\n                singlePageName = formFieldPart.value();\n            }\n\n            part = multipartData.getFirst(\"file\");\n            if (!(part instanceof FilePart file)) {\n                throw new ServerWebInputException(\"Invalid type of parameter 'file'.\");\n            }\n\n            return new PostAttachmentRequest(file, postName, singlePageName);\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/uc/UcPostEndpoint.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.Objects;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.Content;\nimport run.halo.app.content.ContentUpdateParam;\nimport run.halo.app.content.ListedPost;\nimport run.halo.app.content.PostQuery;\nimport run.halo.app.content.PostRequest;\nimport run.halo.app.content.PostService;\nimport run.halo.app.content.SnapshotService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@Component\npublic class UcPostEndpoint implements CustomEndpoint {\n\n    private static final String CONTENT_JSON_ANNO = \"content.halo.run/content-json\";\n\n    private final PostService postService;\n\n    private final SnapshotService snapshotService;\n\n    public UcPostEndpoint(PostService postService, SnapshotService snapshotService) {\n        this.postService = postService;\n        this.snapshotService = snapshotService;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"PostV1alpha1Uc\";\n        var namePathParam = parameterBuilder().name(\"name\")\n            .description(\"Post name\")\n            .in(ParameterIn.PATH)\n            .required(true);\n        return route().nest(\n                path(\"/posts\"),\n                () -> route()\n                    .GET(this::listMyPost, builder -> {\n                            builder.operationId(\"ListMyPosts\")\n                                .description(\"List posts owned by the current user.\")\n                                .tag(tag)\n                                .response(responseBuilder().implementation(\n                                    ListResult.generateGenericClass(ListedPost.class)));\n                            PostQuery.buildParameters(builder);\n                        }\n                    )\n                    .POST(this::createMyPost, builder -> builder.operationId(\"CreateMyPost\")\n                        .tag(tag)\n                        .description(\"\"\"\n                            Create my post. If you want to create a post with content, please set\n                             annotation: \"content.halo.run/content-json\" into annotations and refer\n                             to Content for corresponding data type.\n                            \"\"\")\n                        .requestBody(requestBodyBuilder().implementation(Post.class))\n                        .response(responseBuilder().implementation(Post.class))\n                    )\n                    .GET(\"/{name}\", this::getMyPost, builder -> builder.operationId(\"GetMyPost\")\n                        .tag(tag)\n                        .parameter(namePathParam)\n                        .description(\"Get post that belongs to the current user.\")\n                        .response(responseBuilder().implementation(Post.class))\n                    )\n                    .PUT(\"/{name}\", this::updateMyPost, builder ->\n                        builder.operationId(\"UpdateMyPost\")\n                            .tag(tag)\n                            .parameter(namePathParam)\n                            .description(\"Update my post.\")\n                            .requestBody(requestBodyBuilder().implementation(Post.class))\n                            .response(responseBuilder().implementation(Post.class))\n                    )\n                    .GET(\"/{name}/draft\", this::getMyPostDraft, builder -> builder.tag(tag)\n                        .operationId(\"GetMyPostDraft\")\n                        .description(\"Get my post draft.\")\n                        .parameter(namePathParam)\n                        .parameter(parameterBuilder()\n                            .name(\"patched\")\n                            .in(ParameterIn.QUERY)\n                            .required(false)\n                            .implementation(Boolean.class)\n                            .description(\"Should include patched content and raw or not.\")\n                        )\n                        .response(responseBuilder().implementation(Snapshot.class))\n                    )\n                    .PUT(\"/{name}/draft\", this::updateMyPostDraft, builder -> builder.tag(tag)\n                        .operationId(\"UpdateMyPostDraft\")\n                        .description(\"\"\"\n                            Update draft of my post. Please make sure set annotation: \n                            \"content.halo.run/content-json\" into annotations and refer to \n                            Content for corresponding data type.\n                             \"\"\")\n                        .parameter(namePathParam)\n                        .requestBody(requestBodyBuilder().implementation(Snapshot.class))\n                        .response(responseBuilder().implementation(Snapshot.class)))\n                    .PUT(\"/{name}/publish\", this::publishMyPost, builder -> builder.tag(tag)\n                        .operationId(\"PublishMyPost\")\n                        .description(\"Publish my post.\")\n                        .parameter(namePathParam)\n                        .response(responseBuilder().implementation(Post.class)))\n                    .PUT(\"/{name}/unpublish\", this::unpublishMyPost, builder -> builder.tag(tag)\n                        .operationId(\"UnpublishMyPost\")\n                        .description(\"Unpublish my post.\")\n                        .parameter(namePathParam)\n                        .response(responseBuilder().implementation(Post.class))\n                    )\n                    .DELETE(\"/{name}/recycle\", this::recycleMyPost, builder -> builder.tag(tag)\n                        .operationId(\"RecycleMyPost\")\n                        .description(\"Move my post to recycle bin.\")\n                        .parameter(namePathParam)\n                        .response(responseBuilder().implementation(Post.class))\n                    )\n                    .build()\n                )\n            .build();\n    }\n\n    private Mono<ServerResponse> recycleMyPost(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return getCurrentUser()\n            .flatMap(username -> postService.recycleBy(name, username))\n            .flatMap(post -> ServerResponse.ok().bodyValue(post));\n    }\n\n    private Mono<ServerResponse> getMyPostDraft(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var patched = request.queryParam(\"patched\").map(Boolean::valueOf).orElse(false);\n        var draft = getMyPost(name)\n            .flatMap(post -> {\n                var headSnapshotName = post.getSpec().getHeadSnapshot();\n                var baseSnapshotName = post.getSpec().getBaseSnapshot();\n                if (StringUtils.isBlank(headSnapshotName)) {\n                    headSnapshotName = baseSnapshotName;\n                }\n                if (patched) {\n                    return snapshotService.getPatchedBy(headSnapshotName, baseSnapshotName);\n                }\n                return snapshotService.getBy(headSnapshotName);\n            });\n        return ServerResponse.ok().body(draft, Snapshot.class);\n    }\n\n    private Mono<ServerResponse> unpublishMyPost(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var postMono = getCurrentUser()\n            .flatMap(username -> postService.getByUsername(name, username));\n        var unpublishedPost = postMono.flatMap(postService::unpublish);\n        return ServerResponse.ok().body(unpublishedPost, Post.class);\n    }\n\n    private Mono<ServerResponse> publishMyPost(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var postMono = getCurrentUser()\n            .flatMap(username -> postService.getByUsername(name, username));\n\n        var publishedPost = postMono.flatMap(postService::publish);\n        return ServerResponse.ok().body(publishedPost, Post.class);\n    }\n\n    private Mono<ServerResponse> updateMyPostDraft(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var postMono = getMyPost(name).cache();\n        var snapshotMono = request.bodyToMono(Snapshot.class).cache();\n\n        var contentMono = snapshotMono\n            .map(Snapshot::getMetadata)\n            .filter(metadata -> {\n                var annotations = metadata.getAnnotations();\n                return annotations != null && annotations.containsKey(CONTENT_JSON_ANNO);\n            })\n            .map(metadata -> {\n                var contentJson = metadata.getAnnotations().remove(CONTENT_JSON_ANNO);\n                return JsonUtils.jsonToObject(contentJson, Content.class);\n            })\n            .cache();\n\n        // check the snapshot belongs to the post.\n        var checkSnapshot = postMono.flatMap(post -> snapshotMono.filter(\n                snapshot -> Ref.equals(snapshot.getSpec().getSubjectRef(), post)\n            ).switchIfEmpty(Mono.error(() ->\n                new ServerWebInputException(\"The snapshot does not belong to the given post.\"))\n            ).filter(snapshot -> {\n                var snapshotName = snapshot.getMetadata().getName();\n                var headSnapshotName = post.getSpec().getHeadSnapshot();\n                return Objects.equals(snapshotName, headSnapshotName);\n            }).switchIfEmpty(Mono.error(() ->\n                new ServerWebInputException(\"The snapshot was not the head snapshot of the post.\")))\n        ).then();\n\n        var setContributor = getCurrentUser().flatMap(username ->\n            snapshotMono.doOnNext(snapshot -> Snapshot.addContributor(snapshot, username)));\n\n        var getBaseSnapshot = postMono.map(post -> post.getSpec().getBaseSnapshot())\n            .flatMap(snapshotService::getBy);\n\n        var updatedSnapshot = getBaseSnapshot.flatMap(\n            baseSnapshot -> contentMono.flatMap(content -> postMono.flatMap(post -> {\n                var postName = post.getMetadata().getName();\n                var headSnapshotName = post.getSpec().getHeadSnapshot();\n                var releaseSnapshotName = post.getSpec().getReleaseSnapshot();\n                if (!Objects.equals(headSnapshotName, releaseSnapshotName)) {\n                    // patch and update\n                    return snapshotMono.flatMap(\n                        s -> snapshotService.patchAndUpdate(s, baseSnapshot, content));\n                }\n                // patch and create\n                return getCurrentUser().map(\n                        username -> {\n                            var metadata = new Metadata();\n                            metadata.setGenerateName(postName + \"-snapshot-\");\n                            var spec = new Snapshot.SnapShotSpec();\n                            spec.setParentSnapshotName(headSnapshotName);\n                            spec.setOwner(username);\n                            spec.setSubjectRef(Ref.of(post));\n\n                            var snapshot = new Snapshot();\n                            snapshot.setMetadata(metadata);\n                            snapshot.setSpec(spec);\n                            Snapshot.addContributor(snapshot, username);\n                            return snapshot;\n                        })\n                    .flatMap(s -> snapshotService.patchAndCreate(s, baseSnapshot, content))\n                    .flatMap(createdSnapshot -> {\n                        post.getSpec().setHeadSnapshot(createdSnapshot.getMetadata().getName());\n                        return postService.updateBy(post).thenReturn(createdSnapshot);\n                    });\n            })));\n\n        return ServerResponse.ok()\n            .body(checkSnapshot.and(setContributor).then(updatedSnapshot), Snapshot.class);\n    }\n\n    private Mono<ServerResponse> updateMyPost(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n\n        var postBody = request.bodyToMono(Post.class)\n            .doOnNext(post -> {\n                var annotations = post.getMetadata().getAnnotations();\n                if (annotations != null) {\n                    // we don't support updating content while updating post.\n                    annotations.remove(CONTENT_JSON_ANNO);\n                }\n            })\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Request body required.\")));\n\n        var updatedPost = getMyPost(name).flatMap(oldPost ->\n                postBody.doOnNext(post -> {\n                    var oldSpec = oldPost.getSpec();\n                    // restrict fields of post.spec.\n                    var spec = post.getSpec();\n                    spec.setOwner(oldSpec.getOwner());\n                    spec.setPublish(oldSpec.getPublish());\n                    spec.setHeadSnapshot(oldSpec.getHeadSnapshot());\n                    spec.setBaseSnapshot(oldSpec.getBaseSnapshot());\n                    spec.setReleaseSnapshot(oldSpec.getReleaseSnapshot());\n                    spec.setDeleted(oldSpec.getDeleted());\n                    post.getMetadata().setName(oldPost.getMetadata().getName());\n                }))\n            .flatMap(postService::updateBy);\n        return ServerResponse.ok().body(updatedPost, Post.class);\n    }\n\n    private Mono<ServerResponse> createMyPost(ServerRequest request) {\n        var postFromRequest = request.bodyToMono(Post.class)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Request body required.\")));\n\n        var createdPost = getCurrentUser()\n            .flatMap(username -> postFromRequest\n                .doOnNext(post -> {\n                    if (post.getSpec() == null) {\n                        post.setSpec(new Post.PostSpec());\n                    }\n                    post.getSpec().setOwner(username);\n                }))\n            .map(post -> new PostRequest(post, ContentUpdateParam.from(getContent(post))))\n            .flatMap(postService::draftPost);\n        return ServerResponse.ok().body(createdPost, Post.class);\n    }\n\n    private Content getContent(Post post) {\n        Content content = null;\n        var annotations = post.getMetadata().getAnnotations();\n        if (annotations != null && annotations.containsKey(CONTENT_JSON_ANNO)) {\n            var contentJson = annotations.remove(CONTENT_JSON_ANNO);\n            content = JsonUtils.jsonToObject(contentJson, Content.class);\n        }\n        return content;\n    }\n\n    private Mono<ServerResponse> listMyPost(ServerRequest request) {\n        var posts = getCurrentUser()\n            .map(username -> new PostQuery(request, username))\n            .flatMap(postService::listPost);\n        return ServerResponse.ok().body(posts, ListedPost.class);\n    }\n\n    private Mono<ServerResponse> getMyPost(ServerRequest request) {\n        var postName = request.pathVariable(\"name\");\n        var post = getMyPost(postName);\n        return ServerResponse.ok().body(post, Post.class);\n    }\n\n    private Mono<Post> getMyPost(String postName) {\n        return getCurrentUser()\n            .flatMap(username -> postService.getByUsername(postName, username)\n                .switchIfEmpty(\n                    Mono.error(() -> new NotFoundException(\"The post was not found or deleted\"))\n                )\n            );\n    }\n\n    private Mono<String> getCurrentUser() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Authentication::getName);\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.content.halo.run/v1alpha1\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/uc/UcSnapshotEndpoint.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.PostService;\nimport run.halo.app.content.SnapshotService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.exception.NotFoundException;\n\n@Component\npublic class UcSnapshotEndpoint implements CustomEndpoint {\n\n    private final PostService postService;\n\n    private final SnapshotService snapshotService;\n\n    public UcSnapshotEndpoint(PostService postService, SnapshotService snapshotService) {\n        this.postService = postService;\n        this.snapshotService = snapshotService;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"SnapshotV1alpha1Uc\";\n        return route().nest(path(\"/snapshots\"),\n                () -> route()\n                    .GET(\"/{name}\",\n                        this::getSnapshot,\n                        builder -> builder.operationId(\"GetSnapshotForPost\")\n                            .description(\"Get snapshot for one post.\")\n                            .parameter(parameterBuilder()\n                                .name(\"name\")\n                                .in(ParameterIn.PATH)\n                                .required(true)\n                                .description(\"Snapshot name.\")\n                            )\n                            .parameter(parameterBuilder()\n                                .name(\"postName\")\n                                .in(ParameterIn.QUERY)\n                                .required(true)\n                                .description(\"Post name.\")\n                            )\n                            .parameter(parameterBuilder()\n                                .name(\"patched\")\n                                .in(ParameterIn.QUERY)\n                                .required(false)\n                                .implementation(Boolean.class)\n                                .description(\"Should include patched content and raw or not.\")\n                            )\n                            .response(responseBuilder().implementation(Snapshot.class))\n                            .tag(tag))\n                    .build()\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> getSnapshot(ServerRequest request) {\n        var snapshotName = request.pathVariable(\"name\");\n        var postName = request.queryParam(\"postName\")\n            .orElseThrow(() -> new ServerWebInputException(\"Query parameter postName is required\"));\n        var patched = request.queryParam(\"patched\").map(Boolean::valueOf).orElse(false);\n\n        var postNotFoundError = Mono.<Post>error(\n            () -> new NotFoundException(\"The post was not found or deleted.\")\n        );\n        var snapshotNotFoundError = Mono.<Snapshot>error(\n            () -> new NotFoundException(\"The snapshot was not found or deleted.\")\n        );\n\n        var postMono = getCurrentUser().flatMap(username ->\n            postService.getByUsername(postName, username).switchIfEmpty(postNotFoundError)\n        );\n\n        // check the post belongs to the current user.\n        var snapshotMono = postMono.flatMap(post -> Mono.defer(\n                () -> {\n                    if (patched) {\n                        var baseSnapshotName = post.getSpec().getBaseSnapshot();\n                        return snapshotService.getPatchedBy(snapshotName, baseSnapshotName);\n                    }\n                    return snapshotService.getBy(snapshotName);\n                })\n            .filter(snapshot -> {\n                var subjectRef = snapshot.getSpec().getSubjectRef();\n                return Ref.equals(subjectRef, post);\n            })\n            .switchIfEmpty(snapshotNotFoundError)\n        );\n\n        return ServerResponse.ok().body(snapshotMono, Snapshot.class);\n    }\n\n    private Mono<String> getCurrentUser() {\n        return ReactiveSecurityContextHolder\n            .getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Authentication::getName);\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.content.halo.run/v1alpha1\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/uc/UcUserPreferenceEndpoint.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.HashMap;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.requestbody.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ResponseStatusException;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport tools.jackson.databind.JsonNode;\nimport tools.jackson.databind.json.JsonMapper;\n\n/**\n * User preference endpoint for UC (User Center).\n * This endpoint allows users to get and update their preferences by group.\n *\n * @author JohnNiang\n * @since 2.21.0\n */\n@Component\n@RequiredArgsConstructor\nclass UcUserPreferenceEndpoint implements CustomEndpoint {\n\n    private static final String PREFERENCE_PREFIX = \"user-preferences-\";\n\n    private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();\n\n    private final ReactiveExtensionClient client;\n\n    private final JsonMapper mapper;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"UserPreferenceV1alpha1Uc\";\n        return SpringdocRouteBuilder.route()\n            .GET(\n                \"/user-preferences/{group}\",\n                this::getMyPreference,\n                builder -> builder.operationId(\"getMyPreference\")\n                    .tag(tag)\n                    .description(\"Get my preference by group.\")\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"group\")\n                        .description(\"Group of user preference, e.g. `notification`.\")\n                        .implementation(String.class)\n                        .required(true)\n                    )\n                    .response(responseBuilder().implementation(Object.class))\n            )\n            .PUT(\n                \"/user-preferences/{group}\",\n                this::updateMyPreference,\n                builder -> builder.operationId(\"updateMyPreference\")\n                    .tag(tag)\n                    .description(\"Create or update my preference by group.\")\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"group\")\n                        .description(\"Group of user preference, e.g. `notification`.\")\n                        .implementation(String.class)\n                        .required(true)\n                    )\n                    .requestBody(Builder.requestBodyBuilder()\n                        .required(true)\n                        .implementation(Object.class))\n                    .response(responseBuilder()\n                        .description(\"No content, preference updated successfully.\")\n                        .responseCode(String.valueOf(HttpStatus.NO_CONTENT.value()))\n                    )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> updateMyPreference(ServerRequest serverRequest) {\n        var group = serverRequest.pathVariable(\"group\");\n        return authenticated()\n            .map(Authentication::getName)\n            .flatMap(username -> client.fetch(ConfigMap.class, PREFERENCE_PREFIX + username)\n                .switchIfEmpty(Mono.fromSupplier(() -> {\n                    var cm = new ConfigMap();\n                    cm.setMetadata(new Metadata());\n                    cm.getMetadata().setName(PREFERENCE_PREFIX + username);\n                    return cm;\n                }))\n            )\n            .flatMap(cm -> serverRequest.bodyToMono(JsonNode.class)\n                .switchIfEmpty(\n                    Mono.error(() -> new ServerWebInputException(\"Request body is required.\"))\n                )\n                .flatMap(jsonNode -> Mono.fromCallable(() -> {\n                    if (cm.getData() == null) {\n                        cm.setData(new HashMap<>());\n                    }\n                    var json = mapper.writeValueAsString(jsonNode);\n                    if (Objects.equals(json, cm.getData().get(group))) {\n                        return null;\n                    }\n                    cm.getData().put(group, json);\n                    return cm;\n                }))\n                .flatMap(extension -> {\n                    if (extension.getMetadata().getVersion() == null) {\n                        return client.create(extension);\n                    }\n                    return client.update(extension);\n                })\n                .defaultIfEmpty(cm)\n            )\n            .flatMap(cm -> ServerResponse.noContent().build());\n    }\n\n    private Mono<ServerResponse> getMyPreference(ServerRequest serverRequest) {\n        var group = serverRequest.pathVariable(\"group\");\n        return authenticated()\n            .map(Authentication::getName)\n            .flatMap(username -> client.fetch(ConfigMap.class, PREFERENCE_PREFIX + username))\n            .mapNotNull(ConfigMap::getData)\n            .mapNotNull(data -> data.get(group))\n            .flatMap(json -> Mono.fromCallable(() -> mapper.readTree(json)))\n            .switchIfEmpty(Mono.fromSupplier(mapper::nullNode))\n            .flatMap(jsonNode -> ServerResponse.ok().bodyValue(jsonNode));\n    }\n\n    private Mono<Authentication> authenticated() {\n        return ReactiveSecurityContextHolder.getContext()\n            .mapNotNull(SecurityContext::getAuthentication)\n            .filter(trustResolver::isAuthenticated)\n            .switchIfEmpty(Mono.error(() -> new ResponseStatusException(HttpStatus.FORBIDDEN,\n                \"Anonymous user is not allowed to access user preference.\"\n            )));\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/endpoint/uc/UserConnectionEndpoint.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport org.springdoc.core.fn.builders.parameter.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.core.extension.UserConnection;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.user.service.UserConnectionService;\nimport run.halo.app.extension.GroupVersion;\n\n/**\n * User connection endpoint.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\npublic class UserConnectionEndpoint implements CustomEndpoint {\n\n    private final UserConnectionService connectionService;\n\n    private final AuthenticationTrustResolver authenticationTrustResolver =\n        new AuthenticationTrustResolverImpl();\n\n    public UserConnectionEndpoint(UserConnectionService connectionService) {\n        this.connectionService = connectionService;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"UserConnectionV1alpha1Uc\";\n        return SpringdocRouteBuilder.route()\n            .PUT(\n                \"/user-connections/{registerId}/disconnect\",\n                request -> {\n                    var removedUserConnections = ReactiveSecurityContextHolder.getContext()\n                        .map(SecurityContext::getAuthentication)\n                        .filter(authenticationTrustResolver::isAuthenticated)\n                        .map(Authentication::getName)\n                        .flatMapMany(username -> connectionService.removeUserConnection(\n                            request.pathVariable(\"registerId\"), username)\n                        );\n                    return ServerResponse.ok().body(removedUserConnections, UserConnection.class);\n                },\n                builder -> builder.operationId(\"DisconnectMyConnection\")\n                    .description(\"Disconnect my connection from a third-party platform.\")\n                    .tag(tag)\n                    .parameter(Builder.parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"registerId\")\n                        .description(\"The registration ID of the third-party platform.\")\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder().implementationArray(UserConnection.class))\n            )\n            .build();\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.auth.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/AnnotationSettingReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.util.StringUtils;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.GroupKind;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n/**\n * Reconciler for {@link AnnotationSetting}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class AnnotationSettingReconciler implements Reconciler<Reconciler.Request> {\n\n    private final ExtensionClient client;\n\n    @Override\n    public Result reconcile(Request request) {\n        populateDefaultLabels(request.name());\n        return new Result(false, null);\n    }\n\n    private void populateDefaultLabels(String name) {\n        client.fetch(AnnotationSetting.class, name).ifPresent(annotationSetting -> {\n            Map<String, String> labels = MetadataUtil.nullSafeLabels(annotationSetting);\n            String oldTargetRef = labels.get(AnnotationSetting.TARGET_REF_LABEL);\n\n            GroupKind targetRef = annotationSetting.getSpec().getTargetRef();\n            String targetRefLabel = targetRef.group() + \"/\" + targetRef.kind();\n            labels.put(AnnotationSetting.TARGET_REF_LABEL, targetRefLabel);\n\n            if (!StringUtils.equals(oldTargetRef, targetRefLabel)) {\n                client.update(annotationSetting);\n            }\n        });\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new AnnotationSetting())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/AuthProviderReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport java.time.Duration;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.AuthProvider;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.security.AuthProviderService;\n\n/**\n * Reconciler for {@link AuthProvider}.\n *\n * @author guqing\n * @since 2.4.0\n */\n@Component\n@RequiredArgsConstructor\npublic class AuthProviderReconciler implements Reconciler<Reconciler.Request> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private final ExtensionClient client;\n    private final AuthProviderService authProviderService;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(AuthProvider.class, request.name())\n            .ifPresent(this::handlePrivileged);\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new AuthProvider())\n            .build();\n    }\n\n    private void handlePrivileged(AuthProvider authProvider) {\n        if (privileged(authProvider)) {\n            authProviderService.enable(authProvider.getMetadata().getName())\n                .block(BLOCKING_TIMEOUT);\n        }\n    }\n\n    private boolean privileged(AuthProvider authProvider) {\n        return BooleanUtils.TRUE.equals(MetadataUtil.nullSafeLabels(authProvider)\n            .get(AuthProvider.PRIVILEGED_LABEL));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/CategoryReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;\n\nimport java.time.Duration;\nimport java.util.Set;\nimport lombok.AllArgsConstructor;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.content.CategoryService;\nimport run.halo.app.content.permalinks.CategoryPermalinkPolicy;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.event.post.CategoryHiddenStateChangeEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * Reconciler for {@link Category}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class CategoryReconciler implements Reconciler<Reconciler.Request> {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    static final String FINALIZER_NAME = \"category-protection\";\n    private final ExtensionClient client;\n    private final CategoryPermalinkPolicy categoryPermalinkPolicy;\n    private final CategoryService categoryService;\n    private final ApplicationEventPublisher eventPublisher;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Category.class, request.name())\n            .ifPresent(category -> {\n                if (ExtensionUtil.isDeleted(category)) {\n                    if (removeFinalizers(category.getMetadata(), Set.of(FINALIZER_NAME))) {\n                        refreshHiddenState(category, false);\n                        updateCategoryForPost(category.getMetadata().getName());\n                        client.update(category);\n                    }\n                    return;\n                }\n                addFinalizers(category.getMetadata(), Set.of(FINALIZER_NAME));\n\n                populatePermalinkPattern(category);\n                populatePermalink(category);\n                checkHiddenState(category);\n\n                client.update(category);\n            });\n        return Result.doNotRetry();\n    }\n\n    private void checkHiddenState(Category category) {\n        final boolean hidden = categoryService.isCategoryHidden(category.getMetadata().getName())\n            .blockOptional(BLOCKING_TIMEOUT)\n            .orElse(false);\n        refreshHiddenState(category, hidden);\n    }\n\n    /**\n     * TODO move this logic to before-create/update hook in the future see {@code gh-4343}.\n     */\n    private void refreshHiddenState(Category category, boolean hidden) {\n        category.getSpec().setHideFromList(hidden);\n        if (isHiddenStateChanged(category)) {\n            publishHiddenStateChangeEvent(category);\n        }\n        var children = category.getSpec().getChildren();\n        if (CollectionUtils.isEmpty(children)) {\n            return;\n        }\n        for (String childName : children) {\n            client.fetch(Category.class, childName)\n                .ifPresent(child -> {\n                    child.getSpec().setHideFromList(hidden);\n                    if (isHiddenStateChanged(child)) {\n                        publishHiddenStateChangeEvent(child);\n                    }\n                    client.update(child);\n                });\n        }\n    }\n\n    private void publishHiddenStateChangeEvent(Category category) {\n        var hidden = category.getSpec().isHideFromList();\n        nullSafeAnnotations(category).put(Category.LAST_HIDDEN_STATE_ANNO, String.valueOf(hidden));\n        eventPublisher.publishEvent(new CategoryHiddenStateChangeEvent(this,\n            category.getMetadata().getName(), hidden));\n    }\n\n    boolean isHiddenStateChanged(Category category) {\n        var lastHiddenState = nullSafeAnnotations(category).get(Category.LAST_HIDDEN_STATE_ANNO);\n        return !String.valueOf(category.getSpec().isHideFromList()).equals(lastHiddenState);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Category())\n            .build();\n    }\n\n    void populatePermalinkPattern(Category category) {\n        var annotations = nullSafeAnnotations(category);\n        if (!annotations.containsKey(Constant.PERMALINK_PATTERN_ANNO)) {\n            var newPattern = categoryPermalinkPolicy.pattern();\n            annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);\n        }\n    }\n\n    void populatePermalink(Category category) {\n        category.getStatusOrDefault()\n            .setPermalink(categoryPermalinkPolicy.permalink(category));\n    }\n\n    private void updateCategoryForPost(String categoryName) {\n        var posts = client.listAll(Post.class, ListOptions.builder()\n            .fieldQuery(Queries.equal(\"spec.categories\", categoryName))\n            .build(), Sort.by(\"metadata.creationTimestamp\", \"metadata.name\")\n        );\n        for (Post post : posts) {\n            var categoryNames = post.getSpec().getCategories();\n            if (!CollectionUtils.isEmpty(categoryNames)) {\n                categoryNames.remove(categoryName);\n            }\n            client.update(post);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/CommentReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.isDeleted;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.comment.ReplyNotificationSubscriptionHelper;\nimport run.halo.app.content.comment.ReplyService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.event.post.CommentCreatedEvent;\nimport run.halo.app.event.post.CommentUnreadReplyCountChangedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * Reconciler for {@link Comment}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class CommentReconciler implements Reconciler<Reconciler.Request> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    public static final String FINALIZER_NAME = \"comment-protection\";\n    private final ExtensionClient client;\n    private final SchemeManager schemeManager;\n    private final ReplyService replyService;\n    private final ApplicationEventPublisher eventPublisher;\n\n    private final ReplyNotificationSubscriptionHelper replyNotificationSubscriptionHelper;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Comment.class, request.name())\n            .ifPresent(comment -> {\n                if (isDeleted(comment)) {\n                    if (removeFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) {\n                        cleanUpResources(comment);\n                        client.update(comment);\n                    }\n                    return;\n                }\n                if (addFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) {\n                    replyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment);\n                    client.update(comment);\n                    eventPublisher.publishEvent(new CommentCreatedEvent(this, comment));\n                }\n\n                compatibleCreationTime(comment);\n                Comment.CommentStatus status = comment.getStatusOrDefault();\n                status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);\n\n                updateUnReplyCountIfNecessary(comment);\n                updateSameSubjectRefCommentCounter(comment);\n\n                // version + 1 is required to truly equal version\n                // as a version will be incremented after the update\n                comment.getStatusOrDefault()\n                    .setObservedVersion(comment.getMetadata().getVersion() + 1);\n\n                client.update(comment);\n            });\n        return new Result(false, null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        var extension = new Comment();\n        return builder\n            .extension(extension)\n            .syncAllListOptions(ListOptions.builder()\n                .andQuery(equal(Comment.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, true))\n                .build())\n            .build();\n    }\n\n    /**\n     * If the comment creation time is null, set it to the approved time or the current time.\n     * TODO remove this method in the future and fill in attributes in hook mode instead.\n     */\n    void compatibleCreationTime(Comment comment) {\n        var creationTime = comment.getSpec().getCreationTime();\n        if (creationTime == null) {\n            creationTime = defaultIfNull(comment.getSpec().getApprovedTime(),\n                comment.getMetadata().getCreationTimestamp());\n        }\n        comment.getSpec().setCreationTime(creationTime);\n    }\n\n    private void updateUnReplyCountIfNecessary(Comment comment) {\n        Instant lastReadTime = comment.getSpec().getLastReadTime();\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(comment);\n        String lastReadTimeAnno = annotations.get(Constant.LAST_READ_TIME_ANNO);\n        if (lastReadTime != null && lastReadTime.toString().equals(lastReadTimeAnno)) {\n            return;\n        }\n        // delegate to other handler though event\n        String commentName = comment.getMetadata().getName();\n        eventPublisher.publishEvent(new CommentUnreadReplyCountChangedEvent(this, commentName));\n        // handled flag\n        if (lastReadTime != null) {\n            annotations.put(Constant.LAST_READ_TIME_ANNO, lastReadTime.toString());\n        } else {\n            annotations.remove(Constant.LAST_READ_TIME_ANNO);\n        }\n    }\n\n    private void updateSameSubjectRefCommentCounter(Comment comment) {\n        var commentSubjectRef = comment.getSpec().getSubjectRef();\n        var totalCount = countTotalComments(commentSubjectRef);\n        var approvedTotalCount = countApprovedComments(commentSubjectRef);\n        var findScheme = schemeManager.schemes().stream()\n            .filter(scheme -> {\n                var gvk = scheme.groupVersionKind();\n                return Objects.equals(gvk.group(), commentSubjectRef.getGroup())\n                    && Objects.equals(gvk.kind(), commentSubjectRef.getKind());\n            })\n            .findFirst();\n        findScheme.ifPresent(scheme -> {\n            String counterName =\n                MeterUtils.nameOf(commentSubjectRef.getGroup(), scheme.plural(),\n                    commentSubjectRef.getName());\n            client.fetch(Counter.class, counterName).ifPresentOrElse(counter -> {\n                counter.setTotalComment(totalCount);\n                counter.setApprovedComment(approvedTotalCount);\n                client.update(counter);\n            }, () -> {\n                Counter counter = Counter.emptyCounter(counterName);\n                counter.setTotalComment(totalCount);\n                counter.setApprovedComment(approvedTotalCount);\n                client.create(counter);\n            });\n        });\n    }\n\n    int countTotalComments(Ref commentSubjectRef) {\n        var totalListOptions = new ListOptions();\n        totalListOptions.setFieldSelector(FieldSelector.of(getBaseQuery(commentSubjectRef)));\n        return (int) client.listBy(Comment.class, totalListOptions, PageRequestImpl.ofSize(1))\n            .getTotal();\n    }\n\n    int countApprovedComments(Ref commentSubjectRef) {\n        var approvedListOptions = new ListOptions();\n        approvedListOptions.setFieldSelector(FieldSelector.of(and(\n            getBaseQuery(commentSubjectRef),\n            equal(\"spec.approved\", BooleanUtils.TRUE)\n        )));\n        return (int) client.listBy(Comment.class, approvedListOptions, PageRequestImpl.ofSize(1))\n            .getTotal();\n    }\n\n    private static Condition getBaseQuery(Ref commentSubjectRef) {\n        return and(equal(\"spec.subjectRef\", Comment.toSubjectRefKey(commentSubjectRef)),\n            isNull(\"metadata.deletionTimestamp\"));\n    }\n\n    private void cleanUpResources(Comment comment) {\n        // delete all replies under current comment\n        replyService.removeAllByComment(comment.getMetadata().getName()).block(BLOCKING_TIMEOUT);\n\n        // decrement total comment count\n        updateSameSubjectRefCommentCounter(comment);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/MenuItemReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport java.time.Duration;\nimport java.util.Objects;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.core.extension.MenuItem.MenuItemSpec;\nimport run.halo.app.core.extension.MenuItem.MenuItemStatus;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\n\n@Slf4j\n@Component\npublic class MenuItemReconciler implements Reconciler<Request> {\n\n    private final ExtensionClient client;\n\n    public MenuItemReconciler(ExtensionClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        return client.fetch(MenuItem.class, request.name())\n            .map(menuItem -> {\n                final var spec = menuItem.getSpec();\n\n                if (menuItem.getStatus() == null) {\n                    menuItem.setStatus(new MenuItemStatus());\n                }\n                var status = menuItem.getStatus();\n                var targetRef = spec.getTargetRef();\n                if (targetRef != null) {\n                    if (Ref.groupKindEquals(targetRef, Category.GVK)) {\n                        return handleCategoryRef(request.name(), status, targetRef);\n                    }\n                    if (Ref.groupKindEquals(targetRef, Tag.GVK)) {\n                        return handleTagRef(request.name(), status, targetRef);\n                    }\n                    if (Ref.groupKindEquals(targetRef, SinglePage.GVK)) {\n                        return handleSinglePageSpec(request.name(), status, targetRef);\n                    }\n                    if (Ref.groupKindEquals(targetRef, Post.GVK)) {\n                        return handlePostRef(request.name(), status, targetRef);\n                    }\n                    // unsupported ref\n                    log.error(\"Unsupported MenuItem targetRef \" + targetRef);\n                    return Result.doNotRetry();\n                } else {\n                    return handleMenuSpec(request.name(), status, spec);\n                }\n            }).orElseGet(() -> new Result(false, null));\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new MenuItem())\n            .build();\n    }\n\n    private Result handleCategoryRef(String menuItemName, MenuItemStatus status, Ref categoryRef) {\n        client.fetch(Category.class, categoryRef.getName())\n            .filter(category -> category.getStatus() != null)\n            .filter(category -> StringUtils.hasText(category.getStatus().getPermalink()))\n            .ifPresent(category -> {\n                status.setHref(category.getStatus().getPermalink());\n                status.setDisplayName(category.getSpec().getDisplayName());\n                updateStatus(menuItemName, status);\n            });\n        return new Result(true, Duration.ofMinutes(1));\n    }\n\n    private Result handleTagRef(String menuItemName, MenuItemStatus status, Ref tagRef) {\n        client.fetch(Tag.class, tagRef.getName()).filter(tag -> tag.getStatus() != null)\n            .filter(tag -> StringUtils.hasText(tag.getStatus().getPermalink())).ifPresent(tag -> {\n                status.setHref(tag.getStatus().getPermalink());\n                status.setDisplayName(tag.getSpec().getDisplayName());\n                updateStatus(menuItemName, status);\n            });\n        return new Result(true, Duration.ofMinutes(1));\n    }\n\n    private Result handlePostRef(String menuItemName, MenuItemStatus status, Ref postRef) {\n        client.fetch(Post.class, postRef.getName()).filter(post -> post.getStatus() != null)\n            .filter(post -> StringUtils.hasText(post.getStatus().getPermalink()))\n            .ifPresent(post -> {\n                status.setHref(post.getStatus().getPermalink());\n                status.setDisplayName(post.getSpec().getTitle());\n                updateStatus(menuItemName, status);\n            });\n        return new Result(true, Duration.ofMinutes(1));\n    }\n\n    private Result handleSinglePageSpec(String menuItemName, MenuItemStatus status, Ref pageRef) {\n        client.fetch(SinglePage.class, pageRef.getName())\n            .filter(page -> page.getStatus() != null)\n            .filter(page -> StringUtils.hasText(page.getStatus().getPermalink()))\n            .ifPresent(page -> {\n                status.setHref(page.getStatus().getPermalink());\n                status.setDisplayName(page.getSpec().getTitle());\n                updateStatus(menuItemName, status);\n            });\n        return new Result(true, Duration.ofMinutes(1));\n    }\n\n    private Result handleMenuSpec(String menuItemName, MenuItemStatus status, MenuItemSpec spec) {\n        if (spec.getHref() != null && StringUtils.hasText(spec.getDisplayName())) {\n            status.setHref(spec.getHref());\n            status.setDisplayName(spec.getDisplayName());\n            updateStatus(menuItemName, status);\n        }\n        return new Result(false, null);\n    }\n\n    private void updateStatus(String menuItemName, MenuItemStatus status) {\n        client.fetch(MenuItem.class, menuItemName)\n            .filter(menuItem -> !Objects.deepEquals(menuItem.getStatus(), status))\n            .ifPresent(menuItem -> {\n                menuItem.setStatus(status);\n                client.update(menuItem);\n            });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/PluginReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static run.halo.app.core.extension.Plugin.PluginStatus.nullSafeConditions;\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;\nimport static run.halo.app.plugin.PluginConst.PLUGIN_PATH;\nimport static run.halo.app.plugin.PluginConst.RELOAD_ANNO;\nimport static run.halo.app.plugin.PluginConst.REQUEST_TO_UNLOAD_LABEL;\nimport static run.halo.app.plugin.PluginExtensionLoaderUtils.isSetting;\nimport static run.halo.app.plugin.PluginExtensionLoaderUtils.lookupExtensions;\nimport static run.halo.app.plugin.PluginUtils.generateFileName;\nimport static run.halo.app.plugin.PluginUtils.isDevelopmentMode;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.net.MalformedURLException;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.FileSystemNotFoundException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginWrapper;\nimport org.pf4j.RuntimeMode;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.ResourceUtils;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.Disposable;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.extension.controller.RequeueException;\nimport run.halo.app.infra.Condition;\nimport run.halo.app.infra.ConditionList;\nimport run.halo.app.infra.ConditionStatus;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.infra.utils.SettingUtils;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\nimport run.halo.app.plugin.OptionalDependentResolver;\nimport run.halo.app.plugin.PluginConst;\nimport run.halo.app.plugin.PluginProperties;\nimport run.halo.app.plugin.PluginService;\nimport run.halo.app.plugin.SpringPluginManager;\n\n/**\n * Plugin reconciler.\n *\n * @author guqing\n * @author johnniang\n * @since 2.0.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass PluginReconciler implements Reconciler<Request>, DisposableBean {\n    private static final String FINALIZER_NAME = \"plugin-protection\";\n\n    private static final Set<String> UNUSED_ANNOTATIONS =\n        Set.of(\"plugin.halo.run/dependents-snapshot\");\n\n    private final ExtensionClient client;\n\n    private final SpringPluginManager pluginManager;\n\n    private final PluginProperties pluginProperties;\n\n    private final PluginService pluginService;\n\n    private final ConcurrentMap<String, Disposable> pluginStartTasks = new ConcurrentHashMap<>();\n\n    private Scheduler scheduler = Schedulers.newBoundedElastic(\n        1, Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, \"plugin-starter\"\n    );\n\n    private Clock clock = Clock.systemUTC();\n\n    @Override\n    public void destroy() throws Exception {\n        pluginStartTasks.clear();\n        this.scheduler.dispose();\n    }\n\n    /**\n     * Only for testing.\n     *\n     * @param clock new clock.\n     */\n    void setClock(Clock clock) {\n        Assert.notNull(clock, \"clock must not be null\");\n        this.clock = clock;\n    }\n\n    /**\n     * Only for testing.\n     *\n     * @param scheduler new scheduler.\n     */\n    void setScheduler(Scheduler scheduler) {\n        Assert.notNull(scheduler, \"scheduler must not be null\");\n        this.scheduler = scheduler;\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        return client.fetch(Plugin.class, request.name())\n            .map(plugin -> {\n                if (ExtensionUtil.isDeleted(plugin)) {\n                    if (!checkDependents(plugin)) {\n                        client.update(plugin);\n                        // Check dependents every 10 seconds\n                        return Result.requeue(Duration.ofSeconds(10));\n                    }\n                    // CleanUp resources and remove finalizer.\n                    if (removeFinalizers(plugin.getMetadata(), Set.of(FINALIZER_NAME))) {\n                        cleanupResources(plugin);\n                        syncPluginState(plugin);\n                        client.update(plugin);\n                    }\n                    return Result.doNotRetry();\n                }\n                addFinalizers(plugin.getMetadata(), Set.of(FINALIZER_NAME));\n                removeUnusedAnnotations(plugin);\n\n                var status = plugin.getStatus();\n                if (status == null) {\n                    status = new Plugin.PluginStatus();\n                    plugin.setStatus(status);\n                }\n                if (status.getPhase() == null) {\n                    // reset phase to pending\n                    status.setPhase(Plugin.Phase.PENDING);\n                }\n                // init condition list if not exists\n                if (status.getConditions() == null) {\n                    status.setConditions(new ConditionList());\n                }\n\n                var steps = new LinkedList<Supplier<Result>>();\n                steps.add(() -> resolveLoadLocation(plugin));\n                steps.add(() -> loadOrReload(plugin));\n                steps.add(() -> createOrUpdateSetting(plugin));\n                steps.add(() -> createOrUpdateReverseProxy(plugin));\n                steps.add(() -> resolveStaticResources(plugin));\n                if (requestToEnable(plugin)) {\n                    steps.add(() -> enablePlugin(plugin));\n                } else {\n                    steps.add(() -> disablePlugin(plugin));\n                }\n\n                Result result = null;\n                try {\n                    for (var step : steps) {\n                        result = step.get();\n                        if (result != null) {\n                            break;\n                        }\n                    }\n                    return result;\n                } catch (Throwable e) {\n                    status.getConditions().addAndEvictFIFO(Condition.builder()\n                        .type(ConditionType.READY)\n                        .status(ConditionStatus.FALSE)\n                        .reason(ConditionReason.SYSTEM_ERROR)\n                        .message(e.getMessage())\n                        .lastTransitionTime(clock.instant())\n                        .build());\n                    status.setPhase(Plugin.Phase.UNKNOWN);\n                    throw e;\n                } finally {\n                    var pw = pluginManager.getPlugin(plugin.getMetadata().getName());\n                    if (pw != null) {\n                        status.setLastProbeState(pw.getPluginState());\n                    }\n                    client.update(plugin);\n                }\n            })\n            .orElseGet(Result::doNotRetry);\n    }\n\n    private void removeUnusedAnnotations(Plugin plugin) {\n        var annotations = plugin.getMetadata().getAnnotations();\n        if (annotations != null) {\n            UNUSED_ANNOTATIONS.forEach(annotations::remove);\n        }\n    }\n\n    private boolean checkDependents(Plugin plugin) {\n        var pluginId = plugin.getMetadata().getName();\n        var dependents = pluginManager.getDependents(pluginId);\n        if (CollectionUtils.isEmpty(dependents)) {\n            return true;\n        }\n        var status = plugin.statusNonNull();\n        var condition = Condition.builder()\n            .type(ConditionType.PROGRESSING)\n            .status(ConditionStatus.UNKNOWN)\n            .reason(ConditionReason.WAIT_FOR_DEPENDENTS_DELETED)\n            .message(\n                \"The plugin has dependents %s, please delete them first.\"\n                    .formatted(dependents.stream().map(PluginWrapper::getPluginId).toList())\n            )\n            .lastTransitionTime(clock.instant())\n            .build();\n        var conditions = nullSafeConditions(status);\n        removeConditionBy(conditions, ConditionType.INITIALIZED);\n        removeConditionBy(conditions, ConditionType.READY);\n        conditions.addAndEvictFIFO(condition);\n        status.setPhase(Plugin.Phase.UNKNOWN);\n        return false;\n    }\n\n    private void syncPluginState(Plugin plugin) {\n        var pluginName = plugin.getMetadata().getName();\n        var p = pluginManager.getPlugin(pluginName);\n        if (p != null) {\n            plugin.statusNonNull().setLastProbeState(p.getPluginState());\n        } else {\n            plugin.statusNonNull().setLastProbeState(null);\n        }\n    }\n\n    private static String requestToUnload(Plugin plugin) {\n        var labels = plugin.getMetadata().getLabels();\n        if (labels == null) {\n            return null;\n        }\n        return labels.get(REQUEST_TO_UNLOAD_LABEL);\n    }\n\n    private static boolean requestToReload(Plugin plugin) {\n        var annotations = plugin.getMetadata().getAnnotations();\n        return annotations != null && annotations.get(RELOAD_ANNO) != null;\n    }\n\n    private static void removeRequestToReload(Plugin plugin) {\n        var annotations = plugin.getMetadata().getAnnotations();\n        if (annotations != null) {\n            annotations.remove(RELOAD_ANNO);\n        }\n    }\n\n    private void cleanupResources(Plugin plugin) {\n        var pluginName = plugin.getMetadata().getName();\n        var reverseProxyName = buildReverseProxyName(pluginName);\n        log.info(\"Deleting reverse proxy {} for plugin {}\", reverseProxyName, pluginName);\n        client.fetch(ReverseProxy.class, reverseProxyName)\n            .ifPresent(reverseProxy -> {\n                client.delete(reverseProxy);\n                throw new RequeueException(Result.requeue(null),\n                    String.format(\"\"\"\n                        Waiting for reverse proxy %s to be deleted.\"\"\", reverseProxyName)\n                );\n            });\n        var settingName = plugin.getSpec().getSettingName();\n        if (StringUtils.isNotBlank(settingName)) {\n            log.info(\"Deleting settings {} for plugin {}\", settingName, pluginName);\n            client.fetch(Setting.class, settingName)\n                .ifPresent(setting -> {\n                    client.delete(setting);\n                    throw new RequeueException(Result.requeue(null), String.format(\"\"\"\n                        Waiting for setting %s to be deleted.\"\"\", settingName));\n                });\n        }\n        if (pluginManager.getPlugin(pluginName) != null) {\n            log.info(\"Deleting plugin {} in plugin manager.\", pluginName);\n            var deleted = pluginManager.deletePlugin(pluginName);\n            if (!deleted) {\n                log.warn(\"Failed to delete plugin {}\", pluginName);\n            }\n        }\n    }\n\n    private Result enablePlugin(Plugin plugin) {\n        // start the plugin\n        var pluginName = plugin.getMetadata().getName();\n        log.info(\"Starting plugin {}\", pluginName);\n        var status = plugin.getStatus();\n\n        // check if the parent plugin is started\n        var unstartedDependencies = pluginService.getRequiredDependencies(plugin, pw ->\n            pw == null || !PluginState.STARTED.equals(pw.getPluginState())\n        );\n        var conditions = status.getConditions();\n        if (!CollectionUtils.isEmpty(unstartedDependencies)) {\n            removeConditionBy(conditions, ConditionType.READY);\n            conditions.addAndEvictFIFO(Condition.builder()\n                .type(ConditionType.PROGRESSING)\n                .status(ConditionStatus.UNKNOWN)\n                .reason(ConditionReason.WAIT_FOR_DEPENDENCIES_STARTED)\n                .message(\"Wait for parent plugins \" + unstartedDependencies + \" to be started\")\n                .lastTransitionTime(clock.instant())\n                .build());\n            status.setPhase(Plugin.Phase.UNKNOWN);\n            return Result.requeue(Duration.ofSeconds(5));\n        }\n\n        var current = pluginManager.getPlugin(pluginName);\n        if (current == null) {\n            conditions.addAndEvictFIFO(Condition.builder()\n                .type(ConditionType.READY)\n                .status(ConditionStatus.FALSE)\n                .reason(ConditionReason.START_ERROR)\n                .message(\"Plugin \" + pluginName + \" is not loaded.\")\n                .lastTransitionTime(clock.instant())\n                .build());\n            status.setPhase(Plugin.Phase.FAILED);\n            removeStartTaskIfPresent(pluginName);\n            return Result.doNotRetry();\n        }\n        var pluginState = current.getPluginState();\n        if (pluginState.isStarted()) {\n            removeConditionBy(conditions, ConditionType.PROGRESSING);\n            status.setLastStartTime(clock.instant());\n            conditions.addAndEvictFIFO(Condition.builder()\n                .type(ConditionType.READY)\n                .status(ConditionStatus.TRUE)\n                .reason(ConditionReason.STARTED)\n                .message(\"Started successfully\")\n                .lastTransitionTime(clock.instant())\n                .build());\n            status.setPhase(Plugin.Phase.STARTED);\n            removeStartTaskIfPresent(pluginName);\n            requestToReloadPluginsOptionallyDependentOn(pluginName);\n            return Result.doNotRetry();\n        }\n        if (pluginState.isFailed()) {\n            var t = current.getFailedException();\n            log.debug(\"Error occurred when starting plugin {}\", pluginName, t);\n            var writer = new StringWriter();\n            t.printStackTrace(new PrintWriter(writer));\n            conditions.addAndEvictFIFO(Condition.builder()\n                .type(ConditionType.READY)\n                .status(ConditionStatus.FALSE)\n                .reason(ConditionReason.START_ERROR)\n                .message(writer.toString())\n                .lastTransitionTime(clock.instant())\n                .build());\n            status.setPhase(Plugin.Phase.FAILED);\n            removeStartTaskIfPresent(pluginName);\n            return Result.doNotRetry();\n        }\n        if (!Plugin.Phase.STARTING.equals(status.getPhase())) {\n            pluginStartTasks.compute(pluginName, (name, old) -> {\n                if (old != null && !old.isDisposed()) {\n                    log.info(\"Cancelling old starting task for plugin {}.\", name);\n                    old.dispose();\n                    log.info(\"Cancelled old starting task for plugin {}.\", name);\n                }\n                return scheduler.schedule(() -> {\n                    log.info(\"Starting plugin {} in background thread.\", name);\n                    try {\n                        var state = pluginManager.startPlugin(name);\n                        log.info(\"Plugin {} started with state {}.\", name, state);\n                    } catch (Throwable t) {\n                        var pluginWrapper = pluginManager.getPlugin(name);\n                        if (pluginWrapper != null) {\n                            pluginWrapper.setPluginState(PluginState.FAILED);\n                            pluginWrapper.setFailedException(t);\n                        }\n                    }\n                });\n            });\n            status.setPhase(Plugin.Phase.STARTING);\n            conditions.addAndEvictFIFO(Condition.builder()\n                .type(ConditionType.PROGRESSING)\n                .status(ConditionStatus.TRUE)\n                .reason(ConditionReason.STARTING)\n                .message(\"Starting plugin \" + pluginName)\n                .lastTransitionTime(clock.instant())\n                .build());\n        } else {\n            log.debug(\"Plugin {} is starting...\", pluginName);\n        }\n        return Result.requeue(Duration.ofSeconds(2));\n    }\n\n    void requestToReloadPluginsOptionallyDependentOn(String pluginName) {\n        var startedPlugins = pluginManager.startedPlugins()\n            .stream()\n            .map(PluginWrapper::getDescriptor)\n            .toList();\n        var resolver = new OptionalDependentResolver(startedPlugins);\n        var dependents = resolver.getOptionalDependents(pluginName);\n        for (String dependentName : dependents) {\n            client.fetch(Plugin.class, dependentName)\n                .ifPresent(childPlugin -> {\n                    var annotations = MetadataUtil.nullSafeAnnotations(childPlugin);\n                    // loadLocation never be null for started plugins\n                    annotations.put(RELOAD_ANNO,\n                        childPlugin.getStatus().getLoadLocation().toString());\n                    client.update(childPlugin);\n                });\n        }\n    }\n\n    private Result disablePlugin(Plugin plugin) {\n        var pluginName = plugin.getMetadata().getName();\n        var status = plugin.getStatus();\n        if (pluginManager.getPlugin(pluginName) != null) {\n            // check if the plugin has children\n            var dependents = pluginManager.getDependents(pluginName)\n                .stream()\n                .filter(pw -> PluginState.STARTED.equals(pw.getPluginState()))\n                .map(PluginWrapper::getPluginId)\n                .toList();\n            var conditions = status.getConditions();\n            if (!CollectionUtils.isEmpty(dependents)) {\n                removeConditionBy(conditions, ConditionType.READY);\n                conditions.addAndEvictFIFO(Condition.builder()\n                    .type(ConditionType.PROGRESSING)\n                    .status(ConditionStatus.UNKNOWN)\n                    .reason(ConditionReason.WAIT_FOR_DEPENDENTS_DISABLED)\n                    .message(\"Wait for children plugins \" + dependents + \" to be disabled\")\n                    .lastTransitionTime(clock.instant())\n                    .build());\n                status.setPhase(Plugin.Phase.DISABLING);\n                return Result.requeue(Duration.ofSeconds(1));\n            }\n            try {\n                // First, stop starting task if exists\n                removeStartTaskIfPresent(pluginName);\n                pluginManager.disablePlugin(pluginName);\n            } catch (Throwable e) {\n                log.error(\"Error occurred when disabling plugin {}\", pluginName, e);\n                conditions.addAndEvictFIFO(Condition.builder()\n                    .type(ConditionType.READY)\n                    .status(ConditionStatus.FALSE)\n                    .reason(ConditionReason.DISABLE_ERROR)\n                    .message(e.getMessage())\n                    .lastTransitionTime(clock.instant())\n                    .build());\n                status.setPhase(Plugin.Phase.FAILED);\n                return Result.doNotRetry();\n            }\n        }\n        var conditions = plugin.getStatus().getConditions();\n        removeConditionBy(conditions, ConditionType.PROGRESSING);\n        conditions.addAndEvictFIFO(Condition.builder()\n            .type(ConditionType.READY)\n            .status(ConditionStatus.TRUE)\n            .reason(ConditionReason.DISABLED)\n            .lastTransitionTime(clock.instant())\n            .build());\n        plugin.statusNonNull().setPhase(Plugin.Phase.DISABLED);\n        return null;\n    }\n\n    private static boolean requestToEnable(Plugin plugin) {\n        var enabled = plugin.getSpec().getEnabled();\n        return enabled != null && enabled;\n    }\n\n    private Result resolveStaticResources(Plugin plugin) {\n        var pluginName = plugin.getMetadata().getName();\n        var pluginVersion = plugin.getSpec().getVersion();\n        if (isDevelopmentMode(plugin)) {\n            // when we are in dev mode, the plugin version is not always changed.\n            pluginVersion = String.valueOf(clock.instant().toEpochMilli());\n        }\n        var status = plugin.statusNonNull();\n        var specLogo = plugin.getSpec().getLogo();\n        if (StringUtils.isNotBlank(specLogo)) {\n            log.info(\"Resolving logo resource for plugin {}\", pluginName);\n            // the logo might be:\n            // 1. URL\n            // 2. relative path to \"resources\" folder\n            // 3. base64 format data image\n            var logo = specLogo;\n            if (!specLogo.startsWith(\"data:image\")) {\n                try {\n                    logo = new URL(specLogo).toString();\n                } catch (MalformedURLException ignored) {\n                    // indicate the logo is a path\n                    logo = UriComponentsBuilder.newInstance()\n                        .pathSegment(\"plugins\", pluginName, \"assets\")\n                        .path(specLogo)\n                        .queryParam(\"version\", pluginVersion)\n                        .build(true)\n                        .toString();\n                }\n            }\n            status.setLogo(logo);\n        }\n\n        log.info(\"Resolving main.js and style.css for plugin {}\", pluginName);\n        var p = pluginManager.getPlugin(pluginName);\n        var classLoader = p.getPluginClassLoader();\n        var resLoader = new DefaultResourceLoader(classLoader);\n        var entryRes = resLoader.getResource(\"classpath:console/main.js\");\n        var cssRes = resLoader.getResource(\"classpath:console/style.css\");\n        if (entryRes.exists()) {\n            var entry = UriComponentsBuilder.newInstance()\n                .pathSegment(\"plugins\", pluginName, \"assets\", \"console\", \"main.js\")\n                .queryParam(\"version\", pluginVersion)\n                .build(true)\n                .toString();\n            status.setEntry(entry);\n        }\n        if (cssRes.exists()) {\n            var stylesheet = UriComponentsBuilder.newInstance()\n                .pathSegment(\"plugins\", pluginName, \"assets\", \"console\", \"style.css\")\n                .queryParam(\"version\", pluginVersion)\n                .build(true)\n                .toString();\n            status.setStylesheet(stylesheet);\n        }\n        return null;\n    }\n\n    private Result loadOrReload(Plugin plugin) {\n        var pluginName = plugin.getMetadata().getName();\n        var p = pluginManager.getPlugin(pluginName);\n        var conditions = plugin.getStatus().getConditions();\n\n        var requestToUnloadBy = requestToUnload(plugin);\n        var requestToUnload = requestToUnloadBy != null;\n        var notFullyLoaded = p != null && pluginManager.getUnresolvedPlugins().contains(p);\n        var alreadyLoaded = p != null && pluginManager.getResolvedPlugins().contains(p);\n\n        var requestToReload = requestToReload(plugin);\n        // TODO Check load location\n        var shouldUnload = requestToUnload || requestToReload || notFullyLoaded;\n        if (shouldUnload) {\n            // check if the plugin is already loaded or not fully loaded.\n            if (alreadyLoaded || notFullyLoaded) {\n                // get all dependencies\n                var dependents = requestToUnloadChildren(pluginName);\n                if (!CollectionUtils.isEmpty(dependents)) {\n                    removeConditionBy(conditions, ConditionType.READY);\n                    conditions.addAndEvictFIFO(Condition.builder()\n                        .type(ConditionType.PROGRESSING)\n                        .status(ConditionStatus.UNKNOWN)\n                        .reason(ConditionReason.WAIT_FOR_DEPENDENTS_UNLOADED)\n                        .message(\"Wait for children plugins \" + dependents + \"to be unloaded\")\n                        .lastTransitionTime(clock.instant())\n                        .build());\n                    plugin.getStatus().setPhase(Plugin.Phase.UNKNOWN);\n                    // wait for children plugins unloaded\n                    // retry after 1 second\n                    return Result.requeue(Duration.ofSeconds(1));\n                }\n\n                // unload the plugin exactly\n                pluginManager.unloadPlugin(pluginName);\n\n                removeConditionBy(conditions, ConditionType.INITIALIZED);\n                removeConditionBy(conditions, ConditionType.PROGRESSING);\n                removeConditionBy(conditions, ConditionType.READY);\n\n                cancelUnloadRequest(pluginName);\n                p = null;\n            }\n\n            // ensure removing the reload annotation after the plugin is unloaded\n            if (requestToUnload) {\n                // skip loading and wait for removing the annotation by other plugins.\n                var status = plugin.getStatus();\n                status.getConditions().addAndEvictFIFO(Condition.builder()\n                    .type(ConditionType.INITIALIZED)\n                    .status(ConditionStatus.FALSE)\n                    .reason(ConditionReason.REQUEST_TO_UNLOAD)\n                    .message(\"Request to unload by \" + requestToUnloadBy)\n                    .lastTransitionTime(clock.instant())\n                    .build());\n                return Result.doNotRetry();\n            }\n\n            if (requestToReload) {\n                removeRequestToReload(plugin);\n            }\n        }\n\n        // check dependencies before loading\n        var unresolvedParentPlugins = pluginService.getRequiredDependencies(plugin,\n            pw -> pw == null || pluginManager.getUnresolvedPlugins().contains(pw)\n        );\n        if (!unresolvedParentPlugins.isEmpty()) {\n            // requeue if the parent plugin is not loaded yet.\n            removeConditionBy(conditions, ConditionType.INITIALIZED);\n            removeConditionBy(conditions, ConditionType.READY);\n            conditions.addAndEvictFIFO(Condition.builder()\n                .type(ConditionType.PROGRESSING)\n                .status(ConditionStatus.UNKNOWN)\n                .reason(ConditionReason.WAIT_FOR_DEPENDENCIES_LOADED)\n                .message(\"Wait for parent plugins \" + unresolvedParentPlugins + \" to be loaded\")\n                .lastTransitionTime(clock.instant())\n                .build());\n            plugin.getStatus().setPhase(Plugin.Phase.UNKNOWN);\n            return Result.requeue(Duration.ofSeconds(1));\n        }\n\n        if (p == null) {\n            var loadLocation = plugin.getStatus().getLoadLocation();\n            log.info(\"Loading plugin {} from {}\", pluginName, loadLocation);\n            pluginManager.loadPlugin(Paths.get(loadLocation));\n            plugin.getStatus().setPhase(Plugin.Phase.RESOLVED);\n            log.info(\"Loaded plugin {} from {}\", pluginName, loadLocation);\n            conditions.addAndEvictFIFO(Condition.builder()\n                .type(ConditionType.INITIALIZED)\n                .status(ConditionStatus.TRUE)\n                .reason(ConditionReason.LOADED)\n                .lastTransitionTime(clock.instant())\n                .build());\n        }\n\n        return null;\n    }\n\n    private Result createOrUpdateSetting(Plugin plugin) {\n        log.info(\"Initializing setting and config map for plugin {}\",\n            plugin.getMetadata().getName());\n        var settingName = plugin.getSpec().getSettingName();\n        if (StringUtils.isBlank(settingName)) {\n            // do nothing if no setting name provided.\n            return null;\n        }\n\n        var pluginName = plugin.getMetadata().getName();\n        var p = pluginManager.getPlugin(pluginName);\n        var resources = lookupExtensions(p.getPluginClassLoader());\n        var loader = new YamlUnstructuredLoader(resources);\n        var setting = loader.load().stream()\n            .filter(isSetting(settingName))\n            .findFirst()\n            .map(u -> Unstructured.OBJECT_MAPPER.convertValue(u, Setting.class))\n            .orElseThrow(() -> new IllegalStateException(String.format(\"\"\"\n                    Setting name %s was provided but setting extension \\\n                    was not found in plugin %s.\"\"\",\n                settingName, pluginName)));\n\n        client.fetch(Setting.class, settingName)\n            .ifPresentOrElse(oldSetting -> {\n                // overwrite the setting\n                var version = oldSetting.getMetadata().getVersion();\n                setting.getMetadata().setVersion(version);\n                // TODO Remove this line in the future\n                removeFinalizers(setting.getMetadata(), Set.of(\"plugin-protector\"));\n                client.update(setting);\n            }, () -> client.create(setting));\n\n        log.info(\"Initialized setting {} for plugin {}\", settingName, pluginName);\n\n        // create default config map\n        var configMapName = plugin.getSpec().getConfigMapName();\n        if (StringUtils.isBlank(configMapName)) {\n            return null;\n        }\n\n        var defaultConfigMap = SettingUtils.populateDefaultConfig(setting, configMapName);\n\n        client.fetch(ConfigMap.class, configMapName)\n            .ifPresentOrElse(configMap -> {\n                // merge data\n                var oldData = configMap.getData();\n                var defaultData = defaultConfigMap.getData();\n                var mergedData = SettingUtils.mergePatch(oldData, defaultData);\n                configMap.setData(mergedData);\n                client.update(configMap);\n            }, () -> client.create(defaultConfigMap));\n        log.info(\"Initialized config map {} for plugin {}\", configMapName, pluginName);\n        return null;\n    }\n\n    private Result resolveLoadLocation(Plugin plugin) {\n        log.debug(\"Resolving load location for plugin {}\", plugin.getMetadata().getName());\n\n        // populate load location from annotations\n        var pluginName = plugin.getMetadata().getName();\n        var annotations = nullSafeAnnotations(plugin);\n        var pluginPathAnno = annotations.get(PLUGIN_PATH);\n        var status = plugin.statusNonNull();\n        if (isDevelopmentMode(plugin)) {\n            if (!isInDevEnvironment()) {\n                status.getConditions().addAndEvictFIFO(Condition.builder()\n                    .type(ConditionType.INITIALIZED)\n                    .status(ConditionStatus.FALSE)\n                    .reason(ConditionReason.INVALID_RUNTIME_MODE)\n                    .message(\"\"\"\n                        Cannot run the plugin with development mode in non-development environment.\\\n                        \"\"\")\n                    .lastTransitionTime(clock.instant())\n                    .build());\n                status.setPhase(Plugin.Phase.UNKNOWN);\n                return Result.doNotRetry();\n            }\n            log.debug(\"Plugin {} is in development mode\", pluginName);\n            if (StringUtils.isBlank(pluginPathAnno)) {\n                status.getConditions().addAndEvictFIFO(Condition.builder()\n                    .type(ConditionType.INITIALIZED)\n                    .status(ConditionStatus.FALSE)\n                    .reason(ConditionReason.PLUGIN_PATH_NOT_SET)\n                    .message(\"\"\"\n                        Plugin path annotation is not set. \\\n                        Please set plugin path annotation \"%s\" in development mode.\\\n                        \"\"\".formatted(PLUGIN_PATH))\n                    .build());\n                return Result.doNotRetry();\n            }\n            try {\n                var loadLocation = ResourceUtils.getURL(pluginPathAnno).toURI();\n                if (!Objects.equals(status.getLoadLocation(), loadLocation)) {\n                    log.debug(\"Populated load location {} for plugin {} from annotation {}\",\n                        loadLocation, pluginName, pluginPathAnno);\n                    status.setLoadLocation(loadLocation);\n                    status.setPhase(Plugin.Phase.RESOLVED);\n                    status.getConditions().addAndEvictFIFO(Condition.builder()\n                        .type(ConditionType.INITIALIZED)\n                        .status(ConditionStatus.TRUE)\n                        .reason(ConditionReason.LOAD_LOCATION_RESOLVED)\n                        .lastTransitionTime(clock.instant())\n                        .build());\n                    log.debug(\"Populated load location {} for plugin {}\",\n                        status.getLoadLocation(), pluginName\n                    );\n                }\n            } catch (URISyntaxException | FileNotFoundException e) {\n                // TODO Refactor this using event in the future.\n                var condition = Condition.builder()\n                    .type(ConditionType.INITIALIZED)\n                    .status(ConditionStatus.FALSE)\n                    .reason(ConditionReason.INVALID_PLUGIN_PATH)\n                    .message(\"Invalid plugin path \" + pluginPathAnno + \" configured.\")\n                    .lastTransitionTime(clock.instant())\n                    .build();\n                status.getConditions().addAndEvictFIFO(condition);\n                status.setPhase(Plugin.Phase.UNKNOWN);\n                return Result.doNotRetry();\n            }\n        } else {\n            // reset annotation PLUGIN_PATH in non-dev mode\n            var pluginFilename = generateFileName(plugin);\n            var pluginRoot = pluginManager.getPluginsRoots().stream()\n                .filter(root -> Files.exists(root.resolve(pluginFilename)))\n                .findFirst()\n                .orElse(null);\n            if (pluginRoot == null) {\n                var condition = Condition.builder()\n                    .type(ConditionType.INITIALIZED)\n                    .status(ConditionStatus.FALSE)\n                    .reason(ConditionReason.INVALID_PLUGIN_PATH)\n                    .message(\"Cannot find plugin file \" + pluginFilename + \" in plugins roots.\")\n                    .lastTransitionTime(clock.instant())\n                    .build();\n                status.getConditions().addAndEvictFIFO(condition);\n                status.setPhase(Plugin.Phase.UNKNOWN);\n                return Result.doNotRetry();\n            }\n            var pluginPath = pluginRoot.resolve(pluginFilename);\n            annotations.put(PLUGIN_PATH, pluginRoot.relativize(pluginPath).toString());\n\n            // delete old load location if changed.\n            var oldLoadLocation = status.getLoadLocation();\n            var newLoadLocation = pluginPath.toUri();\n            if (oldLoadLocation != null && !Objects.equals(oldLoadLocation, newLoadLocation)) {\n                // delete the old load location\n                log.info(\"Deleting old plugin file {} for plugin {}, and new load location is {}.\",\n                    oldLoadLocation, pluginName, newLoadLocation);\n                try {\n                    var deleted = Files.deleteIfExists(Path.of(oldLoadLocation));\n                    if (deleted) {\n                        log.info(\"Deleted old plugin file {} for plugin {}.\",\n                            oldLoadLocation, pluginName);\n                    }\n                } catch (IOException e) {\n                    log.warn(\"Failed to delete old plugin file {} for plugin {}\",\n                        oldLoadLocation, pluginName, e);\n                } catch (FileSystemNotFoundException e) {\n                    log.warn(\n                        \"Failed to delete old plugin file {} for plugin {}: File system not found.\",\n                        oldLoadLocation, pluginName, e);\n                }\n                status.setPhase(Plugin.Phase.RESOLVED);\n                status.getConditions().addAndEvictFIFO(Condition.builder()\n                    .type(ConditionType.INITIALIZED)\n                    .status(ConditionStatus.TRUE)\n                    .reason(ConditionReason.LOAD_LOCATION_RESOLVED)\n                    .lastTransitionTime(clock.instant())\n                    .build());\n                log.debug(\"Populated load location {} for plugin {}\",\n                    status.getLoadLocation(), pluginName\n                );\n            }\n            status.setLoadLocation(newLoadLocation);\n        }\n        return null;\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Plugin())\n            .syncAllOnStart(true)\n            .build();\n    }\n\n    private void removeStartTaskIfPresent(String pluginName) {\n        pluginStartTasks.computeIfPresent(pluginName, (name, disposable) -> {\n            if (!disposable.isDisposed()) {\n                log.info(\"Cancelling starting task for plugin {}.\", name);\n                disposable.dispose();\n                log.info(\"Cancelled starting task for plugin {}.\", name);\n            }\n            return null;\n        });\n    }\n\n    private Result createOrUpdateReverseProxy(Plugin plugin) {\n        String pluginName = plugin.getMetadata().getName();\n        String reverseProxyName = buildReverseProxyName(pluginName);\n        ReverseProxy reverseProxy = new ReverseProxy();\n        reverseProxy.setMetadata(new Metadata());\n        reverseProxy.getMetadata().setName(reverseProxyName);\n        // put label to identify this reverse\n        reverseProxy.getMetadata().setLabels(new HashMap<>());\n        reverseProxy.getMetadata().getLabels().put(PluginConst.PLUGIN_NAME_LABEL_NAME, pluginName);\n\n        reverseProxy.setRules(new ArrayList<>());\n\n        String logo = plugin.getSpec().getLogo();\n        if (StringUtils.isNotBlank(logo) && !PathUtils.isAbsoluteUri(logo)) {\n            ReverseProxy.ReverseProxyRule logoRule = new ReverseProxy.ReverseProxyRule(logo,\n                new ReverseProxy.FileReverseProxyProvider(null, logo));\n            reverseProxy.getRules().add(logoRule);\n        }\n\n        client.fetch(ReverseProxy.class, reverseProxyName)\n            .ifPresentOrElse(persisted -> {\n                reverseProxy.getMetadata()\n                    .setVersion(persisted.getMetadata().getVersion());\n                client.update(reverseProxy);\n            }, () -> client.create(reverseProxy));\n        return null;\n    }\n\n    private boolean isInDevEnvironment() {\n        return RuntimeMode.DEVELOPMENT.equals(pluginProperties.getRuntimeMode());\n    }\n\n    static String buildReverseProxyName(String pluginName) {\n        return pluginName + \"-system-generated-reverse-proxy\";\n    }\n\n    private List<String> requestToUnloadChildren(String pluginName) {\n        // get all dependencies\n        var dependents = pluginManager.getDependents(pluginName)\n            .stream()\n            .map(PluginWrapper::getPluginId)\n            .toList();\n        // request all dependents to reload.\n        dependents.forEach(dependent -> client.fetch(Plugin.class, dependent)\n            .ifPresent(childPlugin -> {\n                var labels = childPlugin.getMetadata().getLabels();\n                if (labels == null) {\n                    labels = new HashMap<>();\n                    childPlugin.getMetadata().setLabels(labels);\n                }\n                var label = labels.get(REQUEST_TO_UNLOAD_LABEL);\n                if (!pluginName.equals(label)) {\n                    labels.put(REQUEST_TO_UNLOAD_LABEL, pluginName);\n                    client.update(childPlugin);\n                }\n            }));\n        return dependents;\n    }\n\n    private void cancelUnloadRequest(String pluginName) {\n        // remove label REQUEST_TO_UNLOAD_LABEL\n        // TODO Use index mechanism\n        Predicate<Plugin> filter = aplugin -> {\n            var labels = aplugin.getMetadata().getLabels();\n            return labels != null && pluginName.equals(labels.get(REQUEST_TO_UNLOAD_LABEL));\n        };\n\n        client.list(Plugin.class, filter, null)\n            .forEach(aplugin -> {\n                var labels = aplugin.getMetadata().getLabels();\n                if (labels != null && labels.remove(REQUEST_TO_UNLOAD_LABEL) != null) {\n                    client.update(aplugin);\n                }\n            });\n\n    }\n\n    private static void removeConditionBy(ConditionList conditions, String type) {\n        conditions.removeIf(condition -> Objects.equals(type, condition.getType()));\n    }\n\n    public static class ConditionType {\n        /**\n         * Indicates whether the plugin is initialized.\n         */\n        public static final String INITIALIZED = \"Initialized\";\n\n        /**\n         * Indicates whether the plugin is starting, disabling or deleting.\n         */\n        public static final String PROGRESSING = \"Progressing\";\n\n        /**\n         * Indicates whether the plugin is ready.\n         */\n        public static final String READY = \"Ready\";\n    }\n\n    public static class ConditionReason {\n        public static final String LOAD_LOCATION_RESOLVED = \"LoadLocationResolved\";\n        public static final String INVALID_PLUGIN_PATH = \"InvalidPluginPath\";\n\n        public static final String WAIT_FOR_DEPENDENCIES_STARTED = \"WaitForDependenciesStarted\";\n        public static final String WAIT_FOR_DEPENDENCIES_LOADED = \"WaitForDependenciesLoaded\";\n\n        public static final String WAIT_FOR_DEPENDENTS_DELETED = \"WaitForDependentsDeleted\";\n        public static final String WAIT_FOR_DEPENDENTS_DISABLED = \"WaitForDependentsDisabled\";\n        public static final String WAIT_FOR_DEPENDENTS_UNLOADED = \"WaitForDependentsUnloaded\";\n\n        public static final String STARTING = \"Starting\";\n        public static final String STARTED = \"Started\";\n        public static final String DISABLED = \"Disabled\";\n        public static final String SYSTEM_ERROR = \"SystemError\";\n        public static final String REQUEST_TO_UNLOAD = \"RequestToUnload\";\n        public static final String LOADED = \"Loaded\";\n        public static final String START_ERROR = \"StartError\";\n        public static final String DISABLE_ERROR = \"DisableError\";\n        public static final String INVALID_RUNTIME_MODE = \"InvalidRuntimeMode\";\n        public static final String PLUGIN_PATH_NOT_SET = \"PluginPathNotSet\";\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/PostCounterReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static run.halo.app.extension.index.query.Queries.startsWith;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.event.post.PostStatsChangedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n@Component\n@RequiredArgsConstructor\npublic class PostCounterReconciler implements Reconciler<Reconciler.Request> {\n\n    private final ApplicationEventPublisher eventPublisher;\n    private final ExtensionClient client;\n\n    @Override\n    public Result reconcile(Request request) {\n        if (!isSameAsPost(request.name())) {\n            return Result.doNotRetry();\n        }\n        client.fetch(Counter.class, request.name()).ifPresent(counter -> {\n            eventPublisher.publishEvent(new PostStatsChangedEvent(this, counter));\n        });\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        var extension = new Counter();\n        return builder\n            .extension(extension)\n            .syncAllListOptions(ListOptions.builder()\n                .andQuery(startsWith(\"metadata.name\", MeterUtils.nameOf(Post.class, \"\")))\n                .build())\n            .build();\n    }\n\n    static boolean isSameAsPost(String name) {\n        return name.startsWith(MeterUtils.nameOf(Post.class, \"\"));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/PostReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.apache.commons.lang3.BooleanUtils.TRUE;\nimport static org.apache.commons.lang3.BooleanUtils.isFalse;\nimport static org.apache.commons.lang3.BooleanUtils.isTrue;\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;\nimport static run.halo.app.extension.MetadataUtil.nullSafeLabels;\nimport static run.halo.app.extension.index.query.Queries.in;\n\nimport com.google.common.hash.Hashing;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.HashSet;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jsoup.Jsoup;\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.CategoryService;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.ExcerptGenerator;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.content.PostService;\nimport run.halo.app.content.comment.CommentService;\nimport run.halo.app.content.permalinks.PostPermalinkPolicy;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Post.PostPhase;\nimport run.halo.app.core.extension.content.Post.VisibleEnum;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.event.post.PostDeletedEvent;\nimport run.halo.app.event.post.PostPublishedEvent;\nimport run.halo.app.event.post.PostUnpublishedEvent;\nimport run.halo.app.event.post.PostUpdatedEvent;\nimport run.halo.app.event.post.PostVisibleChangedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionOperator;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequeueException;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.Condition;\nimport run.halo.app.infra.ConditionStatus;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * <p>Reconciler for {@link Post}.</p>\n *\n * <p>things to do:</p>\n * <ul>\n * 1. generate permalink\n * 2. generate excerpt if auto generate is enabled\n * </ul>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@AllArgsConstructor\n@Component\npublic class PostReconciler implements Reconciler<Reconciler.Request> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private static final String FINALIZER_NAME = \"post-protection\";\n    private final ExtensionClient client;\n    private final PostService postService;\n    private final PostPermalinkPolicy postPermalinkPolicy;\n    private final CounterService counterService;\n    private final CommentService commentService;\n    private final CategoryService categoryService;\n    private final ExtensionGetter extensionGetter;\n\n    private final ApplicationEventPublisher eventPublisher;\n    private final NotificationCenter notificationCenter;\n\n    @Override\n    public Result reconcile(Request request) {\n        var events = new LinkedHashSet<ApplicationEvent>();\n        client.fetch(Post.class, request.name())\n            .ifPresent(post -> {\n                if (ExtensionOperator.isDeleted(post)) {\n                    removeFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));\n                    unPublishPost(post, events);\n                    events.add(new PostDeletedEvent(this, post));\n                    cleanUpResources(post);\n                    // update post to be able to be collected by gc collector.\n                    client.update(post);\n                    // fire event after updating post\n                    events.forEach(eventPublisher::publishEvent);\n                    return;\n                }\n                addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));\n\n                populateLabels(post, events);\n\n                schedulePublishIfNecessary(post);\n\n                subscribeNewCommentNotification(post);\n\n                var status = post.getStatus();\n                if (status == null) {\n                    status = new Post.PostStatus();\n                    post.setStatus(status);\n                }\n\n                if (post.isPublished() && post.getSpec().getPublishTime() == null) {\n                    post.getSpec().setPublishTime(Instant.now());\n                }\n\n                // calculate the sha256sum\n                var configSha256sum = Hashing.sha256().hashString(post.getSpec().toString(), UTF_8)\n                    .toString();\n\n                var annotations = nullSafeAnnotations(post);\n                var oldConfigChecksum = annotations.get(Constant.CHECKSUM_CONFIG_ANNO);\n                if (!Objects.equals(oldConfigChecksum, configSha256sum)) {\n                    // if the checksum doesn't match\n                    events.add(new PostUpdatedEvent(this, post.getMetadata().getName()));\n                    annotations.put(Constant.CHECKSUM_CONFIG_ANNO, configSha256sum);\n                }\n\n                if (shouldUnPublish(post)) {\n                    unPublishPost(post, events);\n                } else {\n                    publishPost(post, events);\n                }\n\n                if (!annotations.containsKey(Constant.PERMALINK_PATTERN_ANNO)) {\n                    // only set the permalink pattern if not present\n                    var permalinkPattern = postPermalinkPolicy.pattern();\n                    annotations.put(Constant.PERMALINK_PATTERN_ANNO, permalinkPattern);\n                }\n                status.setPermalink(postPermalinkPolicy.permalink(post));\n                if (status.getPhase() == null) {\n                    status.setPhase(PostPhase.DRAFT.toString());\n                }\n\n                var excerpt = post.getSpec().getExcerpt();\n                if (excerpt == null) {\n                    excerpt = new Post.Excerpt();\n                }\n                var isAutoGenerate = defaultIfNull(excerpt.getAutoGenerate(), true);\n                if (isAutoGenerate) {\n                    status.setExcerpt(getExcerpt(post));\n                } else {\n                    status.setExcerpt(excerpt.getRaw());\n                }\n\n                var ref = Ref.of(post);\n                // handle contributors\n                var headSnapshot = post.getSpec().getHeadSnapshot();\n                var contributors = listSnapshots(ref)\n                    .stream()\n                    .map(snapshot -> {\n                        Set<String> usernames = snapshot.getSpec().getContributors();\n                        return Objects.requireNonNullElseGet(usernames,\n                            () -> new HashSet<String>());\n                    })\n                    .flatMap(Set::stream)\n                    .distinct()\n                    .sorted()\n                    .toList();\n                status.setContributors(contributors);\n\n                // update in progress status\n                status.setInProgress(\n                    !StringUtils.equals(headSnapshot, post.getSpec().getReleaseSnapshot()));\n\n                computeHiddenState(post);\n\n                // version + 1 is required to truly equal version\n                // as a version will be incremented after the update\n                status.setObservedVersion(post.getMetadata().getVersion() + 1);\n                client.update(post);\n\n                // fire event after updating post\n                events.forEach(eventPublisher::publishEvent);\n            });\n        return Result.doNotRetry();\n    }\n\n    private void computeHiddenState(Post post) {\n        var categories = post.getSpec().getCategories();\n        if (categories == null) {\n            post.getStatusOrDefault().setHideFromList(false);\n            return;\n        }\n        var hidden = categories.stream()\n            .anyMatch(categoryName -> categoryService.isCategoryHidden(categoryName)\n                .blockOptional(BLOCKING_TIMEOUT).orElse(false)\n            );\n        post.getStatusOrDefault().setHideFromList(hidden);\n    }\n\n    private void populateLabels(Post post, Set<ApplicationEvent> events) {\n        var labels = nullSafeLabels(post);\n        labels.put(Post.DELETED_LABEL, String.valueOf(isTrue(post.getSpec().getDeleted())));\n\n        var expectVisible = defaultIfNull(post.getSpec().getVisible(), VisibleEnum.PUBLIC);\n        var oldVisible = VisibleEnum.from(labels.get(Post.VISIBLE_LABEL));\n        if (!Objects.equals(oldVisible, expectVisible)) {\n            var postName = post.getMetadata().getName();\n            events.add(new PostVisibleChangedEvent(this, postName, oldVisible, expectVisible));\n        }\n        labels.put(Post.VISIBLE_LABEL, expectVisible.toString());\n\n        var ownerName = post.getSpec().getOwner();\n        if (StringUtils.isNotBlank(ownerName)) {\n            labels.put(Post.OWNER_LABEL, ownerName);\n        }\n\n        var publishTime = post.getSpec().getPublishTime();\n        if (publishTime != null) {\n            labels.put(Post.ARCHIVE_YEAR_LABEL, HaloUtils.getYearText(publishTime));\n            labels.put(Post.ARCHIVE_MONTH_LABEL, HaloUtils.getMonthText(publishTime));\n            labels.put(Post.ARCHIVE_DAY_LABEL, HaloUtils.getDayText(publishTime));\n        }\n\n        if (!labels.containsKey(Post.PUBLISHED_LABEL)) {\n            labels.put(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);\n        }\n    }\n\n    private static boolean shouldUnPublish(Post post) {\n        return isTrue(post.getSpec().getDeleted()) || isFalse(post.getSpec().getPublish());\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Post())\n            .syncAllListOptions(ListOptions.builder()\n                .andQuery(Queries.equal(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, true))\n                .build())\n            .build();\n    }\n\n    void schedulePublishIfNecessary(Post post) {\n        var labels = nullSafeLabels(post);\n        // ensure the label is removed\n        labels.remove(Post.SCHEDULING_PUBLISH_LABEL);\n\n        final var now = Instant.now();\n        var publishTime = post.getSpec().getPublishTime();\n        if (post.isPublished() || publishTime == null) {\n            return;\n        }\n\n        // expect to publish in the future\n        if (isTrue(post.getSpec().getPublish()) && publishTime.isAfter(now)) {\n            labels.put(Post.SCHEDULING_PUBLISH_LABEL, TRUE);\n            // update post changes before requeue\n            client.update(post);\n\n            throw new RequeueException(Result.requeue(Duration.between(now, publishTime)),\n                \"Requeue for scheduled publish.\");\n        }\n    }\n\n    void subscribeNewCommentNotification(Post post) {\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(post.getSpec().getOwner());\n\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_POST);\n        interestReason.setExpression(\n            \"props.postOwner == '%s'\".formatted(post.getSpec().getOwner()));\n        notificationCenter.subscribe(subscriber, interestReason).block(BLOCKING_TIMEOUT);\n    }\n\n    private void publishPost(Post post, Set<ApplicationEvent> events) {\n        var expectReleaseSnapshot = post.getSpec().getReleaseSnapshot();\n        if (StringUtils.isBlank(expectReleaseSnapshot)) {\n            // Do nothing if release snapshot is not set\n            return;\n        }\n        var annotations = post.getMetadata().getAnnotations();\n        var lastReleaseSnapshot = annotations.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);\n        if (post.isPublished()\n            && Objects.equals(expectReleaseSnapshot, lastReleaseSnapshot)) {\n            // If the release snapshot is not change\n            return;\n        }\n        var status = post.getStatus();\n        // validate the release snapshot\n        var snapshot = client.fetch(Snapshot.class, expectReleaseSnapshot);\n        if (snapshot.isEmpty()) {\n            Condition condition = Condition.builder()\n                .type(PostPhase.FAILED.name())\n                .reason(\"SnapshotNotFound\")\n                .message(\n                    String.format(\"Snapshot [%s] not found for publish\", expectReleaseSnapshot))\n                .status(ConditionStatus.FALSE)\n                .lastTransitionTime(Instant.now())\n                .build();\n            status.getConditionsOrDefault().addAndEvictFIFO(condition);\n            status.setPhase(PostPhase.FAILED.name());\n            return;\n        }\n        annotations.put(Post.LAST_RELEASED_SNAPSHOT_ANNO, expectReleaseSnapshot);\n        status.setPhase(PostPhase.PUBLISHED.toString());\n        var condition = Condition.builder()\n            .type(PostPhase.PUBLISHED.name())\n            .reason(\"Published\")\n            .message(\"Post published successfully.\")\n            .lastTransitionTime(Instant.now())\n            .status(ConditionStatus.TRUE)\n            .build();\n        status.getConditionsOrDefault().addAndEvictFIFO(condition);\n        var labels = post.getMetadata().getLabels();\n        labels.put(Post.PUBLISHED_LABEL, Boolean.TRUE.toString());\n        if (post.getSpec().getPublishTime() == null) {\n            // TODO Set the field in creation hook in the future.\n            post.getSpec().setPublishTime(Instant.now());\n        }\n        status.setLastModifyTime(snapshot.get().getSpec().getLastModifyTime());\n        events.add(new PostPublishedEvent(this, post.getMetadata().getName()));\n    }\n\n    void unPublishPost(Post post, Set<ApplicationEvent> events) {\n        if (!post.isPublished()) {\n            return;\n        }\n        var labels = post.getMetadata().getLabels();\n        labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString());\n        final var status = post.getStatus();\n\n        var condition = new Condition();\n        condition.setType(\"CancelledPublish\");\n        condition.setStatus(ConditionStatus.TRUE);\n        condition.setReason(condition.getType());\n        condition.setMessage(\"CancelledPublish\");\n        condition.setLastTransitionTime(Instant.now());\n        status.getConditionsOrDefault().addAndEvictFIFO(condition);\n\n        status.setPhase(PostPhase.DRAFT.toString());\n\n        events.add(new PostUnpublishedEvent(this, post.getMetadata().getName()));\n    }\n\n    private void cleanUpResources(Post post) {\n        // clean up snapshots\n        final Ref ref = Ref.of(post);\n        listSnapshots(ref).forEach(client::delete);\n\n        // clean up comments\n        commentService.removeBySubject(ref).block(BLOCKING_TIMEOUT);\n\n        // delete counter\n        counterService.deleteByName(MeterUtils.nameOf(Post.class, post.getMetadata().getName()))\n            .block(BLOCKING_TIMEOUT);\n    }\n\n    private String getExcerpt(Post post) {\n        Optional<ContentWrapper> contentWrapper =\n            postService.getContent(post.getSpec().getReleaseSnapshot(),\n                    post.getSpec().getBaseSnapshot())\n                .blockOptional(BLOCKING_TIMEOUT);\n        if (contentWrapper.isEmpty()) {\n            return StringUtils.EMPTY;\n        }\n        var content = contentWrapper.get();\n\n        if (StringUtils.isAnyBlank(content.getContent(), content.getRaw())) {\n            return StringUtils.EMPTY;\n        }\n\n        var contentChecksum = Hashing.sha256().hashString(content.getContent(), UTF_8).toString();\n        var annotations = MetadataUtil.nullSafeAnnotations(post);\n        var oldChecksum = annotations.get(Constant.CONTENT_CHECKSUM_ANNO);\n        if (Objects.equals(oldChecksum, contentChecksum)) {\n            return post.getStatusOrDefault().getExcerpt();\n        }\n        // update the checksum and generate new excerpt\n        annotations.put(Constant.CONTENT_CHECKSUM_ANNO, contentChecksum);\n\n        var tags = listTagDisplayNames(post);\n\n        var keywords = new HashSet<>(tags);\n        keywords.add(post.getSpec().getTitle());\n\n        var context = new ExcerptGenerator.Context()\n            .setRaw(content.getRaw())\n            .setContent(content.getContent())\n            .setRawType(content.getRawType())\n            .setKeywords(keywords)\n            .setMaxLength(160);\n        return extensionGetter.getEnabledExtension(ExcerptGenerator.class)\n            .defaultIfEmpty(new DefaultExcerptGenerator())\n            .flatMap(generator -> generator.generate(context))\n            .onErrorResume(Throwable.class, e -> {\n                log.error(\"Failed to generate excerpt for post [{}]\",\n                    post.getMetadata().getName(), e);\n                return Mono.empty();\n            })\n            .blockOptional(BLOCKING_TIMEOUT)\n            .orElse(StringUtils.EMPTY);\n    }\n\n    private Set<String> listTagDisplayNames(Post post) {\n        return Optional.ofNullable(post.getSpec().getTags())\n            .map(tags -> client.listAll(Tag.class, ListOptions.builder()\n                .fieldQuery(in(\"metadata.name\", tags))\n                .build(), Sort.unsorted())\n            )\n            .stream()\n            .flatMap(List::stream)\n            .map(tag -> tag.getSpec().getDisplayName())\n            .collect(Collectors.toSet());\n    }\n\n    static class DefaultExcerptGenerator implements ExcerptGenerator {\n        @Override\n        public Mono<String> generate(Context context) {\n            String shortHtmlContent = StringUtils.substring(context.getContent(), 0, 500);\n            String text = Jsoup.parse(shortHtmlContent).text();\n            return Mono.just(StringUtils.substring(text, 0, 150));\n        }\n    }\n\n    List<Snapshot> listSnapshots(Ref ref) {\n        var snapshotListOptions = new ListOptions();\n        snapshotListOptions.setFieldSelector(FieldSelector.of(\n            Queries.equal(\"spec.subjectRef\", Snapshot.toSubjectRefKey(ref))));\n        return client.listAll(Snapshot.class, snapshotListOptions, Sort.unsorted());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/ReplyReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.util.Set;\nimport lombok.AllArgsConstructor;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.comment.ReplyNotificationSubscriptionHelper;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.event.post.ReplyChangedEvent;\nimport run.halo.app.event.post.ReplyCreatedEvent;\nimport run.halo.app.event.post.ReplyDeletedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n/**\n * Reconciler for {@link Reply}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class ReplyReconciler implements Reconciler<Reconciler.Request> {\n    protected static final String FINALIZER_NAME = \"reply-protection\";\n\n    private final ExtensionClient client;\n    private final ApplicationEventPublisher eventPublisher;\n\n    private final ReplyNotificationSubscriptionHelper replyNotificationSubscriptionHelper;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Reply.class, request.name())\n            .ifPresent(reply -> {\n                if (reply.getMetadata().getDeletionTimestamp() != null) {\n                    cleanUpResourcesAndRemoveFinalizer(request.name());\n                    return;\n                }\n                if (addFinalizers(reply.getMetadata(), Set.of(FINALIZER_NAME))) {\n                    replyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply);\n                    client.update(reply);\n                    eventPublisher.publishEvent(new ReplyCreatedEvent(this, reply));\n                }\n\n                if (reply.getSpec().getCreationTime() == null) {\n                    reply.getSpec().setCreationTime(\n                        defaultIfNull(reply.getSpec().getApprovedTime(),\n                            reply.getMetadata().getCreationTimestamp()\n                        )\n                    );\n                }\n\n                // version + 1 is required to truly equal version\n                // as a version will be incremented after the update\n                reply.getStatus().setObservedVersion(reply.getMetadata().getVersion() + 1);\n\n                client.update(reply);\n\n                eventPublisher.publishEvent(new ReplyChangedEvent(this, reply));\n            });\n        return new Result(false, null);\n    }\n\n    private void cleanUpResourcesAndRemoveFinalizer(String replyName) {\n        client.fetch(Reply.class, replyName).ifPresent(reply -> {\n            if (reply.getMetadata().getFinalizers() != null) {\n                reply.getMetadata().getFinalizers().remove(FINALIZER_NAME);\n            }\n            client.update(reply);\n\n            // on reply removing\n            eventPublisher.publishEvent(new ReplyDeletedEvent(this, reply));\n        });\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        var extension = new Reply();\n        return builder\n            .extension(extension)\n            .syncAllListOptions(ListOptions.builder()\n                .andQuery(equal(Reply.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, true))\n                .build())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/ReverseProxyReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.plugin.PluginConst;\nimport run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;\n\n/**\n * Reconciler for {@link ReverseProxy}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class ReverseProxyReconciler implements Reconciler<Reconciler.Request> {\n    private static final String FINALIZER_NAME = \"reverse-proxy-protection\";\n    private final ExtensionClient client;\n    private final ReverseProxyRouterFunctionRegistry routerFunctionRegistry;\n\n    public ReverseProxyReconciler(ExtensionClient client,\n        ReverseProxyRouterFunctionRegistry routerFunctionRegistry) {\n        this.client = client;\n        this.routerFunctionRegistry = routerFunctionRegistry;\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        return client.fetch(ReverseProxy.class, request.name())\n            .map(reverseProxy -> {\n                if (isDeleted(reverseProxy)) {\n                    cleanUpResourcesAndRemoveFinalizer(request.name());\n                    return new Result(false, null);\n                }\n                addFinalizerIfNecessary(reverseProxy);\n                registerReverseProxy(reverseProxy);\n                return new Result(false, null);\n            })\n            .orElse(new Result(false, null));\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new ReverseProxy())\n            .build();\n    }\n\n    private void registerReverseProxy(ReverseProxy reverseProxy) {\n        String pluginId = getPluginId(reverseProxy);\n        routerFunctionRegistry.register(pluginId, reverseProxy);\n    }\n\n    private void cleanUpResources(ReverseProxy reverseProxy) {\n        String pluginId = getPluginId(reverseProxy);\n        routerFunctionRegistry.remove(pluginId, reverseProxy.getMetadata().getName());\n    }\n\n    private void addFinalizerIfNecessary(ReverseProxy oldReverseProxy) {\n        Set<String> finalizers = oldReverseProxy.getMetadata().getFinalizers();\n        if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {\n            return;\n        }\n        client.fetch(ReverseProxy.class, oldReverseProxy.getMetadata().getName())\n            .ifPresent(reverseProxy -> {\n                Set<String> newFinalizers = reverseProxy.getMetadata().getFinalizers();\n                if (newFinalizers == null) {\n                    newFinalizers = new HashSet<>();\n                    reverseProxy.getMetadata().setFinalizers(newFinalizers);\n                }\n                newFinalizers.add(FINALIZER_NAME);\n                client.update(reverseProxy);\n            });\n    }\n\n    private void cleanUpResourcesAndRemoveFinalizer(String name) {\n        client.fetch(ReverseProxy.class, name).ifPresent(reverseProxy -> {\n            cleanUpResources(reverseProxy);\n            if (reverseProxy.getMetadata().getFinalizers() != null) {\n                reverseProxy.getMetadata().getFinalizers().remove(FINALIZER_NAME);\n            }\n            client.update(reverseProxy);\n        });\n    }\n\n    private boolean isDeleted(ReverseProxy reverseProxy) {\n        return reverseProxy.getMetadata().getDeletionTimestamp() != null;\n    }\n\n    private String getPluginId(ReverseProxy reverseProxy) {\n        Map<String, String> labels = reverseProxy.getMetadata().getLabels();\n        if (labels == null) {\n            return PluginConst.SYSTEM_PLUGIN_NAME;\n        }\n        return Objects.toString(labels.get(PluginConst.PLUGIN_NAME_LABEL_NAME),\n            PluginConst.SYSTEM_PLUGIN_NAME);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/RoleReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static java.util.Objects.deepEquals;\n\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\n\n/**\n * Role reconcile.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\npublic class RoleReconciler implements Reconciler<Request> {\n\n    private final ExtensionClient client;\n\n    public RoleReconciler(ExtensionClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Role.class, request.name())\n            .ifPresent(role -> {\n                Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(role);\n                // override dependency rules to annotations\n                annotations.put(Role.ROLE_DEPENDENCY_RULES, \"[]\");\n                annotations.put(Role.UI_PERMISSIONS_AGGREGATED_ANNO, \"[]\");\n\n                updateLabelsAndAnnotations(role);\n            });\n        return new Result(false, null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Role())\n            .build();\n    }\n\n    private void updateLabelsAndAnnotations(Role role) {\n        var annotations = role.getMetadata().getAnnotations();\n        var labels = role.getMetadata().getLabels();\n        client.fetch(Role.class, role.getMetadata().getName())\n            .filter(freshRole -> !deepEquals(annotations, freshRole.getMetadata().getAnnotations())\n            || deepEquals(labels, freshRole.getMetadata().getLabels()))\n            .ifPresent(freshRole -> {\n                freshRole.getMetadata().setAnnotations(annotations);\n                freshRole.getMetadata().setLabels(labels);\n                client.update(freshRole);\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/SinglePageReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.springframework.web.util.UriUtils.encodePath;\n\nimport com.google.common.hash.Hashing;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jsoup.Jsoup;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.ExcerptGenerator;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.content.SinglePageService;\nimport run.halo.app.content.comment.CommentService;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionOperator;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.Condition;\nimport run.halo.app.infra.ConditionList;\nimport run.halo.app.infra.ConditionStatus;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * <p>Reconciler for {@link SinglePage}.</p>\n *\n * <p>things to do:</p>\n * <ul>\n * 1. generate permalink\n * 2. generate excerpt if auto generate is enabled\n * </ul>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@AllArgsConstructor\n@Component\npublic class SinglePageReconciler implements Reconciler<Reconciler.Request> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private static final String FINALIZER_NAME = \"single-page-protection\";\n    private final ExtensionClient client;\n    private final SinglePageService singlePageService;\n    private final CounterService counterService;\n    private final CommentService commentService;\n    private final ExtensionGetter extensionGetter;\n\n    private final ExternalUrlSupplier externalUrlSupplier;\n\n    private final NotificationCenter notificationCenter;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(SinglePage.class, request.name())\n            .ifPresent(singlePage -> {\n                if (ExtensionOperator.isDeleted(singlePage)) {\n                    cleanUpResourcesAndRemoveFinalizer(request.name());\n                    return;\n                }\n\n                if (ExtensionUtil.addFinalizers(singlePage.getMetadata(), Set.of(FINALIZER_NAME))) {\n                    client.update(singlePage);\n                }\n\n                subscribeNewCommentNotification(singlePage);\n\n                // reconcile spec first\n                reconcileSpec(request.name());\n                // then\n                reconcileMetadata(request.name());\n                reconcileStatus(request.name());\n            });\n        return new Result(false, null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new SinglePage())\n            .build();\n    }\n\n    void subscribeNewCommentNotification(SinglePage page) {\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(page.getSpec().getOwner());\n\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_PAGE);\n        interestReason.setExpression(\n            \"props.pageOwner == '%s'\".formatted(page.getSpec().getOwner()));\n        notificationCenter.subscribe(subscriber, interestReason).block(BLOCKING_TIMEOUT);\n    }\n\n    private void reconcileSpec(String name) {\n        client.fetch(SinglePage.class, name).ifPresent(page -> {\n            if (page.isPublished() && page.getSpec().getPublishTime() == null) {\n                page.getSpec().setPublishTime(Instant.now());\n            }\n\n            // un-publish if necessary\n            if (page.isPublished() && Objects.equals(false, page.getSpec().getPublish())) {\n                unPublish(name);\n                return;\n            }\n\n            try {\n                publishPage(name);\n            } catch (Throwable e) {\n                publishFailed(name, e);\n                throw e;\n            }\n        });\n    }\n\n    private void publishPage(String name) {\n        client.fetch(SinglePage.class, name)\n            .filter(page -> Objects.equals(true, page.getSpec().getPublish()))\n            .ifPresent(page -> {\n                Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(page);\n                String lastReleasedSnapshot = annotations.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);\n                String releaseSnapshot = page.getSpec().getReleaseSnapshot();\n                if (StringUtils.isBlank(releaseSnapshot)) {\n                    return;\n                }\n                // do nothing if release snapshot is not changed and page is published\n                if (page.isPublished()\n                    && StringUtils.equals(lastReleasedSnapshot, releaseSnapshot)) {\n                    return;\n                }\n                SinglePage.SinglePageStatus status = page.getStatusOrDefault();\n\n                // validate release snapshot\n                Optional<Snapshot> releasedSnapshotOpt =\n                    client.fetch(Snapshot.class, releaseSnapshot);\n                if (releasedSnapshotOpt.isEmpty()) {\n                    Condition condition = Condition.builder()\n                        .type(Post.PostPhase.FAILED.name())\n                        .reason(\"SnapshotNotFound\")\n                        .message(\n                            String.format(\"Snapshot [%s] not found for publish\", releaseSnapshot))\n                        .status(ConditionStatus.FALSE)\n                        .lastTransitionTime(Instant.now())\n                        .build();\n                    status.getConditionsOrDefault().addAndEvictFIFO(condition);\n                    status.setPhase(Post.PostPhase.FAILED.name());\n                    client.update(page);\n                    return;\n                }\n\n                // do publish\n                annotations.put(SinglePage.LAST_RELEASED_SNAPSHOT_ANNO, releaseSnapshot);\n                status.setPhase(Post.PostPhase.PUBLISHED.name());\n                Condition condition = Condition.builder()\n                    .type(Post.PostPhase.PUBLISHED.name())\n                    .reason(\"Published\")\n                    .message(\"SinglePage published successfully.\")\n                    .lastTransitionTime(Instant.now())\n                    .status(ConditionStatus.TRUE)\n                    .build();\n                status.getConditionsOrDefault().addAndEvictFIFO(condition);\n\n                SinglePage.changePublishedState(page, true);\n                if (page.getSpec().getPublishTime() == null) {\n                    page.getSpec().setPublishTime(Instant.now());\n                }\n\n                // populate lastModifyTime\n                status.setLastModifyTime(releasedSnapshotOpt.get().getSpec().getLastModifyTime());\n\n                client.update(page);\n            });\n    }\n\n    private void unPublish(String name) {\n        client.fetch(SinglePage.class, name).ifPresent(page -> {\n            final SinglePage oldPage = JsonUtils.deepCopy(page);\n\n            SinglePage.changePublishedState(page, false);\n            final SinglePage.SinglePageStatus status = page.getStatusOrDefault();\n\n            Condition condition = new Condition();\n            condition.setType(\"CancelledPublish\");\n            condition.setStatus(ConditionStatus.TRUE);\n            condition.setReason(condition.getType());\n            condition.setMessage(\"CancelledPublish\");\n            condition.setLastTransitionTime(Instant.now());\n            status.getConditionsOrDefault().addAndEvictFIFO(condition);\n\n            status.setPhase(Post.PostPhase.DRAFT.name());\n            if (!oldPage.equals(page)) {\n                client.update(page);\n            }\n        });\n    }\n\n    private void publishFailed(String name, Throwable error) {\n        Assert.notNull(name, \"Name must not be null\");\n        Assert.notNull(error, \"Error must not be null\");\n        client.fetch(SinglePage.class, name).ifPresent(page -> {\n            final SinglePage oldPage = JsonUtils.deepCopy(page);\n\n            SinglePage.SinglePageStatus status = page.getStatusOrDefault();\n            Post.PostPhase phase = Post.PostPhase.FAILED;\n            status.setPhase(phase.name());\n\n            final ConditionList conditions = status.getConditionsOrDefault();\n\n            Condition condition = Condition.builder()\n                .type(phase.name())\n                .reason(\"PublishFailed\")\n                .message(error.getMessage())\n                .lastTransitionTime(Instant.now())\n                .status(ConditionStatus.FALSE)\n                .build();\n            conditions.addAndEvictFIFO(condition);\n            page.setStatus(status);\n\n            if (!oldPage.equals(page)) {\n                client.update(page);\n            }\n        });\n    }\n\n    private void cleanUpResources(SinglePage singlePage) {\n        // clean up snapshot\n        Ref ref = Ref.of(singlePage);\n        listSnapshots(ref).forEach(client::delete);\n\n        // clean up comments\n        commentService.removeBySubject(ref).block(BLOCKING_TIMEOUT);\n\n        // delete counter for single page\n        counterService.deleteByName(\n                MeterUtils.nameOf(SinglePage.class, singlePage.getMetadata().getName()))\n            .block(BLOCKING_TIMEOUT);\n    }\n\n    private void cleanUpResourcesAndRemoveFinalizer(String pageName) {\n        client.fetch(SinglePage.class, pageName).ifPresent(singlePage -> {\n            cleanUpResources(singlePage);\n            if (singlePage.getMetadata().getFinalizers() != null) {\n                singlePage.getMetadata().getFinalizers().remove(FINALIZER_NAME);\n            }\n            client.update(singlePage);\n        });\n    }\n\n    private void reconcileMetadata(String name) {\n        client.fetch(SinglePage.class, name).ifPresent(singlePage -> {\n            final SinglePage oldPage = JsonUtils.deepCopy(singlePage);\n\n            SinglePage.SinglePageSpec spec = singlePage.getSpec();\n            // handle logic delete\n            Map<String, String> labels = MetadataUtil.nullSafeLabels(singlePage);\n            if (isDeleted(singlePage)) {\n                labels.put(SinglePage.DELETED_LABEL, Boolean.TRUE.toString());\n            } else {\n                labels.put(SinglePage.DELETED_LABEL, Boolean.FALSE.toString());\n            }\n            labels.put(SinglePage.VISIBLE_LABEL,\n                Objects.requireNonNullElse(spec.getVisible(), Post.VisibleEnum.PUBLIC).name());\n            labels.put(SinglePage.OWNER_LABEL, spec.getOwner());\n            if (!labels.containsKey(SinglePage.PUBLISHED_LABEL)) {\n                labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString());\n            }\n            if (!oldPage.equals(singlePage)) {\n                client.update(singlePage);\n            }\n        });\n    }\n\n    String createPermalink(SinglePage page) {\n        var permalink = encodePath(page.getSpec().getSlug(), UTF_8);\n        permalink = StringUtils.prependIfMissing(permalink, \"/\");\n        return externalUrlSupplier.get().resolve(permalink).normalize().toString();\n    }\n\n    private void reconcileStatus(String name) {\n        client.fetch(SinglePage.class, name).ifPresent(singlePage -> {\n            final SinglePage oldPage = JsonUtils.deepCopy(singlePage);\n\n            singlePage.getStatusOrDefault()\n                .setPermalink(createPermalink(singlePage));\n\n            SinglePage.SinglePageSpec spec = singlePage.getSpec();\n            SinglePage.SinglePageStatus status = singlePage.getStatusOrDefault();\n            if (status.getPhase() == null) {\n                status.setPhase(Post.PostPhase.DRAFT.name());\n            }\n\n            // handle excerpt\n            Post.Excerpt excerpt = spec.getExcerpt();\n            if (excerpt == null) {\n                excerpt = new Post.Excerpt();\n                excerpt.setAutoGenerate(true);\n                spec.setExcerpt(excerpt);\n            }\n\n            if (excerpt.getAutoGenerate()) {\n                status.setExcerpt(getExcerpt(singlePage));\n            } else {\n                status.setExcerpt(excerpt.getRaw());\n            }\n\n            // handle contributors\n            String headSnapshot = singlePage.getSpec().getHeadSnapshot();\n            List<String> contributors = listSnapshots(Ref.of(singlePage))\n                .stream()\n                .peek(snapshot -> {\n                    snapshot.getSpec().setContentPatch(StringUtils.EMPTY);\n                    snapshot.getSpec().setRawPatch(StringUtils.EMPTY);\n                })\n                .map(snapshot -> {\n                    Set<String> usernames = snapshot.getSpec().getContributors();\n                    return Objects.requireNonNullElseGet(usernames,\n                        () -> new HashSet<String>());\n                })\n                .flatMap(Set::stream)\n                .distinct()\n                .sorted()\n                .toList();\n            status.setContributors(contributors);\n\n            // update in progress status\n            String releaseSnapshot = singlePage.getSpec().getReleaseSnapshot();\n            status.setInProgress(!StringUtils.equals(releaseSnapshot, headSnapshot));\n\n            if (singlePage.isPublished() && status.getLastModifyTime() == null) {\n                client.fetch(Snapshot.class, singlePage.getSpec().getReleaseSnapshot())\n                    .ifPresent(releasedSnapshot ->\n                        status.setLastModifyTime(releasedSnapshot.getSpec().getLastModifyTime()));\n            }\n\n            if (!oldPage.equals(singlePage)) {\n                client.update(singlePage);\n            }\n        });\n    }\n\n    private String getExcerpt(SinglePage singlePage) {\n        Optional<ContentWrapper> contentWrapper =\n            singlePageService.getContent(singlePage.getSpec().getReleaseSnapshot(),\n                    singlePage.getSpec().getBaseSnapshot())\n                .blockOptional(BLOCKING_TIMEOUT);\n        if (contentWrapper.isEmpty()) {\n            return StringUtils.EMPTY;\n        }\n        var content = contentWrapper.get();\n\n        var contentChecksum = Hashing.sha256().hashString(content.getContent(), UTF_8).toString();\n        var annotations = MetadataUtil.nullSafeAnnotations(singlePage);\n        var oldChecksum = annotations.get(Constant.CONTENT_CHECKSUM_ANNO);\n        if (Objects.equals(oldChecksum, contentChecksum)) {\n            return singlePage.getStatusOrDefault().getExcerpt();\n        }\n        // update the checksum and generate new excerpt\n        annotations.put(Constant.CONTENT_CHECKSUM_ANNO, contentChecksum);\n\n        var context = new ExcerptGenerator.Context()\n            .setRaw(content.getRaw())\n            .setContent(content.getContent())\n            .setRaw(content.getRawType())\n            .setKeywords(Set.of())\n            .setMaxLength(160);\n        return extensionGetter.getEnabledExtension(ExcerptGenerator.class)\n            .defaultIfEmpty(new DefaultExcerptGenerator())\n            .flatMap(generator -> generator.generate(context))\n            .onErrorResume(Throwable.class, e -> {\n                log.error(\"Failed to generate excerpt for single page [{}]\",\n                    singlePage.getMetadata().getName(), e);\n                return Mono.empty();\n            })\n            .blockOptional(BLOCKING_TIMEOUT)\n            .orElse(StringUtils.EMPTY);\n    }\n\n    static class DefaultExcerptGenerator implements ExcerptGenerator {\n        @Override\n        public Mono<String> generate(Context context) {\n            String shortHtmlContent = StringUtils.substring(context.getContent(), 0, 500);\n            String text = Jsoup.parse(shortHtmlContent).text();\n            return Mono.just(StringUtils.substring(text, 0, 150));\n        }\n    }\n\n    private boolean isDeleted(SinglePage singlePage) {\n        return Objects.equals(true, singlePage.getSpec().getDeleted())\n            || singlePage.getMetadata().getDeletionTimestamp() != null;\n    }\n\n    List<Snapshot> listSnapshots(Ref ref) {\n        var snapshotListOptions = new ListOptions();\n        snapshotListOptions.setFieldSelector(FieldSelector.of(\n            Queries.equal(\"spec.subjectRef\", Snapshot.toSubjectRefKey(ref))));\n        return client.listAll(Snapshot.class, snapshotListOptions, Sort.unsorted());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/SystemConfigReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static java.util.Objects.requireNonNullElse;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.infra.utils.SystemConfigUtils.mergeMap;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport java.util.Map;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionMatcher;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.SystemConfigChangedEvent;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.SystemConfigUtils;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass SystemConfigReconciler implements Reconciler<Reconciler.Request> {\n\n    private final ExtensionClient client;\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    @Override\n    public Result reconcile(Request request) {\n        Assert.state(\n            Objects.equals(SystemSetting.SYSTEM_CONFIG, request.name()),\n            \"Only system config reconciler is supported to reconcile system config.\"\n        );\n        client.fetch(ConfigMap.class, request.name())\n            .ifPresent(configMap -> {\n                if (ExtensionUtil.isDeleted(configMap)) {\n                    log.warn(\"System config was attempted to be deleted\");\n                    return;\n                }\n                // calculate if the configMap has changed\n                // and publish event if changed\n                var dataSnapshot = SystemConfigUtils.getDataSnapshot(configMap);\n                if (SystemConfigUtils.populateChecksum(configMap)) {\n                    SystemConfigUtils.updateDataSnapshot(configMap);\n                    client.update(configMap);\n                    log.info(\"System config has been detected as changed\");\n                    eventPublisher.publishEvent(\n                        computeChangedEvent(configMap, dataSnapshot)\n                    );\n                }\n                // do nothing if not changed\n            });\n        return null;\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        ExtensionMatcher matcher = extension ->\n            Objects.equals(extension.getMetadata().getName(), SystemSetting.SYSTEM_CONFIG);\n        return builder.extension(new ConfigMap())\n            .syncAllOnStart(true)\n            .syncAllListOptions(ListOptions.builder()\n                .fieldQuery(equal(\"metadata.name\", SystemSetting.SYSTEM_CONFIG))\n                .build()\n            )\n            .onAddMatcher(matcher)\n            .onUpdateMatcher(matcher)\n            .onDeleteMatcher(matcher)\n            .build();\n    }\n\n    private SystemConfigChangedEvent computeChangedEvent(ConfigMap configMap,\n        @Nullable Map<String, String> oldData) {\n        return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT)\n            .map(defaultConfigMap -> {\n                var defaultData =\n                    requireNonNullElse(defaultConfigMap.getData(), Map.<String, String>of());\n                try {\n                    var mergedOldData = mergeMap(\n                        defaultData, requireNonNullElse(oldData, Map.of())\n                    );\n                    var mergedNewData = mergeMap(\n                        defaultData, requireNonNullElse(configMap.getData(), Map.of())\n                    );\n                    return new SystemConfigChangedEvent(this, mergedOldData, mergedNewData);\n                } catch (JsonProcessingException e) {\n                    throw new RuntimeException(e);\n                }\n            })\n            .orElseGet(() -> new SystemConfigChangedEvent(\n                this, oldData, requireNonNullElse(configMap.getData(), Map.of())\n            ));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/TagReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.permalinks.TagPermalinkPolicy;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n/**\n * Reconciler for {@link Tag}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class TagReconciler implements Reconciler<Reconciler.Request> {\n    static final String FINALIZER_NAME = \"tag-protection\";\n    private final ExtensionClient client;\n    private final TagPermalinkPolicy tagPermalinkPolicy;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Tag.class, request.name())\n            .ifPresent(tag -> {\n                if (ExtensionUtil.isDeleted(tag)) {\n                    if (removeFinalizers(tag.getMetadata(), Set.of(FINALIZER_NAME))) {\n                        client.update(tag);\n                    }\n                    return;\n                }\n\n                addFinalizers(tag.getMetadata(), Set.of(FINALIZER_NAME));\n\n                Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(tag);\n\n                if (!annotations.containsKey(Constant.PERMALINK_PATTERN_ANNO)) {\n                    var newPattern = tagPermalinkPolicy.pattern();\n                    annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);\n                }\n\n                var status = tag.getStatusOrDefault();\n                String permalink = tagPermalinkPolicy.permalink(tag);\n                status.setPermalink(permalink);\n\n                if (status.getPostCount() == null) {\n                    status.setPostCount(0);\n                }\n                if (status.getVisiblePostCount() == null) {\n                    status.setVisiblePostCount(0);\n                }\n\n                // Update the observed version.\n                status.setObservedVersion(tag.getMetadata().getVersion() + 1);\n\n                client.update(tag);\n            });\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Tag())\n            .syncAllListOptions(ListOptions.builder()\n                .andQuery(equal(Tag.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, true))\n                .build())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/ThemeReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.isDeleted;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\n\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.core.retry.RetryException;\nimport org.springframework.core.retry.RetryPolicy;\nimport org.springframework.core.retry.RetryTemplate;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.FileSystemUtils;\nimport org.springframework.util.backoff.FixedBackOff;\nimport reactor.core.Exceptions;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.infra.Condition;\nimport run.halo.app.infra.ConditionStatus;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.infra.exception.ThemeUninstallException;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.infra.utils.SettingUtils;\nimport run.halo.app.infra.utils.VersionUtils;\nimport run.halo.app.theme.TemplateEngineManager;\n\n/**\n * Reconciler for theme.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class ThemeReconciler implements Reconciler<Request> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private static final String FINALIZER_NAME = \"theme-protection\";\n\n    private final ExtensionClient client;\n\n    private final ThemeRootGetter themeRoot;\n    private final SystemVersionSupplier systemVersionSupplier;\n    private final TemplateEngineManager templateEngineManager;\n\n    private RetryTemplate retryTemplate = new RetryTemplate(RetryPolicy.builder()\n        .backOff(new FixedBackOff(300, 20))\n        .predicate(IllegalStateException.class::isInstance)\n        .build());\n\n    /**\n     * Set retry template. Only for testing purpose.\n     *\n     * @param retryTemplate the retry template\n     */\n    void setRetryTemplate(RetryTemplate retryTemplate) {\n        this.retryTemplate = retryTemplate;\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Theme.class, request.name())\n            .ifPresent(theme -> {\n                if (isDeleted(theme)) {\n                    if (removeFinalizers(theme.getMetadata(), Set.of(FINALIZER_NAME))) {\n                        cleanUpResources(theme);\n                        client.update(theme);\n                    }\n                    return;\n                }\n                addFinalizers(theme.getMetadata(), Set.of(FINALIZER_NAME));\n\n                themeSettingDefaultConfig(theme);\n                reconcileStatus(theme);\n                client.update(theme);\n            });\n        return new Result(false, null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Theme())\n            .build();\n    }\n\n    void reconcileStatus(Theme theme) {\n        var status = theme.getStatus();\n        if (status == null) {\n            status = new Theme.ThemeStatus();\n            theme.setStatus(status);\n        }\n        var name = theme.getMetadata().getName();\n        var themePath = themeRoot.get().resolve(name);\n        status.setLocation(themePath.toAbsolutePath().toString());\n\n        status.setPhase(Theme.ThemePhase.READY);\n        var conditionBuilder = Condition.builder()\n            .type(Theme.ThemePhase.READY.name())\n            .status(ConditionStatus.TRUE)\n            .reason(Theme.ThemePhase.READY.name())\n            .message(StringUtils.EMPTY)\n            .lastTransitionTime(Instant.now());\n\n        // Check if this theme version is match requires param.\n        var normalVersion = systemVersionSupplier.get().toStableVersion().toString();\n        var requires = theme.getSpec().getRequires();\n        if (!VersionUtils.satisfiesRequires(normalVersion, requires)) {\n            status.setPhase(Theme.ThemePhase.FAILED);\n            conditionBuilder\n                .type(Theme.ThemePhase.FAILED.name())\n                .status(ConditionStatus.FALSE)\n                .reason(\"UnsatisfiedRequiresVersion\")\n                .message(String.format(\n                    \"Theme requires a minimum system version of [%s], and you have [%s].\",\n                    requires, normalVersion));\n        }\n        Theme.nullSafeConditionList(theme).addAndEvictFIFO(conditionBuilder.build());\n    }\n\n    private void themeSettingDefaultConfig(Theme theme) {\n        var spec = theme.getSpec();\n        var settingName = spec.getSettingName();\n        if (StringUtils.isBlank(settingName)) {\n            return;\n        }\n        var configMapName = spec.getConfigMapName();\n        if (StringUtils.isBlank(configMapName)) {\n            configMapName = UUID.randomUUID().toString();\n        }\n        spec.setConfigMapName(configMapName);\n        SettingUtils.createOrUpdateConfigMap(client, settingName, configMapName);\n    }\n\n    private void cleanUpResources(Theme theme) {\n        reconcileThemeDeletion(theme);\n    }\n\n    private void reconcileThemeDeletion(Theme theme) {\n        templateEngineManager.clearCache(theme.getMetadata().getName()).block(BLOCKING_TIMEOUT);\n        // delete theme setting form\n        var settingName = theme.getSpec().getSettingName();\n        if (StringUtils.isNotBlank(settingName)) {\n            client.fetch(Setting.class, settingName).ifPresent(client::delete);\n            try {\n                retryTemplate.execute(() -> {\n                    client.fetch(Setting.class, settingName).ifPresent(setting -> {\n                        throw new IllegalStateException(\"Waiting for setting to be deleted.\");\n                    });\n                    return null;\n                });\n            } catch (RetryException e) {\n                throw Exceptions.propagate(e);\n            }\n        }\n        // delete annotation setting\n        deleteAnnotationSettings(theme.getMetadata().getName());\n        deleteThemeFiles(theme);\n    }\n\n    private void deleteAnnotationSettings(String themeName) {\n        var result = listAnnotationSettingsByThemeName(themeName);\n\n        for (AnnotationSetting annotationSetting : result) {\n            client.delete(annotationSetting);\n        }\n\n        try {\n            retryTemplate.execute(() -> {\n                var annotationSettings = listAnnotationSettingsByThemeName(themeName);\n                if (annotationSettings.isEmpty()) {\n                    return null;\n                }\n                throw new IllegalStateException(\"Waiting for annotation settings to be deleted.\");\n            });\n        } catch (RetryException e) {\n            throw Exceptions.propagate(e);\n        }\n    }\n\n    private List<AnnotationSetting> listAnnotationSettingsByThemeName(String themeName) {\n        return client.list(AnnotationSetting.class, annotationSetting -> {\n            Map<String, String> labels = MetadataUtil.nullSafeLabels(annotationSetting);\n            return themeName.equals(labels.get(Theme.THEME_NAME_LABEL));\n        }, null);\n    }\n\n    private void deleteThemeFiles(Theme theme) {\n        var themeDir = themeRoot.get().resolve(theme.getMetadata().getName());\n        try {\n            FileSystemUtils.deleteRecursively(themeDir);\n        } catch (IOException e) {\n            throw new ThemeUninstallException(\"Failed to delete theme files.\", e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/reconciler/UserReconciler.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.defaultSort;\nimport static run.halo.app.extension.ExtensionUtil.isDeleted;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.net.URI;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.UserConnection;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.extension.controller.RequeueException;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class UserReconciler implements Reconciler<Request> {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private static final String FINALIZER_NAME = \"user-protection\";\n    private final ExtensionClient client;\n    private final ExternalUrlSupplier externalUrlSupplier;\n    private final RoleService roleService;\n    private final AttachmentService attachmentService;\n    private final UserService userService;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(User.class, request.name()).ifPresent(user -> {\n            if (isDeleted(user)) {\n                deleteUserConnections(request.name());\n                removeFinalizers(user.getMetadata(), Set.of(FINALIZER_NAME));\n                client.update(user);\n                return;\n            }\n            addFinalizers(user.getMetadata(), Set.of(FINALIZER_NAME));\n            ensureRoleNamesAnno(user);\n            updatePermalink(user);\n            handleAvatar(user);\n            checkVerifiedEmail(user);\n            client.update(user);\n        });\n        return new Result(false, null);\n    }\n\n    private void checkVerifiedEmail(User user) {\n        var username = user.getMetadata().getName();\n        if (!user.getSpec().isEmailVerified()) {\n            return;\n        }\n        var email = user.getSpec().getEmail();\n        if (StringUtils.isBlank(email)) {\n            return;\n        }\n        if (checkEmailInUse(username, email)) {\n            user.getSpec().setEmailVerified(false);\n        }\n    }\n\n    private Boolean checkEmailInUse(String username, String email) {\n        return userService.listByEmail(email)\n            .filter(existUser -> existUser.getSpec().isEmailVerified())\n            .filter(existUser -> !existUser.getMetadata().getName().equals(username))\n            .hasElements()\n            .blockOptional(BLOCKING_TIMEOUT)\n            .orElse(false);\n    }\n\n    private void handleAvatar(User user) {\n        var annotations = Optional.ofNullable(user.getMetadata().getAnnotations())\n            .orElseGet(HashMap::new);\n        user.getMetadata().setAnnotations(annotations);\n\n        var avatarAttachmentName = annotations.get(User.AVATAR_ATTACHMENT_NAME_ANNO);\n        var oldAvatarAttachmentName =\n            annotations.get(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO);\n        // remove old avatar if needed\n        if (StringUtils.isNotBlank(oldAvatarAttachmentName)\n            && !StringUtils.equals(avatarAttachmentName, oldAvatarAttachmentName)) {\n            client.fetch(Attachment.class, oldAvatarAttachmentName)\n                .ifPresent(client::delete);\n            annotations.remove(User.LAST_AVATAR_ATTACHMENT_NAME_ANNO);\n        }\n\n        var spec = user.getSpec();\n        if (StringUtils.isBlank(avatarAttachmentName)) {\n            if (StringUtils.isNotBlank(spec.getAvatar())) {\n                log.info(\"Remove avatar for user({})\", user.getMetadata().getName());\n            }\n            spec.setAvatar(null);\n            return;\n        }\n        client.fetch(Attachment.class, avatarAttachmentName)\n            .flatMap(attachment -> attachmentService.getPermalink(attachment)\n                .blockOptional(BLOCKING_TIMEOUT)\n            )\n            .map(URI::toString)\n            .ifPresentOrElse(avatar -> {\n                if (!Objects.equals(avatar, spec.getAvatar())) {\n                    log.info(\n                        \"Update avatar for user({}) to {}\",\n                        user.getMetadata().getName(), avatar\n                    );\n                }\n                spec.setAvatar(avatar);\n                // reset last avatar\n                annotations.put(\n                    User.LAST_AVATAR_ATTACHMENT_NAME_ANNO,\n                    avatarAttachmentName\n                );\n            }, () -> {\n                throw new RequeueException(\n                    new Result(true, null),\n                    \"Avatar permalink(%s) is not available yet.\"\n                        .formatted(avatarAttachmentName)\n                );\n            });\n    }\n\n    private void ensureRoleNamesAnno(User user) {\n        roleService.getRolesByUsername(user.getMetadata().getName())\n            .collectList()\n            .map(JsonUtils::objectToJson)\n            .doOnNext(roleNamesJson -> {\n                var annotations = Optional.ofNullable(user.getMetadata().getAnnotations())\n                    .orElseGet(HashMap::new);\n                user.getMetadata().setAnnotations(annotations);\n                annotations.put(User.ROLE_NAMES_ANNO, roleNamesJson);\n            })\n            .block(BLOCKING_TIMEOUT);\n    }\n\n    private void updatePermalink(User user) {\n        var name = user.getMetadata().getName();\n        if (AnonymousUserConst.isAnonymousUser(name)) {\n            // anonymous user is not allowed to have permalink\n            return;\n        }\n        var status = Optional.ofNullable(user.getStatus())\n            .orElseGet(User.UserStatus::new);\n        user.setStatus(status);\n        status.setPermalink(getUserPermalink(user));\n    }\n\n    private String getUserPermalink(User user) {\n        return UriComponentsBuilder.fromUri(externalUrlSupplier.get())\n            .pathSegment(\"authors\", user.getMetadata().getName())\n            .toUriString();\n    }\n\n    void deleteUserConnections(String username) {\n        var userConnections = listConnectionsByUsername(username);\n        if (CollectionUtils.isEmpty(userConnections)) {\n            return;\n        }\n        userConnections.forEach(client::delete);\n        throw new RequeueException(new Result(true, null), \"User connections are not deleted yet\");\n    }\n\n    List<UserConnection> listConnectionsByUsername(String username) {\n        var listOptions = ListOptions.builder()\n            .andQuery(equal(\"spec.username\", username))\n            .build();\n        return client.listAll(UserConnection.class, listOptions, defaultSort());\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new User())\n            .build();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/DefaultRoleService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport static run.halo.app.extension.ExtensionUtil.defaultSort;\nimport static run.halo.app.extension.ExtensionUtil.notDeleting;\nimport static run.halo.app.security.authorization.AuthorityUtils.containsSuperRole;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.RoleBinding;\nimport run.halo.app.core.extension.RoleBinding.RoleRef;\nimport run.halo.app.core.extension.RoleBinding.Subject;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.security.SuperAdminInitializer;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Service\npublic class DefaultRoleService implements RoleService {\n\n    private final ReactiveExtensionClient client;\n\n    public DefaultRoleService(ReactiveExtensionClient client) {\n        this.client = client;\n    }\n\n    private Flux<RoleRef> listRoleRefs(Subject subject) {\n        return listRoleBindings(subject).map(RoleBinding::getRoleRef);\n    }\n\n    @Override\n    public Flux<RoleBinding> listRoleBindings(Subject subject) {\n        var listOptions = ListOptions.builder()\n            .andQuery(notDeleting())\n            .andQuery(Queries.in(\"subjects\", subject.toString()))\n            .build();\n        return client.listAll(RoleBinding.class, listOptions, defaultSort());\n    }\n\n    @Override\n    public Flux<String> getRolesByUsername(String username) {\n        return listRoleRefs(toUserSubject(username))\n            .filter(DefaultRoleService::isRoleKind)\n            .map(RoleRef::getName);\n    }\n\n    @Override\n    public Mono<Map<String, Collection<String>>> getRolesByUsernames(Collection<String> usernames) {\n        if (CollectionUtils.isEmpty(usernames)) {\n            return Mono.just(Map.of());\n        }\n        var subjects = usernames.stream().map(DefaultRoleService::toUserSubject)\n            .map(Object::toString)\n            .collect(Collectors.toSet());\n        var listOptions = ListOptions.builder()\n            .andQuery(notDeleting())\n            .andQuery(Queries.in(\"subjects\", subjects))\n            .build();\n\n        return client.listAll(RoleBinding.class, listOptions, defaultSort())\n            .collect(HashMap::new, (map, roleBinding) -> {\n                for (Subject subject : roleBinding.getSubjects()) {\n                    if (subjects.contains(subject.toString())) {\n                        var username = subject.getName();\n                        var roleRef = roleBinding.getRoleRef();\n                        if (isRoleKind(roleRef)) {\n                            var roleName = roleRef.getName();\n                            map.computeIfAbsent(username, k -> new HashSet<>()).add(roleName);\n                        }\n                    }\n                }\n            });\n    }\n\n    @Override\n    public Mono<Boolean> contains(Collection<String> source, Collection<String> candidates) {\n        if (source.contains(SuperAdminInitializer.SUPER_ROLE_NAME)) {\n            return Mono.just(true);\n        }\n        return listWithDependencies(new HashSet<>(source), shouldExcludeHidden(false))\n            .map(role -> role.getMetadata().getName())\n            .collect(Collectors.toSet())\n            .map(roleNames -> roleNames.containsAll(candidates));\n    }\n\n    @Override\n    public Flux<Role> listPermissions(Set<String> names) {\n        if (containsSuperRole(names)) {\n            // search all permissions\n            return client.listAll(Role.class,\n                shouldExcludeHidden(true),\n                ExtensionUtil.defaultSort());\n        }\n        return listWithDependencies(names, shouldExcludeHidden(true));\n    }\n\n    @Override\n    public Flux<Role> listDependenciesFlux(Set<String> names) {\n        return listWithDependencies(names, shouldExcludeHidden(false));\n    }\n\n    private static boolean isRoleKind(RoleRef roleRef) {\n        return Role.GROUP.equals(roleRef.getApiGroup()) && Role.KIND.equals(roleRef.getKind());\n    }\n\n    private static Subject toUserSubject(String username) {\n        var subject = new Subject();\n        subject.setApiGroup(User.GROUP);\n        subject.setKind(User.KIND);\n        subject.setName(username);\n        return subject;\n    }\n\n    private Flux<Role> listRoles(Set<String> names, ListOptions additionalListOptions) {\n        if (CollectionUtils.isEmpty(names)) {\n            return Flux.empty();\n        }\n\n        var listOptions = Optional.ofNullable(additionalListOptions)\n            .map(ListOptions::builder)\n            .orElseGet(ListOptions::builder)\n            .andQuery(notDeleting())\n            .andQuery(Queries.in(\"metadata.name\", names))\n            .build();\n\n        return client.listAll(Role.class, listOptions, ExtensionUtil.defaultSort());\n    }\n\n    private static ListOptions shouldExcludeHidden(boolean excludeHidden) {\n        if (!excludeHidden) {\n            return null;\n        }\n        return ListOptions.builder().labelSelector()\n            .notEq(Role.HIDDEN_LABEL_NAME, Boolean.TRUE.toString())\n            .end()\n            .build();\n    }\n\n    private Flux<Role> listWithDependencies(Set<String> names, ListOptions additionalListOptions) {\n        var visited = new HashSet<String>();\n        return listRoles(names, additionalListOptions)\n            .expand(role -> {\n                var name = role.getMetadata().getName();\n                if (visited.contains(name)) {\n                    return Flux.empty();\n                }\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Expand role: {}\", role.getMetadata().getName());\n                }\n                visited.add(name);\n                var annotations = MetadataUtil.nullSafeAnnotations(role);\n                var dependenciesJson = annotations.get(Role.ROLE_DEPENDENCIES_ANNO);\n                var dependencies = stringToList(dependenciesJson);\n\n                return Flux.fromIterable(dependencies)\n                    .filter(dep -> !visited.contains(dep))\n                    .collect(Collectors.<String>toSet())\n                    .flatMapMany(deps -> listRoles(deps, additionalListOptions));\n            })\n            .concatWith(Flux.defer(() -> listAggregatedRoles(visited, additionalListOptions)));\n    }\n\n    private Flux<Role> listAggregatedRoles(Set<String> roleNames,\n        ListOptions additionalListOptions) {\n        if (CollectionUtils.isEmpty(roleNames)) {\n            return Flux.empty();\n        }\n        var listOptions = Optional.ofNullable(additionalListOptions)\n            .map(ListOptions::builder)\n            .orElseGet(ListOptions::builder)\n            .andQuery(Queries.in(\"labels.aggregateToRoles\", roleNames))\n            .build();\n        return client.listAll(Role.class, listOptions, ExtensionUtil.defaultSort());\n    }\n\n    Predicate<RoleBinding> getRoleBindingPredicate(Subject targetSubject) {\n        return roleBinding -> {\n            List<Subject> subjects = roleBinding.getSubjects();\n            for (Subject subject : subjects) {\n                return matchSubject(targetSubject, subject);\n            }\n            return false;\n        };\n    }\n\n    private static boolean matchSubject(Subject targetSubject, Subject subject) {\n        if (targetSubject == null || subject == null) {\n            return false;\n        }\n        return StringUtils.equals(targetSubject.getKind(), subject.getKind())\n            && StringUtils.equals(targetSubject.getName(), subject.getName())\n            && StringUtils.defaultString(targetSubject.getApiGroup())\n            .equals(StringUtils.defaultString(subject.getApiGroup()));\n    }\n\n    @Override\n    public Flux<Role> list(Set<String> roleNames) {\n        return list(roleNames, false);\n    }\n\n    @Override\n    public Flux<Role> list(Set<String> roleNames, boolean excludeHidden) {\n        if (CollectionUtils.isEmpty(roleNames)) {\n            return Flux.empty();\n        }\n        var builder = ListOptions.builder()\n            .andQuery(notDeleting())\n            .andQuery(Queries.in(\"metadata.name\", roleNames));\n        if (excludeHidden) {\n            builder.labelSelector().notEq(Role.HIDDEN_LABEL_NAME, Boolean.TRUE.toString());\n        }\n        return client.listAll(Role.class, builder.build(), defaultSort());\n    }\n\n    @NonNull\n    private List<String> stringToList(String str) {\n        if (StringUtils.isBlank(str)) {\n            return Collections.emptyList();\n        }\n        return JsonUtils.jsonToObject(str,\n            new TypeReference<>() {\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/EmailPasswordRecoveryService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.exception.AccessDeniedException;\n\n/**\n * An interface for email password recovery.\n *\n * @author guqing\n * @since 2.11.0\n */\npublic interface EmailPasswordRecoveryService {\n\n    /**\n     * <p>Send password reset email.</p>\n     * if the user does not exist, it will return {@link Mono#empty()}\n     * if the user exists, but the email is not the same, it will return {@link Mono#empty()}\n     *\n     * @param username username to request password reset\n     * @param email email to match the user with the username\n     * @return {@link Mono#empty()} if the user does not exist, or the email is not the same.\n     */\n    Mono<Void> sendPasswordResetEmail(String username, String email);\n\n    Mono<Void> sendPasswordResetEmail(String email);\n\n    /**\n     * <p>Reset password by token.</p>\n     * if the token is invalid, it will return {@link Mono#error(Throwable)}}\n     * if the token is valid, but the username is not the same, it will return\n     * {@link Mono#error(Throwable)}\n     *\n     * @param newPassword new password\n     * @param token token to validate the user\n     * @return {@link Mono#empty()} if the token is invalid or the username is not the same.\n     * @throws AccessDeniedException if the token is invalid\n     */\n    Mono<Void> changePassword(String newPassword, String token);\n\n    Mono<ResetToken> getValidResetToken(String token);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/EmailVerificationService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.exception.EmailVerificationFailed;\n\n/**\n * Email verification service to handle email verification.\n *\n * @author guqing\n * @since 2.11.0\n */\npublic interface EmailVerificationService {\n\n    /**\n     * Send verification code by given username.\n     *\n     * @param username username to verify email must not be blank\n     * @param email email to send must not be blank\n     */\n    Mono<Void> sendVerificationCode(String username, String email);\n\n    /**\n     * Verify email by given username and code.\n     *\n     * @param username username to verify email must not be blank\n     * @param code code to verify email must not be blank\n     * @throws EmailVerificationFailed if send failed\n     */\n    Mono<Void> verify(String username, String code);\n\n    /**\n     * Send verification code.\n     * The only difference is use email as username.\n     *\n     * @param email email to send must not be blank\n     */\n    Mono<Void> sendRegisterVerificationCode(String email);\n\n    /**\n     * Verify email by given code.\n     *\n     * @param email email as username to verify email must not be blank\n     * @param code code to verify email must not be blank\n     * @throws EmailVerificationFailed if send failed\n     */\n    Mono<Boolean> verifyRegisterVerificationCode(String email, String code);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/InMemoryResetTokenRepository.java",
    "content": "package run.halo.app.core.user.service;\n\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport java.time.Duration;\nimport java.util.Objects;\nimport org.springframework.dao.DuplicateKeyException;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\n/**\n * In-memory reset token repository.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\npublic class InMemoryResetTokenRepository implements ResetTokenRepository {\n\n    /**\n     * Key: Token Hash.\n     */\n    private final Cache<String, ResetToken> tokens;\n\n    public InMemoryResetTokenRepository() {\n        this.tokens = Caffeine.newBuilder()\n            .expireAfterWrite(Duration.ofDays(1))\n            .maximumSize(10000)\n            .build();\n    }\n\n    @Override\n    public Mono<Void> save(ResetToken resetToken) {\n        return Mono.defer(() -> {\n            var savedResetToken = tokens.get(resetToken.tokenHash(), k -> resetToken);\n            if (Objects.equals(savedResetToken, resetToken)) {\n                return Mono.empty();\n            }\n            // should never happen\n            return Mono.error(new DuplicateKeyException(\"Reset token already exists\"));\n        });\n    }\n\n    @Override\n    public Mono<ResetToken> findByTokenHash(String tokenHash) {\n        return Mono.fromSupplier(() -> tokens.getIfPresent(tokenHash));\n    }\n\n    @Override\n    public Mono<Void> removeByTokenHash(String tokenHash) {\n        return Mono.fromRunnable(() -> tokens.invalidate(tokenHash));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/InvalidResetTokenException.java",
    "content": "package run.halo.app.core.user.service;\n\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * Invalid reset token exception.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class InvalidResetTokenException extends ServerWebInputException {\n\n    public InvalidResetTokenException() {\n        super(\"Invalid reset token\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/PatService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.PersonalAccessToken;\n\n/**\n * Service for personal access token.\n *\n * @author johnniang\n */\npublic interface PatService {\n\n    /**\n     * Create a new personal access token. We will automatically use the current user as the\n     * owner of the token from the security context.\n     *\n     * @param patRequest the personal access token request\n     * @return the created personal access token\n     */\n    Mono<PersonalAccessToken> create(PersonalAccessToken patRequest);\n\n    /**\n     * Create a new personal access token for the specified user.\n     *\n     * @param patRequest the personal access token request\n     * @param username the username of the user\n     * @return the created personal access token\n     */\n    Mono<PersonalAccessToken> create(PersonalAccessToken patRequest, String username);\n\n    /**\n     * Revoke a personal access token.\n     *\n     * @param patName the name of the personal access token\n     * @param username the username of the user\n     * @return the revoked personal access token\n     */\n    Mono<PersonalAccessToken> revoke(String patName, String username);\n\n    /**\n     * Restore a personal access token.\n     *\n     * @param patName the name of the personal access token\n     * @param username the username of the user\n     * @return the restored personal access token\n     */\n    Mono<PersonalAccessToken> restore(String patName, String username);\n\n    /**\n     * Delete a personal access token.\n     *\n     * @param patName the name of the personal access token\n     * @param username the username of the user\n     * @return the deleted personal access token\n     */\n    Mono<PersonalAccessToken> delete(String patName, String username);\n\n    /**\n     * Get a personal access token by name.\n     *\n     * @param patName the name of the personal access token\n     * @param username the username of the user\n     * @return the personal access token\n     */\n    Mono<PersonalAccessToken> get(String patName, String username);\n\n    /**\n     * Generate a personal access token.\n     *\n     * @param pat the personal access token\n     * @return the generated token\n     */\n    Mono<String> generateToken(PersonalAccessToken pat);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/ResetToken.java",
    "content": "package run.halo.app.core.user.service;\n\nimport java.time.Instant;\n\n/**\n * Reset token data.\n *\n * @param tokenHash The token hash\n * @param username The username\n * @param expiresAt The expires at\n * @author johnniang\n * @since 2.20.0\n */\npublic record ResetToken(String tokenHash, String username, Instant expiresAt) {\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/ResetTokenRepository.java",
    "content": "package run.halo.app.core.user.service;\n\nimport reactor.core.publisher.Mono;\n\n/**\n * Reset token repository.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface ResetTokenRepository {\n\n    /**\n     * Save reset token.\n     *\n     * @param resetToken reset token\n     * @return empty mono if saved successfully.\n     * @throws org.springframework.dao.DuplicateKeyException if token already exists.\n     */\n    Mono<Void> save(ResetToken resetToken);\n\n    /**\n     * Find reset token by token hash.\n     *\n     * @param tokenHash token hash\n     * @return reset token if found, or empty mono.\n     */\n    Mono<ResetToken> findByTokenHash(String tokenHash);\n\n    /**\n     * Remove reset token by token hash.\n     *\n     * @param tokenHash token hash\n     * @return empty mono if removed successfully.\n     */\n    Mono<Void> removeByTokenHash(String tokenHash);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/SettingConfigService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.extension.ConfigMap;\nimport tools.jackson.databind.node.ObjectNode;\n\n/**\n * {@link Setting} related {@link ConfigMap} service.\n *\n * @author guqing\n * @since 2.20.0\n */\npublic interface SettingConfigService {\n\n    Mono<Void> upsertConfig(String configMapName, ObjectNode configJsonData);\n\n    Mono<ObjectNode> fetchConfig(String configMapName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/UserConnectionService.java",
    "content": "package run.halo.app.core.user.service;\n\nimport org.springframework.security.oauth2.core.user.OAuth2User;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.UserConnection;\n\npublic interface UserConnectionService {\n\n    /**\n     * Create user connection.\n     *\n     * @param username Username\n     * @param registrationId Registration id\n     * @param oauth2User OAuth2 user\n     * @return Created user connection\n     */\n    Mono<UserConnection> createUserConnection(\n        String username,\n        String registrationId,\n        OAuth2User oauth2User\n    );\n\n    /**\n     * Update the user connection if present.\n     * If found, update updatedAt timestamp of the user connection.\n     *\n     * @param registrationId Registration id\n     * @param oauth2User OAuth2 user\n     * @return Updated user connection or empty\n     */\n    Mono<UserConnection> updateUserConnectionIfPresent(\n        String registrationId, OAuth2User oauth2User\n    );\n\n    /**\n     * Remove user connection.\n     *\n     * @param registrationId Registration ID\n     * @param username Username\n     * @return A list of user connections\n     */\n    Flux<UserConnection> removeUserConnection(String registrationId, String username);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/UserLoginOrLogoutProcessing.java",
    "content": "package run.halo.app.core.user.service;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.event.user.UserLoginEvent;\nimport run.halo.app.event.user.UserLogoutEvent;\n\n/**\n * User login or logout processing service.\n *\n * @author lywq\n **/\n@Component\n@RequiredArgsConstructor\npublic class UserLoginOrLogoutProcessing {\n\n    private final UserService userService;\n    private final ApplicationEventPublisher eventPublisher;\n\n    public Mono<Void> loginProcessing(String username) {\n        return userService.getUser(username)\n            .doOnNext(user -> {\n                eventPublisher.publishEvent(new UserLoginEvent(this, user));\n            })\n            .then();\n    }\n\n    public Mono<Void> logoutProcessing(String username) {\n        return userService.getUser(username)\n            .doOnNext(user -> {\n                eventPublisher.publishEvent(new UserLogoutEvent(this, user));\n            })\n            .then();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/impl/DefaultAttachmentService.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.server.ResponseStatusException;\nimport org.springframework.web.server.ServerErrorException;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Group;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;\nimport run.halo.app.core.extension.attachment.endpoint.DeleteOption;\nimport run.halo.app.core.extension.attachment.endpoint.SimpleFilePart;\nimport run.halo.app.core.extension.attachment.endpoint.UploadOption;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ReactiveUrlDataBufferFetcher;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n@Component\npublic class DefaultAttachmentService implements AttachmentService {\n\n    private final ReactiveExtensionClient client;\n\n    private final ExtensionGetter extensionGetter;\n\n    private final ReactiveUrlDataBufferFetcher dataBufferFetcher;\n\n    public DefaultAttachmentService(ReactiveExtensionClient client,\n        ExtensionGetter extensionGetter,\n        ReactiveUrlDataBufferFetcher dataBufferFetcher) {\n        this.client = client;\n        this.extensionGetter = extensionGetter;\n        this.dataBufferFetcher = dataBufferFetcher;\n    }\n\n    @Override\n    public Mono<Attachment> upload(\n        @NonNull String username,\n        @NonNull String policyName,\n        @Nullable String groupName,\n        @NonNull FilePart filePart,\n        @Nullable Consumer<Attachment> beforeCreating) {\n        var builder = UploadOption.builder();\n        builder.file(filePart);\n        var getPolicyAndConfigMap = client.get(Policy.class, policyName)\n            .doOnNext(builder::policy)\n            .mapNotNull(p -> p.getSpec().getConfigMapName())\n            .filter(StringUtils::hasText)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"ConfigMap name not found in Policy \" + policyName\n            )))\n            .flatMap(configMapName -> client.get(ConfigMap.class, configMapName))\n            .doOnNext(builder::configMap)\n            .then();\n\n        var getGroup = Mono.justOrEmpty(groupName)\n            .filter(StringUtils::hasText)\n            .flatMap(name -> client.get(Group.class, name))\n            .doOnNext(builder::group)\n            .then();\n        return Mono.when(getPolicyAndConfigMap, getGroup).then(Mono.fromSupplier(builder::build))\n            .flatMap(uploadContext -> extensionGetter.getExtensions(AttachmentHandler.class)\n                .concatMap(handler -> handler.upload(uploadContext))\n                .next())\n            .switchIfEmpty(Mono.error(() -> new ServerErrorException(\n                \"No suitable handler found for uploading the attachment.\", null)))\n            .doOnNext(attachment -> {\n                var spec = attachment.getSpec();\n                if (spec == null) {\n                    spec = new Attachment.AttachmentSpec();\n                    attachment.setSpec(spec);\n                }\n                spec.setOwnerName(username);\n                if (StringUtils.hasText(groupName)) {\n                    spec.setGroupName(groupName);\n                }\n                spec.setPolicyName(policyName);\n            })\n            .doOnNext(attachment -> {\n                if (beforeCreating != null) {\n                    beforeCreating.accept(attachment);\n                }\n            })\n            .flatMap(client::create);\n    }\n\n    @Override\n    public Mono<Attachment> upload(@NonNull String policyName,\n        @Nullable String groupName,\n        @NonNull String filename,\n        @NonNull Flux<DataBuffer> content,\n        @Nullable MediaType mediaType) {\n        var file = new SimpleFilePart(filename, content, mediaType);\n        return authenticationConsumer(\n            authentication -> upload(authentication.getName(), policyName, groupName, file, null));\n    }\n\n    @Override\n    public Mono<Attachment> delete(Attachment attachment) {\n        var spec = attachment.getSpec();\n        return client.get(Policy.class, spec.getPolicyName())\n            .flatMap(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName())\n                .map(configMap -> new DeleteOption(attachment, policy, configMap)))\n            .flatMap(deleteOption -> extensionGetter.getExtensions(AttachmentHandler.class)\n                .concatMap(handler -> handler.delete(deleteOption))\n                .next());\n    }\n\n    @Override\n    public Mono<URI> getPermalink(Attachment attachment) {\n        return client.get(Policy.class, attachment.getSpec().getPolicyName())\n            .flatMap(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName())\n                .flatMap(configMap -> extensionGetter.getExtensions(AttachmentHandler.class)\n                    .concatMap(handler -> handler.getPermalink(attachment, policy, configMap))\n                    .next()\n                )\n            );\n    }\n\n    @Override\n    public Mono<URI> getSharedURL(Attachment attachment, Duration ttl) {\n        return client.get(Policy.class, attachment.getSpec().getPolicyName())\n            .flatMap(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName())\n                .flatMap(configMap -> extensionGetter.getExtensions(AttachmentHandler.class)\n                    .concatMap(handler -> handler.getSharedURL(attachment, policy, configMap, ttl))\n                    .next()\n                )\n            );\n    }\n\n    @Override\n    public Mono<Map<ThumbnailSize, URI>> getThumbnailLinks(Attachment attachment) {\n\n        return client.get(Policy.class, attachment.getSpec().getPolicyName())\n            .zipWhen(policy -> client.get(ConfigMap.class, policy.getSpec().getConfigMapName()))\n            .flatMap(tuple2 -> {\n                var policy = tuple2.getT1();\n                var configMap = tuple2.getT2();\n                return extensionGetter.getExtensions(AttachmentHandler.class)\n                    .concatMap(handler -> handler.getThumbnailLinks(attachment, policy, configMap))\n                    .next();\n            });\n    }\n\n    @Override\n    public Mono<Attachment> uploadFromUrl(@NonNull URL url, @NonNull String policyName,\n        String groupName, String filename) {\n        var uri = URI.create(url.toString());\n        AtomicReference<MediaType> mediaTypeRef = new AtomicReference<>();\n        AtomicReference<String> fileNameRef = new AtomicReference<>(filename);\n\n        Mono<Flux<DataBuffer>> contentMono = dataBufferFetcher.head(uri)\n            .map(httpHeaders -> {\n                if (!StringUtils.hasText(fileNameRef.get())) {\n                    fileNameRef.set(getExternalUrlFilename(uri, httpHeaders));\n                }\n                MediaType contentType = httpHeaders.getContentType();\n                mediaTypeRef.set(contentType);\n                return httpHeaders;\n            })\n            .map(response -> dataBufferFetcher.fetch(uri));\n\n        return contentMono.flatMap(\n                (content) -> upload(policyName, groupName, fileNameRef.get(), content,\n                    mediaTypeRef.get())\n            )\n            .onErrorResume(throwable -> Mono.error(\n                new ServerWebInputException(\n                    \"Failed to transfer the attachment from the external URL.\"))\n            );\n    }\n\n    private static String getExternalUrlFilename(URI externalUrl, HttpHeaders httpHeaders) {\n        String fileName = httpHeaders.getContentDisposition().getFilename();\n        if (!StringUtils.hasText(fileName)) {\n            var path = externalUrl.getPath();\n            fileName = Paths.get(path).getFileName().toString();\n        }\n        // TODO get file extension from media type\n        return fileName;\n    }\n\n    private <T> Mono<T> authenticationConsumer(Function<Authentication, Mono<T>> func) {\n        return ReactiveSecurityContextHolder.getContext()\n            .switchIfEmpty(Mono.error(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED,\n                \"Authentication required.\")))\n            .map(SecurityContext::getAuthentication)\n            .flatMap(func);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/impl/EmailPasswordRecoveryServiceImpl.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport java.time.Clock;\nimport java.time.Duration;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.security.core.token.Sha512DigestUtils;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.core.user.service.EmailPasswordRecoveryService;\nimport run.halo.app.core.user.service.InvalidResetTokenException;\nimport run.halo.app.core.user.service.ResetToken;\nimport run.halo.app.core.user.service.ResetTokenRepository;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ExternalLinkProcessor;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.notification.NotificationReasonEmitter;\nimport run.halo.app.notification.UserIdentity;\n\n/**\n * A default implementation for {@link EmailPasswordRecoveryService}.\n *\n * @author guqing\n * @since 2.11.0\n */\n@Component\n@RequiredArgsConstructor\npublic class EmailPasswordRecoveryServiceImpl implements EmailPasswordRecoveryService {\n\n    public static final int MAX_ATTEMPTS = 5;\n    public static final long LINK_EXPIRATION_MINUTES = 30;\n    private static final Duration RESET_TOKEN_LIFE_TIME =\n        Duration.ofMinutes(LINK_EXPIRATION_MINUTES);\n    static final String RESET_PASSWORD_BY_EMAIL_REASON_TYPE = \"reset-password-by-email\";\n\n    private final ExternalLinkProcessor externalLinkProcessor;\n    private final ReactiveExtensionClient client;\n    private final NotificationReasonEmitter reasonEmitter;\n    private final NotificationCenter notificationCenter;\n    private final UserService userService;\n    private final ResetTokenRepository resetTokenRepository;\n\n    private Clock clock = Clock.systemDefaultZone();\n\n    @Override\n    public Mono<Void> sendPasswordResetEmail(String username, String email) {\n        return client.fetch(User.class, username)\n            .flatMap(user -> {\n                var userEmail = user.getSpec().getEmail();\n                if (!StringUtils.equals(userEmail, email)) {\n                    return Mono.empty();\n                }\n                if (!user.getSpec().isEmailVerified()) {\n                    return Mono.empty();\n                }\n                return sendResetPasswordNotification(username, email);\n            });\n    }\n\n    @Override\n    public Mono<Void> sendPasswordResetEmail(String email) {\n        if (StringUtils.isBlank(email)) {\n            return Mono.empty();\n        }\n        return userService.listByEmail(email)\n            .filter(user -> user.getSpec().isEmailVerified())\n            .next()\n            .flatMap(user -> sendResetPasswordNotification(user.getMetadata().getName(), email));\n    }\n\n    @Override\n    public Mono<Void> changePassword(String newPassword, String token) {\n        Assert.state(StringUtils.isNotBlank(newPassword), \"NewPassword must not be blank\");\n        Assert.state(StringUtils.isNotBlank(token), \"Token for reset password must not be blank\");\n        var tokenHash = hashToken(token);\n        return getValidResetToken(token).flatMap(resetToken ->\n            userService.updateWithRawPassword(resetToken.username(), newPassword)\n                .flatMap(user -> unSubscribeResetPasswordEmailNotification(\n                    user.getSpec().getEmail())\n                )\n                .then(resetTokenRepository.removeByTokenHash(tokenHash))\n        );\n    }\n\n    @Override\n    public Mono<ResetToken> getValidResetToken(String token) {\n        return resetTokenRepository.findByTokenHash(hashToken(token))\n            .filter(resetToken -> clock.instant().isBefore(resetToken.expiresAt()))\n            .switchIfEmpty(Mono.error(InvalidResetTokenException::new));\n    }\n\n    Mono<Void> unSubscribeResetPasswordEmailNotification(String email) {\n        if (StringUtils.isBlank(email)) {\n            return Mono.empty();\n        }\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(UserIdentity.anonymousWithEmail(email).name());\n        return notificationCenter.unsubscribe(subscriber, createInterestReason(email))\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    private Mono<Void> sendResetPasswordNotification(String username, String email) {\n        var token = generateToken();\n        var tokenHash = hashToken(token);\n        var expiresAt = clock.instant().plus(RESET_TOKEN_LIFE_TIME);\n        var uri = UriComponentsBuilder.fromUriString(\"/\")\n            .pathSegment(\"password-reset\", \"email\", token)\n            .build(true)\n            .toUri();\n        var resetToken = new ResetToken(tokenHash, username, expiresAt);\n        return resetTokenRepository.save(resetToken)\n            .then(externalLinkProcessor.processLink(uri).flatMap(link -> {\n                var interestReasonSubject = createInterestReason(email).getSubject();\n                var emitReasonMono = reasonEmitter.emit(RESET_PASSWORD_BY_EMAIL_REASON_TYPE,\n                    builder -> builder.attribute(\"expirationAtMinutes\", LINK_EXPIRATION_MINUTES)\n                        .attribute(\"username\", username)\n                        .attribute(\"link\", link)\n                        .author(UserIdentity.of(username))\n                        .subject(Reason.Subject.builder()\n                            .apiVersion(interestReasonSubject.getApiVersion())\n                            .kind(interestReasonSubject.getKind())\n                            .name(interestReasonSubject.getName())\n                            .title(\"使用邮箱地址重置密码：\" + email)\n                            .build()\n                        )\n                );\n                return autoSubscribeResetPasswordEmailNotification(email).then(emitReasonMono);\n            }));\n    }\n\n    Mono<Void> autoSubscribeResetPasswordEmailNotification(String email) {\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(UserIdentity.anonymousWithEmail(email).name());\n        var interestReason = createInterestReason(email);\n        return notificationCenter.subscribe(subscriber, interestReason)\n            .then();\n    }\n\n    Subscription.InterestReason createInterestReason(String email) {\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(RESET_PASSWORD_BY_EMAIL_REASON_TYPE);\n        interestReason.setSubject(Subscription.ReasonSubject.builder()\n            .apiVersion(new GroupVersion(User.GROUP, User.KIND).toString())\n            .kind(User.KIND)\n            .name(UserIdentity.anonymousWithEmail(email).name())\n            .build());\n        return interestReason;\n    }\n\n    private static String hashToken(String token) {\n        return Sha512DigestUtils.shaHex(token);\n    }\n\n    private static String generateToken() {\n        return RandomStringUtils.secure().nextAlphanumeric(64);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/impl/EmailVerificationServiceImpl.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport com.google.common.cache.Cache;\nimport com.google.common.cache.CacheBuilder;\nimport java.time.Duration;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.experimental.Accessors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.core.user.service.EmailVerificationService;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.infra.exception.EmailVerificationFailed;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.notification.NotificationReasonEmitter;\nimport run.halo.app.notification.UserIdentity;\n\n/**\n * A default implementation of {@link EmailVerificationService}.\n *\n * @author guqing\n * @since 2.11.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class EmailVerificationServiceImpl implements EmailVerificationService {\n    public static final int MAX_ATTEMPTS = 5;\n    public static final long CODE_EXPIRATION_MINUTES = 10;\n    static final String EMAIL_VERIFICATION_REASON_TYPE = \"email-verification\";\n\n    private final EmailVerificationManager emailVerificationManager =\n        new EmailVerificationManager();\n    private final ReactiveExtensionClient client;\n    private final NotificationReasonEmitter reasonEmitter;\n    private final NotificationCenter notificationCenter;\n\n    @Override\n    public Mono<Void> sendVerificationCode(String username, String email) {\n        Assert.state(StringUtils.isNotBlank(username), \"Username must not be blank\");\n        Assert.state(StringUtils.isNotBlank(email), \"Email must not be blank\");\n        return Mono.defer(() -> client.get(User.class, username)\n                .flatMap(user -> {\n                    var userEmail = user.getSpec().getEmail();\n                    var isVerified = user.getSpec().isEmailVerified();\n                    if (StringUtils.equalsIgnoreCase(userEmail, email) && isVerified) {\n                        return Mono.error(\n                            () -> new ServerWebInputException(\"Email already verified.\"));\n                    }\n                    var annotations = MetadataUtil.nullSafeAnnotations(user);\n                    var oldEmailToVerify = annotations.get(User.EMAIL_TO_VERIFY);\n                    var unsubMono = unSubscribeVerificationEmailNotification(oldEmailToVerify);\n                    var updateUserAnnoMono = Mono.defer(() -> {\n                        annotations.put(User.EMAIL_TO_VERIFY, email);\n                        return client.update(user);\n                    });\n                    emailVerificationManager.removeCode(username, oldEmailToVerify);\n                    return Mono.when(unsubMono, updateUserAnnoMono).thenReturn(user);\n                })\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance))\n            .flatMap(user -> sendVerificationNotification(username, email));\n    }\n\n    @Override\n    public Mono<Void> verify(String username, String code) {\n        Assert.state(StringUtils.isNotBlank(username), \"Username must not be blank\");\n        Assert.state(StringUtils.isNotBlank(code), \"Code must not be blank\");\n        return Mono.defer(() -> client.get(User.class, username)\n                .flatMap(user -> verifyUserEmail(user, code))\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    private Mono<Void> verifyUserEmail(User user, String code) {\n        var username = user.getMetadata().getName();\n        var annotations = MetadataUtil.nullSafeAnnotations(user);\n        var emailToVerify = annotations.get(User.EMAIL_TO_VERIFY);\n\n        if (StringUtils.isBlank(emailToVerify)) {\n            return Mono.error(EmailVerificationFailed::new);\n        }\n\n        var verified = emailVerificationManager.verifyCode(username, emailToVerify, code);\n        if (!verified) {\n            return Mono.error(EmailVerificationFailed::new);\n        }\n\n        return isEmailInUse(username, emailToVerify)\n            .flatMap(inUse -> {\n                if (inUse) {\n                    return Mono.error(new EmailVerificationFailed(\"Email already in use.\",\n                        null,\n                        \"problemDetail.user.email.verify.emailInUse\",\n                        null)\n                    );\n                }\n                // remove code when verified\n                emailVerificationManager.removeCode(username, emailToVerify);\n                user.getSpec().setEmailVerified(true);\n                user.getSpec().setEmail(emailToVerify);\n                return client.update(user);\n            })\n            .then();\n    }\n\n    Mono<Boolean> isEmailInUse(String username, String emailToVerify) {\n        var listOptions = ListOptions.builder()\n            .andQuery(Queries.equal(\"spec.email\", emailToVerify.toLowerCase()))\n            .build();\n        return client.listAll(User.class, listOptions, ExtensionUtil.defaultSort())\n            .filter(user -> user.getSpec().isEmailVerified())\n            .filter(user -> !user.getMetadata().getName().equals(username))\n            .hasElements();\n    }\n\n    @Override\n    public Mono<Void> sendRegisterVerificationCode(String email) {\n        Assert.state(StringUtils.isNotBlank(email), \"Email must not be blank\");\n        return sendVerificationNotification(email.toLowerCase(), email);\n    }\n\n    @Override\n    public Mono<Boolean> verifyRegisterVerificationCode(String email, String code) {\n        Assert.state(StringUtils.isNotBlank(email), \"Username must not be blank\");\n        Assert.state(StringUtils.isNotBlank(code), \"Code must not be blank\");\n        return Mono.fromSupplier(() -> emailVerificationManager.verifyCode(email, email, code))\n            // Why use boundedElastic? Because the verification uses synchronized block.\n            .subscribeOn(Schedulers.boundedElastic());\n    }\n\n    Mono<Void> sendVerificationNotification(String username, String email) {\n        var code = emailVerificationManager.generateCode(username, email);\n        if (log.isDebugEnabled()) {\n            log.debug(\"Generated verification code for user '{}' and email '{}': {}\",\n                username, email, code);\n        }\n        var subscribeNotification = autoSubscribeVerificationEmailNotification(email);\n        var interestReasonSubject = createInterestReason(email).getSubject();\n        var emitReasonMono = reasonEmitter.emit(EMAIL_VERIFICATION_REASON_TYPE,\n            builder -> builder.attribute(\"code\", code)\n                .attribute(\"expirationAtMinutes\", CODE_EXPIRATION_MINUTES)\n                .attribute(\"username\", username)\n                .author(UserIdentity.of(username))\n                .subject(Reason.Subject.builder()\n                    .apiVersion(interestReasonSubject.getApiVersion())\n                    .kind(interestReasonSubject.getKind())\n                    .name(interestReasonSubject.getName())\n                    .title(\"验证邮箱：\" + email)\n                    .build()\n                )\n        );\n        return Mono.when(subscribeNotification).then(emitReasonMono);\n    }\n\n    Mono<Void> autoSubscribeVerificationEmailNotification(String email) {\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(UserIdentity.anonymousWithEmail(email).name());\n        var interestReason = createInterestReason(email);\n        return notificationCenter.subscribe(subscriber, interestReason)\n            .then();\n    }\n\n    Mono<Void> unSubscribeVerificationEmailNotification(String oldEmail) {\n        if (StringUtils.isBlank(oldEmail)) {\n            return Mono.empty();\n        }\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(UserIdentity.anonymousWithEmail(oldEmail).name());\n        return notificationCenter.unsubscribe(subscriber,\n            createInterestReason(oldEmail));\n    }\n\n    Subscription.InterestReason createInterestReason(String email) {\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(EMAIL_VERIFICATION_REASON_TYPE);\n        interestReason.setSubject(Subscription.ReasonSubject.builder()\n            .apiVersion(new GroupVersion(User.GROUP, User.KIND).toString())\n            .kind(User.KIND)\n            .name(UserIdentity.anonymousWithEmail(email).name())\n            .build());\n        return interestReason;\n    }\n\n    /**\n     * A simple email verification manager that stores the verification code in memory.\n     * It is a thread-safe class.\n     *\n     * @author guqing\n     * @since 2.11.0\n     */\n    static class EmailVerificationManager {\n        private final Cache<UsernameEmail, Verification> emailVerificationCodeCache =\n            CacheBuilder.newBuilder()\n                .expireAfterWrite(CODE_EXPIRATION_MINUTES, TimeUnit.MINUTES)\n                .maximumSize(10000)\n                .build();\n\n        private final Cache<UsernameEmail, Boolean> blackListCache = CacheBuilder.newBuilder()\n            .expireAfterWrite(Duration.ofHours(1))\n            .maximumSize(1000)\n            .build();\n\n        public boolean verifyCode(String username, String email, String code) {\n            var key = new UsernameEmail(username, email);\n            var verification = emailVerificationCodeCache.getIfPresent(key);\n            if (verification == null) {\n                // expired or not generated\n                return false;\n            }\n            if (blackListCache.getIfPresent(key) != null) {\n                // in blacklist\n                throw new EmailVerificationFailed(\"Too many attempts. Please try again later.\",\n                    null,\n                    \"problemDetail.user.email.verify.maxAttempts\",\n                    null);\n            }\n            synchronized (verification) {\n                if (verification.getAttempts().get() >= MAX_ATTEMPTS) {\n                    // add to blacklist to prevent brute force attack\n                    blackListCache.put(key, true);\n                    return false;\n                }\n                if (!verification.getCode().equals(code)) {\n                    verification.getAttempts().incrementAndGet();\n                    return false;\n                }\n            }\n            return true;\n        }\n\n        public void removeCode(String username, String email) {\n            var key = new UsernameEmail(username, email);\n            emailVerificationCodeCache.invalidate(key);\n        }\n\n        public String generateCode(String username, String email) {\n            Assert.state(StringUtils.isNotBlank(username), \"Username must not be blank\");\n            Assert.state(StringUtils.isNotBlank(email), \"Email must not be blank\");\n            var key = new UsernameEmail(username, email);\n            var verification = new Verification();\n            verification.setCode(RandomStringUtils.randomNumeric(6));\n            verification.setAttempts(new AtomicInteger(0));\n            emailVerificationCodeCache.put(key, verification);\n            return verification.getCode();\n        }\n\n        /**\n         * Only for test.\n         */\n        boolean contains(String username, String email) {\n            return emailVerificationCodeCache\n                .getIfPresent(new UsernameEmail(username, email)) != null;\n        }\n\n        record UsernameEmail(String username, String email) {\n            public UsernameEmail {\n                // convert to lower case to make it case-insensitive\n                email = StringUtils.lowerCase(email);\n            }\n        }\n\n        @Data\n        @Accessors(chain = true)\n        static class Verification {\n            private String code;\n            private AtomicInteger attempts;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/impl/PatServiceImpl.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport com.nimbusds.jose.jwk.JWKSet;\nimport com.nimbusds.jose.jwk.source.ImmutableJWKSet;\nimport java.time.Clock;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;\nimport org.springframework.security.oauth2.jwt.JwsHeader;\nimport org.springframework.security.oauth2.jwt.JwtClaimsSet;\nimport org.springframework.security.oauth2.jwt.JwtEncoder;\nimport org.springframework.security.oauth2.jwt.JwtEncoderParameters;\nimport org.springframework.security.oauth2.jwt.NimbusJwtEncoder;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.AlternativeJdkIdGenerator;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.IdGenerator;\nimport org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.PatService;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.security.PersonalAccessToken;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\n/**\n * Service for managing personal access tokens (PATs).\n *\n * @author johnniang\n */\n@Service\nclass PatServiceImpl implements PatService {\n\n    private final RoleService roleService;\n\n    private final IdGenerator idGenerator;\n\n    private final ReactiveExtensionClient client;\n\n    private final AuthenticationTrustResolver authTrustResolver =\n        new AuthenticationTrustResolverImpl();\n\n    private final JwtEncoder jwtEncoder;\n\n    private final ExternalUrlSupplier externalUrl;\n\n    private final ReactiveUserDetailsService userDetailsService;\n\n    private final String keyId;\n\n    private Clock clock;\n\n    public PatServiceImpl(RoleService roleService,\n        ReactiveExtensionClient client,\n        ExternalUrlSupplier externalUrl,\n        CryptoService cryptoService, ReactiveUserDetailsService userDetailsService) {\n        this.roleService = roleService;\n        this.client = client;\n        this.externalUrl = externalUrl;\n        this.userDetailsService = userDetailsService;\n        this.clock = Clock.systemUTC();\n        idGenerator = new AlternativeJdkIdGenerator();\n        var jwk = cryptoService.getJwk();\n        this.jwtEncoder = new NimbusJwtEncoder(new ImmutableJWKSet<>(new JWKSet(jwk)));\n        this.keyId = jwk.getKeyID();\n    }\n\n    /**\n     * Set clock for testing.\n     *\n     * @param clock the clock to set\n     */\n    void setClock(Clock clock) {\n        this.clock = clock;\n    }\n\n    @Override\n    public Mono<PersonalAccessToken> create(PersonalAccessToken patRequest) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            // TODO We only allow authenticated users to create PATs.\n            .filter(authTrustResolver::isAuthenticated)\n            .switchIfEmpty(\n                Mono.error(() -> new ServerWebInputException(\"Authentication required.\"))\n            )\n            .flatMap(auth ->\n                create(patRequest, auth.getName(), auth.getAuthorities())\n            );\n    }\n\n    @Override\n    public Mono<PersonalAccessToken> create(PersonalAccessToken patRequest, String username) {\n        return userDetailsService.findByUsername(username)\n            .flatMap(userDetails ->\n                create(patRequest, username, userDetails.getAuthorities())\n            );\n    }\n\n    private Mono<PersonalAccessToken> create(PersonalAccessToken patRequest, String username,\n        Collection<? extends GrantedAuthority> authorities) {\n        var patSpec = patRequest.getSpec();\n        // preflight check\n        var expiresAt = patSpec.getExpiresAt();\n        if (expiresAt != null && expiresAt.isBefore(clock.instant())) {\n            return Mono.error(new ServerWebInputException(\"Invalid expiresAt.\"));\n        }\n        var roles = patSpec.getRoles();\n        return hasSufficientRoles(authorities, roles)\n            .filter(has -> has)\n            .switchIfEmpty(\n                Mono.error(() -> new ServerWebInputException(\"Insufficient roles.\"))\n            )\n            .map(has -> {\n                var pat = new PersonalAccessToken();\n                pat.setMetadata(new Metadata());\n                if (patRequest.getMetadata() != null) {\n                    var metadata = patRequest.getMetadata();\n                    if (metadata.getName() != null) {\n                        pat.getMetadata().setName(metadata.getName());\n                    }\n                    if (metadata.getGenerateName() != null) {\n                        pat.getMetadata().setGenerateName(metadata.getGenerateName());\n                    }\n                    if (metadata.getLabels() != null) {\n                        pat.getMetadata().setLabels(new HashMap<>());\n                        pat.getMetadata().getLabels().putAll(metadata.getLabels());\n                    }\n                    if (metadata.getAnnotations() != null) {\n                        pat.getMetadata().setAnnotations(new HashMap<>());\n                        pat.getMetadata().getAnnotations()\n                            .putAll(metadata.getAnnotations());\n                    }\n                    if (metadata.getFinalizers() != null) {\n                        pat.getMetadata().setFinalizers(new HashSet<>());\n                        pat.getMetadata().getFinalizers().addAll(metadata.getFinalizers());\n                    }\n                }\n                if (pat.getMetadata().getGenerateName() == null) {\n                    pat.getMetadata().setGenerateName(\"pat-\" + username + \"-\");\n                }\n                pat.getSpec().setUsername(username);\n                pat.getSpec().setName(patSpec.getName());\n                pat.getSpec().setDescription(patSpec.getDescription());\n                if (patSpec.getRoles() != null) {\n                    pat.getSpec().setRoles(new ArrayList<>());\n                    pat.getSpec().getRoles().addAll(patSpec.getRoles());\n                }\n                if (patSpec.getScopes() != null) {\n                    pat.getSpec().setScopes(new ArrayList<>());\n                    pat.getSpec().getScopes().addAll(patSpec.getScopes());\n                }\n                pat.getSpec().setExpiresAt(patSpec.getExpiresAt());\n                pat.getSpec().setTokenId(idGenerator.generateId().toString());\n                return pat;\n            })\n            .flatMap(client::create);\n    }\n\n    @Override\n    public Mono<PersonalAccessToken> revoke(String patName, String username) {\n        return get(patName, username)\n            .filter(pat -> !pat.getSpec().isRevoked())\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"The token has been revoked before.\"))\n            )\n            .doOnNext(pat -> {\n                pat.getSpec().setRevoked(true);\n                pat.getSpec().setRevokesAt(clock.instant());\n            })\n            .flatMap(client::update);\n    }\n\n    @Override\n    public Mono<PersonalAccessToken> restore(String patName, String username) {\n        return get(patName, username)\n            .filter(pat -> pat.getSpec().isRevoked())\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"The token has not been revoked before.\"))\n            )\n            .doOnNext(pat -> {\n                pat.getSpec().setRevoked(false);\n                pat.getSpec().setRevokesAt(null);\n            })\n            .flatMap(client::update);\n    }\n\n    @Override\n    public Mono<PersonalAccessToken> delete(String patName, String username) {\n        return get(patName, username)\n            .flatMap(client::delete);\n    }\n\n    @Override\n    public Mono<PersonalAccessToken> get(String patName, String username) {\n        return client.fetch(PersonalAccessToken.class, patName)\n            .filter(pat -> Objects.equals(pat.getSpec().getUsername(), username))\n            .switchIfEmpty(Mono.error(() -> new NotFoundException(\n                \"The personal access token was not found or deleted.\"\n            )));\n    }\n\n    @Override\n    public Mono<String> generateToken(PersonalAccessToken pat) {\n        return Mono.deferContextual(\n                contextView -> {\n                    var externalUrl = ServerWebExchangeContextFilter.getExchange(contextView)\n                        .map(exchange -> this.externalUrl.getURL(exchange.getRequest()))\n                        .orElse(null);\n                    if (externalUrl == null) {\n                        return Mono.error(new ServerWebInputException(\"Server web exchange is \"\n                            + \"required\"));\n                    }\n                    var claimsBuilder = JwtClaimsSet.builder()\n                        .issuer(externalUrl.toString())\n                        .id(pat.getSpec().getTokenId())\n                        .subject(pat.getSpec().getUsername())\n                        .issuedAt(clock.instant())\n                        .claim(\"pat_name\", pat.getMetadata().getName());\n                    var expiresAt = pat.getSpec().getExpiresAt();\n                    if (expiresAt != null) {\n                        claimsBuilder.expiresAt(expiresAt);\n                    }\n                    var headerBuilder = JwsHeader.with(SignatureAlgorithm.RS256)\n                        .keyId(this.keyId);\n                    var jwt = jwtEncoder.encode(JwtEncoderParameters.from(\n                        headerBuilder.build(),\n                        claimsBuilder.build()));\n                    return Mono.just(jwt);\n                }\n            )\n            .map(jwt -> PersonalAccessToken.PAT_TOKEN_PREFIX + jwt.getTokenValue());\n    }\n\n    private Mono<Boolean> hasSufficientRoles(\n        Collection<? extends GrantedAuthority> grantedAuthorities, List<String> requestRoles) {\n        if (CollectionUtils.isEmpty(requestRoles)) {\n            return Mono.just(true);\n        }\n        var grantedRoles = AuthorityUtils.authoritiesToRoles(grantedAuthorities);\n        return roleService.contains(grantedRoles, requestRoles);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/impl/SettingConfigServiceImpl.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport java.time.Duration;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.user.service.SettingConfigService;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.SettingUtils;\nimport tools.jackson.databind.node.ObjectNode;\n\n/**\n * {@link Setting} related {@link ConfigMap} service implementation.\n *\n * @author guqing\n * @since 2.20.0\n */\n@Component\n@RequiredArgsConstructor\nclass SettingConfigServiceImpl implements SettingConfigService {\n\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<Void> upsertConfig(String configMapName, ObjectNode configJsonData) {\n        Assert.notNull(configMapName, \"Config map name must not be null\");\n        Assert.notNull(configJsonData, \"Config json data must not be null\");\n        var data = SettingUtils.settingConfigJsonToMap(configJsonData);\n        return Mono.defer(() -> client.fetch(ConfigMap.class, configMapName)\n                .flatMap(persisted -> {\n                    persisted.setData(data);\n                    return client.update(persisted);\n                }))\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance)\n            )\n            .switchIfEmpty(Mono.defer(() -> {\n                var configMap = new ConfigMap();\n                configMap.setMetadata(new Metadata());\n                configMap.getMetadata().setName(configMapName);\n                configMap.setData(data);\n                return client.create(configMap);\n            }))\n            .then();\n    }\n\n    @Override\n    public Mono<ObjectNode> fetchConfig(String configMapName) {\n        return client.fetch(ConfigMap.class, configMapName)\n            .map(SettingUtils::settingConfigToJson);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/impl/UserConnectionServiceImpl.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport static run.halo.app.extension.ExtensionUtil.defaultSort;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.time.Clock;\nimport java.util.HashMap;\nimport java.util.Optional;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.security.oauth2.core.user.OAuth2User;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.UserConnection;\nimport run.halo.app.core.extension.UserConnection.UserConnectionSpec;\nimport run.halo.app.core.user.service.UserConnectionService;\nimport run.halo.app.event.user.UserConnectionDisconnectedEvent;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataOperator;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.exception.OAuth2UserAlreadyBoundException;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@Service\npublic class UserConnectionServiceImpl implements UserConnectionService {\n\n    private final ReactiveExtensionClient client;\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    private Clock clock = Clock.systemDefaultZone();\n\n    public UserConnectionServiceImpl(ReactiveExtensionClient client,\n        ApplicationEventPublisher eventPublisher) {\n        this.client = client;\n        this.eventPublisher = eventPublisher;\n    }\n\n    void setClock(Clock clock) {\n        this.clock = clock;\n    }\n\n    @Override\n    public Mono<UserConnection> createUserConnection(\n        String username,\n        String registrationId,\n        OAuth2User oauth2User\n    ) {\n        return getUserConnection(registrationId, username)\n            .flatMap(connection -> Mono.<UserConnection>error(\n                () -> new OAuth2UserAlreadyBoundException(connection))\n            )\n            .switchIfEmpty(Mono.defer(() -> {\n                var connection = new UserConnection();\n                connection.setMetadata(new Metadata());\n                var metadata = connection.getMetadata();\n                updateUserInfo(metadata, oauth2User);\n                metadata.setGenerateName(username + \"-\");\n                connection.setSpec(new UserConnectionSpec());\n                var spec = connection.getSpec();\n                spec.setUsername(username);\n                spec.setProviderUserId(oauth2User.getName());\n                spec.setRegistrationId(registrationId);\n                spec.setUpdatedAt(clock.instant());\n                return client.create(connection);\n            }));\n    }\n\n    private Mono<UserConnection> updateUserConnection(UserConnection connection,\n        OAuth2User oauth2User) {\n        connection.getSpec().setUpdatedAt(clock.instant());\n        updateUserInfo(connection.getMetadata(), oauth2User);\n        return client.update(connection);\n    }\n\n    private Mono<UserConnection> getUserConnection(String registrationId, String username) {\n        var listOptions = ListOptions.builder()\n            .fieldQuery(and(\n                equal(\"spec.registrationId\", registrationId),\n                equal(\"spec.username\", username)\n            ))\n            .build();\n        return client.listAll(UserConnection.class, listOptions, defaultSort()).next();\n    }\n\n    @Override\n    public Mono<UserConnection> updateUserConnectionIfPresent(String registrationId,\n        OAuth2User oauth2User) {\n        var listOptions = ListOptions.builder()\n            .fieldQuery(and(\n                equal(\"spec.registrationId\", registrationId),\n                equal(\"spec.providerUserId\", oauth2User.getName())\n            ))\n            .build();\n        return client.listAll(UserConnection.class, listOptions, defaultSort()).next()\n            .flatMap(connection -> updateUserConnection(connection, oauth2User));\n    }\n\n    @Override\n    public Flux<UserConnection> removeUserConnection(String registrationId, String username) {\n        var listOptions = ListOptions.builder()\n            .fieldQuery(and(\n                equal(\"spec.registrationId\", registrationId),\n                equal(\"spec.username\", username)\n            ))\n            .build();\n        return client.listAll(UserConnection.class, listOptions, defaultSort())\n            .flatMap(client::delete)\n            .doOnNext(deleted ->\n                eventPublisher.publishEvent(new UserConnectionDisconnectedEvent(this, deleted))\n            );\n    }\n\n    private void updateUserInfo(MetadataOperator metadata, OAuth2User oauth2User) {\n        var annotations = Optional.ofNullable(metadata.getAnnotations())\n            .orElseGet(HashMap::new);\n        metadata.setAnnotations(annotations);\n        annotations.put(\n            \"auth.halo.run/oauth2-user-info\",\n            JsonUtils.objectToJson(oauth2User.getAttributes())\n        );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/core/user/service/impl/UserServiceImpl.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport static run.halo.app.extension.ExtensionUtil.defaultSort;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.core.session.ReactiveSessionRegistry;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport org.springframework.transaction.reactive.TransactionalOperator;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.RoleBinding;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.user.service.EmailVerificationService;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.SignUpData;\nimport run.halo.app.core.user.service.UserPostCreatingHandler;\nimport run.halo.app.core.user.service.UserPreCreatingHandler;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.event.user.PasswordChangedEvent;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.ValidationUtils;\nimport run.halo.app.infra.exception.DuplicateNameException;\nimport run.halo.app.infra.exception.EmailAlreadyTakenException;\nimport run.halo.app.infra.exception.EmailVerificationFailed;\nimport run.halo.app.infra.exception.RestrictedNameException;\nimport run.halo.app.infra.exception.UnsatisfiedAttributeValueException;\nimport run.halo.app.infra.exception.UserNotFoundException;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.security.authorization.AuthorityUtils;\nimport run.halo.app.security.device.DeviceService;\n\n@Slf4j\n@Service\n@RequiredArgsConstructor\npublic class UserServiceImpl implements UserService {\n\n    private final ReactiveExtensionClient client;\n\n    private final PasswordEncoder passwordEncoder;\n\n    private final SystemConfigFetcher environmentFetcher;\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    private final RoleService roleService;\n\n    private final EmailVerificationService emailVerificationService;\n\n    private final ExtensionGetter extensionGetter;\n\n    private final DeviceService deviceService;\n\n    private final ReactiveTransactionManager transactionManager;\n\n    private final ReactiveSessionRegistry sessionRegistry;\n\n    private Clock clock = Clock.systemUTC();\n\n    void setClock(Clock clock) {\n        this.clock = clock;\n    }\n\n    @Override\n    public Mono<User> getUser(String username) {\n        return client.get(User.class, username)\n            .onErrorMap(ExtensionNotFoundException.class, e -> new UserNotFoundException(username));\n    }\n\n    @Override\n    public Mono<User> findUserByVerifiedEmail(String email) {\n        var listOptions = ListOptions.builder()\n            .andQuery(equal(\"spec.emailVerified\", true))\n            .andQuery(equal(\"spec.email\", email.toLowerCase()))\n            .build();\n        return client.listAll(User.class, listOptions, defaultSort()).next();\n    }\n\n    @Override\n    public Mono<User> getUserOrGhost(String username) {\n        return client.fetch(User.class, username)\n            .switchIfEmpty(Mono.defer(() -> client.get(User.class, GHOST_USER_NAME)));\n    }\n\n    @Override\n    public Flux<User> getUsersOrGhosts(Collection<String> names) {\n        if (CollectionUtils.isEmpty(names)) {\n            return Flux.empty();\n        }\n        var nameSet = new HashSet<>(names);\n        nameSet.add(GHOST_USER_NAME);\n        var options = ListOptions.builder()\n            .andQuery(Queries.in(\"metadata.name\", nameSet))\n            .build();\n        return client.listAll(User.class, options, defaultSort())\n            .collectMap(u -> u.getMetadata().getName())\n            .map(map -> {\n                var ghost = map.get(GHOST_USER_NAME);\n                return names.stream()\n                    .map(name -> map.getOrDefault(name, ghost))\n                    .toList();\n            })\n            .flatMapMany(Flux::fromIterable);\n    }\n\n    @Override\n    public Mono<User> updatePassword(String username, String newPassword) {\n        return getUser(username)\n            .filter(user -> !Objects.equals(user.getSpec().getPassword(), newPassword))\n            .flatMap(user -> {\n                user.getSpec().setPassword(newPassword);\n                return client.update(user);\n            })\n            .doOnNext(user -> publishPasswordChangedEvent(username));\n    }\n\n    @Override\n    public Mono<User> updateWithRawPassword(String username, String rawPassword) {\n        if (!ValidationUtils.PASSWORD_PATTERN.matcher(rawPassword).matches()) {\n            return Mono.error(\n                new UnsatisfiedAttributeValueException(\"validation.error.password.pattern\"));\n        }\n        return getUser(username)\n            .filter(user -> {\n                if (!StringUtils.hasText(user.getSpec().getPassword())) {\n                    // Check if the old password is set before, or the passwordEncoder#matches\n                    // will complain an error due to null password.\n                    return true;\n                }\n                return !passwordEncoder.matches(rawPassword, user.getSpec().getPassword());\n            })\n            .flatMap(user -> {\n                user.getSpec().setPassword(passwordEncoder.encode(rawPassword));\n                return client.update(user);\n            })\n            .doOnNext(user -> publishPasswordChangedEvent(username));\n    }\n\n    @Override\n    public Mono<User> grantRoles(String username, Set<String> roles) {\n        var bindingsToUpdate = new HashSet<RoleBinding>();\n        var bindingsToDelete = new HashSet<RoleBinding>();\n        var existingRoles = new HashSet<String>();\n        var subject = new RoleBinding.Subject();\n        subject.setKind(User.KIND);\n        subject.setApiGroup(User.GROUP);\n        subject.setName(username);\n        var tx = TransactionalOperator.create(transactionManager);\n        return roleService.listRoleBindings(subject)\n            .doOnNext(binding -> {\n                var roleName = binding.getRoleRef().getName();\n                existingRoles.add(roleName);\n                if (roles.contains(roleName)) {\n                    return;\n                }\n                binding.getSubjects().removeIf(RoleBinding.Subject.isUser(username));\n                if (CollectionUtils.isEmpty(binding.getSubjects())) {\n                    // remove it if subjects is empty\n                    bindingsToDelete.add(binding);\n                } else {\n                    bindingsToUpdate.add(binding);\n                }\n            })\n            .then(Mono.defer(() -> {\n                if (log.isDebugEnabled()) {\n                    log.debug(\"\"\"\n                            Updating roles for user {}: existingRoles={}, roles={}, \\\n                            bindingsToUpdate={}, bindingsToDelete={}\"\"\",\n                        username, existingRoles, roles, bindingsToUpdate, bindingsToDelete\n                    );\n                }\n                var updateBindings = Flux.fromIterable(bindingsToUpdate)\n                    .flatMap(client::update)\n                    .then();\n                var deleteBindings = Flux.fromIterable(bindingsToDelete)\n                    .flatMap(client::delete)\n                    .then();\n                var createBindings = Flux.fromIterable(roles)\n                    .filter(role -> !existingRoles.contains(role))\n                    .filter(StringUtils::hasText)\n                    .map(role -> RoleBinding.create(username, role))\n                    .flatMap(client::create);\n                return Mono.when(updateBindings, deleteBindings, createBindings);\n            }))\n            .as(tx::transactional)\n            .then(Mono.defer(() -> {\n                if (Objects.equals(roles, existingRoles)) {\n                    // No need to update the user if roles are not changed\n                    log.debug(\"No role changes for user {}, skip updating user annotations.\",\n                        username);\n                    return Mono.empty();\n                }\n                log.info(\"Updated roles for user {}: existingRoles={}, roles={}\",\n                    username, existingRoles, roles\n                );\n                var invalidateSessions = sessionRegistry.getAllSessions(username)\n                    .flatMap(reactiveSessionInformation -> {\n                        log.info(\"Invalidating session {} for user {}\",\n                            reactiveSessionInformation.getSessionId(), username\n                        );\n                        return reactiveSessionInformation.invalidate();\n                    })\n                    .then();\n                var updateUser = client.get(User.class, username)\n                    .doOnNext(u -> {\n                        var annotations = u.getMetadata().getAnnotations();\n                        if (annotations == null) {\n                            annotations = new HashMap<>();\n                            u.getMetadata().setAnnotations(annotations);\n                        }\n                        annotations.put(User.REQUEST_TO_UPDATE, clock.instant().toString());\n                    })\n                    .flatMap(client::update)\n                    .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                        .filter(OptimisticLockingFailureException.class::isInstance)\n                    );\n                return invalidateSessions.then(updateUser);\n            }));\n    }\n\n    @Override\n    public Mono<Boolean> hasSufficientRoles(Collection<String> roles) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(a -> AuthorityUtils.authoritiesToRoles(a.getAuthorities()))\n            .flatMap(userRoles -> roleService.contains(userRoles, roles))\n            .defaultIfEmpty(false);\n    }\n\n    @Override\n    public Mono<User> signUp(SignUpData signUpData) {\n        return environmentFetcher.fetch(SystemSetting.User.GROUP, SystemSetting.User.class)\n            .filter(SystemSetting.User::isAllowRegistration)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"The registration is not allowed by the administrator.\"\n            )))\n            .filter(setting -> isUsernameAllowed(setting, signUpData.getUsername()))\n            .switchIfEmpty(Mono.error(() -> new RestrictedNameException(\n                \"The username is restricted.\",\n                \"problemDetail.user.username.restricted\",\n                new Object[] {signUpData.getUsername()}\n            )))\n            .filter(setting -> isDisplayNameAllowed(setting, signUpData.getDisplayName()))\n            .switchIfEmpty(Mono.error(() -> new RestrictedNameException(\n                \"The display name is restricted.\",\n                \"problemDetail.user.displayName.restricted\",\n                new Object[] {signUpData.getDisplayName()}\n            )))\n            .filter(setting -> StringUtils.hasText(setting.getDefaultRole()))\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"The default role is not configured by the administrator.\"\n            )))\n            .flatMap(setting -> {\n                var email = Optional.ofNullable(signUpData.getEmail())\n                    .map(String::toLowerCase)\n                    .orElse(null);\n                var user = new User();\n                user.setMetadata(new Metadata());\n                var metadata = user.getMetadata();\n                metadata.setName(signUpData.getUsername());\n                user.setSpec(new User.UserSpec());\n                var spec = user.getSpec();\n                spec.setPassword(passwordEncoder.encode(signUpData.getPassword()));\n                spec.setEmailVerified(false);\n                spec.setRegisteredAt(clock.instant());\n                spec.setEmail(email);\n                spec.setDisplayName(signUpData.getDisplayName());\n                Mono<Void> verifyEmail = Mono.empty();\n                if (setting.isMustVerifyEmailOnRegistration()) {\n                    if (!StringUtils.hasText(email)) {\n                        return Mono.error(\n                            new EmailVerificationFailed(\"Email captcha is required\", null)\n                        );\n                    }\n                    verifyEmail = emailVerificationService.verifyRegisterVerificationCode(\n                            email, signUpData.getEmailCode()\n                        )\n                        .filter(Boolean::booleanValue)\n                        .switchIfEmpty(Mono.error(() ->\n                            new EmailVerificationFailed(\"Invalid email captcha.\", null)\n                        ))\n                        .then(this.checkEmailAlreadyVerified(email))\n                        .filter(has -> !has)\n                        .switchIfEmpty(Mono.error(\n                            () -> new EmailAlreadyTakenException(\"Email is already taken\")\n                        ))\n                        .doOnNext(v -> spec.setEmailVerified(true))\n                        .then();\n                }\n                return verifyEmail.then(Mono.defer(() -> {\n                    var defaultRole = setting.getDefaultRole();\n                    return createUser(user, Set.of(defaultRole));\n                }));\n            });\n    }\n\n    @Override\n    public Mono<User> createUser(User user, Set<String> roleNames) {\n        Assert.notNull(user, \"User must not be null\");\n        Assert.notNull(roleNames, \"Roles must not be null\");\n        return client.fetch(User.class, user.getMetadata().getName())\n            .hasElement()\n            .flatMap(hasUser -> {\n                if (hasUser) {\n                    return Mono.error(\n                        new DuplicateNameException(\"User name is already in use\", null,\n                            \"problemDetail.user.duplicateName\",\n                            new Object[] {user.getMetadata().getName()}));\n                }\n                // Check if all roles exist\n                return Flux.fromIterable(roleNames)\n                    .flatMap(roleName -> client.fetch(Role.class, roleName)\n                        .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                            \"Role [\" + roleName + \"] is not found.\"))\n                        )\n                    )\n                    .then();\n            })\n            .then(extensionGetter.getExtensions(UserPreCreatingHandler.class)\n                .concatMap(handler -> handler.preCreating(user))\n                .then(Mono.defer(() -> client.create(user)\n                    .flatMap(newUser -> grantRoles(user.getMetadata().getName(), roleNames))\n                ))\n                .flatMap(createdUser -> extensionGetter.getExtensions(UserPostCreatingHandler.class)\n                    .concatMap(handler -> handler.postCreating(createdUser))\n                    .then()\n                    .thenReturn(createdUser)\n                )\n            );\n    }\n\n    @Override\n    public Mono<Boolean> confirmPassword(String username, String rawPassword) {\n        return getUser(username)\n            .filter(user -> {\n                if (!StringUtils.hasText(user.getSpec().getPassword())) {\n                    // If the password is not set, return true directly.\n                    return true;\n                }\n                if (!StringUtils.hasText(rawPassword)) {\n                    return false;\n                }\n                return passwordEncoder.matches(rawPassword, user.getSpec().getPassword());\n            })\n            .hasElement();\n    }\n\n    @Override\n    public Flux<User> listByEmail(String email) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(equal(\"spec.email\", email.toLowerCase())));\n        return client.listAll(User.class, listOptions, defaultSort());\n    }\n\n    @Override\n    public Mono<Boolean> checkEmailAlreadyVerified(String email) {\n        return listByEmail(email)\n            // TODO Use index query in the future\n            .filter(u -> u.getSpec().isEmailVerified())\n            .hasElements();\n    }\n\n    @Override\n    public String encryptPassword(String rawPassword) {\n        return passwordEncoder.encode(rawPassword);\n    }\n\n    @Override\n    public Mono<User> disable(String username) {\n        var tx = TransactionalOperator.create(transactionManager);\n        return client.fetch(User.class, username)\n            .filter(user -> !Boolean.TRUE.equals(user.getSpec().getDisabled()))\n            .flatMap(user -> deviceService.revoke(username).thenReturn(user))\n            .doOnNext(user -> user.getSpec().setDisabled(true))\n            .flatMap(client::update)\n            .as(tx::transactional);\n    }\n\n    @Override\n    public Mono<User> enable(String username) {\n        return client.fetch(User.class, username)\n            .filter(user -> Boolean.TRUE.equals(user.getSpec().getDisabled()))\n            .doOnNext(user -> user.getSpec().setDisabled(false))\n            .flatMap(client::update);\n    }\n\n    void publishPasswordChangedEvent(String username) {\n        eventPublisher.publishEvent(new PasswordChangedEvent(this, username));\n    }\n\n    private Set<String> getProtectedUsernamesSet(SystemSetting.User setting) {\n        String protectedUsernamesStr = setting.getProtectedUsernames();\n        if (protectedUsernamesStr == null || protectedUsernamesStr.trim().isEmpty()) {\n            return Set.of();\n        }\n        return Arrays.stream(protectedUsernamesStr.split(\",\"))\n            .map(String::trim)\n            .filter(n -> !n.isEmpty())\n            .map(String::toLowerCase)\n            .collect(Collectors.toUnmodifiableSet());\n    }\n\n    private boolean isUsernameAllowed(SystemSetting.User setting, String username) {\n        Set<String> protectedLowerSet = getProtectedUsernamesSet(setting);\n        return !protectedLowerSet.contains(username.trim().toLowerCase());\n    }\n\n    private boolean isDisplayNameAllowed(SystemSetting.User setting, String displayName) {\n        Set<String> protectedLowerSet = getProtectedUsernamesSet(setting);\n        return !protectedLowerSet.contains(displayName.trim().toLowerCase());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/CategoryHiddenStateChangeEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.content.Category;\n\n/**\n * When the category {@link Category.CategorySpec#isHideFromList()} state changes, this event is\n * triggered.\n *\n * @author guqing\n * @since 2.17.0\n */\n@Getter\npublic class CategoryHiddenStateChangeEvent extends ApplicationEvent {\n    private final String categoryName;\n    private final boolean hidden;\n\n    public CategoryHiddenStateChangeEvent(Object source, String categoryName, boolean hidden) {\n        super(source);\n        this.categoryName = categoryName;\n        this.hidden = hidden;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/CommentCreatedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.content.Comment;\n\n/**\n * Comment created event.\n *\n * @author guqing\n * @since 2.9.0\n */\n@Getter\npublic class CommentCreatedEvent extends ApplicationEvent {\n\n    private final Comment comment;\n\n    public CommentCreatedEvent(Object source, Comment comment) {\n        super(source);\n        this.comment = comment;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/CommentUnreadReplyCountChangedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * <p>This event will be triggered when the unread reply count of the comment is changed.</p>\n * <p>It is used to update the unread reply count of the comment,such as when the user reads the\n * reply(lastReadTime changed in comment), the unread reply count will be updated.</p>\n *\n * @author guqing\n * @since 2.14.0\n */\n@Getter\npublic class CommentUnreadReplyCountChangedEvent extends ApplicationEvent {\n    private final String commentName;\n\n    public CommentUnreadReplyCountChangedEvent(Object source, String commentName) {\n        super(source);\n        this.commentName = commentName;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/DownvotedEvent.java",
    "content": "package run.halo.app.event.post;\n\n/**\n * Downvote event.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class DownvotedEvent extends VotedEvent {\n\n    public DownvotedEvent(Object source, String group, String name, String plural) {\n        super(source, group, name, plural);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/PostStatsChangedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport lombok.Getter;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.content.Post;\n\n@Getter\npublic class PostStatsChangedEvent extends ApplicationEvent {\n    private final Counter counter;\n\n    public PostStatsChangedEvent(Object source, Counter counter) {\n        super(source);\n        this.counter = counter;\n    }\n\n    public String getPostName() {\n        var counterName = counter.getMetadata().getName();\n        return StringUtils.removeStart(counterName, MeterUtils.nameOf(Post.class, \"\"));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/ReplyChangedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport run.halo.app.core.extension.content.Reply;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class ReplyChangedEvent extends ReplyEvent {\n\n    public ReplyChangedEvent(Object source, Reply reply) {\n        super(source, reply);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/ReplyCreatedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport run.halo.app.core.extension.content.Reply;\n\n/**\n * Reply created event.\n *\n * @author guqing\n * @since 2.9.0\n */\npublic class ReplyCreatedEvent extends ReplyEvent {\n\n    public ReplyCreatedEvent(Object source, Reply reply) {\n        super(source, reply);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/ReplyDeletedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport run.halo.app.core.extension.content.Reply;\n\npublic class ReplyDeletedEvent extends ReplyEvent {\n\n    public ReplyDeletedEvent(Object source, Reply reply) {\n        super(source, reply);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/ReplyEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.content.Reply;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic abstract class ReplyEvent extends ApplicationEvent {\n    private final Reply reply;\n\n    public ReplyEvent(Object source, Reply reply) {\n        super(source);\n        this.reply = reply;\n    }\n\n    public Reply getReply() {\n        return reply;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/UpvotedEvent.java",
    "content": "package run.halo.app.event.post;\n\n/**\n * Upvote event.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class UpvotedEvent extends VotedEvent {\n\n    public UpvotedEvent(Object source, String group, String name, String plural) {\n        super(source, group, name, plural);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/VisitedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Getter\npublic class VisitedEvent extends ApplicationEvent {\n    private final String group;\n    private final String name;\n    private final String plural;\n\n    public VisitedEvent(Object source, String group, String name, String plural) {\n        super(source);\n        this.group = group;\n        this.name = name;\n        this.plural = plural;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/post/VotedEvent.java",
    "content": "package run.halo.app.event.post;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Getter\npublic abstract class VotedEvent extends ApplicationEvent {\n    private final String group;\n    private final String name;\n    private final String plural;\n\n    public VotedEvent(Object source, String group, String name, String plural) {\n        super(source);\n        this.group = group;\n        this.name = name;\n        this.plural = plural;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/event/user/PasswordChangedEvent.java",
    "content": "package run.halo.app.event.user;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\n\n@Getter\npublic class PasswordChangedEvent extends ApplicationEvent {\n    private final String username;\n\n    public PasswordChangedEvent(Object source, String username) {\n        super(source);\n        this.username = username;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/DefaultSchemeManager.java",
    "content": "package run.halo.app.extension;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.function.Consumer;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.event.SchemeAddedEvent;\nimport run.halo.app.extension.event.SchemeRemovedEvent;\nimport run.halo.app.extension.index.IndexEngine;\nimport run.halo.app.extension.index.IndexSpecs;\nimport run.halo.app.extension.index.ValueIndexSpec;\n\n@Component\npublic class DefaultSchemeManager implements SchemeManager {\n\n    private final List<Scheme> schemes;\n\n    private final IndexEngine indexEngine;\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    public DefaultSchemeManager(IndexEngine indexEngine,\n        ApplicationEventPublisher eventPublisher) {\n        this.indexEngine = indexEngine;\n        this.eventPublisher = eventPublisher;\n        // we have to use CopyOnWriteArrayList at here to prevent concurrent modification between\n        // registering and listing.\n        schemes = new CopyOnWriteArrayList<>();\n    }\n\n    @Override\n    public <E extends Extension> void register(Class<E> type,\n        Consumer<IndexSpecs<E>> specsConsumer) {\n        var scheme = Scheme.buildFromType(type);\n        if (schemes.contains(scheme)) {\n            return;\n        }\n        var indexSpecs = new DefaultIndexSpecs<E>();\n        if (specsConsumer != null) {\n            specsConsumer.accept(indexSpecs);\n        }\n        indexEngine.getIndicesManager().add(type, indexSpecs.getIndexSpecs());\n        schemes.add(scheme);\n        eventPublisher.publishEvent(new SchemeAddedEvent(this, scheme));\n    }\n\n    @Override\n    public void unregister(@NonNull Scheme scheme) {\n        if (schemes.contains(scheme)) {\n            indexEngine.getIndicesManager().remove(scheme.type());\n            schemes.remove(scheme);\n            eventPublisher.publishEvent(new SchemeRemovedEvent(this, scheme));\n        }\n    }\n\n    @Override\n    @NonNull\n    public List<Scheme> schemes() {\n        return Collections.unmodifiableList(schemes);\n    }\n\n    private static class DefaultIndexSpecs<E extends Extension> implements IndexSpecs<E> {\n\n        private final Map<String, ValueIndexSpec<E, ?>> specMap;\n\n        private DefaultIndexSpecs() {\n            this.specMap = new HashMap<>();\n        }\n\n        @Override\n        public <K extends Comparable<K>> void add(ValueIndexSpec<E, K> indexSpec) {\n            Assert.isTrue(!specMap.containsKey(indexSpec.getName()),\n                \"Index spec with name \" + indexSpec.getName() + \" already exists.\");\n            this.specMap.put(indexSpec.getName(), indexSpec);\n        }\n\n        @Override\n        public List<ValueIndexSpec<E, ?>> getIndexSpecs() {\n            return specMap.values().stream().toList();\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/DelegateExtensionClient.java",
    "content": "package run.halo.app.extension;\n\nimport java.time.Duration;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.extension.index.IndexedQueryEngine;\n\n/**\n * DelegateExtensionClient fully delegates ReactiveExtensionClient.\n *\n * @author johnniang\n */\n@Component\npublic class DelegateExtensionClient implements ExtensionClient {\n\n    private static final Duration TIMEOUT = Duration.ofSeconds(30);\n\n    private final ReactiveExtensionClient client;\n\n    public DelegateExtensionClient(ReactiveExtensionClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public <E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator) {\n        return client.list(type, predicate, comparator).collectList().block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> ListResult<E> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator, int page, int size) {\n        return client.list(type, predicate, comparator, page, size).block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> List<E> listAll(Class<E> type, ListOptions options, Sort sort) {\n        return client.listAll(type, options, sort).collectList().block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> List<String> listAllNames(Class<E> type, ListOptions options,\n        Sort sort) {\n        return client.listAllNames(type, options, sort).collectList().block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> List<String> listTopNames(Class<E> type, ListOptions options,\n        Sort sort, int topN) {\n        return client.listTopNames(type, options, sort, topN).collectList().block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> ListResult<E> listBy(Class<E> type, ListOptions options,\n        PageRequest page) {\n        return client.listBy(type, options, page).block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> ListResult<String> listNamesBy(Class<E> type, ListOptions options,\n        PageRequest page) {\n        return client.listNamesBy(type, options, page).block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> long countBy(Class<E> type, ListOptions options) {\n        return client.countBy(type, options).blockOptional(TIMEOUT).orElse(0L);\n    }\n\n    @Override\n    public <E extends Extension> Optional<E> fetch(Class<E> type, String name) {\n        return client.fetch(type, name).blockOptional(TIMEOUT);\n    }\n\n    @Override\n    public Optional<Unstructured> fetch(GroupVersionKind gvk, String name) {\n        return client.fetch(gvk, name).blockOptional(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> void create(E extension) {\n        client.create(extension).block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> void update(E extension) {\n        client.update(extension).block(TIMEOUT);\n    }\n\n    @Override\n    public <E extends Extension> void delete(E extension) {\n        client.delete(extension).block(TIMEOUT);\n    }\n\n    @Override\n    public IndexedQueryEngine indexedQueryEngine() {\n        return client.indexedQueryEngine();\n    }\n\n    @Override\n    public void watch(Watcher watcher) {\n        client.watch(watcher);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/ExtensionConverter.java",
    "content": "package run.halo.app.extension;\n\nimport run.halo.app.extension.store.ExtensionStore;\n\n/**\n * ExtensionConverter contains bidirectional conversions between Extension and ExtensionStore.\n *\n * @author johnniang\n */\npublic interface ExtensionConverter {\n\n    /**\n     * Converts Extension to ExtensionStore.\n     *\n     * @param extension is an Extension to be converted.\n     * @param <E> is Extension type.\n     * @return an ExtensionStore.\n     */\n    <E extends Extension> ExtensionStore convertTo(E extension);\n\n    /**\n     * Converts Extension from ExtensionStore.\n     *\n     * @param type is Extension type.\n     * @param extensionStore is an ExtensionStore\n     * @param <E> is Extension type.\n     * @return an Extension\n     */\n    <E extends Extension> E convertFrom(Class<E> type, ExtensionStore extensionStore);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/ExtensionStoreUtil.java",
    "content": "package run.halo.app.extension;\n\nimport org.springframework.util.StringUtils;\n\n/**\n * Extension utilities.\n *\n * @author johnniang\n */\npublic final class ExtensionStoreUtil {\n\n    private ExtensionStoreUtil() {\n    }\n\n    /**\n     * Builds the name prefix of ExtensionStore.\n     *\n     * @param scheme is scheme of an Extension.\n     * @return name prefix of ExtensionStore.\n     */\n    public static String buildStoreNamePrefix(Scheme scheme) {\n        // rule of key: /registry/[group]/plural-name/extension-name\n        StringBuilder builder = new StringBuilder(\"/registry/\");\n        if (StringUtils.hasText(scheme.groupVersionKind().group())) {\n            builder.append(scheme.groupVersionKind().group()).append('/');\n        }\n        builder.append(scheme.plural());\n        return builder.toString();\n    }\n\n    /**\n     * Builds full name of ExtensionStore.\n     *\n     * @param scheme is scheme of an Extension.\n     * @param name the exact name of Extension.\n     * @return full name of ExtensionStore.\n     */\n    public static String buildStoreName(Scheme scheme, String name) {\n        return buildStoreNamePrefix(scheme) + \"/\" + name;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/JSONExtensionConverter.java",
    "content": "package run.halo.app.extension;\n\nimport static run.halo.app.extension.ExtensionStoreUtil.buildStoreName;\nimport static run.halo.app.extension.Unstructured.OBJECT_MAPPER;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.IOException;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.openapi4j.core.exception.ResolutionException;\nimport org.openapi4j.core.model.v3.OAI3;\nimport org.openapi4j.core.model.v3.OAI3Context;\nimport org.openapi4j.schema.validator.ValidationContext;\nimport org.openapi4j.schema.validator.ValidationData;\nimport org.openapi4j.schema.validator.v3.SchemaValidator;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.Exceptions;\nimport run.halo.app.extension.event.SchemeRemovedEvent;\nimport run.halo.app.extension.exception.ExtensionConvertException;\nimport run.halo.app.extension.exception.SchemaViolationException;\nimport run.halo.app.extension.store.ExtensionStore;\n\n/**\n * JSON implementation of ExtensionConverter.\n *\n * @author johnniang\n */\n@Slf4j\n@Component\nclass JSONExtensionConverter implements ExtensionConverter {\n\n    @Getter\n    public ObjectMapper objectMapper;\n\n    private final SchemeManager schemeManager;\n\n    private final ConcurrentMap<Scheme, SchemaValidator> validatorMap = new ConcurrentHashMap<>();\n\n    public JSONExtensionConverter(SchemeManager schemeManager) {\n        this.schemeManager = schemeManager;\n        setObjectMapper(OBJECT_MAPPER);\n    }\n\n    /**\n     * Sets ObjectMapper.\n     *\n     * @param objectMapper the object mapper, must not be null\n     */\n    void setObjectMapper(ObjectMapper objectMapper) {\n        Assert.notNull(objectMapper, \"ObjectMapper must not be null\");\n        this.objectMapper = objectMapper;\n    }\n\n    @Override\n    public <E extends Extension> ExtensionStore convertTo(E extension) {\n        var gvk = extension.groupVersionKind();\n        var scheme = schemeManager.get(gvk);\n\n        try {\n            var convertedExtension = Optional.of(extension)\n                .map(item -> scheme.type().isAssignableFrom(item.getClass()) ? item\n                    : objectMapper.convertValue(item, scheme.type())\n                )\n                .orElseThrow();\n            var validation = new ValidationData<>(extension);\n\n            var extensionJsonNode = objectMapper.valueToTree(convertedExtension);\n            var validator = getValidator(scheme);\n            validator.validate(extensionJsonNode, validation);\n            if (!validation.isValid()) {\n                log.debug(\"Failed to validate Extension: {}, and errors were: {}\",\n                    extension.getClass(), validation.results());\n                throw new SchemaViolationException(extension.groupVersionKind(),\n                    validation.results());\n            }\n\n            var version = extension.getMetadata().getVersion();\n            var storeName = buildStoreName(scheme, extension.getMetadata().getName());\n            var data = objectMapper.writeValueAsBytes(extensionJsonNode);\n            return new ExtensionStore(storeName, data, version);\n        } catch (IOException e) {\n            throw new ExtensionConvertException(\"Failed write Extension as bytes\", e);\n        } catch (ResolutionException e) {\n            throw new RuntimeException(\"Failed to create schema validator\", e);\n        }\n    }\n\n    @Override\n    public <E extends Extension> E convertFrom(Class<E> type, ExtensionStore extensionStore) {\n        try {\n            var extension = objectMapper.readValue(extensionStore.getData(), type);\n            extension.getMetadata().setVersion(extensionStore.getVersion());\n            return extension;\n        } catch (IOException e) {\n            throw new ExtensionConvertException(\"Failed to read Extension \" + type + \" from bytes\",\n                e);\n        }\n    }\n\n    @EventListener\n    void onSchemeRemovedEvent(SchemeRemovedEvent event) {\n        var removed = validatorMap.remove(event.getScheme());\n        if (log.isDebugEnabled()) {\n            if (removed == null) {\n                log.debug(\"No available validator found while removing validator for scheme: {}\",\n                    event.getScheme().groupVersionKind()\n                );\n            } else {\n                log.debug(\"Removed schema validator {} for scheme: {}\",\n                    removed, event.getScheme().groupVersionKind()\n                );\n            }\n        }\n    }\n\n    private SchemaValidator getValidator(Scheme scheme)\n        throws MalformedURLException, ResolutionException {\n        return validatorMap.computeIfAbsent(scheme, s -> {\n            try {\n                var context = new ValidationContext<OAI3>(\n                    new OAI3Context(new URL(\"file:/\"), scheme.openApiSchema())\n                );\n                context.setFastFail(false);\n                var validator = new SchemaValidator(context, null, scheme.openApiSchema());\n                if (log.isDebugEnabled()) {\n                    log.debug(\"Created schema validator {} for scheme: {}\",\n                        validator, scheme.groupVersionKind()\n                    );\n                }\n                return validator;\n            } catch (ResolutionException | MalformedURLException e) {\n                throw Exceptions.propagate(e);\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java",
    "content": "package run.halo.app.extension;\n\nimport static org.apache.commons.lang3.RandomStringUtils.secure;\nimport static org.springframework.util.StringUtils.hasText;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Comparator;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.function.Predicate;\nimport java.util.stream.StreamSupport;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.dao.DataIntegrityViolationException;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.data.util.Predicates;\nimport org.springframework.stereotype.Component;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport org.springframework.transaction.reactive.TransactionalOperator;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\nimport run.halo.app.extension.index.IndexEngine;\nimport run.halo.app.extension.index.IndexedQueryEngine;\nimport run.halo.app.extension.store.ReactiveExtensionStoreClient;\n\n@Slf4j\n@Component\npublic class ReactiveExtensionClientImpl implements ReactiveExtensionClient {\n\n    public static final int GENERATE_NAME_RANDOM_LENGTH = 8;\n\n    private final ReactiveExtensionStoreClient client;\n\n    private final ExtensionConverter converter;\n\n    private final SchemeManager schemeManager;\n\n    private final Watcher.WatcherComposite watchers = new Watcher.WatcherComposite();\n\n    private final ObjectMapper objectMapper;\n\n    private final IndexEngine indexEngine;\n\n    private Scheduler scheduler;\n\n    private TransactionalOperator transactionalOperator;\n\n    public ReactiveExtensionClientImpl(ReactiveExtensionStoreClient client,\n        ExtensionConverter converter, SchemeManager schemeManager, ObjectMapper objectMapper,\n        IndexEngine indexEngine,\n        ReactiveTransactionManager reactiveTransactionManager) {\n        this.client = client;\n        this.converter = converter;\n        this.schemeManager = schemeManager;\n        this.objectMapper = objectMapper;\n        this.indexEngine = indexEngine;\n        this.transactionalOperator = TransactionalOperator.create(reactiveTransactionManager);\n        this.scheduler = Schedulers.boundedElastic();\n    }\n\n    /**\n     * Only for test.\n     *\n     * @param scheduler the scheduler to set\n     */\n    void setScheduler(Scheduler scheduler) {\n        this.scheduler = scheduler;\n    }\n\n    /**\n     * Only for test.\n     */\n    void setTransactionalOperator(TransactionalOperator transactionalOperator) {\n        this.transactionalOperator = transactionalOperator;\n    }\n\n    @Override\n    public <E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator) {\n        var scheme = schemeManager.get(type);\n        var prefix = ExtensionStoreUtil.buildStoreNamePrefix(scheme);\n\n        return client.listByNamePrefix(prefix)\n            .map(extensionStore -> converter.convertFrom(type, extensionStore))\n            .filter(predicate == null ? Predicates.isTrue() : predicate)\n            .sort(comparator == null ? Comparator.naturalOrder() : comparator);\n    }\n\n    @Override\n    public <E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,\n        Comparator<E> comparator, int page, int size) {\n        var extensions = list(type, predicate, comparator);\n        var totalMono = extensions.count();\n        if (page > 0) {\n            extensions = extensions.skip(((long) (page - 1)) * (long) size);\n        }\n        if (size > 0) {\n            extensions = extensions.take(size);\n        }\n        return extensions.collectList().zipWith(totalMono)\n            .map(tuple -> {\n                List<E> content = tuple.getT1();\n                Long total = tuple.getT2();\n                return new ListResult<>(page, size, total, content);\n            });\n    }\n\n    @Override\n    public <E extends Extension> Flux<E> listAll(Class<E> type, ListOptions options, Sort sort) {\n        var nullSafeSort = Optional.ofNullable(sort)\n            .orElseGet(() -> {\n                log.warn(\"The sort parameter is null, it is recommended to use Sort.unsorted() \"\n                    + \"instead and the compatibility support for null will be removed in the \"\n                    + \"subsequent version.\");\n                return Sort.unsorted();\n            });\n        var scheme = schemeManager.get(type);\n\n        return Mono.fromCallable(\n                () -> indexEngine.retrieveAll(scheme.type(), options, nullSafeSort)\n            )\n            .flatMapMany(objectKeys -> {\n                var storeNames = StreamSupport.stream(objectKeys.spliterator(), false)\n                    .map(objectKey -> ExtensionStoreUtil.buildStoreName(scheme, objectKey))\n                    .toList();\n                if (log.isDebugEnabled()) {\n                    if (storeNames.size() > 500) {\n                        log.warn(\"\"\"\n                                The number of objects retrieved by listAll is too large ({}) \\\n                                and it is recommended to use paging query.\\\n                                \"\"\",\n                            storeNames.size()\n                        );\n                    }\n                }\n                long startTimeMs = System.currentTimeMillis();\n                return client.listByNames(storeNames)\n                    .map(extensionStore -> converter.convertFrom(type, extensionStore))\n                    .doOnComplete(() -> log.debug(\n                        \"Successfully retrieved all by names from db for {} in {}ms\",\n                        scheme.groupVersionKind(), System.currentTimeMillis() - startTimeMs)\n                    );\n            });\n    }\n\n    @Override\n    public <E extends Extension> Flux<String> listAllNames(\n        Class<E> type, ListOptions options, Sort sort\n    ) {\n        var scheme = schemeManager.get(type);\n        return Mono.fromCallable(() -> indexEngine.retrieveAll(scheme.type(), options, sort))\n            .flatMapMany(Flux::fromIterable);\n    }\n\n    @Override\n    public <E extends Extension> Flux<String> listTopNames(\n        Class<E> type, ListOptions options, Sort sort, int topN\n    ) {\n        var scheme = schemeManager.get(type);\n        return Mono.fromCallable(() -> indexEngine.retrieveTopN(scheme.type(), options, sort, topN))\n            .flatMapMany(Flux::fromIterable);\n    }\n\n    @Override\n    public <E extends Extension> Mono<ListResult<E>> listBy(Class<E> type, ListOptions options,\n        PageRequest page) {\n        var scheme = schemeManager.get(type);\n        return Mono.fromCallable(() -> indexEngine.retrieve(scheme.type(), options, page))\n            .flatMap(listResult -> {\n                var storeNames = listResult.get()\n                    .map(objectKey -> ExtensionStoreUtil.buildStoreName(scheme, objectKey))\n                    .toList();\n                final long startTimeMs = System.currentTimeMillis();\n                return client.listByNames(storeNames)\n                    .map(extensionStore -> converter.convertFrom(type, extensionStore))\n                    .doOnComplete(() -> log.debug(\n                        \"Successfully retrieved by names from db for {} in {}ms\",\n                        scheme.groupVersionKind(), System.currentTimeMillis() - startTimeMs)\n                    )\n                    .collectList()\n                    .map(items -> new ListResult<>(page.getPageNumber(), page.getPageSize(),\n                        listResult.getTotal(), items)\n                    );\n            })\n            .defaultIfEmpty(ListResult.emptyResult());\n    }\n\n    @Override\n    public <E extends Extension> Mono<ListResult<String>> listNamesBy(Class<E> type,\n        ListOptions options, PageRequest pageable) {\n        var scheme = schemeManager.get(type);\n        return Mono.fromCallable(() -> indexEngine.retrieve(scheme.type(), options, pageable));\n    }\n\n    @Override\n    public <E extends Extension> Mono<Long> countBy(Class<E> type, ListOptions options) {\n        var scheme = schemeManager.get(type);\n        return Mono.fromCallable(() -> indexEngine.count(scheme.type(), options));\n    }\n\n    @Override\n    public <E extends Extension> Mono<E> fetch(Class<E> type, String name) {\n        var storeName = ExtensionStoreUtil.buildStoreName(schemeManager.get(type), name);\n        return client.fetchByName(storeName)\n            .map(extensionStore -> converter.convertFrom(type, extensionStore));\n    }\n\n    @Override\n    public Mono<Unstructured> fetch(GroupVersionKind gvk, String name) {\n        var storeName = ExtensionStoreUtil.buildStoreName(schemeManager.get(gvk), name);\n        return client.fetchByName(storeName)\n            .map(extensionStore -> converter.convertFrom(Unstructured.class, extensionStore));\n    }\n\n    private Mono<JsonExtension> fetchJsonExtension(GroupVersionKind gvk, String name) {\n        var storeName = ExtensionStoreUtil.buildStoreName(schemeManager.get(gvk), name);\n        return client.fetchByName(storeName)\n            .map(extensionStore -> converter.convertFrom(JsonExtension.class, extensionStore));\n    }\n\n    @Override\n    public <E extends Extension> Mono<E> get(Class<E> type, String name) {\n        return fetch(type, name)\n            .switchIfEmpty(Mono.error(() -> {\n                var gvk = GroupVersionKind.fromExtension(type);\n                return new ExtensionNotFoundException(gvk, name);\n            }));\n    }\n\n    private Mono<Unstructured> get(GroupVersionKind gvk, String name) {\n        return fetch(gvk, name)\n            .switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(gvk, name)));\n    }\n\n    @Override\n    public Mono<JsonExtension> getJsonExtension(GroupVersionKind gvk, String name) {\n        return fetchJsonExtension(gvk, name)\n            .switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(gvk, name)));\n    }\n\n    @Override\n    public <E extends Extension> Mono<E> create(E extension) {\n        return Mono.fromCallable(\n                () -> {\n                    var metadata = extension.getMetadata();\n                    // those fields should be managed by halo.\n                    metadata.setCreationTimestamp(Instant.now());\n                    metadata.setDeletionTimestamp(null);\n                    metadata.setVersion(null);\n\n                    if (!hasText(metadata.getName())) {\n                        if (!hasText(metadata.getGenerateName())) {\n                            throw new IllegalArgumentException(\n                                \"The metadata.generateName must not be blank when metadata.name is \"\n                                    + \"blank\");\n                        }\n\n                        // generate name with random text\n                        metadata.setName(metadata.getGenerateName() + secure()\n                            .nextAlphanumeric(GENERATE_NAME_RANDOM_LENGTH)\n                            // Prevent data conflicts caused by database case sensitivity\n                            .toLowerCase()\n                        );\n                    }\n                    extension.setMetadata(metadata);\n                    return converter.convertTo(extension);\n                })\n            .subscribeOn(this.scheduler)\n            // the method secureStrong() may invoke blocking SecureRandom, so we need to subscribe\n            // on boundedElastic thread pool.\n            .flatMap(extStore -> doCreate(extension, extStore.getName(), extStore.getData()))\n            .flatMap(created -> Mono.fromCallable(\n                    () -> {\n                        watchers.onAdd(convertToRealExtension(created));\n                        return created;\n                    })\n                .subscribeOn(this.scheduler)\n            )\n            .retryWhen(Retry.backoff(3, Duration.ofMillis(100))\n                // retry when generateName is set\n                .filter(t -> t instanceof DataIntegrityViolationException\n                    && hasText(extension.getMetadata().getGenerateName()))\n            );\n    }\n\n    @Override\n    public <E extends Extension> Mono<E> update(E extension) {\n        // Refactor the atomic reference if we have a better solution.\n        return getLatest(extension).flatMap(old -> {\n            var oldJsonExt = new JsonExtension(objectMapper, old);\n            var newJsonExt = new JsonExtension(objectMapper, extension);\n            // reset some mandatory fields\n            var oldMetadata = oldJsonExt.getMetadata();\n            var newMetadata = newJsonExt.getMetadata();\n            newMetadata.setCreationTimestamp(oldMetadata.getCreationTimestamp());\n            newMetadata.setGenerateName(oldMetadata.getGenerateName());\n\n            // If the extension is an unstructured, the version type may be integer instead of long.\n            // reset metadata.version for long type.\n            oldMetadata.setVersion(oldMetadata.getVersion());\n            newMetadata.setVersion(newMetadata.getVersion());\n\n            if (Objects.equals(oldJsonExt, newJsonExt)) {\n                // skip updating if not data changed.\n                return Mono.just(extension);\n            }\n\n            var onlyStatusChanged =\n                isOnlyStatusChanged(oldJsonExt.getInternal(), newJsonExt.getInternal());\n\n            var store = this.converter.convertTo(newJsonExt);\n            var doUpdate =\n                doUpdate(extension, store.getName(), store.getVersion(), store.getData());\n            if (!onlyStatusChanged) {\n                doUpdate = doUpdate.flatMap(updated -> Mono.fromCallable(\n                        () -> {\n                            watchers.onUpdate(convertToRealExtension(old),\n                                convertToRealExtension(updated));\n                            return updated;\n                        })\n                    .subscribeOn(this.scheduler)\n                );\n            }\n            return doUpdate;\n        });\n    }\n\n    private Mono<? extends Extension> getLatest(Extension extension) {\n        if (extension instanceof Unstructured) {\n            return get(extension.groupVersionKind(), extension.getMetadata().getName());\n        }\n        if (extension instanceof JsonExtension) {\n            return getJsonExtension(\n                extension.groupVersionKind(),\n                extension.getMetadata().getName()\n            );\n        }\n        return get(extension.getClass(), extension.getMetadata().getName());\n    }\n\n    @Override\n    public <E extends Extension> Mono<E> delete(E extension) {\n        // make sure the version is not null, or it will cause extension insertion.\n        Assert.notNull(extension.getMetadata().getVersion(), \"Extension version must not be null\");\n        // set deletionTimestamp\n        extension.getMetadata().setDeletionTimestamp(Instant.now());\n        var es = converter.convertTo(extension);\n        return doUpdate(extension, es.getName(), es.getVersion(), es.getData())\n            .flatMap(deleted -> Mono.fromCallable(\n                    () -> {\n                        watchers.onDelete(convertToRealExtension(extension));\n                        return deleted;\n                    })\n                .subscribeOn(this.scheduler)\n            );\n    }\n\n    @Override\n    public IndexedQueryEngine indexedQueryEngine() {\n        return new IndexedQueryEngine() {\n            @Override\n            public ListResult<String> retrieve(GroupVersionKind gvk, ListOptions options,\n                PageRequest page) {\n                var scheme = schemeManager.get(gvk);\n                return indexEngine.retrieve(scheme.type(), options, page);\n            }\n\n            @Override\n            public List<String> retrieveAll(GroupVersionKind gvk, ListOptions options, Sort sort) {\n                var scheme = schemeManager.get(gvk);\n                return StreamSupport.stream(\n                        indexEngine.retrieveAll(scheme.type(), options, sort).spliterator(), false\n                    )\n                    .toList();\n            }\n        };\n    }\n\n    /**\n     * Create extension in store and update index. Please make sure subscribe on proper scheduler.\n     *\n     * @param oldExtension the extension to create\n     * @param name the name of the extension\n     * @param data the data of the extension\n     * @param <E> the type of the extension\n     * @return the created extension\n     */\n    @SuppressWarnings(\"unchecked\")\n    <E extends Extension> Mono<E> doCreate(E oldExtension, String name, byte[] data) {\n        return Mono.defer(() -> {\n            var type = (Class<E>) oldExtension.getClass();\n            return client.create(name, data)\n                .map(created -> converter.convertFrom(type, created))\n                .flatMap(extension -> Mono.fromCallable(\n                        () -> {\n                            this.indexEngine.insert(List.of(convertToRealExtension(extension)));\n                            return extension;\n                        })\n                    .subscribeOn(this.scheduler)\n                )\n                .as(transactionalOperator::transactional);\n        });\n    }\n\n    /**\n     * Update extension in store and update index. Please make sure subscribe on proper scheduler.\n     *\n     * @param oldExtension the extension to update\n     * @param name the name of the extension\n     * @param version the version of the extension\n     * @param data the data of the extension\n     * @param <E> the type of the extension\n     * @return the updated extension\n     */\n    @SuppressWarnings(\"unchecked\")\n    <E extends Extension> Mono<E> doUpdate(E oldExtension, String name, Long version, byte[] data) {\n        return Mono.defer(() -> {\n            var type = (Class<E>) oldExtension.getClass();\n            return client.update(name, version, data)\n                .map(updated -> converter.convertFrom(type, updated))\n                .flatMap(extension -> Mono.fromCallable(\n                        () -> {\n                            this.indexEngine.update(List.of(convertToRealExtension(extension)));\n                            return extension;\n                        })\n                    .subscribeOn(this.scheduler)\n                )\n                .as(transactionalOperator::transactional);\n        });\n    }\n\n    private Extension convertToRealExtension(Extension extension) {\n        var gvk = extension.groupVersionKind();\n        var realType = schemeManager.get(gvk).type();\n        Extension realExtension = extension;\n        if (extension instanceof Unstructured) {\n            realExtension = Unstructured.OBJECT_MAPPER.convertValue(extension, realType);\n        } else if (extension instanceof JsonExtension jsonExtension) {\n            realExtension = jsonExtension.getObjectMapper().convertValue(jsonExtension, realType);\n        }\n        return realExtension;\n    }\n\n    @Override\n    public void watch(Watcher watcher) {\n        this.watchers.addWatcher(watcher);\n    }\n\n    private static boolean isOnlyStatusChanged(ObjectNode oldNode, ObjectNode newNode) {\n        if (Objects.equals(oldNode, newNode)) {\n            return false;\n        }\n        // WARNING!!!\n        // Do not edit the ObjectNode\n        var oldFields = new HashSet<String>();\n        var newFields = new HashSet<String>();\n        oldNode.fieldNames().forEachRemaining(oldFields::add);\n        newNode.fieldNames().forEachRemaining(newFields::add);\n        oldFields.remove(\"status\");\n        newFields.remove(\"status\");\n        if (!Objects.equals(oldFields, newFields)) {\n            return false;\n        }\n        for (var field : oldFields) {\n            if (!Objects.equals(oldNode.get(field), newNode.get(field))) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/availability/IndexBuildState.java",
    "content": "package run.halo.app.extension.availability;\n\nimport org.springframework.boot.availability.AvailabilityState;\n\npublic enum IndexBuildState implements AvailabilityState {\n    BUILDING,\n    BUILT;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/controller/DefaultControllerManager.java",
    "content": "package run.halo.app.extension.controller;\n\nimport static org.springframework.core.ResolvableType.forClassWithGenerics;\n\nimport java.util.concurrent.ConcurrentHashMap;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.context.SmartLifecycle;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.infra.InitializationPhase;\n\n@Slf4j\npublic class DefaultControllerManager implements ApplicationContextAware, SmartLifecycle {\n\n    private final ExtensionClient client;\n\n    private ApplicationContext applicationContext;\n\n    /**\n     * Map with key: reconciler class name, value: controller self.\n     */\n    private final ConcurrentHashMap<String, Controller> controllers;\n\n    private volatile boolean running;\n\n    public DefaultControllerManager(ExtensionClient client) {\n        this.client = client;\n        controllers = new ConcurrentHashMap<>();\n    }\n\n    @Override\n    public void start() {\n        if (this.running) {\n            return;\n        }\n        this.running = true;\n        // register reconcilers in system after scheme initialized\n        applicationContext.<Reconciler<Request>>getBeanProvider(\n                forClassWithGenerics(Reconciler.class, Request.class))\n            .orderedStream()\n            .forEach(this::start);\n    }\n\n    void start(Reconciler<Request> reconciler) {\n        var builder = new ControllerBuilder(reconciler, client);\n        var controller = reconciler.setupWith(builder);\n        controllers.put(reconciler.getClass().getName(), controller);\n        controller.start();\n    }\n\n    @Override\n    public void stop() {\n        if (!running) {\n            return;\n        }\n        this.running = false;\n        log.info(\"Shutting down {} controllers...\", controllers.size());\n        controllers.forEach((name, controller) -> disposeSilently(controller));\n        log.info(\"Shutdown {} controllers.\", controllers.size());\n    }\n\n    private static void disposeSilently(Controller controller) {\n        if (controller == null) {\n            return;\n        }\n        try {\n            log.info(\"Shutting down controller {}...\", controller.getName());\n            controller.dispose();\n            log.info(\"Shutdown controller {} successfully\", controller.getName());\n        } catch (Throwable t) {\n            log.error(\"Failed to dispose controller {}\", controller.getName(), t);\n        }\n    }\n\n    @Override\n    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n        this.applicationContext = applicationContext;\n    }\n\n\n    @Override\n    public boolean isRunning() {\n        return running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.CONTROLLERS.getPhase();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/event/IndexerBuiltEvent.java",
    "content": "package run.halo.app.extension.event;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.extension.Scheme;\n\n/**\n * IndexBuildEvent is fired when index build of a scheme is triggered and completed.\n */\npublic class IndexerBuiltEvent extends ApplicationEvent {\n\n    @Getter\n    private final Scheme scheme;\n\n    public IndexerBuiltEvent(Object source, Scheme scheme) {\n        super(source);\n        this.scheme = scheme;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/event/SchemeAddedEvent.java",
    "content": "package run.halo.app.extension.event;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.extension.Scheme;\n\n/**\n * Event published when a scheme is added.\n *\n * @author johnniang\n */\npublic class SchemeAddedEvent extends ApplicationEvent {\n\n    @Getter\n    private final Scheme scheme;\n\n    public SchemeAddedEvent(Object source, Scheme scheme) {\n        super(source);\n        this.scheme = scheme;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/event/SchemeRemovedEvent.java",
    "content": "package run.halo.app.extension.event;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.extension.Scheme;\n\n/**\n * Event published when a scheme is removed.\n *\n * @author johnniang\n */\npublic class SchemeRemovedEvent extends ApplicationEvent {\n\n    @Getter\n    private final Scheme scheme;\n\n    public SchemeRemovedEvent(Object source, Scheme scheme) {\n        super(source);\n        this.scheme = scheme;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/exception/ExtensionConvertException.java",
    "content": "package run.halo.app.extension.exception;\n\n/**\n * ExtensionConvertException is thrown when an Extension conversion error occurs.\n *\n * @author johnniang\n */\npublic class ExtensionConvertException extends ExtensionException {\n\n    public ExtensionConvertException(String reason) {\n        super(reason);\n    }\n\n    public ExtensionConvertException(String reason, Throwable cause) {\n        super(reason, cause);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/exception/ExtensionNotFoundException.java",
    "content": "package run.halo.app.extension.exception;\n\nimport java.net.URI;\nimport org.springframework.http.HttpStatus;\nimport run.halo.app.extension.GroupVersionKind;\n\npublic class ExtensionNotFoundException extends ExtensionException {\n\n    public static final URI TYPE =\n        URI.create(\"https://www.halo.run/api/errors/extension-not-found\");\n\n    public ExtensionNotFoundException(GroupVersionKind gvk, String name) {\n        super(HttpStatus.NOT_FOUND, \"Extension \" + gvk + \"/\" + name + \" was not found.\",\n            null, null, new Object[] {gvk, name});\n        setType(TYPE);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/exception/SchemaViolationException.java",
    "content": "package run.halo.app.extension.exception;\n\nimport org.openapi4j.core.validation.ValidationResults;\nimport org.springframework.http.HttpStatus;\nimport run.halo.app.extension.GroupVersionKind;\n\n/**\n * This exception is thrown when Schema is violation.\n *\n * @author johnniang\n */\npublic class SchemaViolationException extends ExtensionException {\n\n    /**\n     * Validation errors.\n     */\n    private final ValidationResults errors;\n\n    public SchemaViolationException(GroupVersionKind gvk, ValidationResults errors) {\n        super(HttpStatus.BAD_REQUEST, \"Failed to validate \" + gvk, null, null,\n            new Object[] {gvk, errors});\n        this.errors = errors;\n    }\n\n    public ValidationResults getErrors() {\n        return errors;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/gc/GcControllerInitializer.java",
    "content": "package run.halo.app.extension.gc;\n\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.infra.InitializationPhase;\n\n@Component\nclass GcControllerInitializer implements SmartLifecycle {\n\n    private volatile boolean running;\n\n    private final Controller gcController;\n\n    public GcControllerInitializer(GcReconciler gcReconciler) {\n        this.gcController = gcReconciler.setupWith(null);\n    }\n\n    @Override\n    public void start() {\n        if (running) {\n            return;\n        }\n        running = true;\n        gcController.start();\n    }\n\n    @Override\n    public void stop() {\n        if (!running) {\n            return;\n        }\n        running = false;\n        gcController.dispose();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.GC_CONTROLLER.getPhase();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/gc/GcReconciler.java",
    "content": "package run.halo.app.extension.gc;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport org.springframework.transaction.reactive.TransactionalOperator;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionConverter;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.extension.event.SchemeAddedEvent;\nimport run.halo.app.extension.index.IndexEngine;\nimport run.halo.app.extension.store.ReactiveExtensionStoreClient;\n\n@Slf4j\n@Component\nclass GcReconciler implements Reconciler<GcRequest> {\n\n    private final ExtensionClient client;\n\n    private final ReactiveExtensionStoreClient storeClient;\n\n    private final ExtensionConverter converter;\n\n    private final IndexEngine indexEngine;\n\n    private final SchemeManager schemeManager;\n\n    private final RequestQueue<GcRequest> queue;\n\n    private final GcSynchronizer synchronizer;\n\n    private final ReactiveTransactionManager transactionManager;\n\n    private Scheduler scheduler;\n\n    GcReconciler(ExtensionClient client,\n        ReactiveExtensionStoreClient storeClient,\n        ExtensionConverter converter,\n        SchemeManager schemeManager,\n        IndexEngine indexEngine, ReactiveTransactionManager transactionManager) {\n        this.client = client;\n        this.storeClient = storeClient;\n        this.converter = converter;\n        this.indexEngine = indexEngine;\n        this.transactionManager = transactionManager;\n        this.queue = new DefaultQueue<>(Instant::now, Duration.ofMillis(500));\n        this.synchronizer = new GcSynchronizer(client, queue, schemeManager);\n        this.schemeManager = schemeManager;\n        this.scheduler = Schedulers.boundedElastic();\n    }\n\n    @Override\n    public Result reconcile(GcRequest request) {\n        log.debug(\"Extension {} is being deleted\", request);\n        var scheme = schemeManager.get(request.gvk());\n        client.fetch(scheme.type(), request.name())\n            .filter(deletable())\n            .ifPresent(extension -> doDelete(extension).blockOptional(Duration.ofSeconds(30)));\n        return null;\n    }\n\n    private <E extends Extension> Mono<Void> doDelete(E extension) {\n        var extensionStore = converter.convertTo(extension);\n        var tx = TransactionalOperator.create(transactionManager);\n\n        return storeClient.delete(extensionStore.getName(), extensionStore.getVersion())\n            .flatMap(deleted -> Mono.fromRunnable(() -> indexEngine.delete(List.of(extension)))\n                .subscribeOn(this.scheduler)\n            )\n            .as(tx::transactional)\n            .then()\n            .doOnSuccess(ignored ->\n                log.info(\"Extension {}/{} was deleted\", extension.groupVersionKind(), extension)\n            );\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return new DefaultController<>(\n            \"garbage-collector-controller\",\n            this,\n            queue,\n            synchronizer,\n            Duration.ofMillis(500),\n            Duration.ofSeconds(1000),\n            // TODO Make it configurable\n            10);\n    }\n\n    @EventListener\n    void onSchemeAddedEvent(SchemeAddedEvent event) {\n        synchronizer.onApplicationEvent(event);\n    }\n\n    private Predicate<Extension> deletable() {\n        return extension -> CollectionUtils.isEmpty(extension.getMetadata().getFinalizers())\n            && extension.getMetadata().getDeletionTimestamp() != null;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/gc/GcRequest.java",
    "content": "package run.halo.app.extension.gc;\n\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.GroupVersionKind;\n\nrecord GcRequest(GroupVersionKind gvk, String name) {\n\n    public GcRequest {\n        Assert.notNull(gvk, \"Group, version and kind must not be null\");\n        Assert.hasText(name, \"Extension name must not be blank\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/gc/GcSynchronizer.java",
    "content": "package run.halo.app.extension.gc;\n\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.extension.index.query.Queries.not;\n\nimport java.util.List;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.Watcher;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.extension.controller.Synchronizer;\nimport run.halo.app.extension.event.SchemeAddedEvent;\nimport run.halo.app.extension.router.selector.FieldSelector;\n\nclass GcSynchronizer implements Synchronizer<GcRequest>, ApplicationListener<SchemeAddedEvent> {\n\n    private final ExtensionClient client;\n\n    private final SchemeManager schemeManager;\n\n    private boolean disposed = false;\n\n    private boolean started = false;\n\n    private final Watcher watcher;\n\n    GcSynchronizer(ExtensionClient client,\n        RequestQueue<GcRequest> queue,\n        SchemeManager schemeManager) {\n        this.client = client;\n        this.schemeManager = schemeManager;\n        this.watcher = new GcWatcher(queue);\n    }\n\n    @Override\n    public void dispose() {\n        if (isDisposed()) {\n            return;\n        }\n        this.disposed = true;\n        this.watcher.dispose();\n    }\n\n    @Override\n    public boolean isDisposed() {\n        return disposed;\n    }\n\n    @Override\n    public void onApplicationEvent(SchemeAddedEvent event) {\n        if (started) {\n            var scheme = event.getScheme();\n            listDeleted(scheme.type()).forEach(watcher::onDelete);\n        }\n    }\n\n    @Override\n    public void start() {\n        if (isDisposed() || started) {\n            return;\n        }\n        this.started = true;\n        client.watch(watcher);\n        schemeManager.schemes().stream()\n            .map(Scheme::type)\n            .forEach(type -> listDeleted(type).forEach(watcher::onDelete));\n    }\n\n    <E extends Extension> List<E> listDeleted(Class<E> type) {\n        var options = new ListOptions()\n            .setFieldSelector(\n                FieldSelector.of(not(isNull(\"metadata.deletionTimestamp\")))\n            );\n        // TODO Refine with scrolling query\n        return client.listAll(type, options, Sort.by(Sort.Order.asc(\"metadata.creationTimestamp\")));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/gc/GcWatcher.java",
    "content": "package run.halo.app.extension.gc;\n\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.Watcher;\nimport run.halo.app.extension.controller.RequestQueue;\n\nclass GcWatcher implements Watcher {\n\n    private final RequestQueue<GcRequest> queue;\n\n    private Runnable disposeHook;\n\n    private boolean disposed = false;\n\n    GcWatcher(RequestQueue<GcRequest> queue) {\n        this.queue = queue;\n    }\n\n    @Override\n    public void onAdd(Extension extension) {\n        // TODO Should we ignore finalizers here?\n        if (!isDisposed() && extension.getMetadata().getDeletionTimestamp() != null) {\n            queue.addImmediately(\n                new GcRequest(extension.groupVersionKind(), extension.getMetadata().getName()));\n        }\n    }\n\n    @Override\n    public void onUpdate(Extension oldExt, Extension newExt) {\n        if (!isDisposed() && newExt.getMetadata().getDeletionTimestamp() != null) {\n            queue.addImmediately(\n                new GcRequest(newExt.groupVersionKind(), newExt.getMetadata().getName()));\n        }\n    }\n\n    @Override\n    public void onDelete(Extension extension) {\n        if (!isDisposed() && extension.getMetadata().getDeletionTimestamp() != null) {\n            queue.addImmediately(\n                new GcRequest(extension.groupVersionKind(), extension.getMetadata().getName()));\n        }\n    }\n\n    @Override\n    public void registerDisposeHook(Runnable dispose) {\n        this.disposeHook = dispose;\n    }\n\n    @Override\n    public void dispose() {\n        if (isDisposed()) {\n            return;\n        }\n        this.disposed = true;\n        if (this.disposeHook != null) {\n            this.disposeHook.run();\n        }\n    }\n\n    @Override\n    public boolean isDisposed() {\n        return disposed;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/DefaultIndexEngine.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Comparator;\nimport java.util.LinkedList;\nimport java.util.PriorityQueue;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.core.convert.ConversionService;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.index.query.QueryVisitor;\n\n/**\n * Default implementation of {@link IndexEngine}.\n *\n * @author johnniang\n * @since 2.22.0\n */\n@Component\nclass DefaultIndexEngine implements IndexEngine, DisposableBean {\n\n    private IndicesManager indicesManager;\n\n    private final ConversionService conversionService;\n\n    public DefaultIndexEngine(ConversionService conversionService) {\n        this.conversionService = conversionService;\n        this.indicesManager = new DefaultIndicesManager();\n    }\n\n    /**\n     * Set the indices manager. Only for testing purpose.\n     *\n     * @param indicesManager the indices manager\n     */\n    void setIndicesManager(IndicesManager indicesManager) {\n        Assert.notNull(indicesManager, \"indicesManager must not be null\");\n        this.indicesManager = indicesManager;\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        this.indicesManager.close();\n    }\n\n    @Override\n    public <E extends Extension> void insert(@NonNull Iterable<E> extensions) {\n        extensions.forEach(extension -> {\n            // get indices manager\n            var indices = indicesManager.get((Class<E>) extension.getClass());\n            indices.insert(extension);\n        });\n    }\n\n    @Override\n    public <E extends Extension> void update(@NonNull Iterable<E> extensions) {\n        extensions.forEach(extension -> {\n            var indices = indicesManager.get((Class<E>) extension.getClass());\n            indices.update(extension);\n        });\n    }\n\n    @Override\n    public <E extends Extension> void delete(@NonNull Iterable<E> extensions) {\n        extensions.forEach(extension -> {\n            var indices = indicesManager.get((Class<E>) extension.getClass());\n            indices.delete(extension);\n        });\n    }\n\n    @Override\n    public <E extends Extension> ListResult<String> retrieve(\n        Class<E> type, ListOptions options, PageRequest page) {\n        if (options == null) {\n            options = ListOptions.builder().build();\n        }\n        var finalCondition = options.toCondition();\n        var indices = indicesManager.get(type);\n        var queryVisitor = new QueryVisitor<>(indices, conversionService);\n        queryVisitor.enter(finalCondition);\n        var result = queryVisitor.getResult();\n        var total = result.size();\n        // create comparator\n        var sort = page.getSort();\n        var comparator = buildComparator(sort, indices);\n\n        int offset = (page.getPageNumber() - 1) * page.getPageSize();\n        int limit = page.getPageSize();\n        if (limit <= 0) {\n            // return all results for backward compatibility\n            var finalResult = result.stream().sorted(comparator).toList();\n            return new ListResult<>(\n                page.getPageNumber(), page.getPageSize(), total, finalResult\n            );\n        }\n        if (offset >= total) {\n            return new ListResult<>(\n                page.getPageNumber(), page.getPageSize(), total, new LinkedList<>()\n            );\n        }\n        if (offset + limit > total) {\n            limit = total - offset;\n        }\n        var n = offset + limit;\n        if (n > 1000) {\n            var finalResult = result.stream().sorted(comparator)\n                .skip(offset)\n                .limit(limit)\n                .toList();\n            return new ListResult<>(\n                page.getPageNumber(), page.getPageSize(), total, finalResult\n            );\n        }\n        var pq = new PriorityQueue<>(n, comparator.reversed());\n        result.forEach(primaryKey -> {\n            pq.offer(primaryKey);\n            if (pq.size() > n) {\n                pq.poll();\n            }\n        });\n        var finalResult = new LinkedList<String>();\n        while (!pq.isEmpty()) {\n            finalResult.addFirst(pq.poll());\n            if (finalResult.size() >= limit) {\n                // no need to compare further\n                break;\n            }\n        }\n        pq.clear();\n        return new ListResult<>(\n            page.getPageNumber(), page.getPageSize(), total, finalResult\n        );\n    }\n\n\n    @Override\n    public <E extends Extension> Iterable<String> retrieveAll(\n        Class<E> type, ListOptions options, Sort sort) {\n        if (options == null) {\n            options = ListOptions.builder().build();\n        }\n        if (sort == null) {\n            sort = Sort.unsorted();\n        }\n        var finalCondition = options.toCondition();\n        var indices = indicesManager.get(type);\n        var queryVisitor = new QueryVisitor<>(indices, conversionService);\n        queryVisitor.enter(finalCondition);\n        var result = queryVisitor.getResult();\n        if (sort.isUnsorted()) {\n            // no need to sort the result\n            return result.stream()::iterator;\n        }\n        // create comparator\n        var comparator = buildComparator(sort, indices);\n        return result.stream().sorted(comparator)::iterator;\n    }\n\n    @Override\n    public <E extends Extension> Iterable<String> retrieveTopN(\n        Class<E> type, ListOptions options, Sort sort, int topN) {\n        Assert.isTrue(topN > 0, \"topN must be greater than 0\");\n        if (options == null) {\n            options = ListOptions.builder().build();\n        }\n        if (sort == null) {\n            sort = Sort.unsorted();\n        }\n        var finalCondition = options.toCondition();\n        var indices = indicesManager.get(type);\n        var queryVisitor = new QueryVisitor<>(indices, conversionService);\n        queryVisitor.enter(finalCondition);\n        var result = queryVisitor.getResult();\n        // create comparator\n        var comparator = buildComparator(sort, indices);\n        // make sure using reversed comparator to get top N\n        var pq = new PriorityQueue<>(topN + 1, comparator.reversed());\n        result.forEach(primaryKey -> {\n            pq.offer(primaryKey);\n            if (pq.size() > topN) {\n                pq.poll();\n            }\n        });\n        var finalResult = new LinkedList<String>();\n        while (!pq.isEmpty()) {\n            finalResult.addFirst(pq.poll());\n        }\n        return finalResult;\n    }\n\n    @Override\n    public <E extends Extension> long count(Class<E> type, ListOptions options) {\n        if (options == null) {\n            options = ListOptions.builder().build();\n        }\n        var finalCondition = options.toCondition();\n        var indices = indicesManager.get(type);\n        var queryVisitor = new QueryVisitor<>(indices, conversionService);\n        queryVisitor.enter(finalCondition);\n        return queryVisitor.getResult().size();\n    }\n\n    @Override\n    @NonNull\n    public IndicesManager getIndicesManager() {\n        return this.indicesManager;\n    }\n\n    private <E extends Extension> Comparator<String> buildComparator(\n        Sort sort, Indices<E> indices\n    ) {\n        return sort.stream()\n            .map(order -> buildComparator(order, indices))\n            .reduce(Comparator::thenComparing)\n            .orElseGet(Comparator::naturalOrder);\n    }\n\n    private <K extends Comparable<K>, E extends Extension> Comparator<String> buildComparator(\n        Sort.Order order, Indices<E> indices\n    ) {\n        var index = indices.<K>getIndex(order.getProperty());\n        Comparator<String> comparator;\n        if (index instanceof MultiValueIndex<E, K> multiValueIndex) {\n            comparator = (left, right) -> {\n                var leftKeys = multiValueIndex.getKeys(left);\n                var rightKeys = multiValueIndex.getKeys(right);\n                // null first by default\n                if (CollectionUtils.isEmpty(leftKeys)) {\n                    return CollectionUtils.isEmpty(rightKeys) ? 0 : -1;\n                }\n                if (CollectionUtils.isEmpty(rightKeys)) {\n                    return 1;\n                }\n                // compare the first key\n                K leftKey = leftKeys.iterator().next();\n                K rightKey = rightKeys.iterator().next();\n                return Comparator.<K>naturalOrder().compare(leftKey, rightKey);\n            };\n        } else if (index instanceof SingleValueIndex<E, K> singleValueIndex) {\n            comparator = (left, right) -> {\n                K leftKey = singleValueIndex.getKey(left);\n                K rightKey = singleValueIndex.getKey(right);\n                // null first by default\n                if (leftKey == null) {\n                    return rightKey == null ? 0 : -1;\n                }\n                if (rightKey == null) {\n                    return 1;\n                }\n                return leftKey.compareTo(rightKey);\n            };\n        } else {\n            throw new UnsupportedOperationException(\n                \"Unsupported index type for sorting: \" + index.getClass()\n            );\n        }\n        if (order.isDescending()) {\n            comparator = comparator.reversed();\n        }\n        return comparator;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/DefaultIndices.java",
    "content": "package run.halo.app.extension.index;\n\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.io.IOUtils;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.Extension;\n\n/**\n * Default implementation of {@link Indices}.\n *\n * @param <E> the type of extension\n * @author johnniang\n * @since 2.22.0\n */\n@Slf4j\nclass DefaultIndices<E extends Extension> implements Indices<E> {\n\n    private final Map<String, Index<E, ?>> indexMap;\n\n    private final Cache<String, ReadWriteLock> lockCache;\n\n    private volatile boolean closed;\n\n    public DefaultIndices(List<Index<E, ?>> indices) {\n        this.indexMap = indices.stream()\n            .collect(Collectors.toMap(\n                Index::getName,\n                Function.identity(),\n                // keep existing in case of duplicate names\n                (existing, replacing) -> existing,\n                // keep insertion order\n                LinkedHashMap::new)\n            );\n        this.lockCache = Caffeine.newBuilder()\n            .expireAfterAccess(Duration.ofHours(1))\n            .maximumSize(10_000)\n            .build();\n    }\n\n    @Override\n    public void close() throws IOException {\n        closed = true;\n        IOUtils.close(indexMap.values().toArray(Index[]::new));\n        lockCache.invalidateAll();\n    }\n\n    @Override\n    public void insert(E extension) {\n        ensureNotClosed();\n        // get primary key\n        var primaryKey = extension.getMetadata().getName();\n        var lock = Objects.requireNonNull(\n            lockCache.get(primaryKey, pk -> new ReentrantReadWriteLock())\n        ).writeLock();\n        var ops = new ArrayList<TransactionalOperation>();\n        lock.lock();\n        try {\n            for (var index : indexMap.values()) {\n                var op = index.prepareInsert(extension);\n                op.prepare();\n                ops.add(op);\n            }\n            ops.forEach(TransactionalOperation::commit);\n        } catch (Exception e) {\n            log.warn(\"Failed to insert extension {} and trying to rollback\", primaryKey, e);\n            ops.forEach(TransactionalOperation::rollback);\n            throw e;\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public void update(E extension) {\n        ensureNotClosed();\n        var primaryKey = extension.getMetadata().getName();\n        var lock = Objects.requireNonNull(\n            lockCache.get(primaryKey, pk -> new ReentrantReadWriteLock())\n        ).writeLock();\n        var updaters = new ArrayList<TransactionalOperation>();\n        lock.lock();\n        try {\n            for (var index : indexMap.values()) {\n                var updater = index.prepareUpdate(extension);\n                updater.prepare();\n                updaters.add(updater);\n            }\n            updaters.forEach(TransactionalOperation::commit);\n        } catch (Exception e) {\n            updaters.forEach(TransactionalOperation::rollback);\n            throw e;\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    public void delete(E extension) {\n        ensureNotClosed();\n        var primaryKey = extension.getMetadata().getName();\n        var lock = Objects.requireNonNull(\n            lockCache.get(primaryKey, pk -> new ReentrantReadWriteLock())\n        ).writeLock();\n        var updaters = new ArrayList<TransactionalOperation>();\n        lock.lock();\n        try {\n            for (var index : indexMap.values()) {\n                var updater = index.prepareDelete(primaryKey);\n                updater.prepare();\n                updaters.add(updater);\n            }\n            updaters.forEach(TransactionalOperation::commit);\n        } catch (Exception e) {\n            updaters.forEach(TransactionalOperation::rollback);\n            throw e;\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    @Override\n    @NonNull\n    public <K extends Comparable<K>> Index<E, K> getIndex(String indexName) {\n        ensureNotClosed();\n        var index = (Index<E, K>) indexMap.get(indexName);\n        if (index == null) {\n            throw new IllegalArgumentException(\"No index found with name: \" + indexName);\n        }\n        return index;\n    }\n\n    private void ensureNotClosed() {\n        if (closed) {\n            throw new IllegalStateException(\"Indices has been closed\");\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/DefaultIndicesManager.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.stream.Stream;\nimport org.apache.commons.io.IOUtils;\nimport run.halo.app.extension.Extension;\n\n/**\n * Default implementation of {@link IndicesManager}.\n *\n * @author johnniang\n * @since 2.22.0\n */\nclass DefaultIndicesManager implements IndicesManager {\n\n    private final ConcurrentMap<Class<? extends Extension>, Indices<? extends Extension>>\n        indicesMap;\n\n    DefaultIndicesManager() {\n        indicesMap = new ConcurrentHashMap<>();\n    }\n\n    @Override\n    public <E extends Extension> void add(Class<E> type, List<ValueIndexSpec<E, ?>> indexSpecs) {\n        indicesMap.computeIfAbsent(type, t -> {\n            var indices = new ArrayList<Index<E, ?>>();\n            // the default index specs should be added first in case of index overwriting\n            Stream.concat(this.<E>createDefaultIndexSpecs().stream(), indexSpecs.stream())\n                .distinct()\n                .forEach(indexSpec -> {\n                    if (indexSpec instanceof MultiValueIndexSpec<E, ?> spec) {\n                        indices.add(new MultiValueIndex<>(spec));\n                    } else if (indexSpec instanceof SingleValueIndexSpec<E, ?> spec) {\n                        indices.add(new SingleValueIndex<>(spec));\n                    }\n                    // ignore other implementations, should never happen\n                });\n            indices.add(new LabelIndex<>());\n            return new DefaultIndices<>(indices);\n        });\n    }\n\n    @Override\n    public void close() throws IOException {\n        IOUtils.close(indicesMap.values().toArray(Indices[]::new));\n        indicesMap.clear();\n    }\n\n    @Override\n    public <E extends Extension> Indices<E> get(Class<E> type) {\n        var indices = (Indices<E>) indicesMap.get(type);\n        if (indices == null) {\n            throw new IllegalArgumentException(\"No indices found for type: \" + type.getName());\n        }\n        return indices;\n    }\n\n    @Override\n    public <E extends Extension> void remove(Class<E> type) {\n        var indices = indicesMap.remove(type);\n        IOUtils.closeQuietly(indices);\n    }\n\n    private <E extends Extension> List<ValueIndexSpec<E, ?>> createDefaultIndexSpecs() {\n        var metadataNameSpec = IndexSpecs.<E, String>single(\"metadata.name\", String.class)\n            .indexFunc(e -> e.getMetadata().getName())\n            .unique(true)\n            .nullable(false)\n            .build();\n        var creationTimestampSpec =\n            IndexSpecs.<E, Instant>single(\"metadata.creationTimestamp\", Instant.class)\n                .indexFunc(e -> e.getMetadata().getCreationTimestamp())\n                .unique(false)\n                .nullable(false)\n                .build();\n        var deletionTimestampSpec =\n            IndexSpecs.<E, Instant>single(\"metadata.deletionTimestamp\", Instant.class)\n                .indexFunc(e -> e.getMetadata().getDeletionTimestamp())\n                .unique(false)\n                .nullable(true)\n                .build();\n        return List.of(metadataNameSpec, creationTimestampSpec, deletionTimestampSpec);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/Index.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.io.Closeable;\nimport run.halo.app.extension.Extension;\n\n/**\n * Index for Extensions.\n *\n * @param <E> the type of the extension.\n * @param <K> the type of the index key.\n * @author johnniang\n * @since 2.22.0\n */\npublic interface Index<E extends Extension, K extends Comparable<K>> extends Closeable {\n\n    /**\n     * Get the name of the index.\n     *\n     * @return the name of the index.\n     */\n    String getName();\n\n    /**\n     * Get the type of the index key.\n     *\n     * @return the type of the index key.\n     */\n    Class<K> getKeyType();\n\n    /**\n     * Whether the index is unique.\n     *\n     * @return true if the index is unique, false otherwise.\n     */\n    default boolean isUnique() {\n        return false;\n    }\n\n    /**\n     * Prepare insert operation.\n     *\n     * @param extension the extension to insert.\n     * @return the transactional operation.\n     */\n    TransactionalOperation prepareInsert(E extension);\n\n    /**\n     * Prepare update operation.\n     *\n     * @param newExtension the new extension.\n     * @return the transactional operation.\n     */\n    TransactionalOperation prepareUpdate(E newExtension);\n\n    /**\n     * Prepare delete operation.\n     *\n     * @param primaryKey the primary key of the extension to delete.\n     * @return the transactional operation.\n     */\n    TransactionalOperation prepareDelete(String primaryKey);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/IndexEngine.java",
    "content": "package run.halo.app.extension.index;\n\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\n\n/**\n * Index engine for managing extension indices.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface IndexEngine {\n\n    /**\n     * Insert extensions into the index.\n     *\n     * @param extensions the extensions to insert\n     * @param <E> the type of the extension\n     */\n    <E extends Extension> void insert(@NonNull Iterable<E> extensions);\n\n    /**\n     * Update extension in the index.\n     *\n     * @param extension the extension to update\n     * @param <E> the type of the extension\n     */\n    <E extends Extension> void update(@NonNull Iterable<E> extension);\n\n    /**\n     * Delete extensions from the index.\n     *\n     * @param extensions the extensions to delete\n     * @param <E> the type of the extension\n     */\n    <E extends Extension> void delete(@NonNull Iterable<E> extensions);\n\n    /**\n     * Retrieve extension names from the index.\n     *\n     * @param type the type of the extension\n     * @param options the list options\n     * @param page the page request\n     * @param <E> the type of the extension\n     * @return the list result of extension names\n     */\n    <E extends Extension> ListResult<String> retrieve(\n        Class<E> type, @Nullable ListOptions options, @NonNull PageRequest page\n    );\n\n    /**\n     * Retrieve all extension names from the index.\n     *\n     * @param type the type of the extension\n     * @param options the list options\n     * @param sort the sort options\n     * @param <E> the type of the extension\n     * @return the iterable of extension names\n     */\n    <E extends Extension> Iterable<String> retrieveAll(\n        Class<E> type, @Nullable ListOptions options, @Nullable Sort sort\n    );\n\n    /**\n     * Retrieve top N extension names from the index.\n     *\n     * @param type the type of the extension\n     * @param options the list options\n     * @param sort the sort options\n     * @param topN the number of top extensions to retrieve\n     * @param <E> the type of the extension\n     * @return the iterable of extension names\n     */\n    <E extends Extension> Iterable<String> retrieveTopN(\n        Class<E> type, @Nullable ListOptions options, @Nullable Sort sort, int topN\n    );\n\n    /**\n     * Count the number of extensions in the index.\n     *\n     * @param type the type of the extension\n     * @param options the list options\n     * @param <E> the type of the extension\n     * @return the count of extensions\n     */\n    <E extends Extension> long count(Class<E> type, ListOptions options);\n\n    /**\n     * Get the indices' manager.\n     *\n     * @return the indices manager\n     */\n    @NonNull\n    IndicesManager getIndicesManager();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/Indices.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.io.Closeable;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.Extension;\n\npublic interface Indices<E extends Extension> extends Closeable {\n\n    void insert(E extension);\n\n    void update(E extension);\n\n    void delete(E extension);\n\n    /**\n     * Get index by name.\n     *\n     * @param indexName index name\n     * @param <K> the key type\n     * @return the index\n     * @throws IllegalArgumentException if the index with the given name does not exist\n     */\n    @NonNull\n    <K extends Comparable<K>> Index<E, K> getIndex(String indexName);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/IndicesInitializer.java",
    "content": "package run.halo.app.extension.index;\n\nimport run.halo.app.extension.Scheme;\n\npublic interface IndicesInitializer {\n\n    void initialize(Scheme scheme);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/IndicesManager.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.io.Closeable;\nimport java.util.List;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.Extension;\n\npublic interface IndicesManager extends Closeable {\n\n    /**\n     * Add a new {@link Indices} for the given extension type and index specifications.\n     *\n     * @param type the type of the extension\n     * @param specs the list of index specifications\n     * @param <E> the type of the extension\n     */\n    <E extends Extension> void add(Class<E> type, List<ValueIndexSpec<E, ?>> specs);\n\n    /**\n     * Get the {@link Indices} for the given extension type.\n     *\n     * @param type the type of the extension\n     * @param <E> the type of the extension\n     * @return the indices for the given extension type\n     * @throws IllegalArgumentException if the indices for the given extension type does not exist\n     */\n    @NonNull\n    <E extends Extension> Indices<E> get(Class<E> type);\n\n    /**\n     * Remove the {@link Indices} for the given extension type and release resources.\n     *\n     * @param type type of the extension\n     * @param <E> type of the extension\n     */\n    <E extends Extension> void remove(Class<E> type);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/LabelIndex.java",
    "content": "package run.halo.app.extension.index;\n\nimport static java.util.stream.Collectors.toUnmodifiableMap;\n\nimport java.io.IOException;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.concurrent.ConcurrentNavigableMap;\nimport java.util.concurrent.ConcurrentSkipListMap;\nimport java.util.stream.Collectors;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.Extension;\n\n/**\n * Label index implementation.\n *\n * @param <E> the type of extension\n * @author johnniang\n * @since 2.22.0\n */\nclass LabelIndex<E extends Extension> implements LabelIndexQuery, Index<E, String> {\n\n    private final ConcurrentNavigableMap<LabelEntry, Set<String>> index;\n\n    private final ConcurrentMap<String, Set<LabelEntry>> invertedIndex;\n\n    /**\n     * Set of primary keys of extensions with empty labels.\n     */\n    private final Set<String> emptyLabelsSet;\n\n    public LabelIndex() {\n        this.index = new ConcurrentSkipListMap<>();\n        this.invertedIndex = new ConcurrentHashMap<>();\n        this.emptyLabelsSet = ConcurrentHashMap.newKeySet();\n    }\n\n    @Override\n    public void close() throws IOException {\n        this.index.clear();\n        this.invertedIndex.clear();\n        this.emptyLabelsSet.clear();\n    }\n\n    @Override\n    public String getName() {\n        return \"metadata.labels\";\n    }\n\n    @Override\n    public Class<String> getKeyType() {\n        return String.class;\n    }\n\n    @Override\n    public TransactionalOperation prepareInsert(E extension) {\n        var primaryKey = extension.getMetadata().getName();\n        var labels = extension.getMetadata().getLabels();\n        return new UpsertTransactionalOperation(primaryKey, labels);\n    }\n\n    @Override\n    public TransactionalOperation prepareUpdate(E extension) {\n        var primaryKey = extension.getMetadata().getName();\n        var labels = extension.getMetadata().getLabels();\n        return new UpsertTransactionalOperation(primaryKey, labels);\n    }\n\n    @Override\n    public TransactionalOperation prepareDelete(String primaryKey) {\n        return new DeleteTransactionalOperation(primaryKey);\n    }\n\n    @Override\n    public Set<String> exists(String labelKey) {\n        return index.subMap(\n                new LabelEntry(labelKey, null), true,\n                new LabelEntry(labelKey, Character.MAX_VALUE + \"\"), true\n            ).values().stream()\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> equal(String labelKey, String labelValue) {\n        return Optional.ofNullable(index.get(new LabelEntry(labelKey, labelValue)))\n            .orElse(Set.of());\n    }\n\n    @Override\n    public Set<String> notEqual(String labelKey, String labelValue) {\n        // collect all primary keys\n        var labelEntry = new LabelEntry(labelKey, labelValue);\n        return index.subMap(\n                new LabelEntry(labelKey, null), true,\n                new LabelEntry(labelKey, Character.MAX_VALUE + \"\"), true\n            )\n            .entrySet().stream()\n            .filter(entry -> !Objects.equals(entry.getKey(), labelEntry))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> in(String labelKey, Collection<String> labelValues) {\n        if (CollectionUtils.isEmpty(labelValues)) {\n            return Set.of();\n        }\n        return labelValues.stream()\n            .distinct()\n            .map(labelValue -> new LabelEntry(labelKey, labelValue))\n            .map(index::get)\n            .filter(Objects::nonNull)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> notIn(String labelKey, Collection<String> labelValues) {\n        if (CollectionUtils.isEmpty(labelValues)) {\n            return Set.of();\n        }\n        var valueSet =\n            labelValues instanceof Set<String> set ? set : Set.copyOf(labelValues);\n        return index.subMap(\n                new LabelEntry(labelKey, null), true,\n                new LabelEntry(labelKey, Character.MAX_VALUE + \"\"), true\n            )\n            .entrySet()\n            .stream()\n            .filter(entry -> !valueSet.contains(entry.getKey().labelValue()))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    record LabelEntry(@NonNull String labelKey, @Nullable String labelValue)\n        implements Comparable<LabelEntry> {\n\n        public LabelEntry {\n            Objects.requireNonNull(labelKey, \"labelKey must not be null\");\n        }\n\n        @Override\n        public int compareTo(@NotNull LabelEntry o) {\n            var compare = Comparator.<String>naturalOrder().compare(this.labelKey, o.labelKey);\n            if (compare != 0) {\n                return compare;\n            }\n            return Comparator.nullsFirst(Comparator.<String>naturalOrder())\n                .compare(this.labelValue, o.labelValue);\n        }\n    }\n\n    class UpsertTransactionalOperation implements TransactionalOperation {\n\n        private final String primaryKey;\n\n        private final Map<String, String> labels;\n\n        private Map<String, String> previousLabels;\n\n        private boolean committed;\n\n        UpsertTransactionalOperation(String primaryKey, Map<String, String> labels) {\n            this.primaryKey = primaryKey;\n            this.labels = labels;\n        }\n\n        @Override\n        public void prepare() {\n            this.previousLabels = Optional.ofNullable(invertedIndex.get(primaryKey))\n                .map(labelEntries -> labelEntries.stream()\n                    .filter(entry -> entry.labelValue() != null)\n                    .collect(toUnmodifiableMap(LabelEntry::labelKey, LabelEntry::labelValue))\n                )\n                .orElse(null);\n        }\n\n        @Override\n        public void commit() {\n            if (committed) {\n                return;\n            }\n            this.committed = true;\n            // remove old labels\n            removeLabels(primaryKey, previousLabels);\n            addLabels(primaryKey, labels);\n        }\n\n        @Override\n        public void rollback() {\n            if (!committed) {\n                return;\n            }\n            removeLabels(primaryKey, labels);\n            addLabels(primaryKey, previousLabels);\n        }\n\n    }\n\n    class DeleteTransactionalOperation implements TransactionalOperation {\n\n        private final String primaryKey;\n\n        private Map<String, String> previousLabels;\n\n        private boolean committed;\n\n        DeleteTransactionalOperation(String primaryKey) {\n            this.primaryKey = primaryKey;\n        }\n\n        @Override\n        public void prepare() {\n            this.previousLabels = Optional.ofNullable(invertedIndex.get(primaryKey))\n                .map(labelEntries -> labelEntries.stream()\n                    .filter(entry -> entry.labelValue() != null)\n                    .collect(toUnmodifiableMap(LabelEntry::labelKey, LabelEntry::labelValue))\n                )\n                .orElse(null);\n        }\n\n        @Override\n        public void commit() {\n            if (committed) {\n                return;\n            }\n            this.committed = true;\n            removeLabels(primaryKey, previousLabels);\n        }\n\n        @Override\n        public void rollback() {\n            if (!committed) {\n                return;\n            }\n            addLabels(primaryKey, previousLabels);\n        }\n    }\n\n\n    private void removeLabels(String primaryKey, Map<String, String> labels) {\n        if (CollectionUtils.isEmpty(labels)) {\n            emptyLabelsSet.remove(primaryKey);\n            return;\n        }\n        invertedIndex.remove(primaryKey);\n        labels.forEach((labelKey, labelValue) -> {\n            var labelEntry = new LabelEntry(labelKey, labelValue);\n            index.computeIfPresent(labelEntry, (key, primaryKeys) -> {\n                primaryKeys.remove(primaryKey);\n                if (primaryKeys.isEmpty()) {\n                    return null;\n                }\n                return primaryKeys;\n            });\n        });\n    }\n\n    private void addLabels(String primaryKey, Map<String, String> labels) {\n        if (CollectionUtils.isEmpty(labels)) {\n            emptyLabelsSet.add(primaryKey);\n            return;\n        }\n        emptyLabelsSet.remove(primaryKey);\n        labels.forEach((labelKey, labelValue) -> {\n            var labelEntry = new LabelEntry(labelKey, labelValue);\n            index.compute(labelEntry, (key, primaryKeys) -> {\n                if (primaryKeys == null) {\n                    primaryKeys = ConcurrentHashMap.newKeySet();\n                }\n                primaryKeys.add(primaryKey);\n                return primaryKeys;\n            });\n            invertedIndex.compute(primaryKey, (pk, labelEntries) -> {\n                if (labelEntries == null) {\n                    labelEntries = ConcurrentHashMap.newKeySet();\n                }\n                labelEntries.add(labelEntry);\n                return labelEntries;\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/LabelIndexQuery.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Collection;\nimport java.util.Set;\n\n/**\n * Label index query interface.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface LabelIndexQuery {\n\n    /**\n     * Checks if the label with the given key exists.\n     *\n     * @param labelKey the label key\n     * @return the set of entity IDs that have the label key\n     */\n    Set<String> exists(String labelKey);\n\n    /**\n     * Checks if the label with the given key does not exist.\n     *\n     * @param labelKey the label key\n     * @param labelValue the label value\n     * @return the set of entity IDs that do not have the label key\n     */\n    Set<String> equal(String labelKey, String labelValue);\n\n    /**\n     * Checks if the label with the given key does not equal the specified value.\n     *\n     * @param labelKey the label key\n     * @param labelValue the label value\n     * @return the set of entity IDs that do not have the label key equal to the specified value\n     */\n    Set<String> notEqual(String labelKey, String labelValue);\n\n    /**\n     * Checks if the label with the given key has a value in the specified collection.\n     *\n     * @param labelKey the label key\n     * @param labelValues the collection of label values\n     * @return the set of entity IDs that have the label key with values in the specified collection\n     */\n    Set<String> in(String labelKey, Collection<String> labelValues);\n\n    /**\n     * Checks if the label with the given key has a value not in the specified collection.\n     *\n     * @param labelKey the label key\n     * @param labelValues the collection of label values\n     * @return the set of entity IDs that have the label key with values not in the specified\n     * collection.\n     */\n    Set<String> notIn(String labelKey, Collection<String> labelValues);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/MultiValueIndex.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.io.IOException;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.concurrent.ConcurrentNavigableMap;\nimport java.util.concurrent.ConcurrentSkipListMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.dao.DuplicateKeyException;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.Extension;\n\n/**\n * Multi-value index implementation.\n *\n * @param <E> the extension type\n * @param <K> the key type\n * @author johnniang\n * @since 2.22.0\n */\n@Slf4j\nclass MultiValueIndex<E extends Extension, K extends Comparable<K>>\n    implements ValueIndexQuery<K>, Index<E, K> {\n\n    private final ConcurrentNavigableMap<K, Set<String>> index;\n\n    private final ConcurrentMap<String, Set<K>> invertedIndex;\n\n    private final Set<String> nullKeyValues;\n\n    private final MultiValueIndexSpec<E, K> spec;\n\n    public MultiValueIndex(MultiValueIndexSpec<E, K> spec) {\n        this.spec = spec;\n        this.index = new ConcurrentSkipListMap<>(Comparator.naturalOrder());\n        this.invertedIndex = new ConcurrentHashMap<>();\n        this.nullKeyValues = ConcurrentHashMap.newKeySet();\n    }\n\n    @Override\n    public void close() throws IOException {\n        this.index.clear();\n        this.invertedIndex.clear();\n        this.nullKeyValues.clear();\n    }\n\n    @Override\n    public String getName() {\n        return spec.getName();\n    }\n\n    @Override\n    public Class<K> getKeyType() {\n        return spec.getKeyType();\n    }\n\n    @Override\n    public boolean isUnique() {\n        return spec.isUnique();\n    }\n\n    @Override\n    public TransactionalOperation prepareInsert(E extension) {\n        var keys = spec.getValues(extension);\n        return new UpsertTransactionalOperation(extension.getMetadata().getName(), keys);\n    }\n\n    @Override\n    public TransactionalOperation prepareUpdate(E extension) {\n        // find old state\n        var newKeys = spec.getValues(extension);\n        var primaryKey = extension.getMetadata().getName();\n        return new UpsertTransactionalOperation(primaryKey, newKeys);\n    }\n\n    @Override\n    public TransactionalOperation prepareDelete(String primaryKey) {\n        return new DeleteTransactionalOperation(primaryKey);\n    }\n\n    /**\n     * Get the keys associated with the given primary key.\n     *\n     * @param primaryKey the primary key\n     * @return the associated keys\n     */\n    Set<K> getKeys(String primaryKey) {\n        return Collections.unmodifiableSet(invertedIndex.get(primaryKey));\n    }\n\n    @Override\n    public Set<String> between(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support between operation\");\n    }\n\n    @Override\n    public Set<String> notBetween(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support notBetween operation\");\n    }\n\n    @Override\n    public Set<String> in(Collection<K> keys) {\n        if (CollectionUtils.isEmpty(keys)) {\n            return Set.of();\n        }\n        return keys.stream()\n            .distinct()\n            .map(index::get)\n            .filter(Objects::nonNull)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> notIn(Collection<K> keys) {\n        if (CollectionUtils.isEmpty(keys)) {\n            return all();\n        }\n        var inResult = in(keys);\n        return index.values().stream()\n            .flatMap(Set::stream)\n            .filter(v -> !inResult.contains(v))\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> lessThan(K key, boolean inclusive) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support lessThan operation\"\n        );\n    }\n\n    @Override\n    public Set<String> greaterThan(K key, boolean inclusive) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support greaterThan operation\"\n        );\n    }\n\n    @Override\n    public Set<String> isNull() {\n        return Collections.unmodifiableSet(nullKeyValues);\n    }\n\n    @Override\n    public Set<String> isNotNull() {\n        return index.values()\n            .stream()\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> stringContains(String keyword) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support stringContains operation\"\n        );\n    }\n\n    @Override\n    public Set<String> stringNotContains(String keyword) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support stringNotContains operation\"\n        );\n    }\n\n    @Override\n    public Set<String> stringStartsWith(String prefix) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support stringStartsWith operation\"\n        );\n    }\n\n    @Override\n    public Set<String> stringNotStartsWith(String prefix) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support stringNotStartsWith operation\"\n        );\n    }\n\n    @Override\n    public Set<String> stringEndsWith(String suffix) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support stringEndsWith operation\"\n        );\n    }\n\n    @Override\n    public Set<String> stringNotEndsWith(String suffix) {\n        throw new UnsupportedOperationException(\n            \"Multi-value index does not support stringNotEndsWith operation\"\n        );\n    }\n\n    @Override\n    public Set<String> notEqual(K key) {\n        return notIn(Collections.singleton(key));\n    }\n\n    @Override\n    public Set<String> equal(K key) {\n        return index.getOrDefault(key, Set.of());\n    }\n\n    @Override\n    public Set<String> all() {\n        return Stream.concat(index.values().stream(), Stream.of(nullKeyValues))\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    class UpsertTransactionalOperation implements TransactionalOperation {\n\n        @NonNull\n        private final String primaryKey;\n\n        @Nullable\n        private final Set<K> newKeys;\n\n        private boolean committed;\n\n        private Set<K> previousKeys;\n\n        private boolean previousNullKey;\n\n        UpsertTransactionalOperation(\n            @NonNull String primaryKey, @Nullable Set<K> newKeys\n        ) {\n            this.primaryKey = primaryKey;\n            this.newKeys = newKeys;\n        }\n\n        @Override\n        public void prepare() {\n            this.previousKeys = invertedIndex.get(primaryKey);\n            this.previousNullKey = nullKeyValues.contains(primaryKey);\n        }\n\n        @Override\n        public void commit() {\n            if (committed) {\n                return;\n            }\n            committed = true;\n            if (Objects.equals(previousKeys, newKeys)) {\n                return;\n            }\n            invertedIndex.put(primaryKey, newKeys);\n            // remove previous keys\n            if (!CollectionUtils.isEmpty(previousKeys)) {\n                previousKeys.forEach(key -> index.computeIfPresent(key, (k, v) -> {\n                    v.remove(primaryKey);\n                    return v.isEmpty() ? null : v;\n                }));\n            }\n            // add new keys\n            if (!CollectionUtils.isEmpty(newKeys)) {\n                for (K key : newKeys) {\n                    index.compute(key, (k, v) -> {\n                        if (v == null) {\n                            v = ConcurrentHashMap.newKeySet();\n                        }\n                        if (spec.isUnique() && !v.isEmpty()) {\n                            throw new DuplicateKeyException(\n                                String.format(\"Duplicate key '%s' for extension '%s'\", k,\n                                    primaryKey)\n                            );\n                        }\n                        v.add(primaryKey);\n                        return v;\n                    });\n                }\n                nullKeyValues.remove(primaryKey);\n            } else {\n                nullKeyValues.add(primaryKey);\n            }\n        }\n\n        @Override\n        public void rollback() {\n            if (Objects.equals(this.previousKeys, newKeys) || !committed) {\n                return;\n            }\n            // remove possibly added new keys\n            if (!CollectionUtils.isEmpty(newKeys)) {\n                newKeys.forEach(key -> index.computeIfPresent(key, (k, v) -> {\n                    v.remove(primaryKey);\n                    return v.isEmpty() ? null : v;\n                }));\n            }\n            // add previous keys\n            if (this.previousKeys == null) {\n                // remove from inverted index\n                invertedIndex.remove(primaryKey);\n            } else {\n                this.previousKeys.forEach(key -> index.compute(key, (k, v) -> {\n                    if (v == null) {\n                        v = ConcurrentHashMap.newKeySet();\n                    }\n                    // No need to check duplicate here, as it was already present before.\n                    v.add(primaryKey);\n                    return v;\n                }));\n                invertedIndex.put(primaryKey, this.previousKeys);\n            }\n            if (previousNullKey) {\n                nullKeyValues.add(primaryKey);\n            } else {\n                nullKeyValues.remove(primaryKey);\n            }\n        }\n\n    }\n\n    class DeleteTransactionalOperation implements TransactionalOperation {\n\n        @NonNull\n        private final String primaryKey;\n\n        private boolean committed;\n\n        private Set<K> previousKeys;\n\n        private boolean previousNullKey;\n\n        DeleteTransactionalOperation(@NonNull String primaryKey) {\n            this.primaryKey = primaryKey;\n        }\n\n        @Override\n        public void prepare() {\n            this.previousKeys = invertedIndex.get(primaryKey);\n            this.previousNullKey = nullKeyValues.contains(primaryKey);\n        }\n\n        @Override\n        public void commit() {\n            if (committed) {\n                return;\n            }\n            committed = true;\n            invertedIndex.remove(primaryKey);\n            if (this.previousKeys != null) {\n                this.previousKeys.forEach(key -> index.computeIfPresent(key, (k, v) -> {\n                    v.remove(primaryKey);\n                    return v.isEmpty() ? null : v;\n                }));\n            }\n            nullKeyValues.remove(primaryKey);\n        }\n\n        @Override\n        public void rollback() {\n            if (this.previousKeys == null || !committed) {\n                return;\n            }\n            if (previousNullKey) {\n                nullKeyValues.add(primaryKey);\n            } else {\n                nullKeyValues.remove(primaryKey);\n            }\n            // add previous keys\n            this.previousKeys.forEach(key -> index.compute(key, (k, v) -> {\n                if (v == null) {\n                    v = ConcurrentHashMap.newKeySet();\n                }\n                v.add(primaryKey);\n                return v;\n            }));\n            invertedIndex.put(primaryKey, this.previousKeys);\n        }\n    }\n\n    private void ensureStringKeyType() {\n        Assert.isTrue(\n            getKeyType() == String.class || getKeyType() == UnknownKey.class,\n            \"Key type must be String for this operation\"\n        );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/SingleValueIndex.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.io.IOException;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.concurrent.ConcurrentNavigableMap;\nimport java.util.concurrent.ConcurrentSkipListMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.dao.DuplicateKeyException;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.extension.Extension;\n\n/**\n * Single value index implementation.\n *\n * @param <E> the type of extension\n * @param <K> the type of index key\n * @author johnniang\n * @since 2.22.0\n */\nclass SingleValueIndex<E extends Extension, K extends Comparable<K>>\n    implements ValueIndexQuery<K>, Index<E, K> {\n\n    private final ConcurrentNavigableMap<K, Set<String>> index;\n\n    private final ConcurrentMap<String, K> invertedIndex;\n\n    private final Set<String> nullKeyValues;\n\n    private final SingleValueIndexSpec<E, K> spec;\n\n    public SingleValueIndex(SingleValueIndexSpec<E, K> spec) {\n        this.spec = spec;\n        this.index = new ConcurrentSkipListMap<>(Comparator.naturalOrder());\n        this.invertedIndex = new ConcurrentHashMap<>();\n        this.nullKeyValues = ConcurrentHashMap.newKeySet();\n    }\n\n    @Override\n    public void close() throws IOException {\n        this.index.clear();\n        this.invertedIndex.clear();\n        this.nullKeyValues.clear();\n    }\n\n    @Override\n    public Class<K> getKeyType() {\n        return spec.getKeyType();\n    }\n\n    @Override\n    public Set<String> equal(K key) {\n        var primaryKeys = index.get(key);\n        return CollectionUtils.isEmpty(primaryKeys) ? Set.of() : new HashSet<>(primaryKeys);\n    }\n\n    @Override\n    public Set<String> notEqual(K key) {\n        return index.entrySet().stream()\n            .filter(entry -> !Objects.equals(entry.getKey(), key))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> all() {\n        return invertedIndex.keySet();\n    }\n\n    @Override\n    public Set<String> between(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {\n        Assert.notNull(fromKey, \"From key must not be null\");\n        Assert.notNull(toKey, \"To key must not be null\");\n        Assert.isTrue(fromKey.compareTo(toKey) <= 0,\n            \"From key must be less than or equal to to key\"\n        );\n        return index.subMap(fromKey, fromInclusive, toKey, toInclusive).values()\n            .stream()\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> notBetween(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {\n        Assert.notNull(fromKey, \"From key must not be null\");\n        Assert.notNull(toKey, \"To key must not be null\");\n        Assert.isTrue(fromKey.compareTo(toKey) <= 0,\n            \"From key must be less than or equal to to key\"\n        );\n        return Stream.concat(\n                index.headMap(fromKey, !fromInclusive).values().stream(),\n                index.tailMap(toKey, !toInclusive).values().stream()\n            )\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> in(Collection<K> keys) {\n        if (CollectionUtils.isEmpty(keys)) {\n            return Set.of();\n        }\n        return keys.stream()\n            .distinct()\n            .map(index::get)\n            .filter(Objects::nonNull)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> notIn(Collection<K> keys) {\n        if (CollectionUtils.isEmpty(keys)) {\n            return Set.of();\n        }\n        var keySet = keys instanceof Set<K> set ? set : new HashSet<>(keys);\n        return index.entrySet().stream()\n            .distinct()\n            .filter(entry -> !keySet.contains(entry.getKey()))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> lessThan(K key, boolean inclusive) {\n        Assert.notNull(key, \"Key must not be null\");\n        return index.headMap(key, inclusive).values().stream()\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> greaterThan(K key, boolean inclusive) {\n        Assert.notNull(key, \"Key must not be null\");\n        return index.tailMap(key, inclusive).values().stream()\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> isNull() {\n        Assert.isTrue(spec.isNullable(), \"Index \" + getName() + \" is not nullable\");\n        return new HashSet<>(nullKeyValues);\n    }\n\n    @Override\n    public Set<String> isNotNull() {\n        Assert.isTrue(spec.isNullable(), \"Index is not nullable\");\n        return index.values().stream()\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> stringContains(String keyword) {\n        ensureStringKeyType();\n        return index.entrySet().stream()\n            .filter(entry -> StringUtils.containsIgnoreCase(entry.getKey().toString(), keyword))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> stringNotContains(String keyword) {\n        ensureStringKeyType();\n        return index.entrySet().stream()\n            .filter(entry -> !StringUtils.containsIgnoreCase(entry.getKey().toString(), keyword))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> stringStartsWith(String prefix) {\n        ensureStringKeyType();\n        var toKey = prefix + Character.MAX_VALUE;\n        return index.subMap((K) prefix, true, (K) toKey, true).values().stream()\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> stringNotStartsWith(String prefix) {\n        ensureStringKeyType();\n        var toKey = prefix + Character.MAX_VALUE;\n        return Stream.concat(\n                index.headMap((K) prefix, false).values().stream(),\n                index.tailMap((K) toKey, true).values().stream()\n            )\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> stringEndsWith(String suffix) {\n        ensureStringKeyType();\n        return index.entrySet().stream()\n            .filter(entry -> StringUtils.endsWithIgnoreCase(entry.getKey().toString(), suffix))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Set<String> stringNotEndsWith(String suffix) {\n        ensureStringKeyType();\n        return index.entrySet().stream()\n            .filter(entry -> !StringUtils.endsWithIgnoreCase(entry.getKey().toString(), suffix))\n            .map(Map.Entry::getValue)\n            .flatMap(Set::stream)\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public String getName() {\n        return spec.getName();\n    }\n\n    @Override\n    public TransactionalOperation prepareInsert(E extension) {\n        var primaryKey = extension.getMetadata().getName();\n        var key = spec.getValue(extension);\n        return new UpsertTransactionalOperation(primaryKey, key);\n    }\n\n    @Override\n    public TransactionalOperation prepareUpdate(E extension) {\n        var primaryKey = extension.getMetadata().getName();\n        var key = spec.getValue(extension);\n        return new UpsertTransactionalOperation(primaryKey, key);\n    }\n\n    @Override\n    public TransactionalOperation prepareDelete(String primaryKey) {\n        return new DeleteTransactionalOperation(primaryKey);\n    }\n\n    /**\n     * Get key for the given primary key.\n     *\n     * @param primaryKey the primary key\n     * @return the index key, or null if not found\n     */\n    @Nullable\n    K getKey(String primaryKey) {\n        return invertedIndex.get(primaryKey);\n    }\n\n    @Override\n    public boolean isUnique() {\n        return spec.isUnique();\n    }\n\n    class UpsertTransactionalOperation implements TransactionalOperation {\n\n        private final String primaryKey;\n\n        @Nullable\n        private final K newKey;\n\n        private K previousKey;\n\n        private boolean previousNull;\n\n        private boolean committed;\n\n        UpsertTransactionalOperation(String primaryKey, @Nullable K newKey) {\n            this.primaryKey = primaryKey;\n            this.newKey = newKey;\n        }\n\n        @Override\n        public void prepare() {\n            // preflight checks\n            if (!spec.isNullable() && newKey == null) {\n                throw new IllegalArgumentException(\n                    \"Index %s of %s is not nullable\".formatted(getName(), primaryKey)\n                );\n            }\n            previousKey = invertedIndex.get(primaryKey);\n            previousNull = nullKeyValues.contains(primaryKey);\n            if (isUnique() && newKey != null && !Objects.equals(previousKey, newKey)) {\n                var existingPrimaryKeys = index.get(newKey);\n                if (!CollectionUtils.isEmpty(existingPrimaryKeys)) {\n                    throw new DuplicateKeyException(\n                        \"Duplicate key '\" + newKey + \"' for index '\" + getName() + \"'\"\n                    );\n                }\n            }\n        }\n\n        @Override\n        public void commit() {\n            if (committed) {\n                return;\n            }\n            committed = true;\n            removeKey(primaryKey, previousKey);\n            addKey(primaryKey, newKey);\n        }\n\n        @Override\n        public void rollback() {\n            if (!committed) {\n                return;\n            }\n            removeKey(primaryKey, newKey);\n            if (spec.isNullable() || previousKey != null) {\n                addKey(primaryKey, previousKey);\n            }\n            if (previousNull) {\n                nullKeyValues.add(primaryKey);\n            } else {\n                nullKeyValues.remove(primaryKey);\n            }\n        }\n\n    }\n\n    class DeleteTransactionalOperation implements TransactionalOperation {\n\n        private final String primaryKey;\n\n        private K previousKey;\n\n        private boolean previousNull;\n\n        private boolean committed;\n\n        DeleteTransactionalOperation(String primaryKey) {\n            this.primaryKey = primaryKey;\n        }\n\n        @Override\n        public void prepare() {\n            previousKey = invertedIndex.get(primaryKey);\n            previousNull = nullKeyValues.contains(primaryKey);\n        }\n\n        @Override\n        public void commit() {\n            if (committed) {\n                return;\n            }\n            committed = true;\n            removeKey(primaryKey, previousKey);\n        }\n\n        @Override\n        public void rollback() {\n            if (!committed) {\n                return;\n            }\n            if (spec.isNullable() || previousKey != null) {\n                addKey(primaryKey, previousKey);\n            }\n            if (previousNull) {\n                nullKeyValues.add(primaryKey);\n            } else {\n                nullKeyValues.remove(primaryKey);\n            }\n        }\n\n    }\n\n    private void removeKey(String primaryKey, K key) {\n        nullKeyValues.remove(primaryKey);\n        if (key == null) {\n            var oldKey = invertedIndex.remove(primaryKey);\n            if (oldKey != null) {\n                index.computeIfPresent(oldKey, (k, v) -> {\n                    v.remove(primaryKey);\n                    return v.isEmpty() ? null : v;\n                });\n            }\n            return;\n        }\n        index.computeIfPresent(key, (k, v) -> {\n            v.remove(primaryKey);\n            return v.isEmpty() ? null : v;\n        });\n        invertedIndex.remove(primaryKey, key);\n    }\n\n    private void addKey(String primaryKey, K key) {\n        if (!spec.isNullable() && key == null) {\n            throw new IllegalArgumentException(\n                \"Index %s of %s is not nullable\".formatted(getName(), primaryKey)\n            );\n        }\n        if (key == null) {\n            var oldKey = invertedIndex.remove(primaryKey);\n            if (oldKey != null) {\n                index.computeIfPresent(oldKey, (k, v) -> {\n                    v.remove(primaryKey);\n                    return v.isEmpty() ? null : v;\n                });\n            }\n            nullKeyValues.add(primaryKey);\n            return;\n        }\n        nullKeyValues.remove(primaryKey);\n        index.compute(key, (k, v) -> {\n            if (v == null) {\n                v = ConcurrentHashMap.newKeySet();\n            }\n            if (!v.add(primaryKey) && spec.isUnique()) {\n                throw new DuplicateKeyException(\n                    \"Duplicate key '\" + key + \"' for index '\" + getName() + \"'\"\n                );\n            }\n            return v;\n        });\n        invertedIndex.put(primaryKey, key);\n    }\n\n    private void ensureStringKeyType() {\n        Assert.isTrue(\n            getKeyType() == String.class || getKeyType() == UnknownKey.class,\n            \"Key type must be String for this operation\"\n        );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/StringUnknownKeyConverter.java",
    "content": "package run.halo.app.extension.index;\n\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.stereotype.Component;\n\n/**\n * String to UnknownKey converter.\n *\n * @author johnniang\n * @since 2.22.0\n * @deprecated for backward compatibility. May remove with {@link UnknownKey} in the future\n *\n */\n@Component\n@Deprecated(forRemoval = true, since = \"2.22.0\")\nclass StringUnknownKeyConverter implements Converter<String, UnknownKey> {\n\n    @Override\n    public UnknownKey convert(String source) {\n        return new UnknownKey(source);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/TransactionalOperation.java",
    "content": "package run.halo.app.extension.index;\n\n/**\n * Represents a transactional operation with prepare, commit, and rollback methods.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface TransactionalOperation {\n\n    /**\n     * Prepares the operation for execution. Implementation should perform necessary checks here and\n     * save any state needed for rollback.\n     */\n    void prepare();\n\n    /**\n     * Commits the operation.\n     */\n    void commit();\n\n    /**\n     * Rolls back the operation.\n     */\n    void rollback();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/ValueIndexQuery.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.Collection;\nimport java.util.Set;\n\n/**\n * Value index query interface.\n *\n * @param <K> the type of the key\n */\npublic interface ValueIndexQuery<K extends Comparable<K>> {\n\n    /**\n     * Gets the type of the key.\n     *\n     * @return the class of the key type\n     */\n    Class<K> getKeyType();\n\n    /**\n     * Checks for equality with the given key.\n     *\n     * @param key the key to compare\n     * @return the set of entity IDs that match the equality condition\n     */\n    Set<String> equal(K key);\n\n    /**\n     * Checks for inequality with the given key.\n     *\n     * @param key the key to compare\n     * @return the set of entity IDs that match the inequality condition\n     */\n    Set<String> notEqual(K key);\n\n    /**\n     * Gets all entity IDs in the index.\n     *\n     * @return the set of all entity IDs\n     */\n    Set<String> all();\n\n    /**\n     * Gets entity IDs between the specified range.\n     *\n     * @param fromKey the starting key\n     * @param fromInclusive whether the starting key is inclusive\n     * @param toKey the ending key\n     * @param toInclusive whether the ending key is inclusive\n     * @return the set of entity IDs within the specified range\n     */\n    Set<String> between(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive);\n\n    /**\n     * Gets entity IDs not between the specified range.\n     *\n     * @param fromKey the starting key\n     * @param fromInclusive whether the starting key is inclusive\n     * @param toKey the ending key\n     * @param toInclusive whether the ending key is inclusive\n     * @return the set of entity IDs outside the specified range\n     */\n    Set<String> notBetween(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive);\n\n    /**\n     * Gets entity IDs with keys in the specified collection.\n     *\n     * @param keys the collection of keys\n     * @return the set of entity IDs with keys in the collection\n     */\n    Set<String> in(Collection<K> keys);\n\n    /**\n     * Gets entity IDs with keys not in the specified collection.\n     *\n     * @param keys the collection of keys\n     * @return the set of entity IDs with keys not in the collection\n     */\n    Set<String> notIn(Collection<K> keys);\n\n    /**\n     * Gets entity IDs with keys less than the specified key.\n     *\n     * @param key the key to compare\n     * @param inclusive whether the comparison is inclusive\n     * @return the set of entity IDs with keys less than the specified key\n     */\n    Set<String> lessThan(K key, boolean inclusive);\n\n    /**\n     * Gets entity IDs with keys greater than the specified key.\n     *\n     * @param key the key to compare\n     * @param inclusive whether the comparison is inclusive\n     * @return the set of entity IDs with keys greater than the specified key\n     */\n    Set<String> greaterThan(K key, boolean inclusive);\n\n    /**\n     * Gets entity IDs with null keys.\n     *\n     * @return the set of entity IDs with null keys\n     * @throws IllegalArgumentException if the key type is not nullable\n     */\n    Set<String> isNull();\n\n    /**\n     * Gets entity IDs with non-null keys.\n     *\n     * @return the set of entity IDs with non-null keys\n     */\n    Set<String> isNotNull();\n\n    /**\n     * Gets entity IDs where the string representation of the key contains the specified keyword.\n     *\n     * @param keyword the keyword to search for\n     * @return the set of entity IDs that contain the keyword\n     * @throws IllegalArgumentException if the key type is not String\n     */\n    Set<String> stringContains(String keyword);\n\n    /**\n     * Gets entity IDs where the string representation of the key does not contain the specified\n     * keyword.\n     *\n     * @param keyword the keyword to search for\n     * @return the set of entity IDs that do not contain the keyword\n     * @throws IllegalArgumentException if the key type is not String\n     */\n    Set<String> stringNotContains(String keyword);\n\n    /**\n     * Gets entity IDs where the string representation of the key starts with the specified prefix.\n     *\n     * @param prefix the prefix to search for\n     * @return the set of entity IDs that start with the prefix\n     * @throws IllegalArgumentException if the key type is not String\n     */\n    Set<String> stringStartsWith(String prefix);\n\n    /**\n     * Gets entity IDs where the string representation of the key does not start with the\n     * specified prefix.\n     *\n     * @param prefix the prefix to search for\n     * @return the set of entity IDs that do not start with the prefix\n     * @throws IllegalArgumentException if the key type is not String\n     */\n    Set<String> stringNotStartsWith(String prefix);\n\n    /**\n     * Gets entity IDs where the string representation of the key ends with the specified suffix.\n     *\n     * @param suffix the suffix to search for\n     * @return the set of entity IDs that end with the suffix\n     * @throws IllegalArgumentException if the key type is not String\n     */\n    Set<String> stringEndsWith(String suffix);\n\n    /**\n     * Gets entity IDs where the string representation of the key does not end with the specified\n     * suffix.\n     *\n     * @param suffix the suffix to search for\n     * @return the set of entity IDs that do not end with the suffix\n     * @throws IllegalArgumentException if the key type is not String\n     */\n    Set<String> stringNotEndsWith(String suffix);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/index/query/QueryVisitor.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\nimport lombok.Getter;\nimport org.springframework.core.convert.ConversionService;\nimport org.springframework.data.relational.core.sql.Visitable;\nimport org.springframework.data.relational.core.sql.Visitor;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.index.Indices;\nimport run.halo.app.extension.index.LabelIndexQuery;\nimport run.halo.app.extension.index.ValueIndexQuery;\n\n/**\n * A visitor that visits a query and returns the matching extension names.\n *\n * @param <E> the type of extension\n * @author johnniang\n * @since 2.22.0\n */\npublic class QueryVisitor<E extends Extension> implements Visitor {\n\n    private final ConversionService conversionService;\n\n    private final Indices<E> indices;\n\n    private final Set<String> result;\n\n    public QueryVisitor(Indices<E> indices, ConversionService conversionService) {\n        this.indices = indices;\n        this.result = new HashSet<>();\n        this.conversionService = conversionService;\n    }\n\n    @Override\n    public void enter(Visitable segment) {\n        var visitor = new ConditionVisitor();\n        segment.visit(visitor);\n        result.addAll(visitor.getResult());\n    }\n\n    @NonNull\n    public Set<String> getResult() {\n        return result;\n    }\n\n    class ConditionVisitor implements Visitor {\n\n        @Getter\n        private final Set<String> result;\n\n        ConditionVisitor() {\n            result = new HashSet<>();\n        }\n\n        @Override\n        public void enter(@NonNull Visitable segment) {\n            switch (segment) {\n                case And(var left, var right) ->\n                    // delegate to AndCondition for backward compatibility\n                    new AndCondition(left, right).visit(this);\n                case EmptyCondition ignored -> result.addAll(allQuery(\"metadata.name\", false));\n                case AndCondition(var left, var right) -> {\n                    if (left instanceof EmptyCondition) {\n                        right.visit(this);\n                        return;\n                    }\n                    if (right instanceof EmptyCondition) {\n                        left.visit(this);\n                        return;\n                    }\n                    left.visit(this);\n                    // fail fast if left result is empty\n                    if (!result.isEmpty()) {\n                        var rightVisitor = new ConditionVisitor();\n                        right.visit(rightVisitor);\n                        result.retainAll(rightVisitor.getResult());\n                    }\n                }\n                case OrCondition(var left, var right) -> {\n                    if (left instanceof EmptyCondition) {\n                        left.visit(this);\n                        return;\n                    }\n                    if (right instanceof EmptyCondition) {\n                        right.visit(this);\n                        return;\n                    }\n                    left.visit(this);\n                    var rightVisitor = new ConditionVisitor();\n                    right.visit(rightVisitor);\n                    result.addAll(rightVisitor.getResult());\n                }\n                case NotCondition(var condition) -> {\n                    if (condition instanceof EmptyCondition) {\n                        return;\n                    }\n                    condition.not().visit(this);\n                }\n                case EqualCondition(var indexName, var key) ->\n                    result.addAll(equalQuery(indexName, key, false));\n                case NotEqualCondition(var indexName, var key) ->\n                    result.addAll(equalQuery(indexName, key, true));\n                case InCondition(var indexName, var keys) ->\n                    result.addAll(inQuery(indexName, keys, false));\n                case NotInCondition(var indexName, var keys) ->\n                    result.addAll(inQuery(indexName, keys, true));\n                case LessThanCondition(var indexName, var upperBound, var inclusive) ->\n                    result.addAll(lessThanQuery(indexName, upperBound, inclusive, false));\n                case GreaterThanCondition(var indexName, var lowerBound, var inclusive) ->\n                    result.addAll(lessThanQuery(indexName, lowerBound, inclusive, true));\n                case BetweenCondition bc -> result.addAll(betweenQuery(\n                        bc.indexName(), bc.fromKey(), bc.fromInclusive(), bc.toKey(),\n                        bc.toInclusive(), false\n                    )\n                );\n                case NotBetweenCondition nbc -> result.addAll(betweenQuery(\n                    nbc.indexName(), nbc.fromKey(), nbc.fromInclusive(), nbc.toKey(),\n                    nbc.toInclusive(), true\n                ));\n                case IsNullCondition(var indexName) ->\n                    result.addAll(isNullQuery(indexName, false));\n                case IsNotNullCondition(var indexName) ->\n                    result.addAll(isNullQuery(indexName, true));\n                case StringContainsCondition(var indexName, var keyword) ->\n                    result.addAll(stringContainsQuery(indexName, keyword, false));\n                case StringNotContainsCondition(var indexName, var keyword) ->\n                    result.addAll(stringContainsQuery(indexName, keyword, true));\n                case StringStartsWithCondition(var indexName, var prefix) ->\n                    result.addAll(stringStartsWithQuery(indexName, prefix, false));\n                case StringNotStartsWithCondition(var indexName, var prefix) ->\n                    result.addAll(stringStartsWithQuery(indexName, prefix, true));\n                case StringEndsWithCondition(var indexName, var suffix) ->\n                    result.addAll(stringEndsWithQuery(indexName, suffix, false));\n                case StringNotEndsWithCondition(var indexName, var suffix) ->\n                    result.addAll(stringEndsWithQuery(indexName, suffix, true));\n                case AllCondition(var indexName) -> result.addAll(allQuery(indexName, false));\n                case NoneCondition(var indexName) -> result.addAll(allQuery(indexName, true));\n                case LabelExistsCondition(var labelKey) ->\n                    result.addAll(labelExistsQuery(labelKey));\n                case LabelNotExistsCondition(var labelKey) -> {\n                    // To get all extensions that do not have the label, we get all extensions\n                    result.addAll(allQuery(\"metadata.name\", false));\n                    result.removeAll(labelExistsQuery(labelKey));\n                }\n                case LabelEqualsCondition(var labelKey, var labelValue) ->\n                    result.addAll(labelEqualsQuery(labelKey, labelValue, false));\n                case LabelNotEqualsCondition(var labelKey, var labelValue) -> {\n                    // Only for backward compatibility\n                    result.addAll(allQuery(\"metadata.name\", false));\n                    result.removeAll(labelEqualsQuery(labelKey, labelValue, false));\n                }\n                case LabelInCondition(var labelKey, var labelValues) ->\n                    result.addAll(labelInQuery(labelKey, labelValues, false));\n                case LabelNotInCondition(var labelKey, var labelValues) ->\n                    result.addAll(labelInQuery(labelKey, labelValues, true));\n                default -> {\n                }\n            }\n        }\n\n        private Set<String> labelInQuery(String labelKey, Collection<String> labelValues,\n            boolean negated) {\n            var index = this.getLabelIndexQuery();\n            if (negated) {\n                return index.notIn(labelKey, labelValues);\n            }\n            return index.in(labelKey, labelValues);\n        }\n\n        private Set<String> labelEqualsQuery(String labelKey, String labelValue,\n            boolean negated) {\n            var index = this.getLabelIndexQuery();\n            if (negated) {\n                return index.notEqual(labelKey, labelValue);\n            }\n            return index.equal(labelKey, labelValue);\n        }\n\n        private Set<String> labelExistsQuery(String labelKey) {\n            var index = getLabelIndexQuery();\n            return index.exists(labelKey);\n        }\n\n        private Set<String> stringEndsWithQuery(String indexName, String suffix, boolean negated) {\n            var index = getValueIndexQuery(indexName);\n            if (negated) {\n                return index.stringNotEndsWith(suffix);\n            }\n            return index.stringEndsWith(suffix);\n        }\n\n        private Set<String> allQuery(String indexName, boolean negated) {\n            var index = getValueIndexQuery(indexName);\n            if (negated) {\n                return Set.of();\n            }\n            return index.all();\n        }\n\n        private Set<String> stringStartsWithQuery(String indexName, String prefix,\n            boolean negated) {\n            var index = getValueIndexQuery(indexName);\n            if (negated) {\n                return index.stringNotStartsWith(prefix);\n            }\n            return index.stringStartsWith(prefix);\n        }\n\n        private Set<String> stringContainsQuery(String indexName, String keyword, boolean negated) {\n            var index = getValueIndexQuery(indexName);\n            if (negated) {\n                return index.stringNotContains(keyword);\n            }\n            return index.stringContains(keyword);\n        }\n\n        private <K extends Comparable<K>> Set<String> isNullQuery(String indexName,\n            boolean negated) {\n            var index = this.<K>getValueIndexQuery(indexName);\n            if (negated) {\n                return index.isNotNull();\n            }\n            return index.isNull();\n        }\n\n        private <K extends Comparable<K>> Set<String> betweenQuery(String indexName, Object fromKey,\n            boolean fromInclusive,\n            Object toKey, boolean toInclusive, boolean negated) {\n            var index = this.<K>getValueIndexQuery(indexName);\n            if (!conversionService.canConvert(fromKey.getClass(), index.getKeyType())) {\n                throw new IllegalArgumentException(\n                    \"Cannot convert key: \" + fromKey + \" to type: \" + index.getKeyType()\n                );\n            }\n            if (!conversionService.canConvert(toKey.getClass(), index.getKeyType())) {\n                throw new IllegalArgumentException(\n                    \"Cannot convert key: \" + toKey + \" to type: \" + index.getKeyType()\n                );\n            }\n            if (negated) {\n                return index.notBetween(\n                    conversionService.convert(fromKey, index.getKeyType()),\n                    fromInclusive,\n                    conversionService.convert(toKey, index.getKeyType()),\n                    toInclusive\n                );\n            } else {\n                return index.between(\n                    conversionService.convert(fromKey, index.getKeyType()),\n                    fromInclusive,\n                    conversionService.convert(toKey, index.getKeyType()),\n                    toInclusive\n                );\n            }\n        }\n\n        private <K extends Comparable<K>> Set<String> lessThanQuery(String indexName, Object bound,\n            boolean inclusive,\n            boolean negated) {\n            var index = this.<K>getValueIndexQuery(indexName);\n            if (!conversionService.canConvert(bound.getClass(), index.getKeyType())) {\n                throw new IllegalArgumentException(\n                    \"Cannot convert key: \" + bound + \" to type: \" + index.getKeyType()\n                );\n            }\n            if (negated) {\n                return index.greaterThan(\n                    conversionService.convert(bound, index.getKeyType()), inclusive\n                );\n            } else {\n                return index.lessThan(conversionService.convert(bound, index.getKeyType()),\n                    inclusive);\n            }\n        }\n\n        private <K extends Comparable<K>> Set<String> equalQuery(String indexName, Object key,\n            boolean negated) {\n            var index = this.<K>getValueIndexQuery(indexName);\n            if (!conversionService.canConvert(key.getClass(), index.getKeyType())) {\n                throw new IllegalArgumentException(\n                    \"Cannot convert key: \" + key + \" to type: \" + index.getKeyType()\n                );\n            }\n            if (negated) {\n                return index.notEqual(conversionService.convert(key, index.getKeyType()));\n            } else {\n                return index.equal(conversionService.convert(key, index.getKeyType()));\n            }\n        }\n\n        private <K extends Comparable<K>> Set<String> inQuery(\n            String indexName, Collection<Object> keys, boolean negated\n        ) {\n            var index = this.<K>getValueIndexQuery(indexName);\n            var convertedKeys = keys.stream()\n                .map(key -> {\n                    if (!conversionService.canConvert(key.getClass(), index.getKeyType())) {\n                        throw new IllegalArgumentException(\n                            \"Cannot convert key: \" + key + \" to type: \" + index.getKeyType()\n                        );\n                    }\n                    return conversionService.convert(key, index.getKeyType());\n                })\n                .toList();\n            if (negated) {\n                return index.notIn(convertedKeys);\n            } else {\n                return index.in(convertedKeys);\n            }\n        }\n\n        private <K extends Comparable<K>> ValueIndexQuery<K> getValueIndexQuery(String indexName) {\n            var index = indices.<K>getIndex(indexName);\n            if (!(index instanceof ValueIndexQuery<?> valueIndexQuery)) {\n                throw new IllegalArgumentException(\"Index is not in-memory: \" + indexName);\n            }\n            return (ValueIndexQuery<K>) valueIndexQuery;\n        }\n\n        private LabelIndexQuery getLabelIndexQuery() {\n            var indexName = \"metadata.labels\";\n            var index = indices.<String>getIndex(indexName);\n            if (!(index instanceof LabelIndexQuery labelIndexQuery)) {\n                throw new IllegalArgumentException(\"Index is not a label index: \" + indexName);\n            }\n            return labelIndexQuery;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/indexer/DefaultIndicesInitializer.java",
    "content": "package run.halo.app.extension.indexer;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StopWatch;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionConverter;\nimport run.halo.app.extension.ExtensionStoreUtil;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.event.SchemeAddedEvent;\nimport run.halo.app.extension.index.IndexEngine;\nimport run.halo.app.extension.index.IndicesInitializer;\nimport run.halo.app.extension.store.ExtensionStore;\nimport run.halo.app.extension.store.ExtensionStoreClient;\n\n@Component\n@Slf4j\nclass DefaultIndicesInitializer implements IndicesInitializer {\n\n    private final IndexEngine indexEngine;\n\n    private final ExtensionStoreClient client;\n\n    private final ExtensionConverter extensionConverter;\n\n    DefaultIndicesInitializer(IndexEngine indexEngine,\n        ExtensionStoreClient client,\n        ExtensionConverter extensionConverter) {\n        this.indexEngine = indexEngine;\n        this.client = client;\n        this.extensionConverter = extensionConverter;\n    }\n\n    @EventListener\n    void onSchemeAddedEvent(SchemeAddedEvent event) {\n        var scheme = event.getScheme();\n        this.initialize(scheme);\n    }\n\n    @Override\n    public void initialize(Scheme scheme) {\n        doInitialize(scheme);\n    }\n\n    public <E extends Extension> void doInitialize(Scheme scheme) {\n        var type = (Class<E>) scheme.type();\n        var prefix = ExtensionStoreUtil.buildStoreNamePrefix(scheme);\n        List<ExtensionStore> extensionStores;\n        String nameCursor = null;\n        log.info(\"Start to initialize indices for type: {}, prefix: {}\", type.getName(), prefix);\n        var watch = new StopWatch(\"Initialize indices for \" + type.getName());\n        var indexedCount = 0L;\n        do {\n            watch.start(\"Indexing from \" + (nameCursor == null ? \"@start\" : nameCursor));\n            extensionStores = client.listBy(prefix, nameCursor, 100);\n            indexEngine.insert(extensionStores.stream()\n                .map(es -> this.extensionConverter.convertFrom(type, es))::iterator\n            );\n            if (!extensionStores.isEmpty()) {\n                nameCursor = extensionStores.getLast().getName();\n            }\n            indexedCount += extensionStores.size();\n            watch.stop();\n        } while (!extensionStores.isEmpty());\n        log.info(\"Total indexed count: {}, initialization summary: {}\",\n            indexedCount, watch.prettyPrint(TimeUnit.MILLISECONDS));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionCompositeRouterFunction.java",
    "content": "package run.halo.app.extension.router;\n\nimport java.util.Collections;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.event.SchemeAddedEvent;\nimport run.halo.app.extension.event.SchemeRemovedEvent;\n\n@Component\npublic class ExtensionCompositeRouterFunction implements RouterFunction<ServerResponse> {\n\n    private final ConcurrentMap<Scheme, RouterFunction<ServerResponse>> schemeRouterFuncMapper;\n\n    private final ReactiveExtensionClient client;\n\n    public ExtensionCompositeRouterFunction(ReactiveExtensionClient client) {\n        this.client = client;\n        schemeRouterFuncMapper = new ConcurrentHashMap<>();\n    }\n\n    @Override\n    @NonNull\n    public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {\n        return Flux.fromIterable(getRouterFunctions())\n            .concatMap(routerFunction -> routerFunction.route(request))\n            .next();\n    }\n\n    @Override\n    public void accept(@NonNull RouterFunctions.Visitor visitor) {\n        getRouterFunctions().forEach(routerFunction -> routerFunction.accept(visitor));\n    }\n\n    private Iterable<RouterFunction<ServerResponse>> getRouterFunctions() {\n        // TODO Copy router functions here\n        return Collections.unmodifiableCollection(schemeRouterFuncMapper.values());\n    }\n\n    @EventListener\n    void onSchemeAddedEvent(SchemeAddedEvent event) {\n        var scheme = event.getScheme();\n        var factory = new ExtensionRouterFunctionFactory(scheme, client);\n        this.schemeRouterFuncMapper.put(scheme, factory.create());\n    }\n\n    @EventListener\n    void onSchemeRemovedEvent(SchemeRemovedEvent event) {\n        this.schemeRouterFuncMapper.remove(event.getScheme());\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionCreateHandler.java",
    "content": "package run.halo.app.extension.router;\n\nimport static run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern;\n\nimport java.net.URI;\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.extension.exception.ExtensionConvertException;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.CreateHandler;\n\nclass ExtensionCreateHandler implements CreateHandler {\n\n    private final Scheme scheme;\n\n    private final ReactiveExtensionClient client;\n\n    public ExtensionCreateHandler(Scheme scheme, ReactiveExtensionClient client) {\n        this.scheme = scheme;\n        this.client = client;\n    }\n\n    @Override\n    @NonNull\n    public Mono<ServerResponse> handle(@NonNull ServerRequest request) {\n        return request.bodyToMono(Unstructured.class)\n            .switchIfEmpty(Mono.error(() -> new ExtensionConvertException(\n                \"Cannot read body to \" + scheme.groupVersionKind())))\n            .flatMap(client::create)\n            .flatMap(createdExt -> ServerResponse\n                .created(URI.create(pathPattern() + \"/\" + createdExt.getMetadata().getName()))\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(createdExt));\n    }\n\n    @Override\n    public String pathPattern() {\n        return buildExtensionPathPattern(scheme);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionDeleteHandler.java",
    "content": "package run.halo.app.extension.router;\n\nimport static run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern;\n\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.DeleteHandler;\n\nclass ExtensionDeleteHandler implements DeleteHandler {\n\n    private final Scheme scheme;\n\n    private final ReactiveExtensionClient client;\n\n    ExtensionDeleteHandler(Scheme scheme, ReactiveExtensionClient client) {\n        this.scheme = scheme;\n        this.client = client;\n    }\n\n    @Override\n    public Mono<ServerResponse> handle(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return client.get(scheme.type(), name)\n            .flatMap(client::delete)\n            .flatMap(deleted -> ServerResponse\n                .ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(deleted));\n    }\n\n    @Override\n    public String pathPattern() {\n        return buildExtensionPathPattern(scheme) + \"/{name}\";\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionGetHandler.java",
    "content": "package run.halo.app.extension.router;\n\nimport static run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern;\n\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.GetHandler;\n\nclass ExtensionGetHandler implements GetHandler {\n    private final Scheme scheme;\n\n    private final ReactiveExtensionClient client;\n\n    public ExtensionGetHandler(Scheme scheme, ReactiveExtensionClient client) {\n        this.scheme = scheme;\n        this.client = client;\n    }\n\n    @Override\n    public String pathPattern() {\n        return buildExtensionPathPattern(scheme) + \"/{name}\";\n    }\n\n    @Override\n    @NonNull\n    public Mono<ServerResponse> handle(@NonNull ServerRequest request) {\n        var extensionName = request.pathVariable(\"name\");\n\n        return client.get(scheme.type(), extensionName)\n            .flatMap(extension -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(extension));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionListHandler.java",
    "content": "package run.halo.app.extension.router;\n\nimport static run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern;\n\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.ListHandler;\n\nclass ExtensionListHandler implements ListHandler {\n    private final Scheme scheme;\n\n    private final ReactiveExtensionClient client;\n\n    public ExtensionListHandler(Scheme scheme, ReactiveExtensionClient client) {\n        this.scheme = scheme;\n        this.client = client;\n    }\n\n    @Override\n    @NonNull\n    public Mono<ServerResponse> handle(@NonNull ServerRequest request) {\n        var queryParams = new SortableRequest(request.exchange());\n        return client.listBy(scheme.type(),\n                queryParams.toListOptions(),\n                queryParams.toPageRequest()\n            )\n            .flatMap(listResult -> ServerResponse\n                .ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(listResult));\n    }\n\n    @Override\n    public String pathPattern() {\n        return buildExtensionPathPattern(scheme);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionPatchHandler.java",
    "content": "package run.halo.app.extension.router;\n\nimport static run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.github.fge.jsonpatch.JsonPatch;\nimport com.github.fge.jsonpatch.JsonPatchException;\nimport java.util.List;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport org.springframework.web.server.UnsupportedMediaTypeStatusException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.JsonExtension;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.PatchHandler;\n\n/**\n * Handler for patching extension.\n *\n * @author johnniang\n */\n@Slf4j\npublic class ExtensionPatchHandler implements PatchHandler {\n\n    private static final MediaType JSON_PATCH_MEDIA_TYPE =\n        MediaType.valueOf(\"application/json-patch+json\");\n\n    private final Scheme scheme;\n\n    private final ReactiveExtensionClient client;\n\n    public ExtensionPatchHandler(Scheme scheme, ReactiveExtensionClient client) {\n        this.scheme = scheme;\n        this.client = client;\n    }\n\n    @Override\n    public Mono<ServerResponse> handle(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var contentTypeOpt = request.headers().contentType();\n        if (contentTypeOpt.isEmpty()) {\n            return Mono.error(\n                new UnsupportedMediaTypeStatusException((MediaType) null,\n                    List.of(JSON_PATCH_MEDIA_TYPE))\n            );\n        }\n        var contentType = contentTypeOpt.get();\n        if (!contentType.isCompatibleWith(JSON_PATCH_MEDIA_TYPE)) {\n            return Mono.error(\n                new UnsupportedMediaTypeStatusException(contentType, List.of(JSON_PATCH_MEDIA_TYPE))\n            );\n        }\n\n        return request.bodyToMono(JsonPatch.class)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Request body required.\")))\n            .flatMap(jsonPatch -> client.getJsonExtension(scheme.groupVersionKind(), name)\n                .flatMap(jsonExtension -> {\n                    try {\n                        // apply the patch\n                        var appliedJsonNode =\n                            (ObjectNode) jsonPatch.apply(jsonExtension.getInternal());\n                        var patchedExtension =\n                            new JsonExtension(jsonExtension.getObjectMapper(), appliedJsonNode);\n                        // update the patched extension\n                        return client.update(patchedExtension);\n                    } catch (JsonPatchException e) {\n                        return Mono.error(e);\n                    }\n                }))\n            .flatMap(updated -> ServerResponse.ok().bodyValue(updated));\n    }\n\n    @Override\n    public String pathPattern() {\n        return buildExtensionPathPattern(scheme) + \"/{name}\";\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionRouterFunctionFactory.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\n\nimport io.swagger.v3.core.util.RefUtils;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\n\npublic class ExtensionRouterFunctionFactory {\n\n    private final Scheme scheme;\n\n    private final ReactiveExtensionClient client;\n\n    public ExtensionRouterFunctionFactory(Scheme scheme, ReactiveExtensionClient client) {\n        this.scheme = scheme;\n        this.client = client;\n    }\n\n    @NonNull\n    public RouterFunction<ServerResponse> create() {\n        var getHandler = new ExtensionGetHandler(scheme, client);\n        var listHandler = new ExtensionListHandler(scheme, client);\n        var createHandler = new ExtensionCreateHandler(scheme, client);\n        var updateHandler = new ExtensionUpdateHandler(scheme, client);\n        var deleteHandler = new ExtensionDeleteHandler(scheme, client);\n        var patchHandler = new ExtensionPatchHandler(scheme, client);\n        // TODO More handlers here\n        var gvk = scheme.groupVersionKind();\n        var kind = gvk.kind();\n        var tagName = gvk.kind() + StringUtils.capitalize(gvk.version());\n        return SpringdocRouteBuilder.route()\n            .GET(getHandler.pathPattern(), getHandler,\n                builder -> builder.operationId(\"get\" + kind)\n                    .description(\"Get \" + kind)\n                    .tag(tagName)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Name of \" + scheme.singular()))\n                    .response(responseBuilder().responseCode(\"200\")\n                        .description(\"Response single \" + scheme.singular())\n                        .implementation(scheme.type())))\n            .GET(listHandler.pathPattern(), listHandler,\n                builder -> {\n                    builder.operationId(\"list\" + kind)\n                        .description(\"List \" + kind)\n                        .tag(tagName)\n                        .response(responseBuilder().responseCode(\"200\")\n                            .description(\"Response \" + scheme.plural())\n                            .implementation(ListResult.generateGenericClass(scheme)));\n                    SortableRequest.buildParameters(builder);\n                })\n            .POST(createHandler.pathPattern(), createHandler,\n                builder -> builder.operationId(\"create\" + kind)\n                    .description(\"Create \" + kind)\n                    .tag(tagName)\n                    .requestBody(requestBodyBuilder()\n                        .description(\"Fresh \" + scheme.singular())\n                        .implementation(scheme.type()))\n                    .response(responseBuilder().responseCode(\"200\")\n                        .description(\"Response \" + scheme.plural() + \" created just now\")\n                        .implementation(scheme.type())))\n            .PUT(updateHandler.pathPattern(), updateHandler,\n                builder -> builder.operationId(\"update\" + kind)\n                    .description(\"Update \" + kind)\n                    .tag(tagName)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Name of \" + scheme.singular()))\n                    .requestBody(requestBodyBuilder()\n                        .description(\"Updated \" + scheme.singular())\n                        .implementation(scheme.type()))\n                    .response(responseBuilder().responseCode(\"200\")\n                        .description(\"Response \" + scheme.plural() + \" updated just now\")\n                        .implementation(scheme.type())))\n            .PATCH(patchHandler.pathPattern(), patchHandler,\n                builder -> builder.operationId(\"patch\" + kind)\n                    .description(\"Patch \" + kind)\n                    .tag(tagName)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Name of \" + scheme.singular()))\n                    .requestBody(requestBodyBuilder()\n                        .content(contentBuilder()\n                            .mediaType(\"application/json-patch+json\")\n                            .schema(\n                                schemaBuilder().ref(RefUtils.constructRef(JsonPatch.SCHEMA_NAME))\n                            )\n                        )\n                    )\n                    .response(responseBuilder().responseCode(\"200\")\n                        .description(\"Response \" + scheme.singular() + \" patched just now\")\n                        .implementation(scheme.type())\n                    )\n            )\n            .DELETE(deleteHandler.pathPattern(), deleteHandler,\n                builder -> builder.operationId(\"delete\" + kind)\n                    .description(\"Delete \" + kind)\n                    .tag(tagName)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Name of \" + scheme.singular()))\n                    .response(responseBuilder().responseCode(\"200\")\n                        .description(\"Response \" + scheme.singular() + \" deleted just now\")))\n            .build();\n    }\n\n    interface PathPatternGenerator {\n\n        String pathPattern();\n\n        static String buildExtensionPathPattern(Scheme scheme) {\n            var gvk = scheme.groupVersionKind();\n            StringBuilder pattern = new StringBuilder();\n            if (gvk.hasGroup()) {\n                pattern.append(\"/apis/\").append(gvk.group());\n            } else {\n                pattern.append(\"/api\");\n            }\n            return pattern.append('/').append(gvk.version()).append('/').append(scheme.plural())\n                .toString();\n        }\n    }\n\n    interface GetHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {\n\n    }\n\n    interface ListHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {\n\n    }\n\n    interface CreateHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {\n\n    }\n\n    interface UpdateHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {\n\n    }\n\n    interface DeleteHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {\n\n    }\n\n    interface PatchHandler extends HandlerFunction<ServerResponse>, PathPatternGenerator {\n\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/ExtensionUpdateHandler.java",
    "content": "package run.halo.app.extension.router;\n\nimport static run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator.buildExtensionPathPattern;\n\nimport java.util.Objects;\nimport org.springframework.http.MediaType;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.UpdateHandler;\n\nclass ExtensionUpdateHandler implements UpdateHandler {\n\n    private final Scheme scheme;\n\n    private final ReactiveExtensionClient client;\n\n    ExtensionUpdateHandler(Scheme scheme, ReactiveExtensionClient client) {\n        this.scheme = scheme;\n        this.client = client;\n    }\n\n    @Override\n    public Mono<ServerResponse> handle(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return request.bodyToMono(Unstructured.class)\n            .filter(unstructured -> unstructured.getMetadata() != null\n                && StringUtils.hasText(unstructured.getMetadata().getName())\n                && Objects.equals(unstructured.getMetadata().getName(), name))\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"Cannot read body to \" + scheme.groupVersionKind())))\n            .flatMap(client::update)\n            .flatMap(updated -> ServerResponse\n                .ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(updated));\n    }\n\n    @Override\n    public String pathPattern() {\n        return buildExtensionPathPattern(scheme) + \"/{name}\";\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/router/JsonPatch.java",
    "content": "package run.halo.app.extension.router;\n\nimport static io.swagger.v3.oas.models.Components.COMPONENTS_SCHEMAS_REF;\n\nimport io.swagger.v3.oas.models.Components;\nimport io.swagger.v3.oas.models.media.Schema;\nimport io.swagger.v3.oas.models.media.StringSchema;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * JSON schema for JSONPatch operations.\n *\n * @author johnniang\n */\npublic final class JsonPatch {\n\n    private JsonPatch() {}\n\n    public static final String SCHEMA_NAME = \"JsonPatch\";\n\n    public static void addSchema(Components components) {\n        Function<String, io.swagger.v3.oas.models.media.Schema<?>> opSchemaFunc =\n            op -> new StringSchema()._enum(List.of(op)).type(\"string\");\n        var pathSchema = new StringSchema()\n            .description(\"A JSON Pointer path\")\n            .pattern(\"^(/[^/~]*(~[01][^/~]*)*)*$\")\n            .example(\"/a/b/c\");\n        var valueSchema = new Schema<>().description(\"Value can be any JSON value\");\n        var operationSchema = new io.swagger.v3.oas.models.media.Schema<>()\n            .oneOf(List.of(\n                new io.swagger.v3.oas.models.media.Schema<>()\n                    .$ref(COMPONENTS_SCHEMAS_REF + \"AddOperation\"),\n                new io.swagger.v3.oas.models.media.Schema<>()\n                    .$ref(COMPONENTS_SCHEMAS_REF + \"ReplaceOperation\"),\n                new io.swagger.v3.oas.models.media.Schema<>()\n                    .$ref(COMPONENTS_SCHEMAS_REF + \"TestOperation\"),\n                new io.swagger.v3.oas.models.media.Schema<>()\n                    .$ref(COMPONENTS_SCHEMAS_REF + \"RemoveOperation\"),\n                new io.swagger.v3.oas.models.media.Schema<>()\n                    .$ref(COMPONENTS_SCHEMAS_REF + \"MoveOperation\"),\n                new io.swagger.v3.oas.models.media.Schema<>()\n                    .$ref(COMPONENTS_SCHEMAS_REF + \"CopyOperation\")\n            ));\n\n        components.addSchemas(\"AddOperation\", new io.swagger.v3.oas.models.media.ObjectSchema()\n            .required(List.of(\"op\", \"path\", \"value\"))\n            .properties(Map.of(\n                \"op\", opSchemaFunc.apply(\"add\"),\n                \"path\", pathSchema,\n                \"value\", valueSchema\n            )))\n        ;\n        components.addSchemas(\"ReplaceOperation\", new io.swagger.v3.oas.models.media.ObjectSchema()\n            .required(List.of(\"op\", \"path\", \"value\"))\n            .properties(Map.of(\n                \"op\", opSchemaFunc.apply(\"replace\"),\n                \"path\", pathSchema,\n                \"value\", valueSchema\n            )))\n        ;\n        components.addSchemas(\"TestOperation\", new io.swagger.v3.oas.models.media.ObjectSchema()\n            .required(List.of(\"op\", \"path\", \"value\"))\n            .properties(Map.of(\n                \"op\", opSchemaFunc.apply(\"test\"),\n                \"path\", pathSchema,\n                \"value\", valueSchema\n            )))\n        ;\n        components.addSchemas(\"RemoveOperation\", new io.swagger.v3.oas.models.media.ObjectSchema()\n            .required(List.of(\"op\", \"path\"))\n            .properties(Map.of(\n                \"op\", opSchemaFunc.apply(\"remove\"),\n                \"path\", pathSchema\n            )))\n        ;\n        components.addSchemas(\"MoveOperation\", new io.swagger.v3.oas.models.media.ObjectSchema()\n            .required(List.of(\"op\", \"from\", \"path\"))\n            .properties(Map.of(\n                \"op\", opSchemaFunc.apply(\"move\"),\n                \"from\", pathSchema\n                    .description(\"A JSON Pointer path pointing to the location to move/copy from.\"),\n                \"path\", pathSchema\n            )))\n        ;\n        components.addSchemas(\"CopyOperation\", new io.swagger.v3.oas.models.media.ObjectSchema()\n            .required(List.of(\"op\", \"from\", \"path\"))\n            .properties(Map.of(\n                \"op\", opSchemaFunc.apply(\"copy\"),\n                \"from\", pathSchema\n                    .description(\"A JSON Pointer path pointing to the location to move/copy from.\"),\n                \"path\", pathSchema\n            )))\n        ;\n        components.addSchemas(SCHEMA_NAME, new io.swagger.v3.oas.models.media.ArraySchema()\n            .description(\"JSON schema for JSONPatch operations\")\n            .uniqueItems(true)\n            .minItems(1)\n            .items(operationSchema)\n        );\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/store/ExtensionStore.java",
    "content": "package run.halo.app.extension.store;\n\nimport lombok.Data;\nimport org.springframework.data.annotation.Id;\nimport org.springframework.data.annotation.Version;\nimport org.springframework.data.relational.core.mapping.Table;\n\n/**\n * ExtensionStore is an entity for storing Extension data into database.\n *\n * @author johnniang\n */\n@Data\n@Table(name = \"extensions\")\npublic class ExtensionStore {\n\n    /**\n     * Extension store name, which is globally unique.\n     * We will use it to query Extensions by using left-like query clause.\n     */\n    @Id\n    private String name;\n\n    /**\n     * Exactly Extension body, which might be base64 format.\n     */\n    private byte[] data;\n\n    /**\n     * This field only for serving optimistic lock value.\n     */\n    @Version\n    private Long version;\n\n    public ExtensionStore() {\n    }\n\n    public ExtensionStore(String name, byte[] data) {\n        this.name = name;\n        this.data = data;\n    }\n\n    public ExtensionStore(String name, Long version) {\n        this.name = name;\n        this.version = version;\n    }\n\n    public ExtensionStore(String name, byte[] data, Long version) {\n        this.name = name;\n        this.data = data;\n        this.version = version;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/store/ExtensionStoreClient.java",
    "content": "package run.halo.app.extension.store;\n\nimport java.util.List;\nimport java.util.Optional;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\n\n/**\n * An interface to query and operate ExtensionStore.\n *\n * @author johnniang\n */\npublic interface ExtensionStoreClient {\n\n    /**\n     * Lists all ExtensionStores by name prefix.\n     *\n     * @param prefix is the prefix of ExtensionStore name.\n     * @return all ExtensionStores which names start with the prefix.\n     */\n    List<ExtensionStore> listByNamePrefix(String prefix);\n\n    Page<ExtensionStore> listByNamePrefix(String prefix, Pageable pageable);\n\n    /**\n     * Lists ExtensionStores by name prefix, after the given cursor name, and limit the result size.\n     *\n     * @param prefix the name prefix\n     * @param nameCursor cursor name, exclusive and can be null\n     * @param limit the max result size\n     * @return a list of extension stores\n     */\n    List<ExtensionStore> listBy(String prefix, String nameCursor, int limit);\n\n    List<ExtensionStore> listByNames(List<String> names);\n\n    /**\n     * Fetches an ExtensionStore by unique name.\n     *\n     * @param name is the full name of an ExtensionStore.\n     * @return an optional ExtensionStore.\n     */\n    Optional<ExtensionStore> fetchByName(String name);\n\n    /**\n     * Creates an ExtensionStore.\n     *\n     * @param name is the full name of an ExtensionStore.\n     * @param data is Extension body to be persisted.\n     * @return a fresh ExtensionStore created just now.\n     */\n    ExtensionStore create(String name, byte[] data);\n\n    /**\n     * Updates an ExtensionStore with version to prevent concurrent update.\n     *\n     * @param name is the full name of an ExtensionStore.\n     * @param version is the expected version of ExtensionStore.\n     * @param data is Extension body to be updated.\n     * @return updated ExtensionStore with a fresh version.\n     */\n    ExtensionStore update(String name, Long version, byte[] data);\n\n    /**\n     * Deletes an ExtensionStore by name and current version.\n     *\n     * @param name is the full name of an ExtensionStore.\n     * @param version is the expected version of ExtensionStore.\n     * @return previous ExtensionStore.\n     */\n    ExtensionStore delete(String name, Long version);\n\n    //TODO add watch method here.\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/store/ExtensionStoreClientJPAImpl.java",
    "content": "package run.halo.app.extension.store;\n\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Optional;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.stereotype.Service;\n\n/**\n * An implementation of ExtensionStoreClient using JPA.\n *\n * @author johnniang\n */\n@Service\npublic class ExtensionStoreClientJPAImpl implements ExtensionStoreClient {\n\n    private static final Duration TIMEOUT = Duration.ofSeconds(30);\n\n    private final ReactiveExtensionStoreClient storeClient;\n\n    public ExtensionStoreClientJPAImpl(ReactiveExtensionStoreClient storeClient) {\n        this.storeClient = storeClient;\n    }\n\n    @Override\n    public List<ExtensionStore> listByNamePrefix(String prefix) {\n        return storeClient.listByNamePrefix(prefix).collectList().block(TIMEOUT);\n    }\n\n    @Override\n    public Page<ExtensionStore> listByNamePrefix(String prefix, Pageable pageable) {\n        return storeClient.listByNamePrefix(prefix, pageable).block(TIMEOUT);\n    }\n\n    @Override\n    public List<ExtensionStore> listBy(String prefix, String nameCursor, int limit) {\n        return storeClient.listBy(prefix, nameCursor, limit).collectList().block(TIMEOUT);\n    }\n\n    @Override\n    public List<ExtensionStore> listByNames(List<String> names) {\n        return storeClient.listByNames(names).collectList().block(TIMEOUT);\n    }\n\n    @Override\n    public Optional<ExtensionStore> fetchByName(String name) {\n        return storeClient.fetchByName(name).blockOptional(TIMEOUT);\n    }\n\n    @Override\n    public ExtensionStore create(String name, byte[] data) {\n        return storeClient.create(name, data).block(TIMEOUT);\n    }\n\n    @Override\n    public ExtensionStore update(String name, Long version, byte[] data) {\n        return storeClient.update(name, version, data).block(TIMEOUT);\n    }\n\n    @Override\n    public ExtensionStore delete(String name, Long version) {\n        return storeClient.delete(name, version).block(TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/store/ExtensionStoreRepository.java",
    "content": "package run.halo.app.extension.store;\n\nimport java.util.Collection;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.r2dbc.repository.R2dbcRepository;\nimport org.springframework.stereotype.Repository;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n/**\n * This repository contains some basic operations on ExtensionStore entity.\n *\n * @author johnniang\n */\n@Repository\npublic interface ExtensionStoreRepository extends R2dbcRepository<ExtensionStore, String> {\n\n    /**\n     * Finds all ExtensionStore by name prefix.\n     *\n     * @param prefix is the prefix of name.\n     * @return all ExtensionStores which names starts with the given prefix.\n     */\n    Flux<ExtensionStore> findAllByNameStartingWith(String prefix);\n\n    Flux<ExtensionStore> findAllByNameStartingWith(String prefix, Pageable pageable);\n\n    Mono<Long> countByNameStartingWith(String prefix);\n\n    /**\n     * <p>Finds all ExtensionStore by name in, the result no guarantee the same order as the given\n     * names, so if you want this, please order the result by yourself.</p>\n     *\n     * @param names names to find\n     * @return a flux of extension stores\n     */\n    Flux<ExtensionStore> findByNameIn(Collection<String> names);\n\n    Flux<ExtensionStore> findAllByNameStartingWithAndNameGreaterThan(\n        String prefix, String nameCursor, Pageable pageable);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/store/ReactiveExtensionStoreClient.java",
    "content": "package run.halo.app.extension.store;\n\nimport java.util.List;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\npublic interface ReactiveExtensionStoreClient {\n\n    Flux<ExtensionStore> listByNamePrefix(String prefix);\n\n    Mono<Page<ExtensionStore>> listByNamePrefix(String prefix, Pageable pageable);\n\n    /**\n     * List stores by name prefix, after the given cursor name, and limit the result size.\n     *\n     * @param prefix the name prefix\n     * @param nameCursor cursor name, exclusive and can be null\n     * @param limit the max result size\n     * @return a flux of extension stores\n     */\n    Flux<ExtensionStore> listBy(String prefix, @Nullable String nameCursor, int limit);\n\n    Mono<Long> countByNamePrefix(String prefix);\n\n    /**\n     * List stores by names and return data according to given names order.\n     *\n     * @param names store names to list\n     * @return a flux of extension stores\n     */\n    Flux<ExtensionStore> listByNames(List<String> names);\n\n    Mono<ExtensionStore> fetchByName(String name);\n\n    Mono<ExtensionStore> create(String name, byte[] data);\n\n    Mono<ExtensionStore> update(String name, Long version, byte[] data);\n\n    Mono<ExtensionStore> delete(String name, Long version);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/extension/store/ReactiveExtensionStoreClientImpl.java",
    "content": "package run.halo.app.extension.store;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.commons.lang3.Strings;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.dao.DuplicateKeyException;\nimport org.springframework.data.domain.Page;\nimport org.springframework.data.domain.Pageable;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.data.r2dbc.core.R2dbcEntityOperations;\nimport org.springframework.data.relational.core.query.Criteria;\nimport org.springframework.data.relational.core.query.Query;\nimport org.springframework.data.support.ReactivePageableExecutionUtils;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.exception.DuplicateNameException;\n\n@Component\n@RequiredArgsConstructor\npublic class ReactiveExtensionStoreClientImpl implements ReactiveExtensionStoreClient {\n\n    private static final int DEFAULT_FETCH_SIZE = 100;\n\n    private final ExtensionStoreRepository repository;\n\n    private final R2dbcEntityOperations entityOperations;\n\n    private int fetchSize = DEFAULT_FETCH_SIZE;\n\n    void setFetchSize(int fetchSize) {\n        Assert.isTrue(fetchSize >= 0, \"fetchSize must be greater than or equal to 0\");\n        this.fetchSize = fetchSize;\n    }\n\n    @Override\n    public Flux<ExtensionStore> listByNamePrefix(String prefix) {\n        Assert.hasText(prefix, \"Prefix must not be blank\");\n\n        prefix = Strings.CS.appendIfMissing(prefix, \"/\");\n        return entityOperations.select(ExtensionStore.class)\n            .withFetchSize(fetchSize)\n            .matching(Query.query(\n                        Criteria.where(\"name\").like(prefix + \"%\")\n                    )\n                    .sort(Sort.by(Sort.Direction.ASC, \"name\"))\n            )\n            .all();\n    }\n\n    @Override\n    public Mono<Page<ExtensionStore>> listByNamePrefix(String prefix, Pageable pageable) {\n        Assert.hasText(prefix, \"Prefix must not be blank\");\n\n        var q = Query.query(\n            Criteria.where(\"name\").like(prefix + \"%\")\n        ).sort(Sort.by(Sort.Direction.ASC, \"name\"));\n        var getItems = entityOperations.select(ExtensionStore.class)\n            .matching(q.with(pageable))\n            .all()\n            .collectList();\n        var getCount = entityOperations.select(ExtensionStore.class)\n            .matching(q)\n            .count();\n        return getItems.flatMap(\n            items -> ReactivePageableExecutionUtils.getPage(items, pageable, getCount)\n        );\n    }\n\n    @Override\n    public Flux<ExtensionStore> listBy(String prefix, @Nullable String nameCursor, int limit) {\n        Assert.hasText(prefix, \"Prefix must not be blank\");\n        Assert.isTrue(limit > 0, \"Limit must be greater than 0\");\n\n        prefix = Strings.CS.appendIfMissing(prefix, \"/\");\n        var criteria = Criteria.where(\"name\").like(prefix + \"%\");\n        if (StringUtils.isNotBlank(nameCursor)) {\n            nameCursor = Strings.CS.prependIfMissing(nameCursor, prefix);\n            criteria = criteria.and(Criteria.where(\"name\").greaterThan(nameCursor));\n        }\n        var q = Query.query(criteria).sort(Sort.by(Sort.Direction.ASC, \"name\"));\n        return entityOperations.select(ExtensionStore.class)\n            .matching(q.limit(limit))\n            .all();\n    }\n\n    @Override\n    public Mono<Long> countByNamePrefix(String prefix) {\n        Assert.hasText(prefix, \"Prefix must not be blank\");\n\n        var q = Query.query(\n                Criteria.where(\"name\").like(prefix + \"%\")\n            )\n            .sort(Sort.by(Sort.Direction.ASC, \"name\"));\n        return entityOperations.select(ExtensionStore.class)\n            .matching(q)\n            .count();\n    }\n\n    @Override\n    public Flux<ExtensionStore> listByNames(List<String> names) {\n        if (CollectionUtils.isEmpty(names)) {\n            return Flux.empty();\n        }\n        // Keep the order of names efficiently\n        var orderMap = IntStream.range(0, names.size())\n            .boxed()\n            .collect(Collectors.toMap(names::get, Function.identity(), (a, b) -> a));\n        return repository.findByNameIn(names)\n            .sort(Comparator.comparingInt(es -> orderMap.get(es.getName())));\n    }\n\n    @Override\n    public Mono<ExtensionStore> fetchByName(String name) {\n        return repository.findById(name);\n    }\n\n    @Override\n    public Mono<ExtensionStore> create(String name, byte[] data) {\n        return repository.save(new ExtensionStore(name, data))\n            .onErrorMap(DuplicateKeyException.class,\n                t -> new DuplicateNameException(\"Duplicate name detected.\", t));\n    }\n\n    @Override\n    public Mono<ExtensionStore> update(String name, Long version, byte[] data) {\n        return repository.save(new ExtensionStore(name, data, version));\n    }\n\n    @Override\n    public Mono<ExtensionStore> delete(String name, Long version) {\n        return repository.findById(name)\n            .flatMap(extensionStore -> {\n                // reset the version\n                extensionStore.setVersion(version);\n                return repository.delete(extensionStore).thenReturn(extensionStore);\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/DefaultBackupRootGetter.java",
    "content": "package run.halo.app.infra;\n\nimport java.nio.file.Path;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@Component\npublic class DefaultBackupRootGetter implements BackupRootGetter {\n\n    private final HaloProperties haloProperties;\n\n    public DefaultBackupRootGetter(HaloProperties haloProperties) {\n        this.haloProperties = haloProperties;\n    }\n\n    @Override\n    public Path get() {\n        return haloProperties.getWorkDir().resolve(\"backups\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/DefaultExternalLinkProcessor.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.utils.PathUtils;\n\n/**\n * Default implementation of {@link ExternalLinkProcessor}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultExternalLinkProcessor implements ExternalLinkProcessor {\n    private final ExternalUrlSupplier externalUrlSupplier;\n\n    @Override\n    public String processLink(String link) {\n        var externalLink = externalUrlSupplier.getRaw();\n        if (StringUtils.isBlank(link) || externalLink == null || PathUtils.isAbsoluteUri(link)) {\n            return link;\n        }\n        return append(externalLink.toString(), link);\n    }\n\n    @Override\n    public Mono<URI> processLink(URI uri) {\n        if (uri.isAbsolute()) {\n            return Mono.just(uri);\n        }\n        return Mono.deferContextual(contextView -> Mono.fromSupplier(\n            () -> ServerWebExchangeContextFilter.getExchange(contextView)\n                .map(exchange -> externalUrlSupplier.getURL(exchange.getRequest()))\n                .or(() -> Optional.ofNullable(externalUrlSupplier.getRaw()))\n                .map(externalUrl -> {\n                    try {\n                        var uriComponents = UriComponentsBuilder.fromUriString(uri.toASCIIString())\n                            .build(true);\n                        return UriComponentsBuilder.fromUri(externalUrl.toURI())\n                            .pathSegment(uriComponents.getPathSegments().toArray(new String[0]))\n                            .queryParams(uriComponents.getQueryParams())\n                            .fragment(uriComponents.getFragment())\n                            .build(true)\n                            .toUri();\n                    } catch (URISyntaxException e) {\n                        // should never happen\n                        return uri;\n                    }\n                })\n                .orElse(uri)\n        ));\n    }\n\n    String append(String externalLink, String link) {\n        return StringUtils.removeEnd(externalLink, \"/\")\n            + StringUtils.prependIfMissing(link, \"/\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/DefaultInitializationStateGetter.java",
    "content": "package run.halo.app.infra;\n\nimport static org.apache.commons.lang3.BooleanUtils.isTrue;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\n\n/**\n * <p>A cache that caches system setup state.</p>\n * when setUp state changed, the cache will be updated.\n *\n * @author guqing\n * @since 2.5.2\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultInitializationStateGetter implements InitializationStateGetter {\n    private final ReactiveExtensionClient client;\n    private final AtomicBoolean userInitialized = new AtomicBoolean(false);\n    private final AtomicBoolean dataInitialized = new AtomicBoolean(false);\n\n    @Override\n    public Mono<Boolean> userInitialized() {\n        // If user is initialized, return true directly.\n        if (userInitialized.get()) {\n            return Mono.just(true);\n        }\n        return hasUser()\n            .doOnNext(userInitialized::set);\n    }\n\n    @Override\n    public Mono<Boolean> dataInitialized() {\n        if (dataInitialized.get()) {\n            return Mono.just(true);\n        }\n        return client.fetch(ConfigMap.class, SystemState.SYSTEM_STATES_CONFIGMAP)\n            .map(config -> {\n                SystemState systemState = SystemState.deserialize(config);\n                return isTrue(systemState.getIsSetup());\n            })\n            .defaultIfEmpty(false)\n            .doOnNext(dataInitialized::set);\n    }\n\n    private Mono<Boolean> hasUser() {\n        var listOptions = new ListOptions();\n        listOptions.setLabelSelector(LabelSelector.builder()\n            .notEq(User.HIDDEN_USER_LABEL, \"true\")\n            .build()\n        );\n        listOptions.setFieldSelector(\n            FieldSelector.of(isNull(\"metadata.deletionTimestamp\")));\n        return client.listBy(User.class, listOptions, PageRequestImpl.ofSize(1))\n            .map(result -> result.getTotal() > 0)\n            .defaultIfEmpty(false);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/DefaultReactiveUrlDataBufferFetcher.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.URI;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.reactive.ReactorClientHttpConnector;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.client.ExchangeStrategies;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.netty.http.client.HttpClient;\n\n/**\n * <p>A default implementation of {@link ReactiveUrlDataBufferFetcher}.</p>\n *\n * @author guqing\n * @since 2.6.0\n */\n@Component\npublic class DefaultReactiveUrlDataBufferFetcher implements ReactiveUrlDataBufferFetcher {\n    private final HttpClient httpClient = HttpClient.create()\n        .followRedirect(true);\n    private final ContentLengthFetcher contentLengthFetcher = new ContentLengthFetcher();\n\n    private final WebClient webClient = WebClient.builder()\n        .clientConnector(new ReactorClientHttpConnector(httpClient))\n        .build();\n\n    @Override\n    public Flux<DataBuffer> fetch(URI uri) {\n        return webClient.get()\n            .uri(uri)\n            .accept(MediaType.APPLICATION_OCTET_STREAM)\n            .retrieve()\n            .bodyToFlux(DataBuffer.class);\n    }\n\n    @Override\n    public Mono<HttpHeaders> head(URI uri) {\n        return contentLengthFetcher.fetchContentLength(uri);\n    }\n\n    static class ContentLengthFetcher {\n\n        private final WebClient webClient;\n\n        ContentLengthFetcher() {\n            this.webClient = WebClient.builder()\n                .exchangeStrategies(ExchangeStrategies.builder()\n                    .codecs(config -> config.defaultCodecs().maxInMemorySize(1))\n                    .build())\n                .build();\n        }\n\n        Mono<HttpHeaders> fetchContentLength(URI url) {\n            return webClient.get()\n                .uri(url)\n                .exchangeToMono(response -> {\n                    HttpHeaders headers = response.headers().asHttpHeaders();\n\n                    return response.bodyToMono(byte[].class)\n                        .onErrorResume(ex -> Mono.empty())\n                        .thenReturn(headers);\n                });\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/DefaultSystemConfigFetcher.java",
    "content": "package run.halo.app.infra;\n\nimport static run.halo.app.infra.SystemSetting.SYSTEM_CONFIG;\nimport static run.halo.app.infra.SystemSetting.SYSTEM_CONFIG_DEFAULT;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicReference;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.core.convert.ConversionService;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonParseException;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.infra.utils.SystemConfigUtils;\nimport tools.jackson.databind.json.JsonMapper;\n\n@Component\n@RequiredArgsConstructor\n@Order(Ordered.HIGHEST_PRECEDENCE)\nclass DefaultSystemConfigFetcher\n    implements SystemConfigFetcher, ApplicationListener<SystemConfigChangedEvent> {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private final JsonMapper mapper;\n\n    private final ReactiveExtensionClient extensionClient;\n\n    private final ConversionService conversionService;\n\n    private final AtomicReference<Map<String, String>> configMapCache = new AtomicReference<>();\n\n    private final Mono<Map<String, String>> configMapMono = Mono.defer(() -> {\n        var currentValue = configMapCache.get();\n        if (currentValue != null) {\n            return Mono.just(currentValue);\n        }\n        return computeSystemConfig().mapNotNull(configMap -> {\n            if (configMapCache.compareAndSet(null, configMap.getData())) {\n                return configMap.getData();\n            } else {\n                return configMapCache.get();\n            }\n        }).defaultIfEmpty(Map.of());\n    }).cacheInvalidateIf(configMap -> {\n        var currentValue = configMapCache.get();\n        return currentValue == null || !currentValue.equals(configMap);\n    });\n\n    @Override\n    public void onApplicationEvent(SystemConfigChangedEvent event) {\n        configMapCache.set(event.getNewData());\n    }\n\n    @Override\n    public <T> Mono<T> fetch(String key, Class<T> type) {\n        return getValuesInternal()\n            .filter(map -> map.containsKey(key))\n            .map(map -> map.get(key))\n            .mapNotNull(stringValue -> {\n                if (conversionService.canConvert(String.class, type)) {\n                    return conversionService.convert(stringValue, type);\n                }\n                return mapper.readValue(stringValue, type);\n            });\n    }\n\n    @Override\n    public Mono<SystemSetting.Basic> getBasic() {\n        return fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class)\n            .switchIfEmpty(Mono.fromSupplier(SystemSetting.Basic::new));\n    }\n\n    @Override\n    public Mono<SystemSetting.Comment> fetchComment() {\n        return fetch(SystemSetting.Comment.GROUP, SystemSetting.Comment.class)\n            .switchIfEmpty(Mono.fromSupplier(SystemSetting.Comment::new));\n    }\n\n    @Override\n    public Mono<SystemSetting.Post> fetchPost() {\n        return fetch(SystemSetting.Post.GROUP, SystemSetting.Post.class)\n            .switchIfEmpty(Mono.fromSupplier(SystemSetting.Post::new));\n    }\n\n    @Override\n    public Mono<SystemSetting.ThemeRouteRules> fetchRouteRules() {\n        return fetch(SystemSetting.ThemeRouteRules.GROUP, SystemSetting.ThemeRouteRules.class);\n    }\n\n    @NonNull\n    private Mono<Map<String, String>> getValuesInternal() {\n        return configMapMono;\n    }\n\n    @Override\n    public Mono<Map<String, String>> getConfig() {\n        return configMapMono;\n    }\n\n    /**\n     * Load the system config map from the extension client.\n     *\n     * @return latest configMap from {@link ReactiveExtensionClient} without any cache.\n     */\n    @Override\n    public Mono<ConfigMap> getConfigMap() {\n        return extensionClient.fetch(ConfigMap.class, SYSTEM_CONFIG);\n    }\n\n    /**\n     * Gets the system config map without any cache.\n     *\n     * @return load configMap from {@link ReactiveExtensionClient}\n     */\n    @Override\n    public Optional<ConfigMap> getConfigMapBlocking() {\n        return getConfigMap().blockOptional(BLOCKING_TIMEOUT);\n    }\n\n    private Mono<ConfigMap> computeSystemConfig() {\n        var getOverrideConfigMap = extensionClient.fetch(ConfigMap.class, SYSTEM_CONFIG);\n        var getDefaultConfigMap = extensionClient.fetch(ConfigMap.class, SYSTEM_CONFIG_DEFAULT)\n            .switchIfEmpty(Mono.fromSupplier(() -> {\n                var defaultConfigMap = new ConfigMap();\n                defaultConfigMap.setData(Map.of());\n                return defaultConfigMap;\n            }));\n        return Mono.zip(getDefaultConfigMap, getOverrideConfigMap,\n            (defaultConfigMap, overrideConfigMap) -> {\n                try {\n                    return SystemConfigUtils.mergeConfigMap(defaultConfigMap, overrideConfigMap);\n                } catch (JsonProcessingException e) {\n                    throw new JsonParseException(e);\n                }\n            });\n    }\n\n    /**\n     * Gets the config map cache. Only for test use.\n     *\n     * @return the config map cache\n     */\n    AtomicReference<Map<String, String>> getConfigMapCache() {\n        return configMapCache;\n    }\n\n    /**\n     * Gets the config map mono. Only for test use.\n     *\n     * @return the config map mono\n     */\n    Mono<Map<String, String>> getConfigMapMono() {\n        return configMapMono;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java",
    "content": "package run.halo.app.infra;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.util.Objects;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.info.BuildProperties;\nimport org.springframework.stereotype.Component;\n\n/**\n * Default implementation of system version supplier.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class DefaultSystemVersionSupplier implements SystemVersionSupplier {\n    private static final String DEFAULT_VERSION = \"0.0.0\";\n\n    private final ObjectProvider<BuildProperties> buildProperties;\n\n    public DefaultSystemVersionSupplier(ObjectProvider<BuildProperties> buildProperties) {\n        this.buildProperties = buildProperties;\n    }\n\n    @Override\n    public Version get() {\n        var properties = buildProperties.getIfUnique();\n        if (properties == null) {\n            return Version.parse(DEFAULT_VERSION);\n        }\n        var projectVersion = Objects.toString(properties.getVersion(), DEFAULT_VERSION);\n        return Version.parse(projectVersion);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/DefaultThemeRootGetter.java",
    "content": "package run.halo.app.infra;\n\nimport java.nio.file.Path;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@Component\npublic class DefaultThemeRootGetter implements ThemeRootGetter {\n\n    private final HaloProperties haloProps;\n\n    public DefaultThemeRootGetter(HaloProperties haloProps) {\n        this.haloProps = haloProps;\n    }\n\n    @Override\n    public Path get() {\n        return haloProps.getWorkDir().resolve(\"themes\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ExtensionInitializedEvent.java",
    "content": "package run.halo.app.infra;\n\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * ExtensionInitializedEvent is fired after extensions have been initialized completely.\n *\n * @author johnniang\n */\npublic class ExtensionInitializedEvent extends ApplicationEvent {\n\n    public ExtensionInitializedEvent(Object source) {\n        super(source);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ExtensionResourceInitializer.java",
    "content": "package run.halo.app.infra;\n\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\n\n/**\n * <p>Extension resources initializer.</p>\n * <p>Check whether {@link HaloProperties#getInitialExtensionLocations()} is configured\n * When the system ready, and load resources according to it to creates {@link Unstructured}</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\npublic class ExtensionResourceInitializer implements SmartLifecycle {\n\n    private volatile boolean running;\n\n    public static final Set<String> REQUIRED_EXTENSION_LOCATIONS =\n        Set.of(\"classpath:/extensions/*.yaml\", \"classpath:/extensions/*.yml\");\n    private final HaloProperties haloProperties;\n    private final ReactiveExtensionClient extensionClient;\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    public ExtensionResourceInitializer(HaloProperties haloProperties,\n        ReactiveExtensionClient extensionClient,\n        ApplicationEventPublisher eventPublisher) {\n        this.haloProperties = haloProperties;\n        this.extensionClient = extensionClient;\n        this.eventPublisher = eventPublisher;\n    }\n\n    @Override\n    public void start() {\n        if (running) {\n            return;\n        }\n        running = true;\n        var locations = new HashSet<String>();\n        if (!haloProperties.isRequiredExtensionDisabled()) {\n            locations.addAll(REQUIRED_EXTENSION_LOCATIONS);\n        }\n        if (haloProperties.getInitialExtensionLocations() != null) {\n            locations.addAll(haloProperties.getInitialExtensionLocations());\n        }\n        if (CollectionUtils.isEmpty(locations)) {\n            return;\n        }\n\n        Flux.fromIterable(locations)\n            .doOnNext(location ->\n                log.debug(\"Trying to initialize extension resources from location: {}\", location))\n            .map(this::listResources)\n            .distinct()\n            .flatMapIterable(resources -> resources)\n            .doOnNext(resource -> log.debug(\"Initializing extension resource from location: {}\",\n                resource))\n            .map(resource -> new YamlUnstructuredLoader(resource).load())\n            .flatMapIterable(extensions -> extensions)\n            .doOnNext(extension -> {\n                if (log.isDebugEnabled()) {\n                    log.debug(\"Initializing extension resource: {}/{}\",\n                        extension.groupVersionKind(), extension.getMetadata().getName());\n                }\n            })\n            .flatMap(this::createOrUpdate)\n            .doOnNext(extension -> {\n                if (log.isDebugEnabled()) {\n                    log.debug(\"Initialized extension resource: {}/{}\", extension.groupVersionKind(),\n                        extension.getMetadata().getName());\n                }\n            })\n            .then()\n            .block(Duration.ofMinutes(1));\n        eventPublisher.publishEvent(new ExtensionInitializedEvent(this));\n    }\n\n    @Override\n    public void stop() {\n        if (!running) {\n            return;\n        }\n        running = false;\n    }\n\n    @Override\n    public boolean isRunning() {\n        return running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.EXTENSION_RESOURCES.getPhase();\n    }\n\n\n    private Mono<Unstructured> createOrUpdate(Unstructured extension) {\n        return Mono.just(extension)\n            .flatMap(ext -> extensionClient.fetch(extension.groupVersionKind(),\n                extension.getMetadata().getName()))\n            .flatMap(existingExt -> {\n                if (ExtensionUtil.hasDoNotOverwriteLabel(existingExt)) {\n                    log.debug(\"Extension {} is marked as do-not-overwrite, skipping update\",\n                        existingExt.getMetadata().getName()\n                    );\n                    // skip update\n                    return Mono.just(existingExt);\n                }\n                // force update\n                extension.getMetadata().setVersion(existingExt.getMetadata().getVersion());\n                return extensionClient.update(extension);\n            })\n            .switchIfEmpty(Mono.defer(() -> {\n                if (ExtensionUtil.isDeleted(extension)) {\n                    // skip deleted extension\n                    return Mono.empty();\n                }\n                return extensionClient.create(extension);\n            }));\n    }\n\n    private List<Resource> listResources(String location) {\n        var resolver = new PathMatchingResourcePatternResolver();\n        try {\n            return List.of(resolver.getResources(location));\n        } catch (IOException ie) {\n            throw new IllegalArgumentException(\"Invalid extension location: \" + location, ie);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ExternalUrlChangedEvent.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.URL;\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * Event triggered when the external URL of the application changes.\n *\n * @author johnniang\n * @since 2.21.0\n */\npublic class ExternalUrlChangedEvent extends ApplicationEvent {\n\n    @Getter\n    private final URL externalUrl;\n\n    public ExternalUrlChangedEvent(Object source, URL externalUrl) {\n        super(source);\n        this.externalUrl = externalUrl;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/InitializationPhase.java",
    "content": "package run.halo.app.infra;\n\n/**\n * Phase of system initialization.\n *\n * @author johnniang\n */\npublic enum InitializationPhase {\n\n    FIRST(Integer.MIN_VALUE),\n\n    SCHEME(Integer.MIN_VALUE + 100),\n\n    EXTENSION_RESOURCES,\n\n    THEME_ROUTER_FUNCTIONS,\n\n    GC_CONTROLLER,\n\n    CONTROLLERS,\n\n    LAST(Integer.MAX_VALUE),\n    ;\n\n    private static final int GAP = 100;\n\n    private final int phase;\n\n    InitializationPhase() {\n        this.phase = ordinal() * GAP;\n    }\n\n    InitializationPhase(int phase) {\n        this.phase = phase;\n    }\n\n    public int getPhase() {\n        return phase;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/InitializationStateGetter.java",
    "content": "package run.halo.app.infra;\n\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>A interface that get system initialization state.</p>\n *\n * @author guqing\n * @since 2.9.0\n */\npublic interface InitializationStateGetter {\n\n    /**\n     * Check if system user is initialized.\n     *\n     * @return <code>true</code> if system user is initialized, <code>false</code> otherwise.\n     */\n    Mono<Boolean> userInitialized();\n\n    /**\n     * Check if system basic data is initialized.\n     *\n     * @return <code>true</code> if system basic data is initialized, <code>false</code> otherwise.\n     */\n    Mono<Boolean> dataInitialized();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ReactiveExtensionPaginatedOperator.java",
    "content": "package run.halo.app.infra;\n\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ListOptions;\n\n/**\n * Reactive extension paginated operator to handle extensions by pagination.\n *\n * @author guqing\n * @since 2.15.0\n */\npublic interface ReactiveExtensionPaginatedOperator {\n\n    /**\n     * <p>Deletes all data, including any new entries added during the execution of this method.</p>\n     * <p>This method continuously monitors and removes data that appears throughout its runtime,\n     * ensuring that even data created during the deletion process is also removed.</p>\n     */\n    <E extends Extension> Mono<Void> deleteContinuously(Class<E> type,\n        ListOptions listOptions);\n\n    /**\n     * <p>Deletes only the data that existed at the start of the operation.</p>\n     * <p>This method takes a snapshot of the data at the beginning and deletes only that dataset;\n     * any data added after the method starts will not be affected or removed.</p>\n     */\n    <E extends Extension> Flux<E> deleteInitialBatch(Class<E> type,\n        ListOptions listOptions);\n\n    /**\n     * <p>Note that: This method can not be used for <code>deletion</code> operation, because\n     * deletion operation will change the total records.</p>\n     */\n    <E extends Extension> Flux<E> list(Class<E> type, ListOptions listOptions);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ReactiveExtensionPaginatedOperatorImpl.java",
    "content": "package run.halo.app.infra;\n\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n@Component\n@RequiredArgsConstructor\npublic class ReactiveExtensionPaginatedOperatorImpl implements ReactiveExtensionPaginatedOperator {\n    private static final int DEFAULT_PAGE_SIZE = 200;\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public <E extends Extension> Mono<Void> deleteContinuously(Class<E> type,\n        ListOptions listOptions) {\n        var pageRequest = createPageRequest();\n        return cleanupContinuously(type, listOptions, pageRequest);\n    }\n\n    private <E extends Extension> Mono<Void> cleanupContinuously(Class<E> type,\n        ListOptions listOptions,\n        PageRequest pageRequest) {\n        // forever loop first page until no more to delete\n        return pageBy(type, listOptions, pageRequest)\n            .flatMap(page -> Flux.fromIterable(page.getItems())\n                .flatMap(client::delete)\n                .then(page.hasNext() ? cleanupContinuously(type, listOptions, pageRequest)\n                    : Mono.empty())\n            );\n    }\n\n    @Override\n    public <E extends Extension> Flux<E> deleteInitialBatch(Class<E> type,\n        ListOptions listOptions) {\n        var pageRequest = createPageRequest();\n        var newFieldQuery = listOptions.getFieldSelector()\n            .andQuery(isNull(\"metadata.deletionTimestamp\"));\n        listOptions.setFieldSelector(newFieldQuery);\n        final Instant now = Instant.now();\n\n        return pageBy(type, listOptions, pageRequest)\n            // forever loop first page until no more to delete\n            .expand(result -> result.hasNext()\n                ? pageBy(type, listOptions, pageRequest) : Mono.empty())\n            .flatMap(result -> Flux.fromIterable(result.getItems()))\n            .takeWhile(item -> shouldTakeNext(item, now))\n            .flatMap(this::deleteWithRetry);\n    }\n\n    static <E extends Extension> boolean shouldTakeNext(E item, Instant now) {\n        var creationTimestamp = item.getMetadata().getCreationTimestamp();\n        return creationTimestamp.isBefore(now)\n            || creationTimestamp.equals(now);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    <E extends Extension> Mono<E> deleteWithRetry(E item) {\n        return client.delete(item)\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> attemptToDelete((Class<E>) item.getClass(), item.getMetadata().getName()));\n    }\n\n    private <E extends Extension> Mono<E> attemptToDelete(Class<E> type, String name) {\n        return Mono.defer(() -> client.fetch(type, name)\n                .flatMap(client::delete)\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    @Override\n    public <E extends Extension> Flux<E> list(Class<E> type, ListOptions listOptions) {\n        var pageRequest = createPageRequest();\n        return list(type, listOptions, pageRequest);\n    }\n\n    /**\n     * Paginated list all items to avoid memory overflow.\n     * <pre>\n     * 1. Retrieve data multiple times until all data is consumed.\n     * 2. Fetch next page if current page has more data and consumed records is less than total\n     * records.\n     * 3. Take while consumed records is less than total records.\n     * 4. totalRecords from first page to ensure new inserted data will not be counted in during\n     * querying to avoid infinite loop.\n     * </pre>\n     */\n    private <E extends Extension> Flux<E> list(Class<E> type, ListOptions listOptions,\n        PageRequest pageRequest) {\n        final var now = Instant.now();\n        return pageBy(type, listOptions, pageRequest)\n            .expand(result -> {\n                if (result.hasNext()) {\n                    // fetch next page\n                    var nextPage = nextPage(result, pageRequest.getSort());\n                    return pageBy(type, listOptions, nextPage);\n                } else {\n                    return Mono.empty();\n                }\n            })\n            .flatMap(page -> Flux.fromIterable(page.getItems()))\n            .takeWhile(item -> shouldTakeNext(item, now));\n    }\n\n    static <E extends Extension> PageRequest nextPage(ListResult<E> result, Sort sort) {\n        return PageRequestImpl.of(result.getPage() + 1, result.getSize(), sort);\n    }\n\n    private PageRequest createPageRequest() {\n        return PageRequestImpl.of(1, DEFAULT_PAGE_SIZE,\n            Sort.by(\"metadata.creationTimestamp\", \"metadata.name\"));\n    }\n\n    private <E extends Extension> Mono<ListResult<E>> pageBy(Class<E> type, ListOptions listOptions,\n        PageRequest pageRequest) {\n        return client.listBy(type, listOptions, pageRequest);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ReactiveUrlDataBufferFetcher.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.URI;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.HttpHeaders;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>{@link DataBuffer} stream fetcher from uri.</p>\n *\n * @author guqing\n * @since 2.6.0\n */\npublic interface ReactiveUrlDataBufferFetcher {\n\n    /**\n     * <p>Fetch data buffer flux from uri.</p>\n     *\n     * @param uri uri to fetch\n     * @return data buffer flux\n     */\n    Flux<DataBuffer> fetch(URI uri);\n\n    /**\n     * <p>Get head of the uri.</p>\n     *\n     * @param uri uri to fetch\n     * @return response entity\n     */\n    Mono<HttpHeaders> head(URI uri);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SchemeInitializer.java",
    "content": "package run.halo.app.infra;\n\nimport static java.util.Objects.requireNonNullElse;\nimport static run.halo.app.core.extension.Role.ROLE_AGGREGATE_LABEL_PREFIX;\nimport static run.halo.app.core.extension.content.Comment.CommentOwner.ownerIdentity;\nimport static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport java.time.Instant;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.content.Stats;\nimport run.halo.app.core.attachment.extension.LocalThumbnail;\nimport run.halo.app.core.attachment.extension.Thumbnail;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.core.extension.AuthProvider;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.Device;\nimport run.halo.app.core.extension.Menu;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.core.extension.RememberMeToken;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.RoleBinding;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.UserConnection;\nimport run.halo.app.core.extension.UserConnection.UserConnectionSpec;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Attachment.AttachmentSpec;\nimport run.halo.app.core.extension.attachment.Attachment.AttachmentStatus;\nimport run.halo.app.core.extension.attachment.Group;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.core.extension.attachment.PolicyTemplate;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Category.CategorySpec;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Comment.CommentSpec;\nimport run.halo.app.core.extension.content.Comment.CommentStatus;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Post.PostSpec;\nimport run.halo.app.core.extension.content.Post.PostStatus;\nimport run.halo.app.core.extension.content.Post.VisibleEnum;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.content.Reply.ReplySpec;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.content.SinglePage.SinglePageSpec;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.core.extension.content.Tag.TagSpec;\nimport run.halo.app.core.extension.content.Tag.TagStatus;\nimport run.halo.app.core.extension.notification.Notification;\nimport run.halo.app.core.extension.notification.Notification.NotificationSpec;\nimport run.halo.app.core.extension.notification.NotificationTemplate;\nimport run.halo.app.core.extension.notification.NotifierDescriptor;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.core.extension.notification.Subscription.InterestReason;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.MetadataOperator;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.Secret;\nimport run.halo.app.extension.index.IndexSpec;\nimport run.halo.app.extension.index.IndexSpecs;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.migration.Backup;\nimport run.halo.app.plugin.extensionpoint.ExtensionDefinition;\nimport run.halo.app.plugin.extensionpoint.ExtensionPointDefinition;\nimport run.halo.app.security.PersonalAccessToken;\n\n@Component\nclass SchemeInitializer implements SmartLifecycle {\n\n    private final SchemeManager schemeManager;\n\n    private volatile boolean running;\n\n    public SchemeInitializer(SchemeManager schemeManager) {\n        this.schemeManager = schemeManager;\n    }\n\n    @Override\n    public void start() {\n        if (running) {\n            return;\n        }\n        running = true;\n        schemeManager.register(Role.class, is -> {\n            is.add(IndexSpecs.<Role, String>multi(\"labels.aggregateToRoles\", String.class)\n                .indexFunc(role -> Optional.ofNullable(role.getMetadata().getLabels()).map(\n                    labels -> labels.keySet().stream()\n                        .filter(key -> key.startsWith(ROLE_AGGREGATE_LABEL_PREFIX))\n                        .filter(key -> Boolean.parseBoolean(labels.get(key)))\n                        .map(key -> StringUtils.removeStart(key, ROLE_AGGREGATE_LABEL_PREFIX))\n                        .collect(Collectors.toSet())).orElseGet(Set::of)\n                )\n            );\n        });\n\n        // plugin.halo.run\n        schemeManager.register(Plugin.class, is -> {\n            is.add(IndexSpecs.<Plugin, String>single(\"spec.displayName\", String.class)\n                .indexFunc(plugin ->\n                    Optional.ofNullable(plugin.getSpec())\n                        .map(Plugin.PluginSpec::getDisplayName)\n                        .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<Plugin, String>single(\"spec.description\", String.class)\n                .indexFunc(plugin -> Optional.ofNullable(plugin.getSpec())\n                    .map(Plugin.PluginSpec::getDescription)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<Plugin, Boolean>single(\"spec.enabled\", Boolean.class)\n                .indexFunc(plugin -> Optional.ofNullable(plugin.getSpec())\n                    .map(Plugin.PluginSpec::getEnabled)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n        });\n        schemeManager.register(ExtensionPointDefinition.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<ExtensionPointDefinition, String>single(\n                        \"spec.className\", String.class\n                    )\n                    .indexFunc(definition -> definition.getSpec().getClassName())\n            );\n        });\n        schemeManager.register(ExtensionDefinition.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<ExtensionDefinition, String>single(\n                        \"spec.extensionPointName\", String.class\n                    )\n                    .indexFunc(definition -> definition.getSpec().getExtensionPointName())\n            );\n        });\n        schemeManager.register(RoleBinding.class, is -> {\n            is.add(IndexSpecs.<RoleBinding, String>single(\"roleRef.name\", String.class)\n                .indexFunc(roleBinding -> roleBinding.getRoleRef().getName())\n            );\n            is.add(IndexSpecs.<RoleBinding, String>multi(\"subjects\", String.class)\n                .indexFunc(roleBinding -> roleBinding.getSubjects().stream()\n                    .map(RoleBinding.Subject::toString)\n                    .collect(Collectors.toSet())\n                )\n            );\n        });\n        schemeManager.register(User.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<User, String>single(\"spec.displayName\", String.class)\n                .indexFunc(user -> user.getSpec().getDisplayName())\n            );\n            indexSpecs.add(IndexSpecs.<User, Boolean>single(\"spec.emailVerified\", Boolean.class)\n                .indexFunc(user -> user.getSpec().isEmailVerified())\n            );\n            indexSpecs.add(IndexSpecs.<User, String>single(\"spec.email\", String.class)\n                .indexFunc(user -> Optional.ofNullable(user.getSpec().getEmail())\n                    .map(String::toLowerCase)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<User, String>multi(\n                        User.USER_RELATED_ROLES_INDEX, String.class\n                    )\n                    .indexFunc(user -> Optional.ofNullable(user.getMetadata())\n                        .map(MetadataOperator::getAnnotations)\n                        .map(annotations -> annotations.get(User.ROLE_NAMES_ANNO))\n                        .filter(StringUtils::isNotBlank)\n                        .map(rolesJson -> JsonUtils.jsonToObject(\n                            rolesJson, new TypeReference<Set<String>>() {\n                            })\n                        )\n                        .orElseGet(Set::of)\n                    )\n            );\n            indexSpecs.add(IndexSpecs.<User, Boolean>single(\"spec.disabled\", Boolean.class)\n                .indexFunc(user -> requireNonNullElse(user.getSpec().getDisabled(), Boolean.FALSE))\n                .nullable(false)\n            );\n        });\n        schemeManager.register(ReverseProxy.class);\n        schemeManager.register(Setting.class);\n        schemeManager.register(AnnotationSetting.class, indexSpecs -> indexSpecs.add(\n            IndexSpecs.<AnnotationSetting, String>single(\"spec.targetRef\", String.class)\n                .indexFunc(annotationSetting -> Optional.ofNullable(annotationSetting.getSpec())\n                    .map(AnnotationSetting.AnnotationSettingSpec::getTargetRef)\n                    .map(ref -> ref.group() + \"/\" + ref.kind())\n                    .orElse(null)\n                )\n        ));\n        schemeManager.register(ConfigMap.class);\n        schemeManager.register(Secret.class);\n        schemeManager.register(Theme.class);\n        schemeManager.register(Menu.class);\n        schemeManager.register(MenuItem.class);\n        schemeManager.register(Post.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Post, String>single(\"spec.title\", String.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getTitle)\n                    .orElse(null)\n                )\n            );\n\n            indexSpecs.add(IndexSpecs.<Post, String>single(\"spec.slug\", String.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getSlug)\n                    .orElse(null)\n                )\n            );\n\n            indexSpecs.add(IndexSpecs.<Post, Instant>single(\"spec.publishTime\", Instant.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getPublishTime)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, String>single(\"spec.owner\", String.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getOwner)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, Boolean>single(\"spec.deleted\", Boolean.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getDeleted)\n                    .orElse(false)\n                ).nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Post, Boolean>single(\"spec.pinned\", Boolean.class)\n                .indexFunc(post ->\n                    Optional.ofNullable(post.getSpec())\n                        .map(PostSpec::getPinned)\n                        .orElse(false)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Post, Integer>single(\"spec.priority\", Integer.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getPriority)\n                    .orElse(0)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Post, VisibleEnum>single(\"spec.visible\", VisibleEnum.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getVisible)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, String>multi(\"spec.tags\", String.class)\n                .indexFunc(\n                    post -> Optional.ofNullable(post.getSpec())\n                        .map(PostSpec::getTags)\n                        .map(Set::copyOf)\n                        .orElse(Set.of())\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, String>multi(\"spec.categories\", String.class)\n                .indexFunc(post -> Optional.ofNullable(post.getSpec())\n                    .map(PostSpec::getCategories)\n                    .map(Set::copyOf)\n                    .orElse(Set.of())\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, String>multi(\"status.contributors\", String.class)\n                .indexFunc(post -> Optional.ofNullable(post.getStatus())\n                    .map(PostStatus::getContributors)\n                    .map(Set::copyOf).orElse(Set.of())\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, String>single(\"status.phase\", String.class)\n                .indexFunc(post -> Optional.ofNullable(post.getStatus())\n                    .map(PostStatus::getPhase)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, String>single(\"status.excerpt\", String.class)\n                .indexFunc(post -> Optional.ofNullable(post.getStatus())\n                    .map(PostStatus::getExcerpt)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, Instant>single(\"status.lastModifyTime\", Instant.class)\n                .indexFunc(post -> Optional.ofNullable(post.getStatus())\n                    .map(PostStatus::getLastModifyTime)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, Boolean>single(\"status.hideFromList\", Boolean.class)\n                .indexFunc(post -> Optional.ofNullable(post.getStatus())\n                    .map(Post.PostStatus::getHideFromList)\n                    .orElse(false)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Post, Boolean>single(\n                        Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, Boolean.class\n                    )\n                    .indexFunc(post -> {\n                        var version = post.getMetadata().getVersion();\n                        var status = post.getStatus();\n                        if (status == null) {\n                            return true;\n                        }\n                        var observedVersion = status.getObservedVersion();\n                        return observedVersion == null || observedVersion < version;\n                        // do not care about the false case so return null to avoid indexing\n                    })\n            );\n            indexSpecs.add(IndexSpecs.<Post, Long>single(\"stats.visit\", Long.class)\n                .indexFunc(post -> Optional.ofNullable(post.getMetadata().getAnnotations())\n                    .map(a -> a.get(Post.STATS_ANNO))\n                    .filter(StringUtils::isNotBlank)\n                    .map(json -> JsonUtils.jsonToObject(json, Stats.class))\n                    .map(Stats::getVisit)\n                    .map(i -> (long) i)\n                    .orElse(0L)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Post, Long>single(\"stats.totalComment\", Long.class)\n                .indexFunc(post -> Optional.ofNullable(post.getMetadata().getAnnotations())\n                    .map(a -> a.get(Post.STATS_ANNO))\n                    .filter(StringUtils::isNotBlank)\n                    .map(json -> JsonUtils.jsonToObject(json, Stats.class))\n                    .map(Stats::getTotalComment)\n                    .map(i -> (long) i)\n                    .orElse(0L)\n                )\n                .nullable(false)\n            );\n        });\n        schemeManager.register(Category.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Category, String>single(\"spec.slug\", String.class)\n                .indexFunc(category -> Optional.ofNullable(category.getSpec())\n                    .map(CategorySpec::getSlug)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Category, Integer>single(\"spec.priority\", Integer.class)\n                .indexFunc(category -> Optional.ofNullable(category.getSpec())\n                    .map(CategorySpec::getPriority)\n                    .orElse(0)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Category, String>multi(\"spec.children\", String.class)\n                .indexFunc(category -> Optional.ofNullable(category.getSpec())\n                    .map(CategorySpec::getChildren)\n                    .map(Set::copyOf)\n                    .orElse(Set.of())\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Category, Boolean>single(\"spec.hideFromList\", Boolean.class)\n                .indexFunc(category -> Optional.ofNullable(category.getSpec())\n                    .map(CategorySpec::isHideFromList)\n                    .orElse(false)\n                )\n            );\n        });\n        schemeManager.register(Tag.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Tag, String>single(\"spec.displayName\", String.class)\n                .indexFunc(tag -> Optional.ofNullable(tag.getSpec())\n                    .map(TagSpec::getDisplayName)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Tag, String>single(\"spec.slug\", String.class)\n                .indexFunc(\n                    tag -> Optional.ofNullable(tag.getSpec())\n                        .map(TagSpec::getSlug)\n                        .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Tag, Integer>single(\"status.postCount\", Integer.class)\n                .indexFunc(tag -> Optional.ofNullable(tag.getStatus())\n                    .map(TagStatus::getPostCount)\n                    .orElse(0)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Tag, Boolean>single(\n                        Tag.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, Boolean.class\n                    )\n                    .indexFunc(tag -> {\n                        var version = tag.getMetadata().getVersion();\n                        var status = tag.getStatus();\n                        if (status == null) {\n                            return true;\n                        }\n                        var observedVersion = status.getObservedVersion();\n                        return observedVersion == null || observedVersion < version;\n                        // do not care about the false case so return null to avoid indexing\n                    })\n            );\n        });\n        schemeManager.register(Snapshot.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Snapshot, String>single(\"spec.subjectRef\", String.class)\n                .indexFunc(snapshot -> Optional.ofNullable(snapshot.getSpec())\n                    .map(Snapshot.SnapShotSpec::getSubjectRef)\n                    .map(Snapshot::toSubjectRefKey)\n                    .orElse(null)\n                )\n            );\n        });\n        schemeManager.register(Comment.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Comment, Instant>single(\"spec.creationTime\", Instant.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .map(CommentSpec::getCreationTime)\n                    .orElseGet(() -> comment.getMetadata().getCreationTimestamp())\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Comment, Boolean>single(\"spec.approved\", Boolean.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .map(CommentSpec::getApproved)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Comment, String>single(\"spec.owner\", String.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .map(CommentSpec::getOwner)\n                    .map(owner -> ownerIdentity(owner.getKind(), owner.getName()))\n                    .orElse(null)\n                )\n                .nullable(true)\n            );\n            indexSpecs.add(IndexSpecs.<Comment, String>single(\"spec.subjectRef\", String.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .map(CommentSpec::getSubjectRef)\n                    .map(Comment::toSubjectRefKey)\n                    .orElse(null)\n                )\n                .nullable(true)\n            );\n            indexSpecs.add(IndexSpecs.<Comment, Boolean>single(\"spec.top\", Boolean.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .map(CommentSpec::getTop)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Comment, Boolean>single(\"spec.hidden\", Boolean.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .map(CommentSpec::getHidden)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Comment, Integer>single(\"spec.priority\", Integer.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .filter(spec -> Boolean.TRUE.equals(spec.getTop()))\n                    .map(Comment.BaseCommentSpec::getPriority)\n                    .orElse(0)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Comment, String>single(\"spec.raw\", String.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getSpec())\n                    .map(CommentSpec::getRaw)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Comment, Instant>single(\n                        \"status.lastReplyTime\", Instant.class\n                    )\n                    .indexFunc(comment -> Optional.ofNullable(comment.getStatus())\n                        .map(CommentStatus::getLastReplyTime)\n                        .orElse(null)\n                    )\n            );\n\n            indexSpecs.add(IndexSpecs.<Comment, Integer>single(\"status.replyCount\", Integer.class)\n                .indexFunc(comment -> Optional.ofNullable(comment.getStatus())\n                    .map(CommentStatus::getReplyCount)\n                    .orElse(0)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Comment, Boolean>single(\n                        Comment.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, Boolean.class\n                    )\n                    .indexFunc(comment -> {\n                        var version = comment.getMetadata().getVersion();\n                        var status = comment.getStatus();\n                        if (status == null) {\n                            return true;\n                        }\n                        var observedVersion = status.getObservedVersion();\n                        return observedVersion == null || observedVersion < version;\n                        // do not care about the false case so return null to avoid indexing\n                    })\n            );\n        });\n        schemeManager.register(Reply.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Reply, Instant>single(\"spec.creationTime\", Instant.class)\n                .indexFunc(reply -> Optional.ofNullable(reply.getSpec())\n                    .map(ReplySpec::getCreationTime)\n                    .orElse(reply.getMetadata().getCreationTimestamp())\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Reply, String>single(\"spec.commentName\", String.class)\n                .indexFunc(reply -> Optional.ofNullable(reply.getSpec())\n                    .map(ReplySpec::getCommentName)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Reply, Boolean>single(\"spec.hidden\", Boolean.class)\n                .indexFunc(reply -> Optional.ofNullable(reply.getSpec())\n                    .map(ReplySpec::getHidden)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Reply, Boolean>single(\"spec.approved\", Boolean.class)\n                .indexFunc(reply -> Optional.ofNullable(reply.getSpec())\n                    .map(ReplySpec::getApproved)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n            indexSpecs.add(IndexSpecs.<Reply, String>single(\"spec.owner\", String.class)\n                .indexFunc(reply -> Optional.ofNullable(reply.getSpec())\n                    .map(ReplySpec::getOwner)\n                    .map(owner -> ownerIdentity(owner.getKind(), owner.getName()))\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Reply, Boolean>single(\n                        Reply.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, Boolean.class\n                    )\n                    .indexFunc(reply -> {\n                        var version = reply.getMetadata().getVersion();\n                        var status = reply.getStatus();\n                        if (status == null) {\n                            return true;\n                        }\n                        var observedVersion = status.getObservedVersion();\n                        return observedVersion == null || observedVersion < version;\n                        // do not care about the false case so return null to avoid indexing\n                    })\n            );\n        });\n        schemeManager.register(SinglePage.class, is -> {\n            is.add(IndexSpecs.<SinglePage, Instant>single(\"spec.publishTime\", Instant.class)\n                .indexFunc(page -> Optional.ofNullable(page.getSpec())\n                    .map(SinglePageSpec::getPublishTime)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<SinglePage, String>single(\"spec.title\", String.class)\n                .indexFunc(page -> Optional.ofNullable(page.getSpec())\n                    .map(SinglePageSpec::getTitle)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<SinglePage, String>single(\"spec.slug\", String.class)\n                .indexFunc(page -> Optional.ofNullable(page.getSpec())\n                    .map(SinglePageSpec::getSlug)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<SinglePage, Boolean>single(\"spec.deleted\", Boolean.class)\n                .indexFunc(page -> Optional.ofNullable(page.getSpec())\n                    .map(SinglePageSpec::getDeleted)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n            is.add(IndexSpecs.<SinglePage, VisibleEnum>single(\"spec.visible\", VisibleEnum.class)\n                .indexFunc(page -> Optional.ofNullable(page.getSpec())\n                    .map(SinglePageSpec::getVisible)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<SinglePage, Boolean>single(\"spec.pinned\", Boolean.class)\n                .indexFunc(page -> Optional.ofNullable(page.getSpec())\n                    .map(SinglePageSpec::getPinned)\n                    .orElse(false)\n                )\n                .nullable(false)\n            );\n\n            is.add(IndexSpecs.<SinglePage, Integer>single(\"spec.priority\", Integer.class)\n                .indexFunc(page -> Optional.ofNullable(page.getSpec())\n                    .map(SinglePageSpec::getPriority)\n                    .orElse(0)\n                )\n                .nullable(false)\n            );\n\n            is.add(IndexSpecs.<SinglePage, String>single(\"status.excerpt\", String.class)\n                .indexFunc(page -> Optional.ofNullable(page.getStatus())\n                    .map(SinglePage.SinglePageStatus::getExcerpt)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<SinglePage, String>single(\"status.phase\", String.class)\n                .indexFunc(page -> Optional.ofNullable(page.getStatus())\n                    .map(SinglePage.SinglePageStatus::getPhase)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<SinglePage, String>multi(\"status.contributors\", String.class)\n                .indexFunc(page -> Optional.ofNullable(page.getStatus())\n                    .map(SinglePage.SinglePageStatus::getContributors)\n                    .map(Set::copyOf)\n                    .orElse(Set.of())\n                )\n            );\n        });\n        // storage.halo.run\n        schemeManager.register(Group.class);\n        schemeManager.register(Policy.class);\n        schemeManager.register(Attachment.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Attachment, String>single(\"spec.displayName\", String.class)\n                .indexFunc(attachment -> Optional.ofNullable(attachment.getSpec())\n                    .map(AttachmentSpec::getDisplayName)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Attachment, String>single(\"spec.policyName\", String.class)\n                .indexFunc(attachment -> Optional.ofNullable(attachment.getSpec())\n                    .map(AttachmentSpec::getPolicyName)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Attachment, String>single(\"spec.groupName\", String.class)\n                .indexFunc(attachment -> Optional.ofNullable(attachment.getSpec())\n                    .map(AttachmentSpec::getGroupName)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Attachment, String>single(\"spec.mediaType\", String.class)\n                .indexFunc(attachment -> Optional.ofNullable(attachment.getSpec())\n                    .map(AttachmentSpec::getMediaType)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Attachment, String>single(\"spec.ownerName\", String.class)\n                .indexFunc(attachment -> Optional.ofNullable(attachment.getSpec())\n                    .map(AttachmentSpec::getOwnerName)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Attachment, Long>single(\"spec.size\", Long.class)\n                .indexFunc(attachment -> Optional.ofNullable(attachment.getSpec())\n                    .map(AttachmentSpec::getSize)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Attachment, String>single(\"status.permalink\", String.class)\n                .indexFunc(attachment -> Optional.ofNullable(attachment.getStatus())\n                    .map(AttachmentStatus::getPermalink)\n                    .orElse(null)\n                )\n            );\n        });\n        schemeManager.register(PolicyTemplate.class);\n        schemeManager.register(Thumbnail.class, indexSpec -> {\n            indexSpec.add(new IndexSpec()\n                // see run.halo.app.core.attachment.ThumbnailMigration\n                // .setUnique(true)\n                .setName(Thumbnail.ID_INDEX)\n                .setIndexFunc(simpleAttribute(Thumbnail.class, Thumbnail::idIndexFunc)));\n        });\n        schemeManager.register(LocalThumbnail.class, indexSpec -> {\n            // make sure image and size are unique\n            indexSpec.add(new IndexSpec()\n                // see run.halo.app.core.attachment.ThumbnailMigration\n                // .setUnique(true)\n                .setName(LocalThumbnail.UNIQUE_IMAGE_AND_SIZE_INDEX)\n                .setIndexFunc(\n                    simpleAttribute(LocalThumbnail.class, LocalThumbnail::uniqueImageAndSize)\n                )\n            );\n            indexSpec.add(new IndexSpec().setName(\"spec.imageSignature\")\n                .setIndexFunc(simpleAttribute(LocalThumbnail.class,\n                    thumbnail -> thumbnail.getSpec().getImageSignature())\n                )\n            );\n            indexSpec.add(new IndexSpec().setName(\"spec.thumbSignature\").setUnique(true)\n                .setIndexFunc(simpleAttribute(LocalThumbnail.class,\n                    thumbnail -> thumbnail.getSpec().getThumbSignature())\n                )\n            );\n            indexSpec.add(new IndexSpec().setName(\"status.phase\").setIndexFunc(\n                simpleAttribute(LocalThumbnail.class,\n                    thumbnail -> Optional.of(thumbnail.getStatus())\n                        .map(LocalThumbnail.Status::getPhase)\n                        .map(LocalThumbnail.Phase::name)\n                        .orElse(null)\n                )\n            ));\n        });\n        // metrics.halo.run\n        schemeManager.register(Counter.class);\n        // auth.halo.run\n        schemeManager.register(AuthProvider.class);\n        schemeManager.register(UserConnection.class, is -> {\n            is.add(IndexSpecs.<UserConnection, String>single(\"spec.username\", String.class)\n                .indexFunc(connection -> Optional.ofNullable(connection.getSpec())\n                    .map(UserConnectionSpec::getUsername)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<UserConnection, String>single(\"spec.registrationId\", String.class)\n                .indexFunc(connection -> Optional.ofNullable(connection.getSpec())\n                    .map(UserConnectionSpec::getRegistrationId)\n                    .orElse(null)\n                )\n            );\n            is.add(IndexSpecs.<UserConnection, String>single(\"spec.providerUserId\", String.class)\n                .indexFunc(connection -> Optional.ofNullable(connection.getSpec())\n                    .map(UserConnectionSpec::getProviderUserId)\n                    .orElse(null)\n                )\n            );\n        });\n\n        // security.halo.run\n        schemeManager.register(PersonalAccessToken.class);\n        schemeManager.register(RememberMeToken.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<RememberMeToken, String>single(\"spec.series\", String.class)\n                .indexFunc(token -> Optional.ofNullable(token.getSpec())\n                    .map(RememberMeToken.Spec::getSeries)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<RememberMeToken, String>single(\"spec.username\", String.class)\n                .indexFunc(token -> Optional.ofNullable(token.getSpec())\n                    .map(RememberMeToken.Spec::getUsername)\n                    .orElse(null)\n                )\n            );\n        });\n        schemeManager.register(Device.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Device, String>single(\"spec.principalName\", String.class)\n                .indexFunc(device -> Optional.ofNullable(device.getSpec())\n                    .map(Device.Spec::getPrincipalName)\n                    .orElse(null)\n                )\n            );\n        });\n\n        // migration.halo.run\n        schemeManager.register(Backup.class);\n\n        // notification.halo.run\n        schemeManager.register(ReasonType.class);\n        schemeManager.register(Reason.class);\n        schemeManager.register(NotificationTemplate.class, indexSpecs -> {\n            indexSpecs.add(\n                IndexSpecs.<NotificationTemplate, String>single(\"spec.reasonSelector.reasonType\",\n                    String.class).indexFunc(template -> Optional.ofNullable(template.getSpec())\n                    .map(NotificationTemplate.Spec::getReasonSelector)\n                    .map(NotificationTemplate.ReasonSelector::getReasonType)\n                    .orElse(null)\n                )\n            );\n        });\n        schemeManager.register(Subscription.class, indexSpecs -> {\n            indexSpecs.add(\n                IndexSpecs.<Subscription, String>single(\"spec.reason.reasonType\", String.class)\n                    .indexFunc(\n                        sub -> Optional.ofNullable(sub.getSpec()).map(Subscription.Spec::getReason)\n                            .map(InterestReason::getReasonType)\n                            .orElse(null)\n                    )\n            );\n            indexSpecs.add(\n                IndexSpecs.<Subscription, String>single(\"spec.reason.subject\", String.class)\n                    .indexFunc(sub -> Optional.ofNullable(sub.getSpec())\n                        .map(Subscription.Spec::getReason)\n                        .map(InterestReason::getSubject)\n                        .map(Object::toString)\n                        .orElse(null)\n                    )\n            );\n            indexSpecs.add(\n                IndexSpecs.<Subscription, String>single(\"spec.reason.expression\", String.class)\n                    .indexFunc(sub -> Optional.ofNullable(sub.getSpec())\n                        .map(Subscription.Spec::getReason)\n                        .map(InterestReason::getExpression)\n                        .orElse(null)\n                    )\n            );\n            indexSpecs.add(IndexSpecs.<Subscription, String>single(\"spec.subscriber\", String.class)\n                .indexFunc(sub -> Optional.ofNullable(sub.getSpec())\n                    .map(Subscription.Spec::getSubscriber)\n                    .map(Object::toString)\n                    .orElse(null)\n                )\n            );\n        });\n        schemeManager.register(NotifierDescriptor.class);\n        schemeManager.register(Notification.class, indexSpecs -> {\n            indexSpecs.add(IndexSpecs.<Notification, Boolean>single(\"spec.unread\", Boolean.class)\n                .indexFunc(notification -> Optional.ofNullable(notification.getSpec())\n                    .map(NotificationSpec::isUnread)\n                    .orElse(false)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Notification, String>single(\"spec.reason\", String.class)\n                .indexFunc(notification -> Optional.ofNullable(notification.getSpec())\n                    .map(NotificationSpec::getReason)\n                    .orElse(null)\n                )\n            );\n            indexSpecs.add(IndexSpecs.<Notification, String>single(\"spec.recipient\", String.class)\n                .indexFunc(notification -> Optional.ofNullable(notification.getSpec())\n                    .map(NotificationSpec::getRecipient)\n                    .orElse(null)\n                )\n            );\n        });\n    }\n\n    @Override\n    public void stop() {\n        if (!running) {\n            return;\n        }\n        running = false;\n        schemeManager.schemes().forEach(schemeManager::unregister);\n    }\n\n    @Override\n    public boolean isRunning() {\n        return running;\n    }\n\n    @Override\n    public int getPhase() {\n        return InitializationPhase.SCHEME.getPhase();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SecureRequestMappingHandlerAdapter.java",
    "content": "package run.halo.app.infra;\n\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.reactive.HandlerResult;\nimport org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * Secure request mapping handler adapter.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class SecureRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {\n\n    @Override\n    @NonNull\n    public Mono<HandlerResult> handle(\n        @NonNull ServerWebExchange exchange,\n        @NonNull Object handler\n    ) {\n        return super.handle(new SecureServerWebExchange(exchange), handler);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SecureServerRequest.java",
    "content": "package run.halo.app.infra;\n\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.support.ServerRequestWrapper;\nimport org.springframework.web.server.ServerWebExchange;\n\n/**\n * Secure server request without application context available.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class SecureServerRequest extends ServerRequestWrapper {\n\n    /**\n     * Create a new {@code ServerRequestWrapper} that wraps the given request.\n     *\n     * @param delegate the request to wrap\n     */\n    public SecureServerRequest(ServerRequest delegate) {\n        super(delegate);\n    }\n\n    @Override\n    @NonNull\n    public ServerWebExchange exchange() {\n        return new SecureServerWebExchange(super.exchange());\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SecureServerWebExchange.java",
    "content": "package run.halo.app.infra;\n\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.ServerWebExchangeDecorator;\n\n/**\n * Secure server web exchange without application context available.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class SecureServerWebExchange extends ServerWebExchangeDecorator {\n\n    public SecureServerWebExchange(ServerWebExchange delegate) {\n        super(delegate);\n    }\n\n    @Override\n    public ApplicationContext getApplicationContext() {\n        // Always return null to prevent access to application context\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SystemConfigChangedEvent.java",
    "content": "package run.halo.app.infra;\n\nimport java.util.Map;\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * Event that is published when the system configuration changes.\n *\n * @author johnniang\n * @since 2.21.0\n */\npublic class SystemConfigChangedEvent extends ApplicationEvent {\n\n    /**\n     * Old configuration data. Unmodifiable.\n     */\n    @Getter\n    private final Map<String, String> oldData;\n\n    /**\n     * New configuration data. Unmodifiable.\n     */\n    @Getter\n    private final Map<String, String> newData;\n\n    public SystemConfigChangedEvent(\n        Object source,\n        Map<String, String> oldData,\n        Map<String, String> newData\n    ) {\n        super(source);\n        this.oldData = Map.copyOf(oldData);\n        this.newData = Map.copyOf(newData);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SystemConfigFetcher.java",
    "content": "package run.halo.app.infra;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * A fetcher that fetches the system configuration from the extension client.\n * If there are {@link ConfigMap}s named <code>system-default</code> and <code>system</code> at\n * the same time, the {@link ConfigMap} named system will be json merge patch to\n * {@link ConfigMap} named <code>system-default</code>\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface SystemConfigFetcher {\n\n    <T> Mono<T> fetch(String key, Class<T> type);\n\n    Mono<SystemSetting.Basic> getBasic();\n\n    Mono<SystemSetting.Comment> fetchComment();\n\n    Mono<SystemSetting.Post> fetchPost();\n\n    Mono<SystemSetting.ThemeRouteRules> fetchRouteRules();\n\n    /**\n     * Gets the system config values as a map(merged). Do not update this map directly.\n     *\n     * @return system config values map, cached, unmodifiable\n     */\n    Mono<Map<String, String>> getConfig();\n\n    /**\n     * Load the system config map from the extension client.\n     *\n     * @return latest configMap from {@link ReactiveExtensionClient} without any cache.\n     */\n    Mono<ConfigMap> getConfigMap();\n\n    /**\n     * Load the system config map from the extension client in a blocking way.\n     *\n     * @return latest configMap from {@link ReactiveExtensionClient} without any cache.\n     */\n    Optional<ConfigMap> getConfigMapBlocking();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplier.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.time.Duration;\nimport java.util.Optional;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.boot.webflux.autoconfigure.WebFluxProperties;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.http.HttpRequest;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\nimport reactor.core.Exceptions;\nimport run.halo.app.infra.properties.HaloProperties;\n\n/**\n * Default implementation for getting external url from system config first, halo properties second.\n *\n * @author johnniang\n */\n@Slf4j\n@Component\nclass SystemConfigFirstExternalUrlSupplier implements ExternalUrlSupplier {\n\n    private final HaloProperties haloProperties;\n\n    private final WebFluxProperties webFluxProperties;\n\n    private final SystemConfigFetcher systemConfigFetcher;\n\n    @Nullable\n    private URL externalUrl;\n\n    public SystemConfigFirstExternalUrlSupplier(HaloProperties haloProperties,\n        WebFluxProperties webFluxProperties,\n        SystemConfigFetcher systemConfigFetcher) {\n        this.haloProperties = haloProperties;\n        this.webFluxProperties = webFluxProperties;\n        this.systemConfigFetcher = systemConfigFetcher;\n    }\n\n    @EventListener\n    void onExtensionInitialized(ExtensionInitializedEvent ignored) {\n        refetchExternalUrl().ifPresent(externalUrl -> this.externalUrl = externalUrl);\n    }\n\n    @EventListener\n    void onExternalUrlChanged(ExternalUrlChangedEvent event) {\n        this.externalUrl = event.getExternalUrl();\n    }\n\n    Optional<URL> refetchExternalUrl() {\n        return systemConfigFetcher.getBasic()\n            .mapNotNull(SystemSetting.Basic::getExternalUrl)\n            .filter(StringUtils::hasText)\n            .mapNotNull(externalUrlString -> {\n                try {\n                    return URI.create(externalUrlString).toURL();\n                } catch (MalformedURLException e) {\n                    log.error(\"\"\"\n                        Cannot parse external URL {} from system config. Fallback to default \\\n                        external URL supplier from properties.\\\n                        \"\"\", externalUrlString, e);\n                    // For continuing the application startup, we need to return null here.\n                    return null;\n                }\n            })\n            .blockOptional(Duration.ofSeconds(10));\n    }\n\n    @Override\n    public URI get() {\n        try {\n            if (!haloProperties.isUseAbsolutePermalink()) {\n                return URI.create(getBasePath());\n            }\n            if (externalUrl != null) {\n                return externalUrl.toURI();\n            }\n            return haloProperties.getExternalUrl().toURI();\n        } catch (URISyntaxException e) {\n            throw Exceptions.propagate(e);\n        }\n    }\n\n    @Override\n    public URL getURL(HttpRequest request) {\n        if (this.externalUrl != null) {\n            return this.externalUrl;\n        }\n        var externalUrl = haloProperties.getExternalUrl();\n        if (externalUrl != null) {\n            return externalUrl;\n        }\n        try {\n            externalUrl = request.getURI().resolve(getBasePath()).toURL();\n        } catch (MalformedURLException e) {\n            throw new RuntimeException(\"Cannot parse request URI to URL.\", e);\n        }\n        return externalUrl;\n    }\n\n    @Nullable\n    @Override\n    public URL getRaw() {\n        return externalUrl != null ? externalUrl : haloProperties.getExternalUrl();\n    }\n\n    private String getBasePath() {\n        var basePath = webFluxProperties.getBasePath();\n        if (!StringUtils.hasText(basePath)) {\n            basePath = \"/\";\n        }\n        return basePath;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SystemConfigInitializer.java",
    "content": "package run.halo.app.infra;\n\nimport java.util.HashMap;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass SystemConfigInitializer {\n\n    private final ReactiveExtensionClient client;\n\n    @EventListener\n    @Order(Ordered.HIGHEST_PRECEDENCE)\n    Mono<Void> onApplicationEvent(ExtensionInitializedEvent ignored) {\n        return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)\n            .switchIfEmpty(Mono.defer(() -> {\n                log.info(\"Initializing system config...\");\n                var configMap = new ConfigMap();\n                configMap.setMetadata(new Metadata());\n                configMap.getMetadata().setName(SystemSetting.SYSTEM_CONFIG);\n                configMap.setData(new HashMap<>());\n                return client.create(configMap)\n                    .doOnSuccess(created -> {\n                        log.info(\"System config initialized: {}\", created.getMetadata().getName());\n                    });\n            }))\n            .then();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SystemInfoGetterImpl.java",
    "content": "package run.halo.app.infra;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.boot.web.server.autoconfigure.ServerProperties;\nimport org.springframework.boot.webflux.autoconfigure.WebFluxProperties;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\n@Component\n@RequiredArgsConstructor\npublic class SystemInfoGetterImpl implements SystemInfoGetter {\n    private final SystemConfigFetcher environmentFetcher;\n    private final SystemVersionSupplier systemVersionSupplier;\n    private final ExternalUrlSupplier externalUrlSupplier;\n    private final ServerProperties serverProperties;\n    private final WebFluxProperties webFluxProperties;\n\n    @Override\n    public Mono<SystemInfo> get() {\n        var systemInfo = new SystemInfo()\n            .setVersion(systemVersionSupplier.get())\n            .setUrl(getExternalUrl())\n            // TODO populate locale and timezone from system settings in the future\n            .setLocale(Locale.getDefault())\n            .setTimeZone(TimeZone.getDefault());\n\n        var basicMono =\n            environmentFetcher.fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class)\n                .doOnNext(basic -> systemInfo.setTitle(basic.getTitle())\n                    .setSubtitle(basic.getSubtitle())\n                    .setLogo(basic.getLogo())\n                    .setFavicon(basic.getFavicon())\n                );\n\n        var seoMono = environmentFetcher.fetch(SystemSetting.Seo.GROUP, SystemSetting.Seo.class)\n            .doOnNext(seo -> systemInfo.setSeo(new SystemInfo.SeoProp()\n                .setBlockSpiders(BooleanUtils.isTrue(seo.blockSpiders))\n                .setKeywords(seo.getKeywords())\n                .setDescription(seo.getDescription())\n            ));\n\n        var themeMono =\n            environmentFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class)\n                .doOnNext(theme -> systemInfo.setActivatedThemeName(theme.getActive()));\n        return Mono.when(basicMono, seoMono, themeMono)\n            .thenReturn(systemInfo);\n    }\n\n    private URL getExternalUrl() {\n        var url = externalUrlSupplier.getRaw();\n        if (url != null) {\n            return url;\n        }\n        var port = serverProperties.getPort();\n        var basePath = StringUtils.defaultIfBlank(webFluxProperties.getBasePath(), \"/\");\n        try {\n            var uriStr = \"http://localhost:\" + port + basePath;\n            return URI.create(StringUtils.removeEnd(uriStr, \"/\")).toURL();\n        } catch (MalformedURLException e) {\n            // Should not happen\n            throw new RuntimeException(\"Cannot create URL from server properties.\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/SystemState.java",
    "content": "package run.halo.app.infra;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.github.fge.jsonpatch.JsonPatchException;\nimport com.github.fge.jsonpatch.mergepatch.JsonMergePatch;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport lombok.Data;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonParseException;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * A model for system state deserialize from {@link run.halo.app.extension.ConfigMap}\n * named {@code system-states}.\n *\n * @author guqing\n * @since 2.8.0\n */\n@Data\npublic class SystemState {\n    public static final String SYSTEM_STATES_CONFIGMAP = \"system-states\";\n\n    static final String GROUP = \"states\";\n\n    private Boolean isSetup;\n\n    /**\n     * Deserialize from {@link ConfigMap}.\n     *\n     * @return config map\n     */\n    public static SystemState deserialize(@NonNull ConfigMap configMap) {\n        Map<String, String> data = configMap.getData();\n        if (data == null) {\n            return new SystemState();\n        }\n        return JsonUtils.jsonToObject(data.getOrDefault(GROUP, emptyJsonObject()),\n            SystemState.class);\n    }\n\n    /**\n     * Update modified system state to config map.\n     *\n     * @param systemState modified system state\n     * @param configMap config map\n     */\n    public static void update(@NonNull SystemState systemState, @NonNull ConfigMap configMap) {\n        Map<String, String> data = configMap.getData();\n        if (data == null) {\n            data = new LinkedHashMap<>();\n            configMap.setData(data);\n        }\n        JsonNode modifiedJson = JsonUtils.mapper()\n            .convertValue(systemState, JsonNode.class);\n        // original\n        JsonNode sourceJson =\n            JsonUtils.jsonToObject(data.getOrDefault(GROUP, emptyJsonObject()), JsonNode.class);\n        try {\n            // patch\n            JsonMergePatch jsonMergePatch = JsonMergePatch.fromJson(modifiedJson);\n            // apply patch to original\n            JsonNode patchedNode = jsonMergePatch.apply(sourceJson);\n            data.put(GROUP, JsonUtils.objectToJson(patchedNode));\n        } catch (JsonPatchException e) {\n            throw new JsonParseException(e);\n        }\n    }\n\n    /**\n     * <p>Update system state by the given {@link Consumer}.</p>\n     * <p>if the system state config map does not exist, it will create a new one.</p>\n     */\n    public static Mono<Void> upsetSystemState(ReactiveExtensionClient client,\n        Consumer<SystemState> consumer) {\n        return Mono.defer(() -> client.fetch(ConfigMap.class, SYSTEM_STATES_CONFIGMAP)\n                .switchIfEmpty(Mono.defer(() -> {\n                    ConfigMap configMap = new ConfigMap();\n                    configMap.setMetadata(new Metadata());\n                    configMap.getMetadata().setName(SYSTEM_STATES_CONFIGMAP);\n                    configMap.setData(new HashMap<>());\n                    return client.create(configMap);\n                }))\n                .flatMap(configMap -> {\n                    SystemState systemState = deserialize(configMap);\n                    consumer.accept(systemState);\n                    update(systemState, configMap);\n                    return client.update(configMap);\n                })\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance)\n            )\n            .then();\n    }\n\n    private static String emptyJsonObject() {\n        return \"{}\";\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ThemeRootGetter.java",
    "content": "package run.halo.app.infra;\n\nimport java.nio.file.Path;\nimport java.util.function.Supplier;\n\n/**\n * ThemeRootGetter allows us to get root path of themes.\n *\n * @author johnniang\n */\npublic interface ThemeRootGetter extends Supplier<Path> {\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/actuator/DatabaseInfoContributor.java",
    "content": "package run.halo.app.infra.actuator;\n\nimport io.r2dbc.spi.Connection;\nimport io.r2dbc.spi.ConnectionFactory;\nimport io.r2dbc.spi.ConnectionMetadata;\nimport java.time.Duration;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.boot.actuate.info.Info;\nimport org.springframework.boot.actuate.info.InfoContributor;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n@Slf4j\n@Component\nclass DatabaseInfoContributor implements InfoContributor, InitializingBean {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private static final String DATABASE_INFO_KEY = \"database\";\n\n    private final ConnectionFactory connectionFactory;\n\n    @Nullable\n    private ConnectionMetadata connectionMetadata;\n\n    public DatabaseInfoContributor(ConnectionFactory connectionFactory) {\n        this.connectionFactory = connectionFactory;\n    }\n\n    @Override\n    public void afterPropertiesSet() throws Exception {\n        var connectionMetadata = Mono.usingWhen(\n                this.connectionFactory.create(),\n                connection -> Mono.just(connection.getMetadata()),\n                Connection::close\n            )\n            .blockOptional(BLOCKING_TIMEOUT)\n            .orElseThrow(() -> new IllegalStateException(\"Unable to get database metadata\"));\n        if (log.isDebugEnabled()) {\n            log.debug(\"Database Metadata initialized: name={}, version={}\",\n                connectionMetadata.getDatabaseProductName(),\n                connectionMetadata.getDatabaseVersion());\n        }\n        this.connectionMetadata = connectionMetadata;\n    }\n\n    @Override\n    public void contribute(Info.Builder builder) {\n        if (this.connectionMetadata != null) {\n            builder.withDetail(DATABASE_INFO_KEY, Map.of(\n                \"name\", this.connectionMetadata.getDatabaseProductName(),\n                \"version\", this.connectionMetadata.getDatabaseVersion()\n            ));\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/actuator/GlobalInfo.java",
    "content": "package run.halo.app.infra.actuator;\n\nimport java.net.URL;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport lombok.Data;\n\n/**\n * Global info.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Data\npublic class GlobalInfo {\n\n    private URL externalUrl;\n\n    private TimeZone timeZone;\n\n    private Locale locale;\n\n    private boolean allowAnonymousComments;\n\n    private boolean allowRegistration;\n\n    private String favicon;\n\n    private String postSlugGenerationStrategy;\n\n    private Boolean mustVerifyEmailOnRegistration;\n\n    private String siteTitle;\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/actuator/GlobalInfoEndpoint.java",
    "content": "package run.halo.app.infra.actuator;\n\nimport org.springframework.boot.actuate.endpoint.annotation.ReadOperation;\nimport org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\n/**\n * Global info endpoint.\n */\n@WebEndpoint(id = \"globalinfo\")\n@Component\nclass GlobalInfoEndpoint {\n\n    private final GlobalInfoService globalInfoService;\n\n    public GlobalInfoEndpoint(GlobalInfoService globalInfoService) {\n        this.globalInfoService = globalInfoService;\n    }\n\n    @ReadOperation\n    public Mono<GlobalInfo> globalInfo() {\n        return globalInfoService.getGlobalInfo();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/actuator/GlobalInfoService.java",
    "content": "package run.halo.app.infra.actuator;\n\nimport reactor.core.publisher.Mono;\n\n/**\n * Global info service.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface GlobalInfoService {\n\n    /**\n     * Get global info.\n     *\n     * @return global info\n     */\n    Mono<GlobalInfo> getGlobalInfo();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/actuator/GlobalInfoServiceImpl.java",
    "content": "package run.halo.app.infra.actuator;\n\nimport java.util.ArrayList;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.TimeZone;\nimport lombok.RequiredArgsConstructor;\nimport org.reactivestreams.Publisher;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n/**\n * Global info service implementation.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Service\n@RequiredArgsConstructor\npublic class GlobalInfoServiceImpl implements GlobalInfoService {\n\n    private final ObjectProvider<SystemConfigFetcher> systemConfigFetcher;\n\n    private final ExternalUrlSupplier externalUrl;\n\n    @Override\n    public Mono<GlobalInfo> getGlobalInfo() {\n        final var info = new GlobalInfo();\n        info.setExternalUrl(externalUrl.getRaw());\n        info.setLocale(Locale.getDefault());\n        info.setTimeZone(TimeZone.getDefault());\n\n        var publishers = new ArrayList<Publisher<?>>(1);\n        publishers.add(handleSettings(info));\n        return Mono.when(publishers).then(Mono.just(info));\n    }\n\n    private Mono<Void> handleSettings(GlobalInfo info) {\n        return Optional.ofNullable(systemConfigFetcher.getIfUnique())\n            .map(fetcher -> fetcher.getConfig()\n                .doOnNext(config -> {\n                    handleCommentSetting(info, config);\n                    handleUserSetting(info, config);\n                    handleBasicSetting(info, config);\n                    handlePostSlugGenerationStrategy(info, config);\n                })\n                .then()\n            )\n            .orElseGet(Mono::empty);\n    }\n\n    private void handleCommentSetting(GlobalInfo info, Map<String, String> config) {\n        var comment =\n            SystemSetting.get(config, SystemSetting.Comment.GROUP, SystemSetting.Comment.class);\n        if (comment == null) {\n            info.setAllowAnonymousComments(true);\n        } else {\n            info.setAllowAnonymousComments(\n                comment.getSystemUserOnly() == null || !comment.getSystemUserOnly());\n        }\n    }\n\n    private void handleUserSetting(GlobalInfo info, Map<String, String> config) {\n        var userSetting =\n            SystemSetting.get(config, SystemSetting.User.GROUP, SystemSetting.User.class);\n        if (userSetting == null) {\n            info.setAllowRegistration(false);\n            info.setMustVerifyEmailOnRegistration(false);\n        } else {\n            info.setAllowRegistration(userSetting.isAllowRegistration());\n            info.setMustVerifyEmailOnRegistration(userSetting.isMustVerifyEmailOnRegistration());\n        }\n    }\n\n    private void handlePostSlugGenerationStrategy(GlobalInfo info, Map<String, String> config) {\n        var post = SystemSetting.get(config, SystemSetting.Post.GROUP, SystemSetting.Post.class);\n        if (post != null) {\n            info.setPostSlugGenerationStrategy(post.getSlugGenerationStrategy());\n        }\n    }\n\n    private void handleBasicSetting(GlobalInfo info, Map<String, String> config) {\n        var basic = SystemSetting.get(config, SystemSetting.Basic.GROUP, SystemSetting.Basic.class);\n        if (basic != null) {\n            info.setFavicon(basic.getFavicon());\n            info.setSiteTitle(basic.getTitle());\n            basic.useSystemLocale().ifPresent(info::setLocale);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/actuator/RestartEndpoint.java",
    "content": "package run.halo.app.infra.actuator;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.actuate.endpoint.annotation.WriteOperation;\nimport org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;\nimport org.springframework.boot.context.event.ApplicationStartedEvent;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.Application;\n\n@WebEndpoint(id = \"restart\")\n@Component\n@Slf4j\nclass RestartEndpoint implements ApplicationListener<ApplicationStartedEvent> {\n\n    private SpringApplication application;\n\n    private String[] args;\n\n    private ConfigurableApplicationContext context;\n\n    @WriteOperation\n    public Mono<Map<String, String>> restart() {\n        return Mono.fromSupplier(() -> {\n            var threadGroup = new ThreadGroup(\"RestartGroup\");\n            var thread = new Thread(threadGroup, this::doRestart, \"restartMain\");\n            thread.setDaemon(false);\n            thread.setContextClassLoader(Application.class.getClassLoader());\n            thread.start();\n            return Map.of(\"message\", \"Restarting\");\n        });\n    }\n\n    private synchronized void doRestart() {\n        log.info(\"Restarting...\");\n        if (this.context != null) {\n            try {\n                closeRecursively(this.context);\n                var shutdownHandlers = SpringApplication.getShutdownHandlers();\n                if (shutdownHandlers instanceof Runnable runnable) {\n                    // clear closedContext in org.springframework.boot.SpringApplicationShutdownHook\n                    runnable.run();\n                }\n                this.context = this.application.run(args);\n                log.info(\"Restarted\");\n            } catch (Throwable t) {\n                log.error(\"Failed to restart.\", t);\n            }\n        }\n    }\n\n    private static void closeRecursively(ApplicationContext ctx) {\n        while (ctx != null) {\n            if (ctx instanceof Closeable closeable) {\n                try {\n                    closeable.close();\n                } catch (IOException e) {\n                    log.error(\"Cannot close context: {}\", ctx.getId(), e);\n                }\n            }\n            ctx = ctx.getParent();\n        }\n    }\n\n    @Override\n    public void onApplicationEvent(ApplicationStartedEvent event) {\n        if (this.context == null) {\n            this.application = event.getSpringApplication();\n            this.args = event.getArgs();\n            this.context = event.getApplicationContext();\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/ExtensionConfiguration.java",
    "content": "package run.halo.app.infra.config;\n\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.controller.DefaultControllerManager;\n\n@Configuration(proxyBeanMethods = false)\npublic class ExtensionConfiguration {\n\n    @Bean\n    @ConditionalOnProperty(\n        name = \"halo.extension.controller.disabled\",\n        havingValue = \"false\",\n        matchIfMissing = true\n    )\n    DefaultControllerManager controllerManager(ExtensionClient client) {\n        return new DefaultControllerManager(client);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/HaloConfiguration.java",
    "content": "package run.halo.app.infra.config;\n\nimport static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.IOException;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;\nimport org.springframework.cache.annotation.EnableCaching;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.scheduling.annotation.EnableAsync;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.search.lucene.LuceneSearchEngine;\nimport tools.jackson.databind.MapperFeature;\n\n@EnableCaching\n@Configuration(proxyBeanMethods = false)\n@EnableAsync\npublic class HaloConfiguration {\n\n    @Bean\n    JsonMapperBuilderCustomizer objectMapperCustomizer(\n        ObjectProvider<ObjectMapper> objectMapperProvider\n    ) {\n        return builder -> {\n            builder.changeDefaultPropertyInclusion(v ->\n                v.withValueInclusion(NON_NULL).withContentInclusion(NON_NULL)\n            );\n            builder.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);\n            builder.addModule(\n                new JacksonAdapterModule(objectMapperProvider::getIfAvailable)\n            );\n        };\n    }\n\n    @Bean\n    @ConditionalOnProperty(prefix = \"halo.search-engine.lucene\", name = \"enabled\",\n        havingValue = \"true\",\n        matchIfMissing = true)\n    LuceneSearchEngine luceneSearchEngine(HaloProperties haloProperties) throws IOException {\n        return new LuceneSearchEngine(haloProperties.getWorkDir()\n            .resolve(\"indices\")\n            .resolve(\"halo\"));\n    }\n}"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/JacksonAdapterModule.java",
    "content": "package run.halo.app.infra.config;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.github.fge.jsonpatch.JsonPatch;\nimport java.util.function.Supplier;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.JsonExtension;\nimport tools.jackson.core.JacksonException;\nimport tools.jackson.core.JsonGenerator;\nimport tools.jackson.core.JsonParser;\nimport tools.jackson.core.Version;\nimport tools.jackson.databind.DeserializationContext;\nimport tools.jackson.databind.SerializationContext;\nimport tools.jackson.databind.ValueDeserializer;\nimport tools.jackson.databind.ValueSerializer;\nimport tools.jackson.databind.exc.InvalidFormatException;\nimport tools.jackson.databind.exc.JsonNodeException;\nimport tools.jackson.databind.module.SimpleModule;\n\n/**\n * Jackson module to adapt legacy {@link JsonNode} serialization and deserialization.\n * This module makes sure the plugin system using legacy Jackson can correctly\n *\n * @author johnniang\n * @since 2.23.0\n */\npublic class JacksonAdapterModule extends SimpleModule {\n\n    private final Supplier<ObjectMapper> objectMapper;\n\n    public JacksonAdapterModule(Supplier<ObjectMapper> objectMapper) {\n        super(JacksonAdapterModule.class.getSimpleName(), new Version(1, 0, 0, null, null, null));\n        this.objectMapper = objectMapper;\n        addSerializer(new JsonNodeSerializer());\n        addSerializer(new JsonPatchSerializer());\n        addSerializer(new JsonExtensionSerializer());\n\n        addDeserializer(JsonNode.class, new JsonNodeDeserializer<>(JsonNode.class));\n        addDeserializer(ObjectNode.class, new JsonNodeDeserializer<>(ObjectNode.class));\n        addDeserializer(JsonExtension.class, new JsonExtensionDeSerializer());\n    }\n\n    class JsonExtensionDeSerializer extends ValueDeserializer<JsonExtension> {\n\n        @Override\n        public JsonExtension deserialize(JsonParser p,\n            DeserializationContext ctxt) throws JacksonException {\n            var json = p.readValueAsTree().toString();\n            try {\n                return objectMapper.get().readValue(json, JsonExtension.class);\n            } catch (JsonProcessingException e) {\n                throw InvalidFormatException.from(p, \"Failed to deserialize JsonExtension\");\n            }\n        }\n\n    }\n\n    class JsonExtensionSerializer extends ValueSerializer<JsonExtension> {\n\n        @Override\n        public void serialize(JsonExtension value, JsonGenerator gen,\n            SerializationContext ctxt) throws JacksonException {\n            try {\n                var raw = objectMapper.get().writeValueAsString(value);\n                gen.writeRawValue(raw);\n            } catch (JsonProcessingException e) {\n                throw InvalidFormatException.from(gen, \"Failed to serialize JsonExtension\");\n            }\n        }\n\n        @Override\n        public Class<?> handledType() {\n            return JsonExtension.class;\n        }\n    }\n\n    class JsonPatchSerializer extends ValueSerializer<JsonPatch> {\n\n        @Override\n        public void serialize(JsonPatch value, JsonGenerator gen,\n            SerializationContext ctxt)\n            throws JacksonException {\n            try {\n                gen.writeRawValue(objectMapper.get().writeValueAsString(value));\n            } catch (JsonProcessingException e) {\n                throw InvalidFormatException.from(gen, \"Failed to serialize JsonPatch\");\n            }\n        }\n\n        @Override\n        public Class<?> handledType() {\n            return JsonPatch.class;\n        }\n\n    }\n\n    class JsonNodeSerializer extends ValueSerializer<JsonNode> {\n\n        @Override\n        public void serialize(JsonNode value, JsonGenerator gen,\n            SerializationContext ctxt)\n            throws JacksonException {\n            try {\n                gen.writeRawValue(objectMapper.get().writeValueAsString(value));\n            } catch (JsonProcessingException e) {\n                throw InvalidFormatException.from(gen, \"Failed to serialize JsonNode\");\n            }\n        }\n\n        @Override\n        public Class<?> handledType() {\n            return JsonNode.class;\n        }\n\n    }\n\n    class JsonNodeDeserializer<T extends JsonNode>\n        extends ValueDeserializer<T> {\n\n        private final Class<T> type;\n\n        JsonNodeDeserializer(Class<T> type) {\n            this.type = type;\n        }\n\n        @Override\n        public T deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {\n            var json = p.readValueAsTree().toString();\n            var mapper = objectMapper.get();\n            Assert.notNull(mapper, \"Legacy ObjectMapper must not be null\");\n            try {\n                return mapper.readValue(json, type);\n            } catch (JsonProcessingException e) {\n                throw JsonNodeException.from(\n                    p, \"Failed to bridge legacy JSON node\", e\n                );\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/R2dbcConfiguration.java",
    "content": "package run.halo.app.infra.config;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.config.BeanPostProcessor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.r2dbc.mapping.R2dbcMappingContext;\n\n@Configuration(proxyBeanMethods = false)\nclass R2dbcConfiguration {\n\n    /**\n     * Modify R2DBC Mapping Context to disable force quote.\n     *\n     * <p>\n     * In Spring Boot 4, the default\n     * behavior is changed to enable force quote, which may cause issues with existing database\n     * schemas that do not use quoted identifiers.\n     *\n     * <p>\n     * See\n     * <a href=\"https://github.com/spring-projects/spring-data-relational/issues/1993\">this issue\n     * </a> for more details.\n     *\n     * <p>\n     * Use static method to ensure that the BeanPostProcessor is registered before any\n     * R2dbcMappingContext beans are initialized.\n     *\n     * @return the bean post processor\n     */\n    @Bean\n    static BeanPostProcessor r2dbcMappingContextQuoteModifier() {\n        return new BeanPostProcessor() {\n            @Override\n            public Object postProcessBeforeInitialization(Object bean, String beanName)\n                throws BeansException {\n                if (bean instanceof R2dbcMappingContext mappingContext) {\n                    mappingContext.setForceQuote(false);\n                }\n                return bean;\n            }\n        };\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/SessionConfiguration.java",
    "content": "package run.halo.app.infra.config;\n\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.session.autoconfigure.SessionProperties;\nimport org.springframework.boot.web.server.autoconfigure.ServerProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.session.ReactiveFindByIndexNameSessionRepository;\nimport org.springframework.session.ReactiveSessionRepository;\nimport org.springframework.session.Session;\nimport org.springframework.session.config.annotation.web.server.EnableSpringWebSession;\nimport org.springframework.session.security.SpringSessionBackedReactiveSessionRegistry;\nimport run.halo.app.security.session.InMemoryReactiveIndexedSessionRepository;\nimport run.halo.app.security.session.ReactiveIndexedSessionRepository;\n\n/**\n * Configuration for Spring Web Session.\n *\n * @param <S> the type of Session\n * @author johnniang\n * @since 2.22.11\n */\n@Configuration\n@EnableSpringWebSession\nclass SessionConfiguration<S extends Session> {\n\n    @Bean\n    SpringSessionBackedReactiveSessionRegistry<S> reactiveSessionRegistry(\n        ReactiveSessionRepository<S> sessionRepository,\n        ReactiveFindByIndexNameSessionRepository<S> indexedSessionRepository\n    ) {\n        return new SpringSessionBackedReactiveSessionRegistry<>(\n            sessionRepository, indexedSessionRepository\n        );\n    }\n\n    @Configuration\n    @ConditionalOnProperty(\n        value = \"halo.session.store-type\", havingValue = \"in-memory\", matchIfMissing = true\n    )\n    static class InMemorySessionConfig {\n\n        @Bean\n        ReactiveIndexedSessionRepository<? extends Session> inMemorySessionRepository(\n            SessionProperties sessionProperties, ServerProperties serverProperties\n        ) {\n            var repository =\n                new InMemoryReactiveIndexedSessionRepository(new ConcurrentHashMap<>());\n            var timeout = Optional.ofNullable(sessionProperties.getTimeout())\n                .orElseGet(() -> serverProperties.getReactive().getSession().getTimeout());\n            repository.setDefaultMaxInactiveInterval(timeout);\n            return repository;\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/SwaggerConfig.java",
    "content": "package run.halo.app.infra.config;\n\nimport static org.springdoc.core.utils.Constants.SPRINGDOC_ENABLED;\n\nimport io.swagger.v3.core.converter.ModelConverter;\nimport io.swagger.v3.core.jackson.ModelResolver;\nimport io.swagger.v3.core.jackson.TypeNameResolver;\nimport io.swagger.v3.oas.models.Components;\nimport io.swagger.v3.oas.models.OpenAPI;\nimport io.swagger.v3.oas.models.info.Info;\nimport io.swagger.v3.oas.models.security.SecurityRequirement;\nimport io.swagger.v3.oas.models.security.SecurityScheme;\nimport java.util.Set;\nimport org.springdoc.core.customizers.GlobalOpenApiCustomizer;\nimport org.springdoc.core.models.GroupedOpenApi;\nimport org.springdoc.core.properties.SpringDocConfigProperties;\nimport org.springdoc.core.providers.ObjectMapperProvider;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;\nimport org.springframework.boot.info.BuildProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport run.halo.app.extension.router.JsonPatch;\n\n@Configuration\n@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)\n@ConditionalOnWebApplication\npublic class SwaggerConfig {\n\n    @Bean\n    OpenAPI haloOpenApi(ObjectProvider<BuildProperties> buildPropertiesProvider,\n        SpringDocConfigProperties docConfigProperties) {\n        var buildProperties = buildPropertiesProvider.getIfAvailable();\n        var version = \"unknown\";\n        if (buildProperties != null) {\n            version = buildProperties.getVersion();\n        }\n        return new OpenAPI()\n            .specVersion(docConfigProperties.getSpecVersion())\n            // See https://swagger.io/docs/specification/authentication/ for more.\n            .components(new Components()\n                .addSecuritySchemes(\"basicAuth\", new SecurityScheme()\n                    .type(SecurityScheme.Type.HTTP)\n                    .scheme(\"basic\"))\n                .addSecuritySchemes(\"bearerAuth\", new SecurityScheme()\n                    .type(SecurityScheme.Type.HTTP)\n                    .scheme(\"bearer\")\n                    .bearerFormat(\"JWT\"))\n            )\n            .addSecurityItem(new SecurityRequirement()\n                .addList(\"basicAuth\")\n                .addList(\"bearerAuth\"))\n            .info(new Info()\n                .title(\"Halo\")\n                .version(version)\n            );\n    }\n\n    @Bean\n    GlobalOpenApiCustomizer openApiCustomizer() {\n        return openApi -> JsonPatch.addSchema(openApi.getComponents());\n    }\n\n    @Bean\n    GroupedOpenApi aggregatedV1alpha1Api() {\n        return GroupedOpenApi.builder()\n            .group(\"apis_aggregated.api_v1alpha1\")\n            .displayName(\"Aggregated API V1alpha1\")\n            .pathsToMatch(\n                \"/apis/*/v1alpha1/**\",\n                \"/api/v1alpha1/**\",\n                \"/login/**\",\n                \"/system/setup\"\n            )\n            .build();\n    }\n\n    @Bean\n    GroupedOpenApi publicV1alpha1Api() {\n        return GroupedOpenApi.builder()\n            .group(\"apis_public.api_v1alpha1\")\n            .displayName(\"Public API V1alpha1\")\n            .pathsToMatch(\n                \"/apis/api.*/**\"\n            )\n            .pathsToExclude(\n                \"/apis/api.console.*/v1alpha1/**\",\n                // compatible with legacy issues\n                \"/apis/api.notification.halo.run/v1alpha1/userspaces/**\",\n                \"/apis/api.notification.halo.run/v1alpha1/notifiers/**\"\n            )\n            .build();\n    }\n\n    @Bean\n    GroupedOpenApi consoleV1alpha1Api() {\n        return GroupedOpenApi.builder()\n            .group(\"apis_console.api_v1alpha1\")\n            .displayName(\"Console API V1alpha1\")\n            .pathsToMatch(\n                \"/apis/console.api.*/v1alpha1/**\",\n                \"/apis/api.console.halo.run/v1alpha1/**\"\n            )\n            .build();\n    }\n\n\n    @Bean\n    GroupedOpenApi ucV1alpha1Api() {\n        return GroupedOpenApi.builder()\n            .group(\"apis_uc.api_v1alpha1\")\n            .displayName(\"User-center API V1alpha1\")\n            .pathsToMatch(\n                \"/apis/uc.api.*/v1alpha1/**\",\n                // compatible with legacy issues\n                \"/apis/api.notification.halo.run/v1alpha1/userspaces/**\",\n                \"/apis/api.notification.halo.run/v1alpha1/notifiers/**\"\n            )\n            .build();\n    }\n\n\n    @Bean\n    GroupedOpenApi extensionV1alpha1Api() {\n        return GroupedOpenApi.builder()\n            .group(\"apis_extension.api_v1alpha1\")\n            .displayName(\"Extension API V1alpha1\")\n            .pathsToMatch(\n                \"/api/v1alpha1/**\",\n                \"/apis/content.halo.run/v1alpha1/**\",\n                \"/apis/theme.halo.run/v1alpha1/**\",\n                \"/apis/security.halo.run/v1alpha1/**\",\n                \"/apis/migration.halo.run/v1alpha1/**\",\n                \"/apis/auth.halo.run/v1alpha1/**\",\n                \"/apis/metrics.halo.run/v1alpha1/**\",\n                \"/apis/storage.halo.run/v1alpha1/**\",\n                \"/apis/plugin.halo.run/v1alpha1/**\",\n                \"/apis/notification.halo.run/**\",\n                \"/apis/migration.halo.run/**\"\n            )\n            .build();\n    }\n\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE)\n    ModelConverter customModelConverter(ObjectMapperProvider objectMapperProvider) {\n        return new ModelResolver(objectMapperProvider.jsonMapper(), new CustomTypeNameResolver());\n    }\n\n    static class CustomTypeNameResolver extends TypeNameResolver {\n        @Override\n        protected String nameForClass(Class<?> cls, Set<Options> options) {\n            // Obey the rule of keys that match the regular expression ^[a-zA-Z0-9\\.\\-_]+$.\n            // See https://spec.openapis.org/oas/v3.0.3#fixed-fields-5 for more.\n            return super.nameForClass(cls, options).replaceAll(\"\\\\$\", \".\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/WebFluxConfig.java",
    "content": "package run.halo.app.infra.config;\n\nimport static org.springframework.util.ResourceUtils.FILE_URL_PREFIX;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\nimport static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;\n\nimport java.util.List;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.boot.webflux.autoconfigure.WebFluxRegistrations;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.CacheControl;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.CodecConfigurer;\nimport org.springframework.http.codec.HttpMessageWriter;\nimport org.springframework.http.codec.ServerCodecConfigurer;\nimport org.springframework.http.codec.json.JacksonJsonDecoder;\nimport org.springframework.http.codec.json.JacksonJsonEncoder;\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;\nimport org.springframework.web.filter.reactive.UrlHandlerFilter;\nimport org.springframework.web.reactive.config.ResourceHandlerRegistration;\nimport org.springframework.web.reactive.config.ResourceHandlerRegistry;\nimport org.springframework.web.reactive.config.WebFluxConfigurer;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.reactive.resource.EncodedResourceResolver;\nimport org.springframework.web.reactive.resource.PathResourceResolver;\nimport org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;\nimport org.springframework.web.reactive.result.view.ViewResolutionResultHandler;\nimport org.springframework.web.reactive.result.view.ViewResolver;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.core.attachment.thumbnail.LocalThumbnailService;\nimport run.halo.app.core.attachment.thumbnail.ThumbnailResourceTransformer;\nimport run.halo.app.core.endpoint.WebSocketHandlerMapping;\nimport run.halo.app.core.endpoint.console.CustomEndpointsBuilder;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.infra.SecureRequestMappingHandlerAdapter;\nimport run.halo.app.infra.properties.AttachmentProperties;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.ui.ProxyFilter;\nimport run.halo.app.infra.ui.WebSocketRequestPredicate;\nimport run.halo.app.infra.webfilter.AdditionalWebFilterChainProxy;\nimport run.halo.app.infra.webfilter.LocaleChangeWebFilter;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.UserLocaleRequestAttributeWriteFilter;\nimport tools.jackson.databind.json.JsonMapper;\n\n@Configuration\n@RequiredArgsConstructor\npublic class WebFluxConfig implements WebFluxConfigurer {\n\n    private final JsonMapper jsonMapper;\n\n    private final HaloProperties haloProp;\n\n    private final WebProperties webProperties;\n\n    private final ApplicationContext applicationContext;\n\n    private final LocalThumbnailService localThumbnailService;\n\n    private final AttachmentRootGetter attachmentRootGetter;\n\n    @Bean\n    WebFluxRegistrations webFluxRegistrations() {\n        return new WebFluxRegistrations() {\n            @Override\n            public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {\n                // Because we have no chance to customize ServerWebExchangeMethodArgumentResolver,\n                // we have to use SecureRequestMappingHandlerAdapter to replace a secure\n                // ServerWebExchange.\n                return new SecureRequestMappingHandlerAdapter();\n            }\n        };\n    }\n\n    @Bean\n    ServerResponse.Context context(CodecConfigurer codec,\n        ViewResolutionResultHandler resultHandler) {\n        return new ServerResponse.Context() {\n            @Override\n            @NonNull\n            public List<HttpMessageWriter<?>> messageWriters() {\n                return codec.getWriters();\n            }\n\n            @Override\n            @NonNull\n            public List<ViewResolver> viewResolvers() {\n                return resultHandler.getViewResolvers();\n            }\n        };\n    }\n\n    @Override\n    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {\n        // we need to customize the Jackson2Json[Decoder][Encoder] here to serialize and\n        // deserialize special types, e.g.: Instant, LocalDateTime. So we use ObjectMapper\n        // created by outside.\n        configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(jsonMapper));\n        configurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(jsonMapper));\n    }\n\n    @Bean\n    RouterFunction<ServerResponse> customEndpoints(ApplicationContext context) {\n        var builder = new CustomEndpointsBuilder();\n        context.getBeansOfType(CustomEndpoint.class).values().forEach(builder::add);\n        return builder.build();\n    }\n\n    @Bean\n    public WebSocketHandlerMapping webSocketHandlerMapping() {\n        WebSocketHandlerMapping handlerMapping = new WebSocketHandlerMapping();\n        handlerMapping.setOrder(-2);\n        return handlerMapping;\n    }\n\n    @Bean\n    RouterFunction<ServerResponse> uiPageEndpoints() {\n        var consolePagePredicate = path(\"/console/**\")\n            .and(accept(MediaType.TEXT_HTML))\n            .and(new WebSocketRequestPredicate().negate());\n\n        var ucPagePredicate = path(\"/uc/**\")\n            .and(accept(MediaType.TEXT_HTML))\n            .and(new WebSocketRequestPredicate().negate());\n\n        var consolePageHtml = applicationContext.getResource(\"classpath:/ui/console.html\");\n\n        var ucPageHtml = applicationContext.getResource(\"classpath:/ui/uc.html\");\n\n        return RouterFunctions.route()\n            .GET(consolePagePredicate,\n                request -> ServerResponse.ok()\n                    .cacheControl(CacheControl.noStore())\n                    .bodyValue(consolePageHtml)\n            )\n            .GET(ucPagePredicate,\n                request -> ServerResponse.ok()\n                    .cacheControl(CacheControl.noStore())\n                    .bodyValue(ucPageHtml)\n            )\n            .build();\n    }\n\n    @Override\n    public void addResourceHandlers(ResourceHandlerRegistry registry) {\n        var attachmentsRoot = attachmentRootGetter.get();\n        var resourceProperties = webProperties.getResources();\n        var cacheControl = resourceProperties.getCache()\n            .getCachecontrol()\n            .toHttpCacheControl();\n        if (cacheControl == null) {\n            cacheControl = CacheControl.empty();\n        }\n        final var useLastModified = resourceProperties.getCache().isUseLastModified();\n\n        // Mandatory resource mapping\n        var uploadRegistration = registry.addResourceHandler(\"/upload/**\")\n            .addResourceLocations(FILE_URL_PREFIX + attachmentsRoot.resolve(\"upload\") + \"/\")\n            .setUseLastModified(useLastModified)\n            .setCacheControl(cacheControl);\n\n        registry.addResourceHandler(\"/ui-assets/**\")\n            .addResourceLocations(\"classpath:/ui/ui-assets/\")\n            .setCacheControl(cacheControl)\n            .setUseLastModified(useLastModified)\n            .resourceChain(true)\n            .addResolver(new EncodedResourceResolver())\n            .addResolver(new PathResourceResolver());\n\n        // Additional resource mappings\n        var staticResources = haloProp.getAttachment().getResourceMappings();\n        for (AttachmentProperties.ResourceMapping staticResource : staticResources) {\n            ResourceHandlerRegistration registration;\n            if (Objects.equals(staticResource.getPathPattern(), \"/upload/**\")) {\n                registration = uploadRegistration;\n            } else {\n                registration = registry.addResourceHandler(staticResource.getPathPattern())\n                    .setCacheControl(cacheControl)\n                    .setUseLastModified(useLastModified);\n            }\n            for (String location : staticResource.getLocations()) {\n                var path = attachmentsRoot.resolve(location);\n                checkDirectoryTraversal(attachmentsRoot, path);\n                registration.addResourceLocations(FILE_URL_PREFIX + path + \"/\");\n            }\n            if (registration != uploadRegistration) {\n                applyThumbnailChain(registration);\n            }\n        }\n        applyThumbnailChain(uploadRegistration);\n\n        var haloStaticPath = haloProp.getWorkDir().resolve(\"static\");\n        registry.addResourceHandler(\"/**\")\n            .addResourceLocations(FILE_URL_PREFIX + haloStaticPath + \"/\")\n            .addResourceLocations(resourceProperties.getStaticLocations())\n            .setCacheControl(cacheControl)\n            .setUseLastModified(useLastModified)\n            .resourceChain(true)\n            .addResolver(new EncodedResourceResolver())\n            .addResolver(new PathResourceResolver());\n    }\n\n    private void applyThumbnailChain(ResourceHandlerRegistration registration) {\n        registration.resourceChain(false)\n            .addTransformer(\n                new ThumbnailResourceTransformer(localThumbnailService)\n            );\n    }\n\n    /**\n     * Order of this filter is higher than\n     * {@link LocaleChangeWebFilter} to allow change locale in dev\n     * mode.\n     * {@link UserLocaleRequestAttributeWriteFilter} is before {@link LocaleChangeWebFilter} to\n     * obtain the locale\n     */\n    @ConditionalOnProperty(name = \"halo.ui.proxy.enabled\", havingValue = \"true\")\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE + 2)\n    ProxyFilter uiProxyFilter() {\n        return new ProxyFilter(haloProp.getUi().getProxy(), \"/console/**\", \"/uc/**\");\n    }\n\n    /**\n     * Create a WebFilterChainProxy for all AdditionalWebFilters.\n     *\n     * <p>The reason why the order is -101 is that the current\n     * AdditionalWebFilterChainProxy should be executed before WebFilterChainProxy\n     * and the order of WebFilterChainProxy is -100.\n     *\n     * <p>See {@code org.springframework.security.config.annotation.web.reactive\n     * .WebFluxSecurityConfiguration#WEB_FILTER_CHAIN_FILTER_ORDER} for more\n     *\n     * @param extensionGetter extension getter.\n     * @return additional web filter chain proxy.\n     */\n    @Bean\n    @Order(-101)\n    AdditionalWebFilterChainProxy additionalWebFilterChainProxy(ExtensionGetter extensionGetter) {\n        return new AdditionalWebFilterChainProxy(extensionGetter);\n    }\n\n    @Bean\n    // We expect this filter to be executed before AdditionalWebFilterChainProxy\n    @Order(-102)\n    ServerWebExchangeContextFilter serverWebExchangeContextFilter() {\n        return new ServerWebExchangeContextFilter();\n    }\n\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE)\n    UrlHandlerFilter urlHandlerFilter() {\n        return UrlHandlerFilter\n            .trailingSlashHandler(\"/**\").mutateRequest()\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java",
    "content": "package run.halo.app.infra.config;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;\n\nimport java.util.HashMap;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;\nimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.crypto.argon2.Argon2PasswordEncoder;\nimport org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;\nimport org.springframework.security.crypto.password.DelegatingPasswordEncoder;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.security.web.server.SecurityWebFilterChain;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.security.DefaultUserDetailService;\nimport run.halo.app.security.HaloServerRequestCache;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authentication.SecurityConfigurer;\nimport run.halo.app.security.authentication.impl.RsaKeyService;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\n/**\n * Security configuration for WebFlux.\n *\n * @author johnniang\n */\n@Configuration\n@EnableWebFluxSecurity\n@EnableReactiveMethodSecurity\n@RequiredArgsConstructor\npublic class WebServerSecurityConfig {\n\n    @Bean\n    SecurityWebFilterChain filterChain(ServerHttpSecurity http,\n        ObjectProvider<SecurityConfigurer> securityConfigurers,\n        ServerSecurityContextRepository securityContextRepository,\n        HaloProperties haloProperties,\n        ServerRequestCache serverRequestCache) {\n\n        var pathMatcher = pathMatchers(\"/**\");\n        var staticResourcesMatcher = pathMatchers(HttpMethod.GET,\n            \"/console/assets/**\",\n            \"/uc/assets/**\",\n            \"/themes/{themeName}/assets/{*resourcePaths}\",\n            \"/plugins/{pluginName}/assets/**\",\n            \"/webjars/**\",\n            \"/js/**\",\n            \"/styles/**\",\n            \"/halo-tracker.js\",\n            \"/images/**\"\n        );\n\n        var securityMatcher = new AndServerWebExchangeMatcher(pathMatcher,\n            new NegatedServerWebExchangeMatcher(staticResourcesMatcher));\n\n        http.securityMatcher(securityMatcher)\n            .anonymous(spec -> {\n                spec.authorities(AuthorityUtils.ROLE_PREFIX + AnonymousUserConst.Role);\n                spec.principal(AnonymousUserConst.PRINCIPAL);\n            })\n            .securityContextRepository(securityContextRepository)\n            .httpBasic(basic -> {\n                if (haloProperties.getSecurity().getBasicAuth().isDisabled()) {\n                    basic.disable();\n                }\n            })\n            .headers(headerSpec -> headerSpec\n                .frameOptions(frameSpec -> {\n                    var frameOptions = haloProperties.getSecurity().getFrameOptions();\n                    frameSpec.mode(frameOptions.getMode());\n                    if (frameOptions.isDisabled()) {\n                        frameSpec.disable();\n                    }\n                })\n                .referrerPolicy(referrerPolicySpec -> referrerPolicySpec.policy(\n                    haloProperties.getSecurity().getReferrerOptions().getPolicy())\n                )\n                .hsts(hstsSpec -> hstsSpec.includeSubdomains(false))\n            )\n            .requestCache(spec -> spec.requestCache(serverRequestCache));\n\n        // Integrate with other configurers separately\n        securityConfigurers.orderedStream()\n            .forEach(securityConfigurer -> securityConfigurer.configure(http));\n        return http.build();\n    }\n\n    @Bean\n    ServerRequestCache serverRequestCache() {\n        return new HaloServerRequestCache();\n    }\n\n    @Bean\n    ServerSecurityContextRepository securityContextRepository() {\n        return new WebSessionServerSecurityContextRepository();\n    }\n\n    @Bean\n    DefaultUserDetailService userDetailsService(UserService userService,\n        RoleService roleService,\n        HaloProperties haloProperties) {\n        var userDetailService = new DefaultUserDetailService(userService, roleService);\n        var twoFactorAuthDisabled = haloProperties.getSecurity().getTwoFactorAuth().isDisabled();\n        userDetailService.setTwoFactorAuthDisabled(twoFactorAuthDisabled);\n        return userDetailService;\n    }\n\n    @Bean\n    @SuppressWarnings(\"deprecation\")\n    PasswordEncoder passwordEncoder() {\n        // For removing the length limit of password, we have to create an argon2 password encoder\n        // as default encoder.\n        // When https://github.com/spring-projects/spring-security/issues/16879 resolved,\n        // we can remove this code.\n        var encodingId = \"argon2@SpringSecurity_v5_8\";\n        var encoders = new HashMap<String, PasswordEncoder>();\n        encoders.put(encodingId, Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());\n        encoders.put(\"bcrypt\", new BCryptPasswordEncoder());\n        return new DelegatingPasswordEncoder(encodingId, encoders);\n    }\n\n    @Bean\n    CryptoService cryptoService(HaloProperties haloProperties) {\n        return new RsaKeyService(haloProperties.getWorkDir().resolve(\"keys\"));\n    }\n\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/AccessDeniedException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\n/**\n * AccessDeniedException will resolve i18n message and response 403 status.\n *\n * @author johnniang\n */\npublic class AccessDeniedException extends ResponseStatusException {\n\n    public AccessDeniedException() {\n        this(\"Access to the resource is forbidden\");\n    }\n\n    public AccessDeniedException(String reason) {\n        this(reason, null, null);\n    }\n\n    public AccessDeniedException(String reason, String detailCode, Object[] detailArgs) {\n        super(HttpStatus.FORBIDDEN, reason, null, detailCode, detailArgs);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/AttachmentAlreadyExistsException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * AttachmentAlreadyExistsException accepts filename parameter as detail message arguments.\n *\n * @author johnniang\n */\npublic class AttachmentAlreadyExistsException extends ServerWebInputException {\n\n    public AttachmentAlreadyExistsException(String filename) {\n        super(\"File \" + filename + \" already exists.\", null, null, null, new Object[] {filename});\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/DuplicateNameException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\npublic class DuplicateNameException extends ResponseStatusException {\n\n    public DuplicateNameException() {\n        this(\"Duplicate name detected\");\n    }\n\n    public DuplicateNameException(String reason) {\n        this(reason, null);\n    }\n\n    public DuplicateNameException(String reason, Throwable cause) {\n        this(reason, cause, null, null);\n    }\n\n    public DuplicateNameException(String reason, Throwable cause, String messageDetailCode,\n        Object[] messageDetailArguments) {\n        super(HttpStatus.BAD_REQUEST, reason, cause, messageDetailCode, messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/EmailAlreadyTakenException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * Exception thrown when email is already verified and taken.\n *\n * @author johnniang\n */\npublic class EmailAlreadyTakenException extends ServerWebInputException {\n\n    public static final URI TYPE = URI.create(\"https://halo.run/errors/email-already-taken\");\n\n    public EmailAlreadyTakenException(String reason) {\n        super(reason);\n        setType(TYPE);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/EmailVerificationFailed.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * Exception thrown when email verification failed.\n *\n * @author guqing\n * @since 2.11.0\n */\npublic class EmailVerificationFailed extends ServerWebInputException {\n\n    public EmailVerificationFailed() {\n        super(\"Invalid verification code\");\n    }\n\n    public EmailVerificationFailed(String reason, @Nullable Throwable cause) {\n        super(reason, null, cause);\n    }\n\n    public EmailVerificationFailed(String reason, @Nullable Throwable cause,\n        @Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {\n        super(reason, null, cause, messageDetailCode, messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/Exceptions.java",
    "content": "package run.halo.app.infra.exception;\n\nimport static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY;\n\nimport io.github.resilience4j.ratelimiter.RequestNotPermitted;\nimport java.net.URI;\nimport java.time.Instant;\nimport java.util.Locale;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.MessageSource;\nimport org.springframework.core.annotation.MergedAnnotations;\nimport org.springframework.dao.ConcurrencyFailureException;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.HttpStatusCode;\nimport org.springframework.http.ProblemDetail;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.web.ErrorResponse;\nimport org.springframework.web.bind.annotation.ResponseStatus;\nimport org.springframework.web.server.ServerWebExchange;\n\n@Slf4j\npublic enum Exceptions {\n    ;\n\n    public static final String DEFAULT_TYPE = \"about:blank\";\n\n    public static final String THEME_ALREADY_EXISTS_TYPE =\n        \"https://halo.run/probs/theme-alreay-exists\";\n\n    public static final String INVALID_CREDENTIAL_TYPE =\n        \"https://halo.run/probs/invalid-credential\";\n\n    public static final String REQUEST_NOT_PERMITTED_TYPE =\n        \"https://halo.run/probs/request-not-permitted\";\n\n    public static final String CONFLICT_TYPE =\n        \"https://halo.run/probs/conflict\";\n\n    /**\n     * Non-ErrorResponse exception to type map.\n     */\n    public static final Map<Class<? extends Throwable>, String> EXCEPTION_TYPE_MAP = Map.of(\n        RequestNotPermitted.class, REQUEST_NOT_PERMITTED_TYPE,\n        BadCredentialsException.class, INVALID_CREDENTIAL_TYPE\n    );\n\n    public static ErrorResponse createErrorResponse(Throwable t, @Nullable HttpStatusCode status,\n        ServerWebExchange exchange, MessageSource messageSource) {\n        final ErrorResponse errorResponse;\n        if (t instanceof ErrorResponse er) {\n            errorResponse = er;\n        } else {\n            var er = handleConflictException(t);\n            if (er == null) {\n                er = handleException(t, status);\n            }\n            errorResponse = er;\n        }\n        var problemDetail = errorResponse.updateAndGetBody(messageSource, getLocale(exchange));\n        problemDetail.setInstance(exchange.getRequest().getURI());\n        problemDetail.setProperty(\"requestId\", exchange.getRequest().getId());\n        problemDetail.setProperty(\"timestamp\", Instant.now());\n        return errorResponse;\n    }\n\n    @NonNull\n    private static ErrorResponse handleException(Throwable t, @Nullable HttpStatusCode status) {\n        var responseStatusAnno = MergedAnnotations.from(t.getClass(), TYPE_HIERARCHY)\n            .get(ResponseStatus.class);\n        if (status == null) {\n            status = responseStatusAnno.getValue(\"code\", HttpStatus.class)\n                .orElse(HttpStatus.INTERNAL_SERVER_ERROR);\n        }\n        var type = EXCEPTION_TYPE_MAP.getOrDefault(t.getClass(), DEFAULT_TYPE);\n        var detail = responseStatusAnno.getValue(\"reason\", String.class)\n            .orElseGet(t::getMessage);\n        var builder = ErrorResponse.builder(t, status, detail)\n            .type(URI.create(type));\n        if (status.is5xxServerError()) {\n            builder.detailMessageCode(\"problemDetail.internalServerError\")\n                .titleMessageCode(\"problemDetail.title.internalServerError\");\n        }\n        return builder.build();\n    }\n\n    @Nullable\n    private static ErrorResponse handleConflictException(Throwable t) {\n        if (t instanceof ConcurrencyFailureException) {\n            return ErrorResponse.builder(t, ProblemDetail.forStatus(HttpStatus.CONFLICT))\n                .type(URI.create(CONFLICT_TYPE))\n                .titleMessageCode(\"problemDetail.title.conflict\")\n                .detailMessageCode(\"problemDetail.conflict\")\n                .build();\n        }\n        return null;\n    }\n\n\n    public static Locale getLocale(ServerWebExchange exchange) {\n        var locale = exchange.getLocaleContext().getLocale();\n        return locale == null ? Locale.getDefault() : locale;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/FileSizeExceededException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\npublic class FileSizeExceededException extends ResponseStatusException {\n\n    public FileSizeExceededException(String reason, String messageDetailCode,\n        Object[] messageDetailArguments) {\n        this(reason, null, messageDetailCode, messageDetailArguments);\n    }\n\n    public FileSizeExceededException(String reason, Throwable cause,\n        String messageDetailCode, Object[] messageDetailArguments) {\n        super(HttpStatus.PAYLOAD_TOO_LARGE, reason, cause, messageDetailCode,\n            messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/FileTypeNotAllowedException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\npublic class FileTypeNotAllowedException extends ResponseStatusException {\n\n    public FileTypeNotAllowedException(String reason, String messageDetailCode,\n        Object[] messageDetailArguments) {\n        this(reason, null, messageDetailCode, messageDetailArguments);\n    }\n\n    public FileTypeNotAllowedException(String reason, Throwable cause,\n        String messageDetailCode, Object[] messageDetailArguments) {\n        super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason, cause, messageDetailCode,\n            messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/NotFoundException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ResponseStatusException;\n\n/**\n * Not found exception.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class NotFoundException extends ResponseStatusException {\n\n    public NotFoundException(@Nullable String reason) {\n        this(reason, null);\n    }\n\n    public NotFoundException(@Nullable String reason,\n        @Nullable Throwable cause) {\n        super(HttpStatus.NOT_FOUND, reason, cause);\n    }\n\n    public NotFoundException(@Nullable Throwable cause) {\n        this(cause == null ? \"\" : cause.getMessage(), cause);\n    }\n\n    public NotFoundException(String messageDetailCode, Object[] messageDetailArgs, String reason) {\n        super(HttpStatus.NOT_FOUND, reason, null, messageDetailCode, messageDetailArgs);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/OAuth2UserAlreadyBoundException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.web.server.ServerWebInputException;\nimport run.halo.app.core.extension.UserConnection;\n\n/**\n * An exception that the user has been bound to another OAuth2 user.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class OAuth2UserAlreadyBoundException extends ServerWebInputException {\n\n    public OAuth2UserAlreadyBoundException(UserConnection connection) {\n        super(\"The user has been bound to another account\", null, null, null, new Object[] {\n            connection.getSpec().getUsername(),\n            connection.getSpec().getProviderUserId(),\n            connection.getSpec().getRegistrationId(),\n            connection.getSpec().getUpdatedAt()\n        });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/PluginAlreadyExistsException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * PluginAlreadyExistsException indicates the provided plugin has already installed before.\n *\n * @author johnniang\n */\npublic class PluginAlreadyExistsException extends ServerWebInputException {\n\n    public static final String PLUGIN_ALREADY_EXISTS_TYPE =\n        \"https://halo.run/probs/plugin-alreay-exists\";\n\n    public PluginAlreadyExistsException(String pluginName) {\n        super(\"Plugin already exists.\", null, null, null, new Object[] {pluginName});\n        setType(URI.create(PLUGIN_ALREADY_EXISTS_TYPE));\n        getBody().setProperty(\"pluginName\", pluginName);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/PluginDependenciesNotEnabledException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport java.util.List;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * Plugin dependencies not enabled exception.\n *\n * @author johnniang\n */\npublic class PluginDependenciesNotEnabledException extends ServerWebInputException {\n\n    public static final URI TYPE =\n        URI.create(\"https://www.halo.run/probs/plugin-dependencies-not-enabled\");\n\n    /**\n     * Instantiates a new Plugin dependencies not enabled exception.\n     *\n     * @param dependencies dependencies that are not enabled\n     */\n    public PluginDependenciesNotEnabledException(List<String> dependencies) {\n        super(\"Plugin dependencies are not fully enabled, please enable them first.\",\n            null,\n            null,\n            null,\n            new Object[] {dependencies});\n        setType(TYPE);\n        getBody().setProperty(\"dependencies\", dependencies);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/PluginDependencyException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport java.util.List;\nimport org.pf4j.DependencyResolver.WrongDependencyVersion;\nimport org.springframework.web.server.ServerWebInputException;\n\npublic abstract class PluginDependencyException extends ServerWebInputException {\n\n    public PluginDependencyException(String reason) {\n        super(reason);\n    }\n\n    public PluginDependencyException(String reason, Throwable cause) {\n        super(reason, null, cause);\n    }\n\n    protected PluginDependencyException(String reason, Throwable cause,\n        String messageDetailCode, Object[] messageDetailArguments) {\n        super(reason, null, cause, messageDetailCode, messageDetailArguments);\n    }\n\n    public static class CyclicException extends PluginDependencyException {\n\n        public static final String TYPE = \"https://halo.run/probs/plugin-cyclic-dependency\";\n\n        public CyclicException() {\n            super(\"A cyclic dependency was detected.\");\n            setType(URI.create(TYPE));\n        }\n    }\n\n    public static class NotFoundException extends PluginDependencyException {\n\n        public static final String TYPE = \"https://halo.run/probs/plugin-dependencies-not-found\";\n\n        public NotFoundException(List<String> dependencies) {\n            super(\"Dependencies were not found.\", null, null, new Object[] {dependencies});\n            setType(URI.create(TYPE));\n            getBody().setProperty(\"dependencies\", dependencies);\n        }\n\n    }\n\n    public static class WrongVersionsException extends PluginDependencyException {\n\n        public static final String TYPE =\n            \"https://halo.run/probs/plugin-dependencies-with-wrong-versions\";\n\n        public WrongVersionsException(List<WrongDependencyVersion> versions) {\n            super(\"Dependencies have wrong version.\", null, null, new Object[] {versions});\n            setType(URI.create(TYPE));\n            getBody().setProperty(\"versions\", versions);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/PluginDependentsNotDisabledException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport java.util.List;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * Plugin dependents not disabled exception.\n *\n * @author johnniang\n */\npublic class PluginDependentsNotDisabledException extends ServerWebInputException {\n\n    public static final URI TYPE =\n        URI.create(\"https://www.halo.run/probs/plugin-dependents-not-disabled\");\n\n    /**\n     * Instantiates a new Plugin dependents not disabled exception.\n     *\n     * @param dependents dependents that are not disabled\n     */\n    public PluginDependentsNotDisabledException(List<String> dependents) {\n        super(\"Plugin dependents are not fully disabled, please disable them first.\",\n            null,\n            null,\n            null,\n            new Object[] {dependents});\n        setType(TYPE);\n        getBody().setProperty(\"dependents\", dependents);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/PluginInstallationException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport jakarta.validation.constraints.Null;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * {@link ServerWebInputException} subclass that indicates plugin installation failure.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class PluginInstallationException extends ServerWebInputException {\n\n    public PluginInstallationException(String reason, @Nullable String messageDetailCode,\n        @Null Object[] messageDetailArguments) {\n        super(reason, null, null, messageDetailCode, messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/PluginRuntimeIncompatibleException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport org.springframework.web.server.ServerErrorException;\n\n/**\n * Exception thrown when an incompatible plugin is detected.\n * This usually occurs when a plugin is not compatible with the current version of Halo at runtime.\n *\n * @author johnniang\n * @since 2.21.0\n */\npublic class PluginRuntimeIncompatibleException extends ServerErrorException {\n\n    private static final URI TYPE =\n        URI.create(\"https://www.halo.run/probs/plugin-runtime-incompatible\");\n\n    public PluginRuntimeIncompatibleException(Throwable cause) {\n        super(\"Incompatible plugin detected.\", cause);\n        setType(TYPE);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/RateLimitExceededException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ResponseStatusException;\n\npublic class RateLimitExceededException extends ResponseStatusException {\n\n    public RateLimitExceededException(@Nullable Throwable cause) {\n        super(HttpStatus.TOO_MANY_REQUESTS, \"You have exceeded your quota\", cause);\n        setType(URI.create(Exceptions.REQUEST_NOT_PERMITTED_TYPE));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/RequestBodyValidationException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport lombok.Getter;\nimport org.springframework.context.MessageSource;\nimport org.springframework.context.MessageSourceResolvable;\nimport org.springframework.http.ProblemDetail;\nimport org.springframework.lang.Nullable;\nimport org.springframework.validation.Errors;\nimport org.springframework.web.server.ServerWebInputException;\nimport org.springframework.web.util.BindErrorUtils;\n\n@Getter\npublic class RequestBodyValidationException extends ServerWebInputException {\n\n    private final Errors errors;\n\n    public RequestBodyValidationException(Errors errors) {\n        super(\"Validation failure\", null, null, null, null);\n        this.errors = errors;\n    }\n\n    @Override\n    public ProblemDetail updateAndGetBody(MessageSource messageSource, Locale locale) {\n        var detail = super.updateAndGetBody(messageSource, locale);\n        detail.setProperty(\"errors\", collectAllErrors(messageSource, locale));\n        return detail;\n    }\n\n    private List<String> collectAllErrors(MessageSource messageSource, Locale locale) {\n        var globalErrors = resolveErrors(errors.getGlobalErrors(), messageSource, locale);\n        var fieldErrors = resolveErrors(errors.getFieldErrors(), messageSource, locale);\n        var errors = new ArrayList<String>(globalErrors.size() + fieldErrors.size());\n        errors.addAll(globalErrors);\n        errors.addAll(fieldErrors);\n        return errors;\n    }\n\n    @Override\n    public Object[] getDetailMessageArguments(MessageSource messageSource, Locale locale) {\n        return new Object[] {\n            resolveErrors(errors.getGlobalErrors(), messageSource, locale),\n            resolveErrors(errors.getFieldErrors(), messageSource, locale)\n        };\n    }\n\n    @Override\n    public Object[] getDetailMessageArguments() {\n        return new Object[] {\n            resolveErrors(errors.getGlobalErrors(), null, Locale.getDefault()),\n            resolveErrors(errors.getFieldErrors(), null, Locale.getDefault())\n        };\n    }\n\n    private static List<String> resolveErrors(\n        List<? extends MessageSourceResolvable> errors,\n        @Nullable MessageSource messageSource,\n        Locale locale) {\n        return messageSource == null\n            ? BindErrorUtils.resolve(errors).values().stream().toList()\n            : BindErrorUtils.resolve(errors, messageSource, locale).values().stream().toList();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/RequestRestrictedException.java",
    "content": "package run.halo.app.infra.exception;\n\n/**\n * <p>{@link RequestRestrictedException} indicates that the client's request was denied because\n * it did not meet certain required conditions.</p>\n * <p>Typically, this exception is thrown when a user attempts to perform an action that\n * requires prior approval or validation, such as replying to a comment that has not yet been\n * approved.</p>\n * <p>The server understands the request but refuses to process it due to the lack of\n * necessary approval.</p>\n *\n * @author guqing\n * @since 2.20.0\n */\npublic class RequestRestrictedException extends AccessDeniedException {\n\n    public RequestRestrictedException(String reason) {\n        super(reason);\n    }\n\n    public RequestRestrictedException(String reason, String detailCode, Object[] detailArgs) {\n        super(reason, detailCode, detailArgs);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/RestrictedNameException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport jakarta.validation.constraints.Null;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * Restricted name exception.\n *\n * @author lywq\n * @since 2025/10/30 11:47\n **/\npublic class RestrictedNameException extends ServerWebInputException {\n\n    public RestrictedNameException() {\n        super(\"The name is restricted\");\n    }\n\n    public RestrictedNameException(String reason, @Nullable String messageDetailCode,\n        @Null Object[] messageDetailArguments) {\n        super(reason, null, null, messageDetailCode, messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/ThemeAlreadyExistsException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport java.net.URI;\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * {@link ThemeAlreadyExistsException} indicates the provided theme has already installed before.\n *\n * @author guqing\n * @since 2.6.0\n */\npublic class ThemeAlreadyExistsException extends ServerWebInputException {\n\n    /**\n     * Constructs a {@code ThemeAlreadyExistsException} with the given theme name.\n     *\n     * @param themeName theme name must not be blank\n     */\n    public ThemeAlreadyExistsException(@NonNull String themeName) {\n        super(\"Theme already exists.\", null, null, \"problemDetail.theme.install.alreadyExists\",\n            new Object[] {themeName});\n        setType(URI.create(Exceptions.THEME_ALREADY_EXISTS_TYPE));\n        getBody().setProperty(\"themeName\", themeName);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\n/**\n * @author guqing\n * @author johnniang\n * @since 2.0.0\n */\npublic class ThemeInstallationException extends ResponseStatusException {\n\n    public ThemeInstallationException(String reason, String detailCode, Object[] detailArgs) {\n        super(HttpStatus.BAD_REQUEST, reason, null, detailCode, detailArgs);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java",
    "content": "package run.halo.app.infra.exception;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class ThemeUninstallException extends RuntimeException {\n\n    public ThemeUninstallException(String message) {\n        super(message);\n    }\n\n    public ThemeUninstallException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/ThemeUpgradeException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\n/**\n * ThemeUpgradeException will response bad request status if failed to upgrade theme.\n *\n * @author johnniang\n */\npublic class ThemeUpgradeException extends ResponseStatusException {\n\n    public ThemeUpgradeException(String reason, String detailCode, Object[] detailArgs) {\n        super(HttpStatus.BAD_REQUEST, reason, null, detailCode, detailArgs);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/UnsatisfiedAttributeValueException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport jakarta.validation.constraints.Null;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * {@link ServerWebInputException} subclass that indicates an unsatisfied\n * attribute value in request parameters.\n *\n * @author guqing\n * @since 2.2.0\n */\npublic class UnsatisfiedAttributeValueException extends ServerWebInputException {\n\n    public UnsatisfiedAttributeValueException(String reason) {\n        super(reason);\n    }\n\n    public UnsatisfiedAttributeValueException(String reason, @Nullable String messageDetailCode,\n        @Null Object[] messageDetailArguments) {\n        super(reason, null, null, messageDetailCode, messageDetailArguments);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/UserNotFoundException.java",
    "content": "package run.halo.app.infra.exception;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\npublic class UserNotFoundException extends ResponseStatusException {\n\n    public UserNotFoundException(String username) {\n        super(HttpStatus.NOT_FOUND, \"User \" + username + \" was not found\", null, null,\n            new Object[] {username});\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorConfiguration.java",
    "content": "package run.halo.app.infra.exception.handlers;\n\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.boot.webflux.autoconfigure.error.ErrorWebFluxAutoConfiguration;\nimport org.springframework.boot.webflux.error.ErrorAttributes;\nimport org.springframework.boot.webflux.error.ErrorWebExceptionHandler;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.MessageSource;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.codec.ServerCodecConfigurer;\nimport org.springframework.web.reactive.result.view.ViewResolver;\nimport org.springframework.web.server.WebExceptionHandler;\n\n/**\n * Configuration to render errors via a WebFlux\n * {@link WebExceptionHandler}.\n * <br/>\n * <br/>\n * See\n * {@link ErrorWebFluxAutoConfiguration}\n * for more.\n *\n * @author guqing\n * @author johnniang\n * @since 2.1.0\n */\n@Configuration\npublic class HaloErrorConfiguration {\n\n    /**\n     * Customize the default {@link ErrorWebExceptionHandler}.\n     */\n    @Bean\n    @Order(-1)\n    ErrorWebExceptionHandler errorWebExceptionHandler(\n        ErrorAttributes errorAttributes,\n        WebProperties webProperties,\n        ObjectProvider<ViewResolver> viewResolvers,\n        ServerCodecConfigurer serverCodecConfigurer,\n        ApplicationContext applicationContext\n    ) {\n        var exceptionHandler = new HaloErrorWebExceptionHandler(\n            errorAttributes,\n            webProperties.getResources(),\n            webProperties.getError(),\n            applicationContext);\n        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().toList());\n        exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());\n        exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());\n        return exceptionHandler;\n    }\n\n    /**\n     * Customize the default {@link ErrorAttributes}.\n     */\n    @Bean\n    ErrorAttributes errorAttributes(MessageSource messageSource) {\n        return new ProblemDetailErrorAttributes(messageSource);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorWebExceptionHandler.java",
    "content": "package run.halo.app.infra.exception.handlers;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport org.springframework.boot.autoconfigure.web.ErrorProperties;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.boot.webflux.autoconfigure.error.DefaultErrorWebExceptionHandler;\nimport org.springframework.boot.webflux.error.ErrorAttributes;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.ProblemDetail;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport reactor.util.context.Context;\nimport run.halo.app.theme.ThemeContext;\nimport run.halo.app.theme.ThemeResolver;\nimport run.halo.app.theme.engine.ThemeTemplateAvailabilityProvider;\n\npublic class HaloErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {\n\n    private final ThemeTemplateAvailabilityProvider templateAvailabilityProvider;\n\n    private final ThemeResolver themeResolver;\n\n    /**\n     * Create a new {@code DefaultErrorWebExceptionHandler} instance.\n     *\n     * @param errorAttributes the error attributes\n     * @param resources the resources configuration properties\n     * @param errorProperties the error configuration properties\n     * @param applicationContext the current application context\n     * @since 2.4.0\n     */\n    public HaloErrorWebExceptionHandler(\n        ErrorAttributes errorAttributes,\n        WebProperties.Resources resources,\n        ErrorProperties errorProperties,\n        ApplicationContext applicationContext) {\n        super(errorAttributes, resources, errorProperties, applicationContext);\n        this.templateAvailabilityProvider =\n            applicationContext.getBean(ThemeTemplateAvailabilityProvider.class);\n        this.themeResolver = applicationContext.getBean(ThemeResolver.class);\n    }\n\n    @Override\n    protected int getHttpStatus(Map<String, Object> errorAttributes) {\n        var problemDetail = (ProblemDetail) errorAttributes.get(\"error\");\n        return problemDetail.getStatus();\n    }\n\n    @Override\n    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {\n        var errorAttributes =\n            getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));\n        return ServerResponse.status(getHttpStatus(errorAttributes))\n            .contentType(MediaType.APPLICATION_PROBLEM_JSON)\n            .bodyValue(errorAttributes.get(\"error\"));\n    }\n\n    @Override\n    protected Mono<ServerResponse> renderErrorView(ServerRequest request) {\n        return themeResolver.getTheme(request.exchange())\n            .flatMap(themeContext -> super.renderErrorView(request)\n                .contextWrite(Context.of(ThemeContext.class, themeContext)));\n    }\n\n    @Override\n    protected Mono<ServerResponse> renderErrorView(String viewName,\n        ServerResponse.BodyBuilder responseBody, Map<String, Object> error) {\n        return Mono.deferContextual(contextView -> {\n            Optional<ThemeContext> themeContext = contextView.getOrEmpty(ThemeContext.class);\n            if (themeContext.isPresent()\n                && templateAvailabilityProvider.isTemplateAvailable(themeContext.get(), viewName)) {\n                return responseBody.render(viewName, error);\n            }\n            return super.renderErrorView(viewName, responseBody, error);\n        });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/exception/handlers/ProblemDetailErrorAttributes.java",
    "content": "package run.halo.app.infra.exception.handlers;\n\nimport static run.halo.app.infra.exception.Exceptions.createErrorResponse;\n\nimport java.util.Map;\nimport org.springframework.boot.web.error.ErrorAttributeOptions;\nimport org.springframework.boot.webflux.error.DefaultErrorAttributes;\nimport org.springframework.context.MessageSource;\nimport org.springframework.web.reactive.function.server.ServerRequest;\n\n/**\n * See {@link DefaultErrorAttributes} for more.\n *\n * @author johnn\n */\npublic class ProblemDetailErrorAttributes extends DefaultErrorAttributes {\n\n    private final MessageSource messageSource;\n\n    public ProblemDetailErrorAttributes(MessageSource messageSource) {\n        this.messageSource = messageSource;\n    }\n\n    @Override\n    public Map<String, Object> getErrorAttributes(ServerRequest request,\n        ErrorAttributeOptions options) {\n        final var errAttributes = super.getErrorAttributes(request, options);\n        var error = getError(request);\n        var errorResponse = createErrorResponse(error, null, request.exchange(), messageSource);\n        errAttributes.put(\"error\", errorResponse.getBody());\n        return errAttributes;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/AttachmentProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport jakarta.validation.Valid;\nimport jakarta.validation.constraints.Max;\nimport jakarta.validation.constraints.Min;\nimport jakarta.validation.constraints.PositiveOrZero;\nimport java.util.LinkedList;\nimport java.util.List;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.NestedConfigurationProperty;\n\n@Data\npublic class AttachmentProperties {\n\n    private List<ResourceMapping> resourceMappings = new LinkedList<>();\n\n    @Valid\n    @NestedConfigurationProperty\n    private final ThumbnailProperties thumbnail = new ThumbnailProperties();\n\n    @Data\n    public static class ThumbnailProperties {\n\n        /**\n         * Whether to disable thumbnail generation.\n         */\n        private boolean disabled;\n\n        /**\n         * The concurrent threads for thumbnail generation.\n         */\n        @Min(1)\n        private Integer concurrentThreads;\n\n        /**\n         * The quality of generated thumbnails, value between 0.0 and 1.0.\n         */\n        @PositiveOrZero\n        @Max(1)\n        private Double quality;\n\n    }\n\n    @Data\n    public static class ResourceMapping {\n\n        /**\n         * Like: {@code /upload/**}.\n         */\n        private String pathPattern;\n\n        /**\n         * The location is a relative path to attachments folder in working directory.\n         */\n        private List<String> locations;\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/CacheProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport lombok.Data;\n\n@Data\npublic class CacheProperties {\n\n    private boolean disabled;\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/ExtensionProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport lombok.Data;\n\n@Data\npublic class ExtensionProperties {\n\n    private Controller controller = new Controller();\n\n    @Data\n    public static class Controller {\n\n        private boolean disabled;\n\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/HaloProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport jakarta.validation.Valid;\nimport jakarta.validation.constraints.NotNull;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.util.HashSet;\nimport java.util.Set;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.context.properties.NestedConfigurationProperty;\nimport org.springframework.validation.Errors;\nimport org.springframework.validation.Validator;\nimport org.springframework.validation.annotation.Validated;\n\n/**\n * @author guqing\n * @since 2022-04-12\n */\n@Data\n@ConfigurationProperties(prefix = \"halo\")\n@Validated\npublic class HaloProperties implements Validator {\n\n    @NotNull\n    private Path workDir;\n\n    /**\n     * External URL must be a URL and it can be null.\n     */\n    private URL externalUrl;\n\n    /**\n     * Indicates if we use absolute permalink to post, page, category, tag and so on.\n     */\n    private boolean useAbsolutePermalink;\n\n    private Set<String> initialExtensionLocations = new HashSet<>();\n\n    /**\n     * This property could stop initializing required Extensions defined in classpath.\n     * See {@link run.halo.app.infra.ExtensionResourceInitializer#REQUIRED_EXTENSION_LOCATIONS}\n     * for more.\n     */\n    private boolean requiredExtensionDisabled;\n\n    @Valid\n    @NestedConfigurationProperty\n    private final ExtensionProperties extension = new ExtensionProperties();\n\n    @Valid\n    @NestedConfigurationProperty\n    private final SecurityProperties security = new SecurityProperties();\n\n    @Valid\n    @NestedConfigurationProperty\n    private final UiProperties ui = new UiProperties();\n\n    @Valid\n    @NestedConfigurationProperty\n    private final ThemeProperties theme = new ThemeProperties();\n\n    @Valid\n    @NestedConfigurationProperty\n    private final AttachmentProperties attachment = new AttachmentProperties();\n\n    @Override\n    public boolean supports(Class<?> clazz) {\n        return HaloProperties.class.isAssignableFrom(clazz);\n    }\n\n    @Override\n    public void validate(Object target, Errors errors) {\n        var props = (HaloProperties) target;\n        var externalUrl = props.getExternalUrl();\n        if (props.isUseAbsolutePermalink() && externalUrl == null) {\n            errors.rejectValue(\"externalUrl\", \"external-url.required.when-using-absolute-permalink\",\n                \"External URL is required when property `use-absolute-permalink` is set to true.\");\n        }\n        // check if the external URL is a http or https URL and is not an opaque URL.\n        if (externalUrl != null && !isValidExternalUrl(externalUrl)) {\n            errors.rejectValue(\"externalUrl\", \"external-url.invalid-format\",\n                \"External URL must be a http or https URL.\");\n        }\n    }\n\n    private boolean isValidExternalUrl(URL externalUrl) {\n        try {\n            var uri = externalUrl.toURI();\n            return !uri.isOpaque()\n                && uri.getAuthority() != null\n                && Set.of(\"http\", \"https\").contains(uri.getScheme());\n        } catch (URISyntaxException e) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/JwtProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport jakarta.validation.constraints.NotNull;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.security.interfaces.RSAPrivateKey;\nimport java.security.interfaces.RSAPublicKey;\nimport org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;\nimport org.springframework.core.io.Resource;\nimport org.springframework.security.converter.RsaKeyConverters;\nimport org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;\nimport org.springframework.util.Assert;\nimport org.springframework.util.StreamUtils;\nimport org.springframework.validation.annotation.Validated;\n\n/**\n * @author guqing\n * @author johnniang\n * @date 2022-04-12\n */\n@Validated\npublic class JwtProperties {\n\n    /**\n     * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0\n     * Authorization Server Metadata endpoint defined by RFC 8414.\n     */\n    private String issuerUri;\n\n    /**\n     * JSON Web Algorithm used for verifying the digital signatures.\n     */\n    private SignatureAlgorithm jwsAlgorithm;\n\n    /**\n     * Location of the file containing the public key used to verify a JWT.\n     */\n    @NotNull\n    private Resource publicKeyLocation;\n\n    @NotNull\n    private Resource privateKeyLocation;\n\n    private final RSAPrivateKey privateKey;\n\n    private final RSAPublicKey publicKey;\n\n    public JwtProperties(String issuerUri, SignatureAlgorithm jwsAlgorithm,\n        Resource publicKeyLocation,\n        Resource privateKeyLocation) throws IOException {\n        this.issuerUri = issuerUri;\n        this.jwsAlgorithm = jwsAlgorithm;\n        if (jwsAlgorithm == null) {\n            this.jwsAlgorithm = SignatureAlgorithm.RS256;\n        }\n        this.publicKeyLocation = publicKeyLocation;\n        this.privateKeyLocation = privateKeyLocation;\n\n        //TODO initialize private and public keys at first startup.\n        this.privateKey = this.readPrivateKey();\n        this.publicKey = this.readPublicKey();\n    }\n\n    public String getIssuerUri() {\n        return issuerUri;\n    }\n\n    public void setIssuerUri(String issuerUri) {\n        this.issuerUri = issuerUri;\n    }\n\n    public SignatureAlgorithm getJwsAlgorithm() {\n        return this.jwsAlgorithm;\n    }\n\n    public void setJwsAlgorithm(SignatureAlgorithm jwsAlgorithm) {\n        this.jwsAlgorithm = jwsAlgorithm;\n    }\n\n    public Resource getPublicKeyLocation() {\n        return this.publicKeyLocation;\n    }\n\n    public void setPublicKeyLocation(Resource publicKeyLocation) {\n        this.publicKeyLocation = publicKeyLocation;\n    }\n\n    public Resource getPrivateKeyLocation() {\n        return privateKeyLocation;\n    }\n\n    public void setPrivateKeyLocation(Resource privateKeyLocation) {\n        this.privateKeyLocation = privateKeyLocation;\n    }\n\n    public RSAPrivateKey getPrivateKey() {\n        return privateKey;\n    }\n\n    public RSAPublicKey getPublicKey() {\n        return publicKey;\n    }\n\n    private RSAPublicKey readPublicKey() throws IOException {\n        String key = \"halo.security.oauth2.jwt.public-key-location\";\n        Assert.notNull(this.publicKeyLocation, \"PublicKeyLocation must not be null\");\n        if (!this.publicKeyLocation.exists()) {\n            throw new InvalidConfigurationPropertyValueException(key, this.publicKeyLocation,\n                \"Public key location does not exist\");\n        }\n        try (InputStream inputStream = this.publicKeyLocation.getInputStream()) {\n            String source = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);\n            return RsaKeyConverters.x509()\n                .convert(new ByteArrayInputStream(source.getBytes()));\n        }\n    }\n\n    private RSAPrivateKey readPrivateKey() throws IOException {\n        String key = \"halo.security.oauth2.jwt.private-key-location\";\n        Assert.notNull(this.privateKeyLocation, \"PrivateKeyLocation must not be null\");\n        if (!this.privateKeyLocation.exists()) {\n            throw new InvalidConfigurationPropertyValueException(key, this.privateKeyLocation,\n                \"Private key location does not exist\");\n        }\n        try (InputStream inputStream = this.privateKeyLocation.getInputStream()) {\n            String source = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);\n            return RsaKeyConverters.pkcs8()\n                .convert(new ByteArrayInputStream(source.getBytes()));\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/ProxyProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport java.net.URI;\nimport lombok.Data;\n\n@Data\npublic class ProxyProperties {\n\n    /**\n     * Console endpoint in development environment to be proxied. e.g.: http://localhost:8090/\n     */\n    private URI endpoint;\n\n    /**\n     * Indicates if the proxy behaviour is enabled. Default is false\n     */\n    private boolean enabled = false;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport static org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN;\n\nimport java.net.URI;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport lombok.Data;\nimport org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;\nimport org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy;\nimport org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode;\n\n@Data\npublic class SecurityProperties {\n\n    private final FrameOptions frameOptions = new FrameOptions();\n\n    private final ReferrerOptions referrerOptions = new ReferrerOptions();\n\n    private final CorsOptions corsOptions = new CorsOptions();\n\n    private final RememberMeOptions rememberMe = new RememberMeOptions();\n\n    private final TwoFactorAuthOptions twoFactorAuth = new TwoFactorAuthOptions();\n\n    private final BasicAuthOptions basicAuth = new BasicAuthOptions();\n\n    private final List<PasswordResetMethod> passwordResetMethods = new ArrayList<>();\n\n    @Data\n    public static class BasicAuthOptions {\n        /**\n         * Whether basic authentication is disabled.\n         */\n        private boolean disabled = true;\n    }\n\n    @Data\n    public static class TwoFactorAuthOptions {\n\n        /**\n         * Whether two-factor authentication is disabled.\n         */\n        private boolean disabled;\n\n    }\n\n    @Data\n    public static class CorsOptions {\n\n        private boolean disabled;\n\n        private final List<CorsConfig> configs = new ArrayList<>();\n\n    }\n\n    @Data\n    public static class CorsConfig {\n\n        private String pathPattern;\n\n        private CorsEndpointProperties config;\n\n    }\n\n    @Data\n    public static class FrameOptions {\n\n        private boolean disabled;\n\n        private Mode mode = Mode.SAMEORIGIN;\n    }\n\n    @Data\n    public static class ReferrerOptions {\n\n        private ReferrerPolicy policy = STRICT_ORIGIN_WHEN_CROSS_ORIGIN;\n\n    }\n\n    @Data\n    public static class RememberMeOptions {\n        private Duration tokenValidity = Duration.ofDays(14);\n    }\n\n    @Data\n    public static class PasswordResetMethod {\n\n        private String name;\n\n        private URI href;\n\n        private URI icon;\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport jakarta.validation.Valid;\nimport lombok.Data;\n\n@Data\npublic class ThemeProperties {\n\n    @Valid\n    private final Initializer initializer = new Initializer();\n\n    /**\n     * Indicates whether the generator meta needs to be disabled.\n     */\n    private boolean generatorMetaDisabled;\n\n    @Data\n    public static class Initializer {\n\n        private boolean disabled = false;\n\n        private String location = \"classpath:themes/theme-earth.zip\";\n\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/properties/UiProperties.java",
    "content": "package run.halo.app.infra.properties;\n\nimport jakarta.validation.Valid;\nimport lombok.Data;\nimport org.springframework.boot.context.properties.NestedConfigurationProperty;\n\n@Data\npublic class UiProperties {\n\n    private String location = \"classpath:/ui/\";\n\n    @Valid\n    @NestedConfigurationProperty\n    private ProxyProperties proxy = new ProxyProperties();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ui/ProxyFilter.java",
    "content": "package run.halo.app.infra.ui;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.properties.ProxyProperties;\n\n@Slf4j\npublic class ProxyFilter implements WebFilter {\n\n    private final ProxyProperties proxyProperties;\n\n    private final ServerWebExchangeMatcher requestMatcher;\n\n    private final WebClient webClient;\n\n    public ProxyFilter(ProxyProperties proxyProperties, String... patterns) {\n        this.proxyProperties = proxyProperties;\n        var requestMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, patterns);\n        requestMatcher = new AndServerWebExchangeMatcher(requestMatcher,\n            new NegatedServerWebExchangeMatcher(new WebSocketServerWebExchangeMatcher()));\n        this.requestMatcher = requestMatcher;\n        this.webClient = WebClient.create(proxyProperties.getEndpoint().toString());\n        log.debug(\"Initialized ProxyFilter to proxy {} to endpoint {}\",\n            java.util.Arrays.toString(patterns),\n            proxyProperties.getEndpoint());\n    }\n\n    @Override\n    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n        return requestMatcher.matches(exchange)\n            .filter(ServerWebExchangeMatcher.MatchResult::isMatch)\n            .filter(matchResult -> isHtmlRequest(exchange))\n            .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))\n            .map(matchResult -> getProxyUri(exchange))\n            .doOnNext(uri -> {\n                if (log.isTraceEnabled()) {\n                    log.trace(\"Proxy {} to {}\", uri, proxyProperties.getEndpoint());\n                }\n            })\n            .flatMap(uri -> webClient.get()\n                .uri(uri)\n                .headers(httpHeaders -> httpHeaders.addAll(exchange.getRequest().getHeaders()))\n                .exchangeToMono(clientResponse -> {\n                    var response = exchange.getResponse();\n                    var httpStatusCode = clientResponse.statusCode();\n                    // set headers\n                    var httpHeaders = clientResponse.headers().asHttpHeaders();\n                    response.getHeaders().putAll(httpHeaders);\n                    // set cookies\n                    response.getCookies().putAll(clientResponse.cookies());\n                    // set status code\n                    response.setStatusCode(httpStatusCode);\n                    var contentLength = clientResponse.headers().contentLength().orElse(0L);\n                    if (httpStatusCode.is3xxRedirection()\n                        || httpStatusCode.equals(HttpStatus.NO_CONTENT)\n                        || contentLength == 0) {\n                        return Mono.empty();\n                    }\n                    var body = clientResponse.bodyToFlux(DataBuffer.class);\n                    return response.writeWith(body);\n                }));\n    }\n\n    private boolean isHtmlRequest(ServerWebExchange exchange) {\n        var acceptHeaders = exchange.getRequest().getHeaders().getAccept();\n        if (acceptHeaders.isEmpty()) {\n            return true;\n        }\n        return acceptHeaders.stream()\n            .anyMatch(mediaType -> mediaType.isCompatibleWith(MediaType.TEXT_HTML));\n    }\n\n    private String getProxyUri(ServerWebExchange exchange) {\n        var requestPath = exchange.getRequest().getPath().pathWithinApplication().value();\n        return UriComponentsBuilder.fromUriString(getUiEntryPath(requestPath))\n            .queryParams(exchange.getRequest().getQueryParams())\n            .build()\n            .toUriString();\n    }\n\n    private String getUiEntryPath(String requestPath) {\n        if (requestPath.startsWith(\"/console\")) {\n            return \"/console\";\n        }\n        if (requestPath.startsWith(\"/uc\")) {\n            return \"/uc\";\n        }\n        return requestPath;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ui/WebSocketRequestPredicate.java",
    "content": "package run.halo.app.infra.ui;\n\nimport static run.halo.app.infra.ui.WebSocketUtils.isWebSocketUpgrade;\n\nimport org.springframework.web.reactive.function.server.RequestPredicate;\nimport org.springframework.web.reactive.function.server.ServerRequest;\n\npublic class WebSocketRequestPredicate implements RequestPredicate {\n\n    @Override\n    public boolean test(ServerRequest request) {\n        var httpHeaders = request.exchange().getRequest().getHeaders();\n        return isWebSocketUpgrade(httpHeaders);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ui/WebSocketServerWebExchangeMatcher.java",
    "content": "package run.halo.app.infra.ui;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match;\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch;\nimport static run.halo.app.infra.ui.WebSocketUtils.isWebSocketUpgrade;\n\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\npublic class WebSocketServerWebExchangeMatcher implements ServerWebExchangeMatcher {\n    @Override\n    public Mono<MatchResult> matches(ServerWebExchange exchange) {\n        return isWebSocketUpgrade(exchange.getRequest().getHeaders()) ? match() : notMatch();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/ui/WebSocketUtils.java",
    "content": "package run.halo.app.infra.ui;\n\nimport java.util.Objects;\nimport org.springframework.http.HttpHeaders;\n\npublic enum WebSocketUtils {\n    ;\n\n    public static boolean isWebSocketUpgrade(HttpHeaders headers) {\n        // See io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionUtil\n        // .isWebsocketUpgrade for more.\n        var upgradeConnection = headers.getConnection().stream().map(String::toLowerCase)\n            .anyMatch(conn -> Objects.equals(conn, \"upgrade\"));\n\n        return headers.containsHeader(HttpHeaders.UPGRADE)\n            && upgradeConnection\n            && \"websocket\".equalsIgnoreCase(headers.getUpgrade());\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/Base62Utils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport io.seruco.encoding.base62.Base62;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport org.apache.commons.lang3.StringUtils;\n\n/**\n * <p>Base62 tool class, which provides the encoding and decoding scheme of base62.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class Base62Utils {\n    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;\n    private static final Base62 INSTANCE = Base62.createInstance();\n\n    public static String encode(String source) {\n        return encode(source, DEFAULT_CHARSET);\n    }\n\n    /**\n     * Base62 encode.\n     *\n     * @param source the encoded base62 string\n     * @param charset the charset default is utf_8\n     * @return encoded string by base62\n     */\n    public static String encode(String source, Charset charset) {\n        return encode(StringUtils.getBytes(source, charset));\n    }\n\n    public static String encode(byte[] source) {\n        return new String(INSTANCE.encode(source));\n    }\n\n    /**\n     * Base62 decode.\n     *\n     * @param base62Str the Base62 decoded string\n     * @return decoded bytes\n     */\n    public static byte[] decode(String base62Str) {\n        return decode(StringUtils.getBytes(base62Str, DEFAULT_CHARSET));\n    }\n\n    public static byte[] decode(byte[] base62bytes) {\n        return INSTANCE.decode(base62bytes);\n    }\n\n    public static String decodeToString(String source) {\n        return decodeToString(source, DEFAULT_CHARSET);\n    }\n\n    public static String decodeToString(String source, Charset charset) {\n        return StringUtils.toEncodedString(decode(source), charset);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/FileNameUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport com.google.common.io.Files;\nimport java.util.function.Supplier;\nimport java.util.regex.Pattern;\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.apache.commons.lang3.StringUtils;\n\npublic final class FileNameUtils {\n\n    private FileNameUtils() {\n    }\n\n    /**\n     * Check whether the file name has an extension.\n     *\n     * @param filename is name of file.\n     * @return True if file name has extension, otherwise false.\n     */\n    public static boolean hasFileExtension(String filename) {\n        if (filename == null || filename.isEmpty()) {\n            return false;\n        }\n        var extensionRegex = \".*\\\\.[a-zA-Z0-9]+$\";\n        return Pattern.matches(extensionRegex, filename);\n    }\n\n    public static String removeFileExtension(String filename, boolean removeAllExtensions) {\n        if (filename == null || filename.isEmpty()) {\n            return filename;\n        }\n        var extPattern = \"(?<!^)[.]\" + (removeAllExtensions ? \".*\" : \"[^.]*$\");\n        return filename.replaceAll(extPattern, \"\");\n    }\n\n    /**\n     * Append random string after file name.\n     * <pre>\n     * Case 1: halo.run -> halo-xyz.run\n     * Case 2: .run -> xyz.run\n     * Case 3: halo -> halo-xyz\n     * </pre>\n     *\n     * @param filename is name of file.\n     * @param length is for generating random string with specific length.\n     * @return File name with random string.\n     */\n    public static String randomFileName(String filename, int length) {\n        return renameFilename(\n            filename, () -> RandomStringUtils.secure().nextAlphabetic(length), false\n        );\n    }\n\n    public static String renameFilename(\n        String filename,\n        Supplier<String> renameSupplier,\n        boolean excludeBasename) {\n        var nameWithoutExt = Files.getNameWithoutExtension(filename);\n        var ext = Files.getFileExtension(filename);\n        var rename = renameSupplier.get();\n        if (StringUtils.isBlank(nameWithoutExt)) {\n            return rename + \".\" + ext;\n        }\n        if (StringUtils.isBlank(ext)) {\n            if (excludeBasename) {\n                return rename;\n            }\n            return nameWithoutExt + \"-\" + rename;\n        }\n        if (excludeBasename) {\n            return rename + \".\" + ext;\n        }\n        return nameWithoutExt + \"-\" + rename + \".\" + ext;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/FileUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\nimport static org.springframework.core.io.buffer.DataBufferUtils.subscriberInputStream;\nimport static org.springframework.util.FileSystemUtils.deleteRecursively;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.nio.file.CopyOption;\nimport java.nio.file.DirectoryNotEmptyException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.jar.JarEntry;\nimport java.util.jar.JarOutputStream;\nimport java.util.stream.Stream;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipInputStream;\nimport java.util.zip.ZipOutputStream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jspecify.annotations.Nullable;\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.AntPathMatcher;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.infra.exception.AccessDeniedException;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\npublic abstract class FileUtils {\n\n    private FileUtils() {\n    }\n\n    /**\n     * Unzip the given content to target path. Please note that no default scheduler will be used.\n     *\n     * @param content the zip content\n     * @param targetPath the target path\n     * @return a Mono signaling when unzip is complete\n     */\n    public static Mono<Void> unzip(Publisher<DataBuffer> content, @NonNull Path targetPath) {\n        return unzip(content, targetPath, null);\n    }\n\n    /**\n     * Unzip the given content to target path.\n     *\n     * @param content the zip content\n     * @param targetPath the target path\n     * @param scheduler the scheduler\n     * @return a Mono signaling when unzip is complete\n     */\n    public static Mono<Void> unzip(\n        Publisher<DataBuffer> content, @NonNull Path targetPath, @Nullable Scheduler scheduler\n    ) {\n        var unzip = Mono.fromCallable(() -> {\n            try (var is = subscriberInputStream(content, 1);\n                 var zis = new ZipInputStream(is)) {\n                log.debug(\"Unzipping to target path: {}\", targetPath);\n                unzip(zis, targetPath);\n                log.debug(\"Unzipped to target path: {}\", targetPath);\n            }\n            return null;\n        }).then();\n        if (scheduler != null) {\n            return unzip.subscribeOn(scheduler);\n        }\n        return unzip;\n    }\n\n    public static void unzip(@NonNull ZipInputStream zis, @NonNull Path targetPath)\n        throws IOException {\n        // 1. unzip file to folder\n        // 2. return the folder path\n        Assert.notNull(zis, \"Zip input stream must not be null\");\n        Assert.notNull(targetPath, \"Target path must not be null\");\n\n        // Create path if absent\n        createIfAbsent(targetPath);\n\n        // Folder must be empty\n        ensureEmpty(targetPath);\n\n        ZipEntry zipEntry = zis.getNextEntry();\n\n        while (zipEntry != null) {\n            // Resolve the entry path\n            Path entryPath = targetPath.resolve(zipEntry.getName());\n\n            checkDirectoryTraversal(targetPath, entryPath);\n\n            if (Files.notExists(entryPath.getParent())) {\n                Files.createDirectories(entryPath.getParent());\n            }\n\n            if (zipEntry.isDirectory()) {\n                // Create directory\n                Files.createDirectory(entryPath);\n            } else {\n                // Copy file\n                Files.copy(zis, entryPath);\n            }\n\n            zipEntry = zis.getNextEntry();\n        }\n    }\n\n    public static void zip(Path sourcePath, Path targetPath) throws IOException {\n        try (var zos = new ZipOutputStream(Files.newOutputStream(targetPath))) {\n            Files.walkFileTree(sourcePath, new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)\n                    throws IOException {\n                    checkDirectoryTraversal(sourcePath, file);\n                    var relativePath = sourcePath.relativize(file);\n                    var entry = new ZipEntry(relativePath.toString());\n                    zos.putNextEntry(entry);\n                    Files.copy(file, zos);\n                    zos.closeEntry();\n                    return super.visitFile(file, attrs);\n                }\n            });\n        }\n    }\n\n    public static void jar(Path sourcePath, Path targetPath) throws IOException {\n        try (var jos = new JarOutputStream(Files.newOutputStream(targetPath))) {\n            Files.walkFileTree(sourcePath, new SimpleFileVisitor<>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)\n                    throws IOException {\n                    checkDirectoryTraversal(sourcePath, file);\n                    var relativePath = sourcePath.relativize(file);\n                    var entry = new JarEntry(relativePath.toString());\n                    jos.putNextEntry(entry);\n                    Files.copy(file, jos);\n                    jos.closeEntry();\n                    return super.visitFile(file, attrs);\n                }\n            });\n        }\n    }\n\n    /**\n     * Creates directories if absent.\n     *\n     * @param path path must not be null\n     * @throws IOException io exception\n     */\n    public static void createIfAbsent(@NonNull Path path) throws IOException {\n        Assert.notNull(path, \"Path must not be null\");\n\n        if (Files.notExists(path)) {\n            // Create directories\n            Files.createDirectories(path);\n\n            log.debug(\"Created directory: [{}]\", path);\n        }\n    }\n\n    /**\n     * The given path must be empty.\n     *\n     * @param path path must not be null\n     * @throws IOException io exception\n     */\n    public static void ensureEmpty(@NonNull Path path) throws IOException {\n        if (!isEmpty(path)) {\n            throw new DirectoryNotEmptyException(\"Target directory: \" + path + \" was not empty\");\n        }\n    }\n\n    /**\n     * Checks if the given path is empty.\n     *\n     * @param path path must not be null\n     * @return true if the given path is empty; false otherwise\n     * @throws IOException io exception\n     */\n    public static boolean isEmpty(@NonNull Path path) throws IOException {\n        Assert.notNull(path, \"Path must not be null\");\n\n        if (!Files.isDirectory(path) || Files.notExists(path)) {\n            return true;\n        }\n\n        try (Stream<Path> pathStream = Files.list(path)) {\n            return pathStream.findAny().isEmpty();\n        }\n    }\n\n    public static void closeQuietly(final Closeable closeable) {\n        closeQuietly(closeable, null);\n    }\n\n    /**\n     * Closes the given {@link Closeable} as a null-safe operation while consuming IOException by\n     * the given {@code consumer}.\n     *\n     * @param closeable The resource to close, may be null.\n     * @param consumer Consumes the IOException thrown by {@link Closeable#close()}.\n     */\n    public static void closeQuietly(final Closeable closeable,\n        final Consumer<IOException> consumer) {\n        if (closeable != null) {\n            try {\n                closeable.close();\n            } catch (IOException e) {\n                if (consumer != null) {\n                    consumer.accept(e);\n                }\n            }\n        }\n    }\n\n    /**\n     * Checks directory traversal vulnerability.\n     *\n     * @param parentPath parent path must not be null.\n     * @param pathToCheck path to check must not be null\n     */\n    public static void checkDirectoryTraversal(@NonNull Path parentPath,\n        @NonNull Path pathToCheck) {\n        Assert.notNull(parentPath, \"Parent path must not be null\");\n        Assert.notNull(pathToCheck, \"Path to check must not be null\");\n\n        if (pathToCheck.normalize().startsWith(parentPath)) {\n            return;\n        }\n\n        throw new AccessDeniedException(\"Directory traversal detected: \" + pathToCheck,\n            \"problemDetail.directoryTraversal\", new Object[] {parentPath, pathToCheck});\n    }\n\n    /**\n     * Checks directory traversal vulnerability.\n     *\n     * @param parentPath parent path must not be null.\n     * @param pathToCheck path to check must not be null\n     */\n    public static void checkDirectoryTraversal(@NonNull String parentPath,\n        @NonNull String pathToCheck) {\n        checkDirectoryTraversal(Paths.get(parentPath), Paths.get(pathToCheck));\n    }\n\n    /**\n     * Checks directory traversal vulnerability.\n     *\n     * @param parentPath parent path must not be null.\n     * @param pathToCheck path to check must not be null\n     */\n    public static void checkDirectoryTraversal(@NonNull Path parentPath,\n        @NonNull String pathToCheck) {\n        checkDirectoryTraversal(parentPath, Paths.get(pathToCheck));\n    }\n\n    /**\n     * Delete folder recursively without exception throwing.\n     *\n     * @param root the root File to delete\n     */\n    public static void deleteRecursivelyAndSilently(Path root) {\n        try {\n            var deleted = deleteRecursively(root);\n            if (log.isDebugEnabled()) {\n                log.debug(\"Delete {} result: {}\", root, deleted);\n            }\n        } catch (IOException ignored) {\n            // Ignore this error\n        }\n    }\n\n    public static Mono<Boolean> deleteRecursivelyAndSilently(\n        Path root, @Nullable Scheduler scheduler\n    ) {\n        var delete =  Mono.fromSupplier(() -> {\n            try {\n                return deleteRecursively(root);\n            } catch (IOException ignored) {\n                return false;\n            }\n        });\n        if (scheduler != null) {\n            return delete.subscribeOn(scheduler);\n        }\n        return delete;\n    }\n\n\n    public static Mono<Boolean> deleteFileSilently(Path file) {\n        return deleteFileSilently(file, Schedulers.boundedElastic());\n    }\n\n    public static Mono<Boolean> deleteFileSilently(Path file, Scheduler scheduler) {\n        return Mono.fromSupplier(\n                () -> {\n                    if (file == null || !Files.isRegularFile(file)) {\n                        return false;\n                    }\n                    try {\n                        return Files.deleteIfExists(file);\n                    } catch (IOException ignored) {\n                        return false;\n                    }\n                })\n            .subscribeOn(scheduler);\n    }\n\n    public static void copyResource(Resource resource, Path path) {\n        try (var inputStream = resource.getInputStream()) {\n            Files.copy(inputStream, path, REPLACE_EXISTING);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void copy(Path source, Path dest, CopyOption... options) {\n        try {\n            Files.copy(source, dest, options);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void copyRecursively(Path src, Path target, Set<String> excludes)\n        throws IOException {\n        var pathMatcher = new AntPathMatcher();\n        Predicate<Path> shouldExclude = path -> excludes.stream()\n            .anyMatch(pattern -> pathMatcher.match(pattern, path.toString()));\n        Files.walkFileTree(src, new SimpleFileVisitor<>() {\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)\n                throws IOException {\n                if (!shouldExclude.test(src.relativize(file))) {\n                    Files.copy(file, target.resolve(src.relativize(file)), REPLACE_EXISTING);\n                }\n                return super.visitFile(file, attrs);\n            }\n\n            @Override\n            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)\n                throws IOException {\n                if (shouldExclude.test(src.relativize(dir))) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n                Files.createDirectories(target.resolve(src.relativize(dir)));\n                return super.preVisitDirectory(dir, attrs);\n            }\n        });\n    }\n\n    public static Mono<Path> createTempDir(String prefix, @Nullable Scheduler scheduler) {\n        var createTempDir = Mono.fromCallable(() -> Files.createTempDirectory(prefix));\n        if (scheduler != null) {\n            return createTempDir.subscribeOn(scheduler);\n        }\n        return createTempDir;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/HaloUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.util.function.UnaryOperator;\nimport lombok.experimental.UtilityClass;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.util.Assert;\nimport org.springframework.util.StreamUtils;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.util.InvalidUrlException;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport org.springframework.web.util.UriComponentsBuilder.ParserType;\nimport org.springframework.web.util.UriUtils;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * Halo utilities.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@UtilityClass\npublic class HaloUtils {\n\n    /**\n     * Check if the request is an XMLHttpRequest.\n     */\n    public static boolean isXhr(HttpHeaders headers) {\n        return headers.getOrEmpty(\"X-Requested-With\").contains(\"XMLHttpRequest\");\n    }\n\n    /**\n     * <p>Read the file under the classpath as a string.</p>\n     *\n     * @param location the file location relative to classpath\n     * @return file content\n     */\n    public static String readClassPathResourceAsString(String location) {\n        ClassPathResource classPathResource = new ClassPathResource(location);\n        try (InputStream inputStream = classPathResource.getInputStream()) {\n            return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);\n        } catch (IOException e) {\n            throw new IllegalArgumentException(\n                String.format(\"Failed to read class path file as string from location [%s]\",\n                    location), e);\n        }\n    }\n\n    /**\n     * Gets user-agent from server request.\n     *\n     * @param request server request\n     * @return user-agent string if found, otherwise \"unknown\"\n     */\n    public static String userAgentFrom(ServerRequest request) {\n        HttpHeaders httpHeaders = request.headers().asHttpHeaders();\n        // https://en.wikipedia.org/wiki/User_agent\n        String userAgent = httpHeaders.getFirst(HttpHeaders.USER_AGENT);\n        if (StringUtils.isBlank(userAgent)) {\n            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA\n            userAgent = httpHeaders.getFirst(\"Sec-CH-UA\");\n        }\n        return StringUtils.defaultIfBlank(userAgent, \"unknown\");\n    }\n\n    public static String getDayText(Instant instant) {\n        Assert.notNull(instant, \"Instant must not be null\");\n        int dayValue = instant.atZone(ZoneId.systemDefault()).getDayOfMonth();\n        return StringUtils.leftPad(String.valueOf(dayValue), 2, '0');\n    }\n\n    public static String getMonthText(Instant instant) {\n        Assert.notNull(instant, \"Instant must not be null\");\n        int monthValue = instant.atZone(ZoneId.systemDefault()).getMonthValue();\n        return StringUtils.leftPad(String.valueOf(monthValue), 2, '0');\n    }\n\n    public static String getYearText(Instant instant) {\n        Assert.notNull(instant, \"Instant must not be null\");\n        return String.valueOf(instant.atZone(ZoneId.systemDefault()).getYear());\n    }\n\n    /**\n     * Mark the response as no cache.\n     *\n     * @return the server request operator\n     */\n    public static UnaryOperator<ServerRequest> noCache() {\n        return request -> {\n            request.exchange().getAttributes().put(ModelConst.NO_CACHE, true);\n            return request;\n        };\n    }\n\n    /**\n     * Safely convert string to URI. This method will assume the input string is already encoded.\n     * If failed, it will try to encode the string.\n     *\n     * @param uri the uri string\n     * @return the uri\n     */\n    public static URI safeToUri(String uri) {\n        // try to decode first\n        var decodedUri = UriUtils.decode(uri, StandardCharsets.UTF_8);\n        UriComponentsBuilder uriBuilder;\n        try {\n            uriBuilder = UriComponentsBuilder.fromUriString(decodedUri);\n        } catch (InvalidUrlException e) {\n            uriBuilder = UriComponentsBuilder.fromUriString(decodedUri, ParserType.WHAT_WG);\n        }\n        return uriBuilder\n            .build(false)\n            .encode()\n            .toUri();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/IpAddressUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.ServerRequest;\n\n/**\n * Ip address utils.\n * Code from internet.\n */\n@Slf4j\npublic class IpAddressUtils {\n    public static final String UNKNOWN = \"unknown\";\n\n    private static final String[] IP_HEADER_NAMES = {\n        \"X-Forwarded-For\",\n        \"X-Real-IP\",\n        \"Proxy-Client-IP\",\n        \"WL-Proxy-Client-IP\",\n        \"CF-Connecting-IP\",\n        \"HTTP_X_FORWARDED_FOR\",\n        \"HTTP_X_FORWARDED\",\n        \"HTTP_X_CLUSTER_CLIENT_IP\",\n        \"HTTP_CLIENT_IP\",\n        \"HTTP_FORWARDED_FOR\",\n        \"HTTP_FORWARDED\",\n        \"HTTP_VIA\",\n        \"REMOTE_ADDR\",\n    };\n\n    /**\n     * Gets the IP address from request.\n     *\n     * @param request is server http request\n     * @return IP address if found, otherwise {@link #UNKNOWN}.\n     */\n    public static String getClientIp(ServerHttpRequest request) {\n        for (String header : IP_HEADER_NAMES) {\n            String ipList = request.getHeaders().getFirst(header);\n            if (StringUtils.hasText(ipList) && !UNKNOWN.equalsIgnoreCase(ipList)) {\n                String[] ips = ipList.trim().split(\"[,;]\");\n                for (String ip : ips) {\n                    if (StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {\n                        return ip;\n                    }\n                }\n            }\n        }\n        var remoteAddress = request.getRemoteAddress();\n        return remoteAddress == null || remoteAddress.isUnresolved()\n            ? UNKNOWN : remoteAddress.getAddress().getHostAddress();\n    }\n\n\n    /**\n     * Gets the ip address from request.\n     *\n     * @param request http request\n     * @return ip address if found, otherwise {@link #UNKNOWN}.\n     */\n    public static String getIpAddress(ServerRequest request) {\n        try {\n            return getClientIp(request.exchange().getRequest());\n        } catch (Exception e) {\n            log.warn(\"Failed to obtain client IP, and fallback to unknown.\", e);\n            return UNKNOWN;\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/ReactiveUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.time.Duration;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.context.Context;\nimport reactor.util.context.ContextView;\n\n/**\n * Utility class for reactive.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic enum ReactiveUtils {\n    ;\n\n    /**\n     * Default timeout for blocking operation.\n     */\n    public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10);\n\n    /**\n     * Resolve reactive value by blocking operation.\n     *\n     * @param value the normal value or reactive value\n     * @return the resolved value\n     */\n    @Nullable\n    public static Object blockReactiveValue(@Nullable Object value) {\n        return blockReactiveValue(value, DEFAULT_TIMEOUT);\n    }\n\n    /**\n     * Resolve reactive value by blocking operation.\n     *\n     * @param value the normal value or reactive value\n     * @return the resolved value\n     */\n    @Nullable\n    public static Object blockReactiveValue(@Nullable Object value, ContextView contextView) {\n        return blockReactiveValue(value, contextView, DEFAULT_TIMEOUT);\n    }\n\n    /**\n     * Resolve reactive value by blocking operation.\n     *\n     * @param value the normal value or reactive value\n     * @param timeout the timeout of blocking operation\n     * @return the resolved value\n     */\n    @Nullable\n    public static Object blockReactiveValue(\n        @Nullable Object value, @Nullable ContextView contextView, @NonNull Duration timeout\n    ) {\n        if (value == null) {\n            return null;\n        }\n        if (contextView == null) {\n            contextView = Context.empty();\n        }\n        Class<?> clazz = value.getClass();\n        if (Mono.class.isAssignableFrom(clazz)) {\n            return ((Mono<?>) value).contextWrite(contextView).blockOptional(timeout).orElse(null);\n        }\n        if (Flux.class.isAssignableFrom(clazz)) {\n            return ((Flux<?>) value).contextWrite(contextView).collectList().block(timeout);\n        }\n        return value;\n    }\n\n    /**\n     * Resolve reactive value by blocking operation.\n     *\n     * @param value the normal value or reactive value\n     * @param timeout the timeout of blocking operation\n     * @return the resolved value\n     */\n    @Nullable\n    public static Object blockReactiveValue(@Nullable Object value, @NonNull Duration timeout) {\n        return blockReactiveValue(value, null, timeout);\n    }\n\n    /**\n     * Check if the class is a reactive type.\n     *\n     * @param clazz the class to check\n     * @return true if the class is a reactive type, false otherwise\n     */\n    public static boolean isReactiveType(@NonNull Class<?> clazz) {\n        return Mono.class.isAssignableFrom(clazz) || Flux.class.isAssignableFrom(clazz);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/SettingUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.github.fge.jsonpatch.JsonPatchException;\nimport com.github.fge.jsonpatch.mergepatch.JsonMergePatch;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler.Result;\nimport run.halo.app.extension.controller.RequeueException;\nimport tools.jackson.core.JacksonException;\nimport tools.jackson.databind.JsonNode;\nimport tools.jackson.databind.json.JsonMapper;\nimport tools.jackson.databind.node.ObjectNode;\n\npublic enum SettingUtils {\n    ;\n\n    private static final JsonMapper MAPPER = JsonMapper.builder()\n        .changeDefaultPropertyInclusion(v ->\n            v.withValueInclusion(NON_NULL).withContentInclusion(NON_NULL)\n        )\n        .build();\n\n    private static final String VALUE_FIELD = \"value\";\n    private static final String NAME_FIELD = \"name\";\n\n    /**\n     * Read setting default value from {@link Setting} forms.\n     *\n     * @param setting {@link Setting} extension\n     * @return a map of setting default value\n     */\n    @NonNull\n    public static Map<String, String> settingDefinedDefaultValueMap(Setting setting) {\n        List<Setting.SettingForm> forms = setting.getSpec().getForms();\n        if (CollectionUtils.isEmpty(forms)) {\n            return Map.of();\n        }\n        Map<String, String> data = new LinkedHashMap<>();\n        for (Setting.SettingForm form : forms) {\n            String group = form.getGroup();\n            Map<String, JsonNode> groupValue = form.getFormSchema().stream()\n                .map(o -> MAPPER.convertValue(o, JsonNode.class))\n                .filter(jsonNode -> jsonNode.isObject() && jsonNode.has(NAME_FIELD)\n                    && jsonNode.has(VALUE_FIELD))\n                .map(jsonNode -> {\n                    String name = jsonNode.get(NAME_FIELD).asString();\n                    JsonNode value = jsonNode.get(VALUE_FIELD);\n                    return Map.entry(name, value);\n                })\n                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n            data.put(group, MAPPER.writeValueAsString(groupValue));\n        }\n        return data;\n    }\n\n    /**\n     * Create or update config map by provided setting name and configMapName.\n     *\n     * @param client extension client\n     * @param settingName a name for {@link Setting}\n     * @param configMapName a name for {@link ConfigMap}\n     */\n    public static void createOrUpdateConfigMap(ExtensionClient client, String settingName,\n        String configMapName) {\n        Assert.notNull(client, \"Extension client must not be null\");\n        Assert.hasText(settingName, \"Setting name must not be blank\");\n        Assert.hasText(configMapName, \"Config map name must not be blank\");\n\n        client.fetch(Setting.class, settingName)\n            .ifPresentOrElse(setting -> {\n                final var source = SettingUtils.settingDefinedDefaultValueMap(setting);\n                client.fetch(ConfigMap.class, configMapName)\n                    .ifPresentOrElse(configMap -> {\n                        Map<String, String> modified =\n                            Objects.requireNonNullElse(configMap.getData(), Map.of());\n                        var copy = new HashMap<>(modified);\n\n                        var merged = SettingUtils.mergePatch(modified, source);\n                        configMap.setData(merged);\n\n                        if (!Objects.equals(copy, configMap.getData())) {\n                            client.update(configMap);\n                        }\n                    }, () -> {\n                        ConfigMap configMap = new ConfigMap();\n                        configMap.setMetadata(new Metadata());\n                        configMap.getMetadata().setName(configMapName);\n                        configMap.setData(source);\n                        client.create(configMap);\n                    });\n            }, () -> {\n                // requeue if setting was not found\n                throw new RequeueException(Result.requeue(null),\n                    \"Theme setting %s was not found\".formatted(settingName)\n                );\n            });\n    }\n\n    public static ConfigMap populateDefaultConfig(Setting setting, String configMapName) {\n        var data = settingDefinedDefaultValueMap(setting);\n        ConfigMap configMap = new ConfigMap();\n        configMap.setMetadata(new Metadata());\n        configMap.getMetadata().setName(configMapName);\n        configMap.setData(data);\n        return configMap;\n    }\n\n    /**\n     * Construct a JsonMergePatch from a difference between two Maps and apply patch to\n     * {@code source}.\n     *\n     * @param modified the modified object\n     * @param source the source object\n     * @return patched map object\n     */\n    public static Map<String, String> mergePatch(Map<String, String> modified,\n        Map<String, String> source) {\n        var modifiedJson = mapToJsonNode(modified);\n        // original\n        var sourceJson = mapToJsonNode(source);\n        try {\n            // patch\n            var jsonMergePatch = JsonMergePatch.fromJson(modifiedJson);\n            // apply patch to original\n            var patchedNode = jsonMergePatch.apply(sourceJson);\n            return jsonNodeToStringMap(patchedNode);\n        } catch (JsonPatchException e) {\n            throw new JsonParseException(e);\n        }\n    }\n\n    /**\n     * Convert {@link Setting} related configMap data to JsonNode.\n     *\n     * @param configMap {@link ConfigMap} instance\n     * @return JsonNode\n     */\n    public static ObjectNode settingConfigToJson(ConfigMap configMap) {\n        if (configMap.getData() == null) {\n            return MAPPER.createObjectNode();\n        }\n        return mapToObjectNode(configMap.getData());\n    }\n\n    /**\n     * Convert the result of {@link #settingConfigToJson(ConfigMap)} in reverse to Map.\n     *\n     * @param node JsonNode object\n     * @return {@link ConfigMap#getData()}\n     */\n    public static Map<String, String> settingConfigJsonToMap(ObjectNode node) {\n        return jsonNodeToStringMap(node);\n    }\n\n    /**\n     * Convert {@code Map<String, String>} to\n     * {@link com.fasterxml.jackson.databind.node.ObjectNode}.\n     *\n     * @param map source map\n     * @return ObjectNode\n     */\n    private static com.fasterxml.jackson.databind.node.ObjectNode mapToJsonNode(\n        Map<String, String> map) {\n        var objectNode = JsonUtils.mapper().createObjectNode();\n        map.forEach((k, v) -> {\n            if (v == null) {\n                objectNode.putNull(k);\n                return;\n            }\n            try {\n                var value = JsonUtils.mapper().readTree(v);\n                objectNode.set(k, value);\n            } catch (JsonProcessingException ignored) {\n                // ignore exception and put as text\n                objectNode.put(k, v);\n            }\n        });\n        return objectNode;\n    }\n\n    private static ObjectNode mapToObjectNode(Map<String, String> map) {\n        var objectNode = MAPPER.createObjectNode();\n        map.forEach((k, v) -> {\n            if (v == null) {\n                objectNode.putNull(k);\n                return;\n            }\n            try {\n                var value = MAPPER.readTree(v);\n                objectNode.set(k, value);\n            } catch (JacksonException ignored) {\n                // ignore exception and put as text\n                objectNode.put(k, v);\n            }\n        });\n        return objectNode;\n    }\n\n    private static Map<String, String> jsonNodeToStringMap(\n        com.fasterxml.jackson.databind.JsonNode node\n    ) {\n        Map<String, String> stringMap = new LinkedHashMap<>();\n        node.forEachEntry((k, v) -> {\n            if (v == null || v.isNull() || v.isMissingNode()) {\n                stringMap.put(k, null);\n                return;\n            }\n            if (v.isTextual()) {\n                stringMap.put(k, v.asText());\n                return;\n            }\n            if (v.isContainerNode()) {\n                stringMap.put(k, v.toString());\n                return;\n            }\n            stringMap.put(k, v.asText());\n        });\n        return stringMap;\n    }\n\n    private static Map<String, String> jsonNodeToStringMap(JsonNode node) {\n        Map<String, String> stringMap = new LinkedHashMap<>();\n        node.forEachEntry((k, v) -> {\n            if (v == null || v.isNull() || v.isMissingNode()) {\n                stringMap.put(k, null);\n                return;\n            }\n            if (v.isString()) {\n                stringMap.put(k, v.asString());\n                return;\n            }\n            if (v.isContainer()) {\n                stringMap.put(k, v.toString());\n                return;\n            }\n            stringMap.put(k, v.asString());\n        });\n        return stringMap;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/SortUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.util.List;\nimport lombok.experimental.UtilityClass;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\n\n@UtilityClass\npublic class SortUtils {\n    static final String delimiter = \",\";\n\n    /**\n     * <p>Resolve from direction params, e.g. \"name,asc\" or \"name\"</p>\n     *\n     * @param directionParams direction params\n     * @return sort object\n     */\n    public static Sort resolve(List<String> directionParams) {\n        if (CollectionUtils.isEmpty(directionParams)) {\n            return Sort.unsorted();\n        }\n        Sort.Order[] orders = new Sort.Order[directionParams.size()];\n        for (int i = 0; i < directionParams.size(); i++) {\n            String[] parts = directionParams.get(i).split(delimiter);\n            if (parts.length == 1) {\n                orders[i] = new Sort.Order(Sort.Direction.ASC, parts[0]);\n            } else {\n                orders[i] = new Sort.Order(toDirection(parts[1]), parts[0]);\n            }\n        }\n        return Sort.by(orders);\n    }\n\n    private static Sort.Direction toDirection(@NonNull String direction) {\n        Assert.notNull(direction, \"Direction must not be null\");\n        if (direction.contains(\" \")) {\n            throw new IllegalArgumentException(\"Direction must not contain whitespace\");\n        }\n        return Sort.Direction.fromString(direction);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/SystemConfigUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.google.common.hash.Hashing;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.extension.ConfigMap;\n\n/**\n * Utility class for merging configuration maps containing JSON strings.\n *\n * @author johnniang\n * @since 2.22.0\n */\n@Slf4j\npublic enum SystemConfigUtils {\n    ;\n\n    private static final ObjectMapper mapper = JsonUtils.mapper();\n\n    private static final String DATA_SNAPSHOT_ANNO = \"halo.run/data-snapshot\";\n\n    /**\n     * Merge two configuration maps containing JSON strings.\n     *\n     * @param defaultMap the default configuration map\n     * @param overrideMap the override configuration map\n     * @return the merged configuration map\n     * @throws JsonProcessingException if JSON processing fails\n     */\n    public static Map<String, String> mergeMap(\n        Map<String, String> defaultMap, Map<String, String> overrideMap\n    ) throws JsonProcessingException {\n        if (CollectionUtils.isEmpty(defaultMap)) {\n            return overrideMap;\n        }\n        if (CollectionUtils.isEmpty(overrideMap)) {\n            return defaultMap;\n        }\n        var result = new HashMap<>(defaultMap);\n        for (Map.Entry<String, String> entry : overrideMap.entrySet()) {\n            var group = entry.getKey();\n            var overrideJson = entry.getValue();\n            if (result.containsKey(group)) {\n                // Perform a deep merge of the two JSON strings\n                String defaultJson = result.get(group);\n                result.put(group, mergeJsonStrings(defaultJson, overrideJson));\n            } else {\n                // Key only exists in override map\n                result.put(group, overrideJson);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Compute the merged ConfigMap from default and override ConfigMaps.\n     *\n     * @param defaultConfigMap the default ConfigMap\n     * @param overrideConfigMap the override ConfigMap\n     * @return the merged ConfigMap\n     * @throws JsonProcessingException if JSON processing fails\n     */\n    public static ConfigMap mergeConfigMap(\n        ConfigMap defaultConfigMap,\n        ConfigMap overrideConfigMap\n    ) throws JsonProcessingException {\n        var mergedData = mergeMap(\n            defaultConfigMap.getData() != null ? defaultConfigMap.getData() : Map.of(),\n            overrideConfigMap.getData() != null ? overrideConfigMap.getData() : Map.of()\n        );\n        var merged = new ConfigMap();\n        merged.setMetadata(overrideConfigMap.getMetadata());\n        merged.setData(mergedData);\n        return merged;\n    }\n\n    private static String mergeJsonStrings(String mainJson, String updateJson)\n        throws JsonProcessingException {\n        var mainNode = mapper.readTree(mainJson);\n        var updateNode = mapper.readTree(updateJson);\n\n        // This performs a deep merge into the mainNode\n        var mergedNode = deepMerge(mainNode, updateNode);\n\n        return mapper.writeValueAsString(mergedNode);\n    }\n\n    private static JsonNode deepMerge(JsonNode mainNode, JsonNode updateNode) {\n        // If they aren't both objects, the update simply replaces the main\n        if (!mainNode.isObject() || !updateNode.isObject()) {\n            return updateNode;\n        }\n        var mainObject = (ObjectNode) mainNode;\n        updateNode.properties().forEach(entry -> {\n            String key = entry.getKey();\n            JsonNode value = entry.getValue();\n\n            if (mainObject.has(key)) {\n                mainObject.set(key, deepMerge(mainObject.get(key), value));\n            } else {\n                mainObject.set(key, value);\n            }\n        });\n        return mainObject;\n    }\n\n    /**\n     * Populate checksum annotation in the ConfigMap.\n     *\n     * @param configMap the ConfigMap to populate checksum for\n     * @return true if the checksum was updated, false otherwise\n     */\n    public static boolean populateChecksum(ConfigMap configMap) {\n        var toHash = Optional.ofNullable(configMap.getData())\n            .map(Objects::toString)\n            .orElse(\"\");\n        var checksum = Hashing.sha256().hashString(toHash, StandardCharsets.UTF_8)\n            .toString();\n        var metadata = configMap.getMetadata();\n        var notChanged = Optional.ofNullable(metadata.getAnnotations())\n            .map(annotations -> annotations.get(Constant.CHECKSUM_CONFIG_ANNO))\n            .stream()\n            .anyMatch(existingChecksum -> Objects.equals(checksum, existingChecksum));\n        if (notChanged) {\n            log.debug(\"ConfigMap '{}' has not changed.\", configMap.getMetadata().getName());\n            return false;\n        }\n        log.debug(\"ConfigMap '{}' has changed, updating checksum {}.\",\n            configMap.getMetadata().getName(), checksum);\n        if (metadata.getAnnotations() == null) {\n            metadata.setAnnotations(new HashMap<>());\n        }\n        metadata.getAnnotations().put(Constant.CHECKSUM_CONFIG_ANNO, checksum);\n        return true;\n    }\n\n    /**\n     * Update data snapshot annotation in the ConfigMap.\n     *\n     * @param configMap the ConfigMap\n     */\n    public static void updateDataSnapshot(ConfigMap configMap) {\n        Optional.ofNullable(configMap.getData())\n            .map(JsonUtils::objectToJson)\n            .ifPresent(dataJson -> {\n                var metadata = configMap.getMetadata();\n                if (metadata.getAnnotations() == null) {\n                    metadata.setAnnotations(new HashMap<>());\n                }\n                metadata.getAnnotations().put(DATA_SNAPSHOT_ANNO, dataJson);\n            });\n    }\n\n    /**\n     * Get data snapshot from ConfigMap annotations.\n     *\n     * @param configMap the ConfigMap\n     * @return the data snapshot\n     */\n    public static Map<String, String> getDataSnapshot(ConfigMap configMap) {\n        return Optional.ofNullable(configMap.getMetadata().getAnnotations())\n            .map(annotations -> annotations.get(DATA_SNAPSHOT_ANNO))\n            .map(dataJson -> JsonUtils.jsonToObject(\n                dataJson,\n                new TypeReference<Map<String, String>>() {\n                }\n            ))\n            .orElse(Map.of());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/VersionUtils.java",
    "content": "package run.halo.app.infra.utils;\n\nimport com.github.zafarkhaja.semver.Version;\nimport com.github.zafarkhaja.semver.expr.Expression;\nimport lombok.experimental.UtilityClass;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.web.server.ServerWebInputException;\n\n@UtilityClass\npublic class VersionUtils {\n\n    /**\n     * Check if this \"requires\" param satisfies for a given (system) version.\n     *\n     * @param version the version to check\n     * @return true if version satisfies the \"requires\" or if requires was left blank\n     */\n    public static boolean satisfiesRequires(String version, String requires) {\n        String requiresVersion = StringUtils.trim(requires);\n\n        // an exact version x.y.z will implicitly mean the same as >=x.y.z\n        if (requiresVersion.matches(\"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+$\")) {\n            // If exact versions are not allowed in requires, rewrite to >= expression\n            requiresVersion = \">=\" + requiresVersion;\n        }\n        return version.equals(\"0.0.0\") || checkVersionConstraint(version, requiresVersion);\n    }\n\n    /**\n     * Checks if a version satisfies the specified SemVer {@link Expression} string.\n     * If the constraint is empty or null then the method returns true.\n     * Constraint examples: {@code >2.0.0} (simple), {@code \">=1.4.0 & <1.6.0\"} (range).\n     * See\n     * <a href=\"https://github.com/zafarkhaja/jsemver#semver-expressions-api-ranges\">semver-expressions-api-ranges</a> for more info.\n     *\n     * @param version the version to check\n     * @param constraint the SemVer Expression string\n     * @return true if version satisfies the constraint or if constraint was left blank\n     */\n    public static boolean checkVersionConstraint(String version, String constraint) {\n        try {\n            return StringUtils.isBlank(constraint)\n                || \"*\".equals(constraint)\n                || Version.parse(version).satisfies(constraint);\n        } catch (Exception e) {\n            throw new ServerWebInputException(\"Illegal requires version expression.\", null, e);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/utils/YamlUnstructuredLoader.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.springframework.beans.factory.config.YamlProcessor;\nimport org.springframework.core.io.Resource;\nimport run.halo.app.extension.Unstructured;\n\n/**\n * <p>Process the content in yaml that matches the {@link DocumentMatcher} and convert it to an\n * unstructured list.</p>\n * <p>Multiple resources can be processed at one time.</p>\n * <p>The following specified key must be included before the resource can be processed:\n * <pre>\n *     apiVersion\n *     kind\n *     metadata.name\n * </pre>\n * Otherwise, skip it and continue to read the next resource.\n * </p>\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class YamlUnstructuredLoader extends YamlProcessor {\n\n    private static final DocumentMatcher DEFAULT_UNSTRUCTURED_MATCHER = properties -> {\n        if (properties.containsKey(\"apiVersion\")\n            && properties.containsKey(\"kind\")\n            && (properties.containsKey(\"metadata.name\")\n            || properties.containsKey(\"metadata.generateName\"))) {\n            return YamlProcessor.MatchStatus.FOUND;\n        }\n        return MatchStatus.NOT_FOUND;\n    };\n\n    public YamlUnstructuredLoader(Resource... resources) {\n        setResources(resources);\n        setDocumentMatchers(DEFAULT_UNSTRUCTURED_MATCHER);\n    }\n\n    public List<Unstructured> load() {\n        List<Unstructured> unstructuredList = new ArrayList<>();\n        process((properties, map) -> {\n            Unstructured unstructured = JsonUtils.mapToObject(map, Unstructured.class);\n            unstructuredList.add(unstructured);\n        });\n        return unstructuredList;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/webfilter/AdditionalWebFilterChainProxy.java",
    "content": "package run.halo.app.infra.webfilter;\n\nimport lombok.Setter;\nimport org.springframework.core.annotation.AnnotationAwareOrderComparator;\nimport org.springframework.security.web.server.WebFilterChainProxy;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.security.AdditionalWebFilter;\n\npublic class AdditionalWebFilterChainProxy implements WebFilter {\n\n    private final ExtensionGetter extensionGetter;\n\n    @Setter\n    private WebFilterChainProxy.WebFilterChainDecorator filterChainDecorator;\n\n    public AdditionalWebFilterChainProxy(ExtensionGetter extensionGetter) {\n        this.extensionGetter = extensionGetter;\n        this.filterChainDecorator = new WebFilterChainProxy.DefaultWebFilterChainDecorator();\n    }\n\n    @Override\n    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n        return extensionGetter.getEnabledExtensions(AdditionalWebFilter.class)\n            .sort(AnnotationAwareOrderComparator.INSTANCE)\n            .cast(WebFilter.class)\n            .collectList()\n            .map(filters -> filterChainDecorator.decorate(chain, filters))\n            .flatMap(decoratedChain -> decoratedChain.filter(exchange));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/infra/webfilter/LocaleChangeWebFilter.java",
    "content": "package run.halo.app.infra.webfilter;\n\nimport static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_COOKIE_NAME;\n\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.Set;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.ResponseCookie;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.theme.ThemeLocaleContextResolver;\nimport run.halo.app.theme.UserLocaleRequestAttributeWriteFilter;\n\n/**\n * {@link UserLocaleRequestAttributeWriteFilter} is before {@link LocaleChangeWebFilter} to\n * obtain the locale.\n */\n@Component\n@Order(Ordered.HIGHEST_PRECEDENCE + 1)\npublic class LocaleChangeWebFilter implements WebFilter {\n\n    private final ServerWebExchangeMatcher matcher;\n    private final ThemeLocaleContextResolver themeLocaleContextResolver;\n\n    public LocaleChangeWebFilter(ThemeLocaleContextResolver themeLocaleContextResolver) {\n        this.themeLocaleContextResolver = themeLocaleContextResolver;\n        var pathMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, \"/**\");\n        var textHtmlMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);\n        textHtmlMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));\n        matcher = new AndServerWebExchangeMatcher(pathMatcher, textHtmlMatcher);\n    }\n\n    @Override\n    @NonNull\n    public Mono<Void> filter(ServerWebExchange exchange, @NonNull WebFilterChain chain) {\n        return matcher.matches(exchange)\n            .filter(MatchResult::isMatch)\n            .doOnNext(result -> {\n                var localeContext = themeLocaleContextResolver.resolveLocaleContext(exchange);\n                var locale = localeContext.getLocale();\n                if (locale != null) {\n                    setLanguageCookieIfAbsent(exchange, locale);\n                }\n            })\n            .then(Mono.defer(() -> chain.filter(exchange)));\n    }\n\n    void setLanguageCookieIfAbsent(ServerWebExchange exchange, Locale locale) {\n        var languageCookie = exchange.getRequest().getCookies().getFirst(LANGUAGE_COOKIE_NAME);\n        if (languageCookie != null\n            && Objects.equals(languageCookie.getValue(), locale.toLanguageTag())) {\n            return;\n        }\n        var cookie = ResponseCookie.from(LANGUAGE_COOKIE_NAME, locale.toLanguageTag())\n            .path(\"/\")\n            .secure(\"https\".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()))\n            .sameSite(\"Lax\")\n            .build();\n        exchange.getResponse().getCookies().set(LANGUAGE_COOKIE_NAME, cookie);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/migration/BackupFile.java",
    "content": "package run.halo.app.migration;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport lombok.Data;\n\n/**\n * Backup file.\n *\n * @author johnniang\n */\n@Data\npublic class BackupFile {\n\n    @JsonIgnore\n    private Path path;\n\n    /**\n     * Filename of backup file.\n     */\n    private String filename;\n\n    /**\n     * Size of backup file.\n     */\n    private long size;\n\n    /**\n     * Last modified time of backup file.\n     */\n    private Instant lastModifiedTime;\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/migration/BackupReconciler.java",
    "content": "package run.halo.app.migration;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.isDeleted;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.controller.Reconciler.Result.doNotRetry;\nimport static run.halo.app.migration.Constant.HOUSE_KEEPER_FINALIZER;\n\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport reactor.core.Exceptions;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.migration.Backup.Phase;\n\n@Slf4j\n@Component\npublic class BackupReconciler implements Reconciler<Request> {\n\n    private final ExtensionClient client;\n\n    private final MigrationService migrationService;\n\n    private Clock clock;\n\n    public BackupReconciler(ExtensionClient client, MigrationService migrationService) {\n        this.client = client;\n        this.migrationService = migrationService;\n        clock = Clock.systemDefaultZone();\n    }\n\n    /**\n     * Set clock. The method is only for unit test.\n     *\n     * @param clock is new clock\n     */\n    void setClock(Clock clock) {\n        this.clock = clock;\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        return client.fetch(Backup.class, request.name())\n            .map(backup -> {\n                var metadata = backup.getMetadata();\n                var status = backup.getStatus();\n                var spec = backup.getSpec();\n                if (isDeleted(backup)) {\n                    if (removeFinalizers(metadata, Set.of(HOUSE_KEEPER_FINALIZER))) {\n                        migrationService.cleanup(backup).block(ReactiveUtils.DEFAULT_TIMEOUT);\n                        client.update(backup);\n                    }\n                    return doNotRetry();\n                }\n                if (addFinalizers(metadata, Set.of(HOUSE_KEEPER_FINALIZER))) {\n                    client.update(backup);\n                }\n\n                if (Phase.PENDING.equals(status.getPhase())) {\n                    // Do backup\n                    try {\n                        status.setPhase(Phase.RUNNING);\n                        status.setStartTimestamp(Instant.now(clock));\n                        updateStatus(request.name(), status);\n                        // Long period execution when backing up\n                        migrationService.backup(backup).block(Duration.ofMinutes(30));\n                        status.setPhase(Phase.SUCCEEDED);\n                        status.setCompletionTimestamp(Instant.now(clock));\n                        updateStatus(request.name(), status);\n                    } catch (Throwable t) {\n                        var unwrapped = Exceptions.unwrap(t);\n                        log.error(\"Failed to backup\", unwrapped);\n                        // Only happen when shutting down\n                        status.setPhase(Phase.FAILED);\n                        if (unwrapped instanceof InterruptedException) {\n                            status.setFailureReason(\"Interrupted\");\n                            status.setFailureMessage(\"The backup process was interrupted.\");\n                        } else {\n                            status.setFailureReason(\"SystemError\");\n                            status.setFailureMessage(\n                                \"Something went wrong! Error message: \" + unwrapped.getMessage());\n                        }\n                        updateStatus(request.name(), status);\n                    }\n                }\n                // Only happen when failing to update status when interrupted\n                if (Phase.RUNNING.equals(status.getPhase())) {\n                    status.setPhase(Phase.FAILED);\n                    status.setFailureReason(\"UnexpectedExit\");\n                    status.setFailureMessage(\"The backup process may exit abnormally.\");\n                    updateStatus(request.name(), status);\n                }\n                // Check the expires at and requeue if necessary\n                if (isTerminal(status.getPhase())) {\n                    var expiresAt = spec.getExpiresAt();\n                    if (expiresAt != null) {\n                        var now = Instant.now(clock);\n                        if (now.isBefore(expiresAt)) {\n                            return new Result(true, Duration.between(now, expiresAt));\n                        }\n                        client.delete(backup);\n                    }\n                }\n                return doNotRetry();\n            }).orElseGet(Result::doNotRetry);\n    }\n\n    private void updateStatus(String name, Backup.Status status) {\n        client.fetch(Backup.class, name)\n            .ifPresent(backup -> {\n                backup.setStatus(status);\n                client.update(backup);\n            });\n    }\n\n    private static boolean isTerminal(Phase phase) {\n        return Phase.FAILED.equals(phase) || Phase.SUCCEEDED.equals(phase);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Backup())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/migration/MigrationEndpoint.java",
    "content": "package run.halo.app.migration;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.net.MalformedURLException;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.core.io.buffer.DefaultDataBufferFactory;\nimport org.springframework.data.util.Optionals;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.http.codec.multipart.FormFieldPart;\nimport org.springframework.http.codec.multipart.Part;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.util.StreamUtils;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ReactiveUrlDataBufferFetcher;\n\n@Component\npublic class MigrationEndpoint implements CustomEndpoint {\n\n    private final MigrationService migrationService;\n\n    private final ReactiveExtensionClient client;\n\n    private final ReactiveUrlDataBufferFetcher dataBufferFetcher;\n\n    public MigrationEndpoint(MigrationService migrationService,\n        ReactiveExtensionClient client,\n        ReactiveUrlDataBufferFetcher dataBufferFetcher) {\n        this.migrationService = migrationService;\n        this.client = client;\n        this.dataBufferFetcher = dataBufferFetcher;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"MigrationV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"/backup-files\",\n                this::getBackups,\n                builder -> builder.operationId(\"getBackupFiles\")\n                    .tag(tag)\n                    .description(\"Get backup files from backup root.\")\n                    .response(responseBuilder()\n                        .implementationArray(BackupFile.class)\n                    )\n            )\n            .GET(\"/backups/{name}/files/{filename}\",\n                request -> {\n                    var name = request.pathVariable(\"name\");\n                    return client.get(Backup.class, name)\n                        .flatMap(migrationService::download)\n                        .flatMap(backupResource -> ServerResponse.ok()\n                            .header(HttpHeaders.CONTENT_DISPOSITION,\n                                \"attachment; filename=\\\"\" + backupResource.getFilename() + \"\\\"\")\n                            .contentType(MediaType.APPLICATION_OCTET_STREAM)\n                            .bodyValue(backupResource));\n                },\n                builder -> builder\n                    .tag(tag)\n                    .operationId(\"DownloadBackups\")\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .description(\"Backup name.\")\n                        .required(true)\n                        .in(ParameterIn.PATH))\n                    .parameter(parameterBuilder()\n                        .name(\"filename\")\n                        .description(\"Backup filename.\")\n                        .required(true)\n                        .in(ParameterIn.PATH))\n                    .build())\n            .POST(\"/restorations\", request -> request.multipartData()\n                    .map(RestoreRequest::new)\n                    .flatMap(restoreRequest -> {\n                        var content = getContent(restoreRequest)\n                            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                                \"Please upload a file \"\n                                    + \"or provide a download link or backup name.\")));\n                        return migrationService.restore(content);\n                    })\n                    .then(Mono.defer(\n                        () -> ServerResponse.ok().bodyValue(\"Restored successfully!\")\n                    )),\n                builder -> builder\n                    .tag(tag)\n                    .description(\"Restore backup by uploading file \"\n                        + \"or providing download link or backup name.\")\n                    .operationId(\"RestoreBackup\")\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder().implementation(RestoreRequest.class))\n                        )\n                    )\n                    .build())\n            .build();\n    }\n\n    private Mono<ServerResponse> getBackups(ServerRequest request) {\n        var backupFiles = migrationService.getBackupFiles();\n        return ServerResponse.ok().body(backupFiles, BackupFile.class);\n    }\n\n    private Flux<DataBuffer> getContent(RestoreRequest request) {\n        Supplier<Optional<Flux<DataBuffer>>> contentFromFilename = () ->\n            request.getFilename().map(filename -> migrationService.getBackupFile(filename)\n                .map(BackupFile::getPath)\n                .flatMapMany(\n                    path -> DataBufferUtils.read(\n                        path,\n                        DefaultDataBufferFactory.sharedInstance,\n                        StreamUtils.BUFFER_SIZE)));\n\n        Supplier<Optional<Flux<DataBuffer>>> contentFromDownloadUrl = () -> request.getDownloadUrl()\n            .map(downloadURL -> {\n                try {\n                    var url = new URL(downloadURL);\n                    return dataBufferFetcher.fetch(url.toURI());\n                } catch (MalformedURLException e) {\n                    return Flux.<DataBuffer>error(new ServerWebInputException(\n                        \"Invalid download URL: \" + downloadURL));\n                } catch (URISyntaxException e) {\n                    // Should never happen\n                    return Flux.<DataBuffer>error(e);\n                }\n            });\n\n        Supplier<Optional<Flux<DataBuffer>>> contentFromUpload = () -> request.getFile()\n            .map(Part::content);\n\n        Supplier<Optional<Flux<DataBuffer>>> contentFromBackupName = () -> request.getBackupName()\n            .map(backupName -> client.get(Backup.class, backupName)\n                .flatMap(migrationService::download)\n                .flatMapMany(resource -> DataBufferUtils.read(resource,\n                    DefaultDataBufferFactory.sharedInstance,\n                    StreamUtils.BUFFER_SIZE)));\n\n        return Optionals.firstNonEmpty(\n                contentFromUpload,\n                contentFromDownloadUrl,\n                contentFromBackupName,\n                contentFromFilename\n            )\n            .orElseGet(() -> Flux.error(new ServerWebInputException(\"\"\"\n                Please upload a file or provide a download link or backup name or backup filename.\\\n                \"\"\")));\n    }\n\n    @Schema(types = \"object\")\n    public static class RestoreRequest {\n        private final MultiValueMap<String, Part> multipart;\n\n        public RestoreRequest(MultiValueMap<String, Part> multipart) {\n            this.multipart = multipart;\n        }\n\n        @Schema(requiredMode = NOT_REQUIRED, name = \"file\", description = \"Backup file.\")\n        public Optional<FilePart> getFile() {\n            var part = multipart.getFirst(\"file\");\n            if (part instanceof FilePart filePart) {\n                return Optional.of(filePart);\n            }\n            return Optional.empty();\n        }\n\n        @Schema(requiredMode = NOT_REQUIRED, name = \"filename\", description = \"\"\"\n            Filename of backup file in backups root.\\\n            \"\"\")\n        public Optional<String> getFilename() {\n            var part = multipart.getFirst(\"filename\");\n            if (part instanceof FormFieldPart filenamePart) {\n                return Optional.of(filenamePart.value())\n                    .filter(StringUtils::hasText);\n            }\n            return Optional.empty();\n        }\n\n        @Schema(requiredMode = NOT_REQUIRED,\n            name = \"downloadUrl\",\n            description = \"Remote backup HTTP URL.\")\n        public Optional<String> getDownloadUrl() {\n            var part = multipart.getFirst(\"downloadUrl\");\n            if (part instanceof FormFieldPart downloadUrlPart) {\n                return Optional.of(downloadUrlPart.value())\n                    .filter(StringUtils::hasText);\n            }\n            return Optional.empty();\n        }\n\n        @Schema(requiredMode = NOT_REQUIRED,\n            name = \"backupName\",\n            description = \"Backup metadata name.\")\n        public Optional<String> getBackupName() {\n            var part = multipart.getFirst(\"backupName\");\n            if (part instanceof FormFieldPart backupNamePart) {\n                return Optional.of(backupNamePart.value())\n                    .filter(StringUtils::hasText);\n            }\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\n            \"console.api.\" + Constant.GROUP + \"/\" + Constant.VERSION);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/migration/MigrationService.java",
    "content": "package run.halo.app.migration;\n\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\npublic interface MigrationService {\n\n    Mono<Void> backup(Backup backup);\n\n    Mono<Resource> download(Backup backup);\n\n    Mono<Void> restore(Publisher<DataBuffer> content);\n\n    /**\n     * Clean up backup file.\n     *\n     * @param backup backup detail.\n     * @return void publisher.\n     */\n    Mono<Void> cleanup(Backup backup);\n\n    /**\n     * Gets backup files.\n     *\n     * @return backup files, sorted by last modified time.\n     */\n    Flux<BackupFile> getBackupFiles();\n\n    /**\n     * Get backup file by filename.\n     *\n     * @param filename filename of backup file\n     * @return backup file or empty if file is not found\n     */\n    Mono<BackupFile> getBackupFile(String filename);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/migration/impl/MigrationServiceImpl.java",
    "content": "package run.halo.app.migration.impl;\n\nimport static java.nio.file.Files.deleteIfExists;\nimport static java.util.Comparator.comparing;\nimport static org.apache.commons.io.FilenameUtils.isExtension;\nimport static org.springframework.util.FileSystemUtils.copyRecursively;\nimport static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;\nimport static run.halo.app.infra.utils.FileUtils.copyRecursively;\nimport static run.halo.app.infra.utils.FileUtils.createTempDir;\nimport static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;\nimport static run.halo.app.infra.utils.FileUtils.unzip;\n\nimport com.fasterxml.jackson.core.util.MinimalPrettyPrinter;\nimport com.fasterxml.jackson.databind.MappingIterator;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Locale;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Predicate;\nimport java.util.stream.BaseStream;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.reactivestreams.Publisher;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.data.r2dbc.core.R2dbcEntityTemplate;\nimport org.springframework.stereotype.Service;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport org.springframework.transaction.TransactionDefinition;\nimport org.springframework.transaction.reactive.TransactionalOperator;\nimport org.springframework.transaction.support.DefaultTransactionDefinition;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.extension.ExtensionStoreUtil;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.store.ExtensionStore;\nimport run.halo.app.extension.store.ExtensionStoreRepository;\nimport run.halo.app.infra.BackupRootGetter;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.migration.Backup;\nimport run.halo.app.migration.BackupFile;\nimport run.halo.app.migration.MigrationService;\n\n@Slf4j\n@Service\n@RequiredArgsConstructor\nclass MigrationServiceImpl implements MigrationService, InitializingBean {\n\n    private static final int BATCH_SIZE = 100;\n\n    private static final String BACKUP_STORE_PREFIX =\n        ExtensionStoreUtil.buildStoreNamePrefix(Scheme.buildFromType(Backup.class)) + \"/\";\n\n    private final ExtensionStoreRepository repository;\n\n    private final HaloProperties haloProperties;\n\n    private final BackupRootGetter backupRoot;\n\n    private final ReactiveTransactionManager txManager;\n\n    private final R2dbcEntityTemplate entityTemplate;\n\n    private final Set<String> excludes = Set.of(\n        \"**/.git/**\",\n        \"**/node_modules/**\",\n        \"backups/**\",\n        \"db/**\",\n        \"logs/**\",\n        \"indices/**\",\n        \"docker-compose.yaml\",\n        \"docker-compose.yml\",\n        \"mysql/**\",\n        \"mysqlBackup/**\",\n        \"**/.idea/**\",\n        \"**/.vscode/**\",\n        \"attachments/thumbnails/**\"\n    );\n\n    private final ObjectMapper objectMapper = JsonMapper.builder()\n        .defaultPrettyPrinter(new MinimalPrettyPrinter())\n        .build();\n\n    private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter\n        .ofPattern(\"yyyyMMddHHmmss\")\n        .withLocale(Locale.getDefault())\n        .withZone(ZoneId.systemDefault());\n\n    private final Scheduler scheduler = Schedulers.newBoundedElastic(10, 1_00, \"migration-worker\");\n\n    DateTimeFormatter getDateTimeFormatter() {\n        return dateTimeFormatter;\n    }\n\n    ObjectMapper getObjectMapper() {\n        return objectMapper;\n    }\n\n    Path getBackupsRoot() {\n        return backupRoot.get();\n    }\n\n    @Override\n    public Mono<Void> backup(Backup backup) {\n        return Mono.usingWhen(\n            createTempDir(\"halo-full-backup-\", scheduler),\n            tempDir -> backupExtensions(tempDir)\n                .then(Mono.defer(() -> backupWorkDir(tempDir)))\n                .then(Mono.defer(() -> packageBackup(tempDir, backup))),\n            tempDir -> deleteRecursivelyAndSilently(tempDir, scheduler)\n        );\n    }\n\n    @Override\n    public Mono<Resource> download(Backup backup) {\n        return Mono.create(sink -> {\n            var status = backup.getStatus();\n            if (!Backup.Phase.SUCCEEDED.equals(status.getPhase()) || status.getFilename() == null) {\n                sink.error(new ServerWebInputException(\"Current backup is not downloadable.\"));\n                return;\n            }\n            var backupFile = getBackupsRoot().resolve(status.getFilename());\n            var resource = new FileSystemResource(backupFile);\n            if (!resource.exists()) {\n                sink.error(\n                    new NotFoundException(\"problemDetail.migration.backup.notFound\",\n                        new Object[] {},\n                        \"Backup file doesn't exist or deleted.\"));\n                return;\n            }\n            sink.success(resource);\n        });\n    }\n\n    @Override\n    public Mono<Void> restore(Publisher<DataBuffer> content) {\n        var tx = TransactionalOperator.create(txManager);\n        return Mono.usingWhen(\n            createTempDir(\"halo-restore-\", scheduler),\n            tempDir -> unpackBackup(content, tempDir)\n                .then(Mono.defer(() ->\n                    // This step skips index verification such as unique index.\n                    // In order to avoid index conflicts after recovery or\n                    // OptimisticLockingFailureException when updating the same record,\n                    // so we need to truncate all extension stores before saving(create or\n                    // update).\n                    repository.deleteAll()\n                        .then(restoreExtensions(tempDir))\n                        .as(tx::transactional)\n                ))\n                .then(Mono.defer(() -> restoreWorkdir(tempDir))),\n            tempDir -> deleteRecursivelyAndSilently(tempDir, scheduler)\n        );\n    }\n\n    @Override\n    public Mono<Void> cleanup(Backup backup) {\n        return Mono.<Void>create(sink -> {\n            var status = backup.getStatus();\n            if (status == null || !StringUtils.hasText(status.getFilename())) {\n                sink.success();\n                return;\n            }\n            var filename = status.getFilename();\n            var backupsRoot = getBackupsRoot();\n            var backupFile = backupsRoot.resolve(filename);\n            try {\n                checkDirectoryTraversal(backupsRoot, backupFile);\n                deleteIfExists(backupFile);\n                sink.success();\n            } catch (IOException e) {\n                sink.error(e);\n            }\n        }).subscribeOn(scheduler);\n    }\n\n    @Override\n    public Flux<BackupFile> getBackupFiles() {\n        return Flux.using(\n                () -> Files.list(getBackupsRoot()),\n                Flux::fromStream,\n                BaseStream::close\n            )\n            .filter(Files::isRegularFile)\n            .filter(Files::isReadable)\n            .filter(path -> isExtension(path.getFileName().toString(), \"zip\"))\n            .map(this::toBackupFile)\n            .sort(comparing(BackupFile::getLastModifiedTime).reversed()\n                .thenComparing(BackupFile::getFilename)\n            )\n            .subscribeOn(this.scheduler);\n    }\n\n    @Override\n    public Mono<BackupFile> getBackupFile(String filename) {\n        return Mono.fromCallable(() -> {\n            var backupsRoot = getBackupsRoot();\n            var backupFilePath = backupsRoot.resolve(filename);\n            checkDirectoryTraversal(backupsRoot, backupFilePath);\n            if (Files.notExists(backupFilePath)) {\n                return null;\n            }\n            return toBackupFile(backupFilePath);\n        }).subscribeOn(this.scheduler);\n    }\n\n    private BackupFile toBackupFile(Path path) {\n        var backupFile = new BackupFile();\n        backupFile.setPath(path);\n        backupFile.setFilename(path.getFileName().toString());\n        try {\n            backupFile.setSize(Files.size(path));\n            backupFile.setLastModifiedTime(Files.getLastModifiedTime(path).toInstant());\n            return backupFile;\n        } catch (IOException e) {\n            throw Exceptions.propagate(e);\n        }\n    }\n\n    private Mono<Void> restoreWorkdir(Path backupRoot) {\n        return Mono.<Void>create(sink -> {\n            try {\n                var workdir = backupRoot.resolve(\"workdir\");\n                if (Files.exists(workdir)) {\n                    copyRecursively(workdir, haloProperties.getWorkDir());\n                }\n                sink.success();\n            } catch (IOException e) {\n                sink.error(e);\n            }\n        }).subscribeOn(scheduler);\n    }\n\n    private Mono<Void> restoreExtensions(Path backupRoot) {\n        var extensionsPath = backupRoot.resolve(\"extensions.data\");\n        if (Files.notExists(extensionsPath)) {\n            return Mono.error(new ServerWebInputException(\"Extensions data file not found.\"));\n        }\n        var reader = objectMapper.readerFor(ExtensionStore.class);\n        var total = new AtomicInteger(0);\n        return Mono.<Void, MappingIterator<ExtensionStore>>using(\n                () -> reader.readValues(extensionsPath.toFile()),\n                itr -> Flux.fromIterable(() -> itr)\n                    // reset version\n                    .filter(Predicate.not(MigrationServiceImpl::isInBlocklist))\n                    .doOnNext(extensionStore -> extensionStore.setVersion(null))\n                    .buffer(100)\n                    .flatMap(repository::saveAll)\n                    .doOnNext(store -> {\n                        var t = total.incrementAndGet();\n                        if (t % BATCH_SIZE == 0) {\n                            log.info(\"Restored {} extension stores so far...\", t);\n                        }\n                        if (log.isDebugEnabled()) {\n                            log.debug(\"Restored extension store: {}\", store.getName());\n                        }\n                    })\n                    .then()\n                    .doOnSuccess(ignored -> log.info(\n                        \"Extension stores restore completed, total {} record(s) restored.\",\n                        total.get())\n                    ),\n                FileUtils::closeQuietly)\n            .subscribeOn(scheduler);\n    }\n\n    private Mono<Void> unpackBackup(Publisher<DataBuffer> content, Path target) {\n        return unzip(content, target, scheduler);\n    }\n\n    private Mono<Void> packageBackup(Path baseDir, Backup backup) {\n        return Mono.fromCallable(\n            () -> {\n                var backupsFolder = getBackupsRoot();\n                Files.createDirectories(backupsFolder);\n                var backupName = backup.getMetadata().getName();\n                var startTimestamp = backup.getStatus().getStartTimestamp();\n                var timePart = this.dateTimeFormatter.format(startTimestamp);\n                var backupFile = backupsFolder.resolve(timePart + '-' + backupName + \".zip\");\n                FileUtils.zip(baseDir, backupFile);\n                backup.getStatus().setFilename(backupFile.getFileName().toString());\n                backup.getStatus().setSize(Files.size(backupFile));\n                return backupsFolder;\n            }\n        ).subscribeOn(scheduler).then();\n    }\n\n    private Mono<Void> backupWorkDir(Path baseDir) {\n        return Mono.fromCallable(() -> {\n            var workdirPath = Files.createDirectory(baseDir.resolve(\"workdir\"));\n            copyRecursively(haloProperties.getWorkDir(), workdirPath, excludes);\n            return workdirPath;\n        }).subscribeOn(scheduler).then();\n    }\n\n    private Mono<Void> backupExtensions(Path baseDir) {\n        var total = new AtomicInteger(0);\n        var excludes = new AtomicInteger(0);\n        return Mono.fromCallable(() -> Files.createFile(baseDir.resolve(\"extensions.data\")))\n            .subscribeOn(scheduler)\n            .flatMap(extensionsPath -> Mono.usingWhen(\n                Mono.fromCallable(\n                        () -> objectMapper.writerFor(ExtensionStore.class)\n                            .writeValuesAsArray(extensionsPath.toFile())\n                    )\n                    .subscribeOn(scheduler),\n                seqWriter -> fetchAllExtensionStores(BATCH_SIZE)\n                    .filter(extensionStore -> {\n                        if (isInBlocklist(extensionStore)) {\n                            excludes.incrementAndGet();\n                            return false;\n                        }\n                        return true;\n                    })\n                    .buffer(100)\n                    .publishOn(scheduler)\n                    .concatMap(stores -> Mono.fromCallable(() -> {\n                        total.addAndGet(stores.size());\n                        seqWriter.writeAll(stores);\n                        log.debug(\"Backed up {} extension stores so far...\", total.get());\n                        return null;\n                    }))\n                    .then()\n                    .doOnSuccess(ignored -> log.info(\n                        \"\"\"\n                            Extension stores backup completed, total {} record(s) backed up, \\\n                            {} record(s) excluded.\"\"\",\n                        total.get(), excludes.get()\n                    )),\n                seqWriter -> Mono.fromCallable(() -> {\n                    seqWriter.flush();\n                    FileUtils.closeQuietly(seqWriter);\n                    return null;\n                }).subscribeOn(scheduler)\n            ));\n    }\n\n    private Flux<ExtensionStore> fetchAllExtensionStores(int batchSize) {\n        var txDefinition = new DefaultTransactionDefinition(TransactionDefinition.withDefaults());\n        txDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);\n        txDefinition.setReadOnly(true);\n        var tx = TransactionalOperator.create(txManager, txDefinition);\n        return entityTemplate.select(ExtensionStore.class)\n            .withFetchSize(batchSize)\n            .all()\n            .as(tx::transactional);\n    }\n\n    @Override\n    public void afterPropertiesSet() throws Exception {\n        Files.createDirectories(getBackupsRoot());\n    }\n\n    private static boolean isInBlocklist(ExtensionStore store) {\n        return store.getName().startsWith(BACKUP_STORE_PREFIX);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/DefaultNotificationCenter.java",
    "content": "package run.halo.app.notification;\n\nimport static org.apache.commons.lang3.StringUtils.defaultString;\n\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Optional;\nimport lombok.Builder;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.notification.Notification;\nimport run.halo.app.core.extension.notification.NotifierDescriptor;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.notification.endpoint.SubscriptionRouter;\n\n/**\n * A default implementation of {@link NotificationCenter}.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class DefaultNotificationCenter implements NotificationCenter {\n    private final ReactiveExtensionClient client;\n    private final NotificationSender notificationSender;\n    private final NotifierConfigStore notifierConfigStore;\n    private final ReasonNotificationTemplateSelector notificationTemplateSelector;\n    private final UserNotificationPreferenceService userNotificationPreferenceService;\n    private final NotificationTemplateRender notificationTemplateRender;\n    private final SubscriptionRouter subscriptionRouter;\n    private final RecipientResolver recipientResolver;\n    private final SubscriptionService subscriptionService;\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    public Mono<Void> notify(Reason reason) {\n        return recipientResolver.resolve(reason)\n            .doOnNext(subscriber -> {\n                log.debug(\"Dispatching notification to subscriber [{}] for reason [{}]\",\n                    subscriber, reason.getMetadata().getName());\n            })\n            .flatMap(subscriber -> dispatchNotification(reason, subscriber))\n            .then();\n    }\n\n    @Override\n    public Mono<Subscription> subscribe(Subscription.Subscriber subscriber,\n        Subscription.InterestReason reason) {\n        return unsubscribe(subscriber, reason)\n            .then(Mono.defer(() -> {\n                var subscription = new Subscription();\n                subscription.setMetadata(new Metadata());\n                subscription.getMetadata().setGenerateName(\"subscription-\");\n                subscription.setSpec(new Subscription.Spec());\n                subscription.getSpec().setUnsubscribeToken(Subscription.generateUnsubscribeToken());\n                subscription.getSpec().setSubscriber(subscriber);\n                Subscription.InterestReason.ensureSubjectHasValue(reason);\n                subscription.getSpec().setReason(reason);\n                return client.create(subscription);\n            }));\n    }\n\n    @Override\n    public Mono<Void> unsubscribe(Subscription.Subscriber subscriber) {\n        return subscriptionService.remove(subscriber).then();\n    }\n\n    @Override\n    public Mono<Void> unsubscribe(Subscription.Subscriber subscriber,\n        Subscription.InterestReason reason) {\n        return subscriptionService.remove(subscriber, reason).then();\n    }\n\n    Flux<String> getNotifiersBySubscriber(Subscriber subscriber, Reason reason) {\n        var reasonType = reason.getSpec().getReasonType();\n        return userNotificationPreferenceService.getByUser(subscriber.name())\n            .map(UserNotificationPreference::getReasonTypeNotifier)\n            .map(reasonTypeNotification -> reasonTypeNotification.getNotifiers(reasonType))\n            .flatMapMany(Flux::fromIterable);\n    }\n\n    Mono<Void> dispatchNotification(Reason reason, Subscriber subscriber) {\n        return getNotifiersBySubscriber(subscriber, reason)\n            .flatMap(notifierName -> client.fetch(NotifierDescriptor.class, notifierName))\n            .flatMap(descriptor -> prepareNotificationElement(subscriber, reason, descriptor))\n            .flatMap(element -> {\n                var dispatchMono = sendNotification(element);\n                if (subscriber.isAnonymous()) {\n                    return dispatchMono;\n                }\n                // create notification for user\n                var innerNofificationMono = createNotification(element);\n                return Mono.when(dispatchMono, innerNofificationMono);\n            })\n            .then();\n    }\n\n    Mono<NotificationElement> prepareNotificationElement(Subscriber subscriber, Reason reason,\n        NotifierDescriptor descriptor) {\n        return getLocaleFromSubscriber(subscriber)\n            .flatMap(locale -> inferenceTemplate(reason, subscriber, locale))\n            .map(notificationContent -> NotificationElement.builder()\n                .descriptor(descriptor)\n                .reason(reason)\n                .subscriber(subscriber)\n                .reasonType(notificationContent.reasonType())\n                .notificationTitle(notificationContent.title())\n                .reasonAttributes(notificationContent.reasonAttributes())\n                .notificationRawBody(defaultString(notificationContent.rawBody()))\n                .notificationHtmlBody(defaultString(notificationContent.htmlBody()))\n                .build()\n            );\n    }\n\n    Mono<Void> sendNotification(NotificationElement notificationElement) {\n        var descriptor = notificationElement.descriptor();\n        var subscriber = notificationElement.subscriber();\n        final var notifierExtName = descriptor.getSpec().getNotifierExtName();\n        return notificationContextFrom(notificationElement)\n            .flatMap(notificationContext -> notificationSender.sendNotification(notifierExtName,\n                    notificationContext)\n                .onErrorResume(throwable -> {\n                    log.error(\n                        \"Failed to send notification to subscriber [{}] through notifier [{}]\",\n                        subscriber,\n                        descriptor.getSpec().getDisplayName(),\n                        throwable);\n                    return Mono.empty();\n                })\n            )\n            .then();\n    }\n\n    Mono<Notification> createNotification(NotificationElement notificationElement) {\n        var reason = notificationElement.reason();\n        var subscriber = notificationElement.subscriber();\n        return client.fetch(User.class, subscriber.name())\n            .flatMap(user -> {\n                Notification notification = new Notification();\n                notification.setMetadata(new Metadata());\n                notification.getMetadata().setGenerateName(\"notification-\");\n                notification.setSpec(new Notification.NotificationSpec());\n                notification.getSpec().setTitle(notificationElement.notificationTitle());\n                notification.getSpec().setRawContent(notificationElement.notificationRawBody());\n                notification.getSpec().setHtmlContent(notificationElement.notificationHtmlBody);\n                notification.getSpec().setRecipient(subscriber.name());\n                notification.getSpec().setReason(reason.getMetadata().getName());\n                notification.getSpec().setUnread(true);\n                return client.create(notification);\n            });\n    }\n\n    private ReasonAttributes toReasonAttributes(Reason reason) {\n        var model = new ReasonAttributes();\n        var attributes = reason.getSpec().getAttributes();\n        if (attributes != null) {\n            model.putAll(attributes);\n        }\n        return model;\n    }\n\n    Mono<NotificationContext> notificationContextFrom(NotificationElement element) {\n        final var descriptorName = element.descriptor().getMetadata().getName();\n        final var reason = element.reason();\n        final var descriptor = element.descriptor();\n        final var subscriber = element.subscriber();\n\n        var messagePayload = new NotificationContext.MessagePayload();\n        messagePayload.setTitle(element.notificationTitle());\n        messagePayload.setRawBody(element.notificationRawBody());\n        messagePayload.setHtmlBody(element.notificationHtmlBody());\n        messagePayload.setAttributes(element.reasonAttributes());\n\n        var message = new NotificationContext.Message();\n        message.setRecipient(subscriber.name());\n        message.setPayload(messagePayload);\n        message.setTimestamp(reason.getMetadata().getCreationTimestamp());\n        var reasonSubject = reason.getSpec().getSubject();\n        var subject = NotificationContext.Subject.builder()\n            .apiVersion(reasonSubject.getApiVersion())\n            .kind(reasonSubject.getKind())\n            .title(reasonSubject.getTitle())\n            .url(reasonSubject.getUrl())\n            .build();\n        message.setSubject(subject);\n\n        var notificationContext = new NotificationContext();\n        notificationContext.setMessage(message);\n\n        return Mono.just(notificationContext)\n            .flatMap(context -> {\n                Mono<Void> receiverConfigMono =\n                    Optional.ofNullable(descriptor.getSpec().getReceiverSettingRef())\n                        .map(ref -> notifierConfigStore.fetchReceiverConfig(descriptorName)\n                            .doOnNext(context::setReceiverConfig)\n                            .then()\n                        )\n                        .orElse(Mono.empty());\n\n                Mono<Void> senderConfigMono =\n                    Optional.ofNullable(descriptor.getSpec().getSenderSettingRef())\n                        .map(ref -> notifierConfigStore.fetchSenderConfig(descriptorName)\n                            .doOnNext(context::setSenderConfig)\n                            .then()\n                        )\n                        .orElse(Mono.empty());\n\n                return Mono.when(receiverConfigMono, senderConfigMono)\n                    .thenReturn(context);\n            });\n    }\n\n    Mono<NotificationContent> inferenceTemplate(Reason reason, Subscriber subscriber,\n        Locale locale) {\n        var reasonTypeName = reason.getSpec().getReasonType();\n        return getReasonType(reasonTypeName)\n            .flatMap(reasonType -> notificationTemplateSelector.select(reasonTypeName, locale)\n                .flatMap(template -> {\n                    final var templateContent = template.getSpec().getTemplate();\n                    var model = toReasonAttributes(reason);\n                    var subscriberInfo = new HashMap<>();\n                    if (subscriber.isAnonymous()) {\n                        subscriberInfo.put(\"displayName\", subscriber.getEmail().orElseThrow());\n                    } else {\n                        subscriberInfo.put(\"displayName\", \"@\" + subscriber.username());\n                    }\n                    subscriberInfo.put(\"id\", subscriber.name());\n                    model.put(\"subscriber\", subscriberInfo);\n\n                    var unsubscriptionMono = getUnsubscribeUrl(subscriber.subscriptionName())\n                        .doOnNext(url -> model.put(\"unsubscribeUrl\", url));\n\n                    var builder = NotificationContent.builder()\n                        .reasonType(reasonType)\n                        .reasonAttributes(model);\n\n                    var titleMono = notificationTemplateRender\n                        .render(templateContent.getTitle(), model)\n                        .doOnNext(builder::title);\n\n                    var rawBodyMono = notificationTemplateRender\n                        .render(templateContent.getRawBody(), model)\n                        .doOnNext(builder::rawBody);\n\n                    var htmlBodyMono = notificationTemplateRender\n                        .render(templateContent.getHtmlBody(), model)\n                        .doOnNext(builder::htmlBody);\n                    return Mono.when(unsubscriptionMono, titleMono, rawBodyMono, htmlBodyMono)\n                        .then(Mono.fromSupplier(builder::build));\n                })\n            );\n    }\n\n    @Builder\n    record NotificationContent(String title, String rawBody, String htmlBody, ReasonType reasonType,\n                               ReasonAttributes reasonAttributes) {\n    }\n\n    Mono<String> getUnsubscribeUrl(String subscriptionName) {\n        return client.get(Subscription.class, subscriptionName)\n            .map(subscriptionRouter::getUnsubscribeUrl);\n    }\n\n    @Builder\n    record NotificationElement(ReasonType reasonType, Reason reason,\n                               Subscriber subscriber, NotifierDescriptor descriptor,\n                               String notificationTitle,\n                               String notificationRawBody,\n                               String notificationHtmlBody,\n                               ReasonAttributes reasonAttributes) {\n    }\n\n    Mono<ReasonType> getReasonType(String reasonTypeName) {\n        return client.get(ReasonType.class, reasonTypeName);\n    }\n\n    Mono<Locale> getLocaleFromSubscriber(Subscriber subscriber) {\n        // TODO get locale from subscriber\n        return environmentFetcher.getBasic()\n            .map(SystemSetting.Basic::useSystemLocale)\n            .map(localeOpt -> localeOpt.orElse(Locale.getDefault()));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/DefaultNotificationReasonEmitter.java",
    "content": "package run.halo.app.notification;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport java.util.List;\nimport java.util.function.Consumer;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.exception.NotFoundException;\n\n/**\n * A default {@link NotificationReasonEmitter} implementation.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultNotificationReasonEmitter implements NotificationReasonEmitter {\n\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<Void> emit(String reasonType,\n        Consumer<ReasonPayload.ReasonPayloadBuilder> builder) {\n        Assert.notNull(reasonType, \"Reason type must not be empty.\");\n        var reason = createReason(reasonType, buildReasonPayload(builder));\n        return validateReason(reason)\n            .then(Mono.defer(() -> client.create(reason)))\n            .then();\n    }\n\n    Mono<Void> validateReason(Reason reason) {\n        String reasonTypeName = reason.getSpec().getReasonType();\n        return client.fetch(ReasonType.class, reasonTypeName)\n            .switchIfEmpty(Mono.error(new NotFoundException(\n                \"ReasonType [\" + reasonTypeName + \"] not found, do you forget to register it?\"))\n            )\n            .doOnNext(reasonType -> {\n                var valueMap = reason.getSpec().getAttributes();\n                nullSafeList(reasonType.getSpec().getProperties())\n                    .forEach(property -> {\n                        if (property.isOptional()) {\n                            return;\n                        }\n                        if (valueMap.get(property.getName()) == null) {\n                            throw new IllegalArgumentException(\n                                \"Reason property [\" + property.getName() + \"] is required.\");\n                        }\n                    });\n            })\n            .then();\n    }\n\n    <T> List<T> nullSafeList(List<T> t) {\n        return defaultIfNull(t, List.of());\n    }\n\n    Reason createReason(String reasonType, ReasonPayload reasonData) {\n        Reason reason = new Reason();\n        reason.setMetadata(new Metadata());\n        reason.getMetadata().setGenerateName(\"reason-\");\n        reason.setSpec(new Reason.Spec());\n        if (reasonData.getAuthor() != null) {\n            reason.getSpec().setAuthor(reasonData.getAuthor().name());\n        }\n        reason.getSpec().setReasonType(reasonType);\n        reason.getSpec().setSubject(reasonData.getSubject());\n\n        var reasonAttributes = new ReasonAttributes();\n        if (reasonData.getAttributes() != null) {\n            reasonAttributes.putAll(reasonData.getAttributes());\n        }\n        reason.getSpec().setAttributes(reasonAttributes);\n        return reason;\n    }\n\n    ReasonPayload buildReasonPayload(Consumer<ReasonPayload.ReasonPayloadBuilder> reasonData) {\n        var builder = ReasonPayload.builder();\n        reasonData.accept(builder);\n        return builder.build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/DefaultNotificationSender.java",
    "content": "package run.halo.app.notification;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.UUID;\nimport lombok.AllArgsConstructor;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.DefaultController;\nimport run.halo.app.extension.controller.DefaultQueue;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequestQueue;\nimport run.halo.app.plugin.extensionpoint.ExtensionDefinition;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * A default {@link NotificationSender} implementation.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Slf4j\n@Component\npublic class DefaultNotificationSender\n    implements NotificationSender, Reconciler<DefaultNotificationSender.QueueItem>,\n    SmartLifecycle {\n    private static final Duration TIMEOUT = Duration.ofSeconds(30);\n    private final ReactiveExtensionClient client;\n    private final ExtensionGetter extensionGetter;\n\n    private final RequestQueue<QueueItem> requestQueue;\n\n    private final Controller controller;\n\n    private boolean running = false;\n\n    /**\n     * Constructs a new notification sender with the given {@link ReactiveExtensionClient} and\n     * {@link ExtensionGetter}.\n     */\n    public DefaultNotificationSender(ReactiveExtensionClient client,\n        ExtensionGetter extensionGetter) {\n        this.client = client;\n        this.extensionGetter = extensionGetter;\n        requestQueue = new DefaultQueue<>(Instant::now);\n        controller = this.setupWith(null);\n    }\n\n    @Override\n    public Mono<Void> sendNotification(String notifierExtensionName, NotificationContext context) {\n        return selectNotifier(notifierExtensionName)\n            .flatMap(notifier -> Mono.fromRunnable(\n                    () -> {\n                        var item = new QueueItem(UUID.randomUUID().toString(),\n                            () -> notifier.notify(context).block(TIMEOUT), 0);\n                        requestQueue.addImmediately(item);\n                    })\n                .subscribeOn(Schedulers.boundedElastic())\n            )\n            .then();\n    }\n\n    Mono<ReactiveNotifier> selectNotifier(String notifierExtensionName) {\n        return client.fetch(ExtensionDefinition.class, notifierExtensionName)\n            .flatMap(extDefinition -> extensionGetter.getEnabledExtensions(\n                    ReactiveNotifier.class)\n                .filter(notifier -> notifier.getClass().getName()\n                    .equals(extDefinition.getSpec().getClassName())\n                )\n                .next()\n            );\n    }\n\n    @Override\n    public Result reconcile(QueueItem request) {\n        if (request.getTimes() > 3) {\n            log.error(\"Failed to send notification after retrying 3 times, discard it.\");\n            return Result.doNotRetry();\n        }\n        log.debug(\"Executing send notification task, [{}] remaining to-do tasks\",\n            requestQueue.size());\n        request.setTimes(request.getTimes() + 1);\n        request.getTask().run();\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return new DefaultController<>(\n            this.getClass().getName(),\n            this,\n            requestQueue,\n            null,\n            Duration.ofMillis(100),\n            Duration.ofSeconds(1000),\n            5\n        );\n    }\n\n    @Override\n    public void start() {\n        controller.start();\n        running = true;\n    }\n\n    @Override\n    public void stop() {\n        running = false;\n        controller.dispose();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return running;\n    }\n\n    /**\n     * <p>Queue item for {@link #requestQueue}.</p>\n     * <p>It holds a {@link Runnable} and a {@link #times} field.</p>\n     * <p>{@link Runnable} used to send email when consuming.</p>\n     * <p>{@link #times} will be used to record the number of\n     * times the task has been executed, if retry three times on failure, it will be discarded.</p>\n     * <p>It also holds a {@link #id} field, which is used to identify the item. queue item with\n     * the same id is considered to be the same item to ensure that controller can\n     * discard the existing item in the queue when item re-queued on failure.</p>\n     */\n    @Getter\n    @AllArgsConstructor\n    @EqualsAndHashCode(onlyExplicitlyIncluded = true)\n    public static class QueueItem {\n\n        @EqualsAndHashCode.Include\n        private final String id;\n\n        private final Runnable task;\n\n        @Setter\n        private int times;\n    }\n\n}\n\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/DefaultNotificationService.java",
    "content": "package run.halo.app.notification;\n\nimport java.time.Instant;\nimport java.util.List;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Notification;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.exception.AccessDeniedException;\n\n/**\n * A default implementation of {@link UserNotificationService}.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultNotificationService implements UserNotificationService {\n\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<ListResult<Notification>> listByUser(String username, UserNotificationQuery query) {\n        return client.listBy(Notification.class, query.toListOptions(), query.toPageRequest());\n    }\n\n    @Override\n    public Mono<Notification> markAsRead(String username, String name) {\n        return client.fetch(Notification.class, name)\n            .filter(notification -> isRecipient(notification, username))\n            .flatMap(notification -> {\n                notification.getSpec().setUnread(false);\n                notification.getSpec().setLastReadAt(Instant.now());\n                return client.update(notification);\n            });\n    }\n\n    @Override\n    public Flux<String> markSpecifiedAsRead(String username, List<String> names) {\n        return Flux.fromIterable(names)\n            .flatMap(name -> markAsRead(username, name))\n            .map(notification -> notification.getMetadata().getName());\n    }\n\n    @Override\n    public Mono<Notification> deleteByName(String username, String name) {\n        return client.get(Notification.class, name)\n            .doOnNext(notification -> {\n                var recipient = notification.getSpec().getRecipient();\n                if (!username.equals(recipient)) {\n                    throw new AccessDeniedException(\n                        \"You have no permission to delete this notification.\");\n                }\n            })\n            .flatMap(client::delete);\n    }\n\n    static boolean isRecipient(Notification notification, String username) {\n        Assert.notNull(notification, \"Notification must not be null\");\n        Assert.notNull(username, \"Username must not be null\");\n        return username.equals(notification.getSpec().getRecipient());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/DefaultNotificationTemplateRender.java",
    "content": "package run.halo.app.notification;\n\nimport static org.apache.commons.lang3.StringUtils.defaultString;\n\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.spring6.SpringTemplateEngine;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n/**\n * <p>Default implementation of {@link NotificationTemplateRender}.</p>\n * <p>This implementation use {@link TemplateEngine} to render template, and the template engine\n * use {@link StringTemplateResolver} to resolve template, so the template\n * in {@link #render(String template, Map)} is template content.</p>\n * <p>Template syntax:\n * <a href=\"https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#textual-syntax\">usingthymeleaf.html#textual-syntax</a>\n * </p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultNotificationTemplateRender implements NotificationTemplateRender {\n\n    private static final TemplateEngine TEMPLATE_ENGINE = createTemplateEngine();\n\n    private final SystemConfigFetcher environmentFetcher;\n    private final ExternalUrlSupplier externalUrlSupplier;\n\n    @Override\n    public Mono<String> render(String template, Map<String, Object> model) {\n        var context = new Context(Locale.getDefault(), model);\n        var externalUrl = Optional.ofNullable(externalUrlSupplier.getRaw())\n            .map(url -> StringUtils.removeEnd(url.toString(), \"/\"))\n            .orElse(StringUtils.EMPTY);\n        var globalAttributeMono = getBasicSetting()\n            .doOnNext(basic -> {\n                var site = new HashMap<>();\n                site.put(\"title\", basic.getTitle());\n                site.put(\"logo\", basic.getLogo());\n                site.put(\"subtitle\", basic.getSubtitle());\n                site.put(\"url\", externalUrl);\n                context.setVariable(\"site\", site);\n            });\n        return Mono.when(globalAttributeMono)\n            .then(Mono.fromSupplier(() ->\n                TEMPLATE_ENGINE.process(defaultString(template), context)));\n    }\n\n    static TemplateEngine createTemplateEngine() {\n        var template = new SpringTemplateEngine();\n        template.setTemplateResolver(new StringTemplateResolver());\n        return template;\n    }\n\n    Mono<SystemSetting.Basic> getBasicSetting() {\n        return environmentFetcher.fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/DefaultNotifierConfigStore.java",
    "content": "package run.halo.app.notification;\n\nimport static run.halo.app.extension.MetadataUtil.SYSTEM_FINALIZER;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Secret;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * A default implementation of {@link NotifierConfigStore}.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultNotifierConfigStore implements NotifierConfigStore {\n    public static final String SECRET_NAME = \"notifier-setting-secret\";\n    public static final String RECEIVER_KEY = \"receiver\";\n    public static final String SENDER_KEY = \"sender\";\n\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<ObjectNode> fetchReceiverConfig(String notifierDescriptorName) {\n        return fetchConfig(notifierDescriptorName)\n            .mapNotNull(setting -> (ObjectNode) setting.get(RECEIVER_KEY))\n            .defaultIfEmpty(JsonNodeFactory.instance.objectNode());\n    }\n\n    @Override\n    public Mono<ObjectNode> fetchSenderConfig(String notifierDescriptorName) {\n        return fetchConfig(notifierDescriptorName)\n            .mapNotNull(setting -> (ObjectNode) setting.get(SENDER_KEY))\n            .defaultIfEmpty(JsonNodeFactory.instance.objectNode());\n    }\n\n    @Override\n    public Mono<Void> saveReceiverConfig(String notifierDescriptorName, ObjectNode config) {\n        return saveConfig(notifierDescriptorName, RECEIVER_KEY, config);\n    }\n\n    @Override\n    public Mono<Void> saveSenderConfig(String notifierDescriptorName, ObjectNode config) {\n        return saveConfig(notifierDescriptorName, SENDER_KEY, config);\n    }\n\n    Mono<Void> saveConfig(String notifierDescriptorName, String key, ObjectNode config) {\n        return client.fetch(Secret.class, SECRET_NAME)\n            .switchIfEmpty(Mono.defer(() -> {\n                Secret secret = new Secret();\n                secret.setMetadata(new Metadata());\n                secret.getMetadata().setName(SECRET_NAME);\n                secret.getMetadata().setFinalizers(Set.of(SYSTEM_FINALIZER));\n                secret.setStringData(new HashMap<>());\n                return client.create(secret);\n            }))\n            .flatMap(secret -> {\n                if (secret.getStringData() == null) {\n                    secret.setStringData(new HashMap<>());\n                }\n                Map<String, String> map = secret.getStringData();\n                ObjectNode wrapperNode = JsonNodeFactory.instance.objectNode();\n                wrapperNode.set(key, config);\n                map.put(resolveKey(notifierDescriptorName), JsonUtils.objectToJson(wrapperNode));\n                return client.update(secret);\n            })\n            .then();\n    }\n\n    Mono<ObjectNode> fetchConfig(String notifierDescriptorName) {\n        return client.fetch(Secret.class, SECRET_NAME)\n            .mapNotNull(Secret::getStringData)\n            .mapNotNull(map -> map.get(resolveKey(notifierDescriptorName)))\n            .filter(StringUtils::isNotBlank)\n            .map(value -> JsonUtils.jsonToObject(value, ObjectNode.class))\n            .defaultIfEmpty(JsonNodeFactory.instance.objectNode());\n    }\n\n    String resolveKey(String notifierDescriptorName) {\n        return notifierDescriptorName + \".json\";\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/DefaultSubscriberEmailResolver.java",
    "content": "package run.halo.app.notification;\n\nimport lombok.NonNull;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * <p>Default implementation of {@link SubscriberEmailResolver}.</p>\n * <p>If the subscriber is an anonymous subscriber, the email will be extracted from the\n * subscriber name.</p>\n * <p>An anonymous subscriber's name is in the format of {@code anonymous#email}.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultSubscriberEmailResolver implements SubscriberEmailResolver {\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<String> resolve(Subscription.Subscriber subscriber) {\n        var identity = UserIdentity.of(subscriber.getName());\n        if (identity.isAnonymous()) {\n            return Mono.fromSupplier(() -> getEmail(subscriber));\n        }\n        return client.fetch(User.class, subscriber.getName())\n            .filter(user -> user.getSpec().isEmailVerified())\n            .mapNotNull(user -> user.getSpec().getEmail());\n    }\n\n    @Override\n    public Subscription.Subscriber ofEmail(String email) {\n        if (StringUtils.isBlank(email)) {\n            throw new IllegalArgumentException(\"Email must not be blank\");\n        }\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(UserIdentity.anonymousWithEmail(email).name());\n        return subscriber;\n    }\n\n    @NonNull\n    String getEmail(Subscription.Subscriber subscriber) {\n        var identity = UserIdentity.of(subscriber.getName());\n        if (!identity.isAnonymous()) {\n            throw new IllegalStateException(\"The subscriber is not an anonymous subscriber\");\n        }\n        return identity.getEmail()\n            .filter(StringUtils::isNotBlank)\n            .orElseThrow(() -> new IllegalStateException(\"The subscriber does not have an email\"));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/EmailNotifier.java",
    "content": "package run.halo.app.notification;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport java.util.concurrent.atomic.AtomicReference;\nimport lombok.NonNull;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.data.util.Pair;\nimport org.springframework.mail.javamail.JavaMailSender;\nimport org.springframework.mail.javamail.JavaMailSenderImpl;\nimport org.springframework.mail.javamail.MimeMessagePreparator;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.notification.EmailSenderHelper.EmailSenderConfig;\n\n/**\n * <p>A notifier that can send email.</p>\n *\n * @author guqing\n * @see ReactiveNotifier\n * @see JavaMailSenderImpl\n * @since 2.10.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class EmailNotifier implements ReactiveNotifier {\n\n    private final SubscriberEmailResolver subscriberEmailResolver;\n    private final NotificationTemplateRender notificationTemplateRender;\n    private final EmailSenderHelper emailSenderHelper;\n    private final AtomicReference<Pair<EmailSenderConfig, JavaMailSender>>\n        emailSenderConfigPairRef = new AtomicReference<>();\n\n    @Override\n    public Mono<Void> notify(NotificationContext context) {\n        JsonNode senderConfig = context.getSenderConfig();\n        var emailSenderConfig =\n            JsonUtils.DEFAULT_JSON_MAPPER.convertValue(senderConfig, EmailSenderConfig.class);\n\n        if (!emailSenderConfig.isEnable()) {\n            log.debug(\"Email notifier is disabled, skip sending email.\");\n            return Mono.empty();\n        }\n\n        JavaMailSender javaMailSender = getJavaMailSender(emailSenderConfig);\n\n        String recipient = context.getMessage().getRecipient();\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(recipient);\n        var payload = context.getMessage().getPayload();\n        return subscriberEmailResolver.resolve(subscriber)\n            .flatMap(toEmail -> {\n                if (StringUtils.isBlank(toEmail)) {\n                    log.debug(\"Cannot resolve email for subscriber: [{}], skip sending email.\",\n                        subscriber);\n                    return Mono.empty();\n                }\n                var htmlMono = appendHtmlBodyFooter(payload.getAttributes())\n                    .doOnNext(footer -> {\n                        if (StringUtils.isNotBlank(payload.getHtmlBody())) {\n                            payload.setHtmlBody(payload.getHtmlBody() + \"\\n\" + footer);\n                        }\n                    });\n                var rawMono = appendRawBodyFooter(payload.getAttributes())\n                    .doOnNext(footer -> {\n                        if (StringUtils.isNotBlank(payload.getRawBody())) {\n                            payload.setRawBody(payload.getRawBody() + \"\\n\" + footer);\n                        }\n                    });\n                return Mono.when(htmlMono, rawMono)\n                    .thenReturn(toEmail);\n            })\n            .map(toEmail -> getMimeMessagePreparator(toEmail, emailSenderConfig, payload))\n            .publishOn(Schedulers.boundedElastic())\n            .doOnNext(javaMailSender::send)\n            .then();\n    }\n\n    @NonNull\n    private MimeMessagePreparator getMimeMessagePreparator(String toEmail,\n        EmailSenderConfig emailSenderConfig, NotificationContext.MessagePayload payload) {\n        return emailSenderHelper.createMimeMessagePreparator(emailSenderConfig, toEmail,\n            payload.getTitle(),\n            payload.getRawBody(), payload.getHtmlBody());\n    }\n\n    JavaMailSender getJavaMailSender(EmailSenderConfig emailSenderConfig) {\n        return emailSenderConfigPairRef.updateAndGet(pair -> {\n            if (pair != null && pair.getFirst().equals(emailSenderConfig)) {\n                return pair;\n            }\n            return Pair.of(emailSenderConfig,\n                emailSenderHelper.createJavaMailSender(emailSenderConfig));\n        }).getSecond();\n    }\n\n    Mono<String> appendRawBodyFooter(ReasonAttributes attributes) {\n        return notificationTemplateRender.render(\"\"\"\n            ---\n            如果您不想再收到此类通知，点击链接退订: [(${unsubscribeUrl})]\n            [(${site.title})]\n            \"\"\", attributes);\n    }\n\n    Mono<String> appendHtmlBodyFooter(ReasonAttributes attributes) {\n        return notificationTemplateRender.render(\"\"\"\n            <div class=\"footer\" style=\"font-size: 12px; color: #666;\">\n            <a th:href=\"${site.url}\" th:text=\"${site.title}\"></a>\n            <p class=\"unsubscribe\">\n            &mdash;<br />请勿直接回复此邮件，\n            <a th:href=\"|${site.url}/uc/notifications|\">查看通知</a>\n            或\n            <a th:href=\"${unsubscribeUrl}\">取消订阅</a>。\n            </p>\n            </div>\n            \"\"\", attributes);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/EmailSenderHelper.java",
    "content": "package run.halo.app.notification;\n\nimport lombok.Data;\nimport lombok.NonNull;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.mail.javamail.JavaMailSender;\nimport org.springframework.mail.javamail.MimeMessagePreparator;\n\npublic interface EmailSenderHelper {\n\n    @NonNull\n    JavaMailSender createJavaMailSender(EmailSenderConfig senderConfig);\n\n    @NonNull\n    MimeMessagePreparator createMimeMessagePreparator(EmailSenderConfig senderConfig,\n        String toEmail, String subject, String raw, String html);\n\n    @Data\n    class EmailSenderConfig {\n        private boolean enable;\n        private String displayName;\n        private String username;\n        private String sender;\n        private String password;\n        private String host;\n        private Integer port;\n        private String encryption;\n\n        /**\n         * Gets email display name.\n         *\n         * @return display name if not blank, otherwise username.\n         */\n        public String getDisplayName() {\n            return StringUtils.defaultIfBlank(displayName, username);\n        }\n\n        /**\n         * Gets email sender address.\n         *\n         * @return sender if not blank, otherwise username\n         */\n        public String getSender() {\n            return StringUtils.defaultIfBlank(sender, username);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/EmailSenderHelperImpl.java",
    "content": "package run.halo.app.notification;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Properties;\nimport lombok.NonNull;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.mail.javamail.JavaMailSender;\nimport org.springframework.mail.javamail.JavaMailSenderImpl;\nimport org.springframework.mail.javamail.MimeMessageHelper;\nimport org.springframework.mail.javamail.MimeMessagePreparator;\nimport org.springframework.stereotype.Component;\n\n/**\n * <p>A default implementation of {@link EmailSenderHelper}.</p>\n *\n * @author guqing\n * @since 2.14.0\n */\n@Slf4j\n@Component\npublic class EmailSenderHelperImpl implements EmailSenderHelper {\n\n    @Override\n    @NonNull\n    public JavaMailSender createJavaMailSender(EmailSenderConfig senderConfig) {\n        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();\n        javaMailSender.setHost(senderConfig.getHost());\n        javaMailSender.setPort(senderConfig.getPort());\n        javaMailSender.setUsername(senderConfig.getUsername());\n        javaMailSender.setPassword(senderConfig.getPassword());\n\n        Properties props = javaMailSender.getJavaMailProperties();\n        props.put(\"mail.transport.protocol\", \"smtp\");\n        props.put(\"mail.smtp.auth\", \"true\");\n        if (\"SSL\".equals(senderConfig.getEncryption())) {\n            props.put(\"mail.smtp.ssl.enable\", \"true\");\n        }\n\n        if (\"TLS\".equals(senderConfig.getEncryption())) {\n            props.put(\"mail.smtp.starttls.enable\", \"true\");\n        }\n\n        if (\"NONE\".equals(senderConfig.getEncryption())) {\n            props.put(\"mail.smtp.ssl.enable\", \"false\");\n            props.put(\"mail.smtp.starttls.enable\", \"false\");\n        }\n\n        if (log.isDebugEnabled()) {\n            props.put(\"mail.debug\", \"true\");\n        }\n        return javaMailSender;\n    }\n\n    @Override\n    @NonNull\n    public MimeMessagePreparator createMimeMessagePreparator(EmailSenderConfig senderConfig,\n        String toEmail, String subject, String raw, String html) {\n        return mimeMessage -> {\n            MimeMessageHelper helper =\n                new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name());\n            helper.setFrom(senderConfig.getSender(), senderConfig.getDisplayName());\n\n            helper.setSubject(subject);\n            helper.setText(raw, html);\n            helper.setTo(toEmail);\n        };\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/LanguageUtils.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport lombok.experimental.UtilityClass;\nimport org.apache.commons.lang3.StringUtils;\n\n/**\n * Language utils to help us to compute the language.\n *\n * @author guqing\n * @since 2.10.0\n */\n@UtilityClass\npublic class LanguageUtils {\n\n    /**\n     * Compute all the possible languages we should use: *_gl_ES-gheada, *_gl_ES, _gl... from a\n     * given locale.\n     * The first element of the list is \"default\" if it can not find the language, use default.\n     *\n     * @param locale locale\n     * @return list of possible languages, from less specific to more specific.\n     */\n    public static List<String> computeLangFromLocale(Locale locale) {\n        final List<String> resourceNames = new ArrayList<>(5);\n\n        if (StringUtils.isBlank(locale.getLanguage())) {\n            throw new IllegalArgumentException(\n                \"Locale \\\"\" + locale + \"\\\" \"\n                    + \"cannot be used as it does not specify a language.\");\n        }\n\n        resourceNames.add(\"default\");\n        resourceNames.add(locale.getLanguage());\n\n        if (StringUtils.isNotBlank(locale.getCountry())) {\n            resourceNames.add(locale.getLanguage() + \"_\" + locale.getCountry());\n        }\n\n        if (StringUtils.isNotBlank(locale.getVariant())) {\n            resourceNames.add(\n                locale.getLanguage() + \"_\" + locale.getCountry() + \"-\" + locale.getVariant());\n        }\n        return resourceNames;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/NotificationSender.java",
    "content": "package run.halo.app.notification;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Reason;\n\n/**\n * <p>{@link NotificationSender} used to send notification.</p>\n * <p>Send notification is a time-consuming task, so we use a queue to send notification\n * asynchronously.</p>\n * <p>The caller may not be reactive, and in many cases it is blocking called\n * {@link NotificationCenter#notify(Reason)}, so here use the queue to ensure asynchronous\n * sending of notification without blocking the calling thread.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@FunctionalInterface\npublic interface NotificationSender {\n\n    Mono<Void> sendNotification(String notifierExtensionName, NotificationContext context);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/NotificationTemplateRender.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.Map;\nimport reactor.core.publisher.Mono;\n\n/**\n * {@link NotificationTemplateRender} is used to render the notification template.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface NotificationTemplateRender {\n\n    Mono<String> render(String template, Map<String, Object> context);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/NotificationTrigger.java",
    "content": "package run.halo.app.notification;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\n\nimport java.time.Duration;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n/**\n * <p>Notification trigger for {@link Reason}.</p>\n * <p>Triggered when a new {@link Reason} is received, and then notify through\n * {@link NotificationCenter}.</p>\n * <p>It will add a finalizer to the {@link Reason} to avoid duplicate notification, In other\n * words, it will only notify once.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class NotificationTrigger implements Reconciler<Reconciler.Request> {\n\n    public static final String TRIGGERED_FINALIZER = \"triggered\";\n    private static final Duration TIMEOUT = Duration.ofMinutes(1);\n\n    private final ExtensionClient client;\n    private final NotificationCenter notificationCenter;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Reason.class, request.name()).ifPresent(reason -> {\n            if (ExtensionUtil.isDeleted(reason)) {\n                if (removeFinalizers(reason.getMetadata(), Set.of(TRIGGERED_FINALIZER))) {\n                    client.update(reason);\n                    log.info(\"Cleaned up notification reason {}\", request.name());\n                }\n                return;\n            }\n            if (addFinalizers(reason.getMetadata(), Set.of(TRIGGERED_FINALIZER))) {\n                // notifier\n                onNewReasonReceived(reason);\n            }\n            // cleanup reason after notified\n            client.delete(reason);\n        });\n        return Result.doNotRetry();\n    }\n\n    private void onNewReasonReceived(Reason reason) {\n        var name = reason.getMetadata().getName();\n        log.info(\"Sending notification for reason: {}\", name);\n        notificationCenter.notify(reason).block(TIMEOUT);\n        log.info(\"Notification sent for reason: {}\", name);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Reason())\n            .workerCount(10)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/NotifierConfigStore.java",
    "content": "package run.halo.app.notification;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>{@link NotifierConfigStore} to store notifier config.</p>\n * <p>It provides methods to fetch and save config for receiver and sender.</p>\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface NotifierConfigStore {\n\n    Mono<ObjectNode> fetchReceiverConfig(String notifierDescriptorName);\n\n    Mono<ObjectNode> fetchSenderConfig(String notifierDescriptorName);\n\n    Mono<Void> saveReceiverConfig(String notifierDescriptorName, ObjectNode config);\n\n    Mono<Void> saveSenderConfig(String notifierDescriptorName, ObjectNode config);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/ReasonNotificationTemplateSelector.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.Locale;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.NotificationTemplate;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.extension.Metadata;\n\n/**\n * Reason notification template selector to select notification template by reason type and locale.\n *\n * @author guqing\n * @see NotificationTemplate\n * @see ReasonType\n * @since 2.10.0\n */\npublic interface ReasonNotificationTemplateSelector {\n\n    /**\n     * Select notification template by reason type and locale.\n     * <p>Locale order is important: as we will let values from more specific to less specific (e.g.\n     * a value for gl_ES will have more precedence than a value for gl).</p>\n     * <p>If specific locale found and has multiple templates, we will order them by\n     * {@link Metadata#getCreationTimestamp()} and return the latest one.</p>\n     *\n     * @param reasonType reason type\n     * @param locale locale\n     * @return notification template if found, or empty\n     */\n    Mono<NotificationTemplate> select(String reasonType, Locale locale);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/ReasonNotificationTemplateSelectorImpl.java",
    "content": "package run.halo.app.notification;\n\nimport static org.apache.commons.lang3.StringUtils.defaultIfBlank;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport lombok.NonNull;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.Nullable;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.NotificationTemplate;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\n\n/**\n * A default implementation of {@link ReasonNotificationTemplateSelector}.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class ReasonNotificationTemplateSelectorImpl implements ReasonNotificationTemplateSelector {\n\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<NotificationTemplate> select(String reasonType, Locale locale) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            equal(\"spec.reasonSelector.reasonType\", reasonType))\n        );\n        return client.listAll(NotificationTemplate.class, listOptions, Sort.unsorted())\n            .collect(Collectors.groupingBy(\n                getLanguageKey(),\n                Collectors.maxBy(Comparator.comparing(t -> t.getMetadata().getCreationTimestamp()))\n            ))\n            .mapNotNull(map -> lookupTemplateByLocale(locale, map));\n    }\n\n    @Nullable\n    static NotificationTemplate lookupTemplateByLocale(Locale locale,\n        Map<String, Optional<NotificationTemplate>> map) {\n        return LanguageUtils.computeLangFromLocale(locale).stream()\n            // reverse order to ensure that the variant is the first element and the default\n            // is the last element\n            .sorted(Collections.reverseOrder())\n            .map(key -> map.getOrDefault(key, Optional.empty()))\n            .filter(Optional::isPresent)\n            .map(Optional::get)\n            .findFirst()\n            .orElse(null);\n    }\n\n    @NonNull\n    static Predicate<NotificationTemplate> matchReasonType(String reasonType) {\n        return template -> template.getSpec().getReasonSelector().getReasonType()\n            .equals(reasonType);\n    }\n\n    static Function<NotificationTemplate, String> getLanguageKey() {\n        return template -> defaultIfBlank(template.getSpec().getReasonSelector().getLanguage(),\n            \"default\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/RecipientResolver.java",
    "content": "package run.halo.app.notification;\n\nimport reactor.core.publisher.Flux;\nimport run.halo.app.core.extension.notification.Reason;\n\npublic interface RecipientResolver {\n\n    Flux<Subscriber> resolve(Reason reason);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/RecipientResolverImpl.java",
    "content": "package run.halo.app.notification;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport com.google.common.base.Throwables;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.expression.MapAccessor;\nimport org.springframework.core.convert.support.DefaultConversionService;\nimport org.springframework.expression.EvaluationContext;\nimport org.springframework.expression.EvaluationException;\nimport org.springframework.expression.Expression;\nimport org.springframework.expression.ExpressionParser;\nimport org.springframework.expression.ParseException;\nimport org.springframework.expression.spel.standard.SpelExpressionParser;\nimport org.springframework.expression.spel.support.DataBindingPropertyAccessor;\nimport org.springframework.expression.spel.support.SimpleEvaluationContext;\nimport org.springframework.integration.json.JsonPropertyAccessor;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.Subscription;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class RecipientResolverImpl implements RecipientResolver {\n    private final ExpressionParser expressionParser = new SpelExpressionParser();\n    private final EvaluationContext evaluationContext = createEvaluationContext();\n    private final SubscriptionService subscriptionService;\n\n    @Override\n    public Flux<Subscriber> resolve(Reason reason) {\n        var reasonType = reason.getSpec().getReasonType();\n        return subscriptionService.listByPerPage(reasonType)\n            .filter(this::isNotDisabled)\n            .filter(subscription -> {\n                var interestReason = subscription.getSpec().getReason();\n                if (hasSubject(interestReason)) {\n                    return subjectMatch(subscription, reason.getSpec().getSubject());\n                } else if (StringUtils.isNotBlank(interestReason.getExpression())) {\n                    return expressionMatch(subscription.getMetadata().getName(),\n                        interestReason.getExpression(), reason);\n                }\n                return false;\n            })\n            .map(subscription -> {\n                var id = UserIdentity.of(subscription.getSpec().getSubscriber().getName());\n                return new Subscriber(id, subscription.getMetadata().getName());\n            })\n            .distinct(Subscriber::name);\n    }\n\n    boolean hasSubject(Subscription.InterestReason interestReason) {\n        return !Subscription.InterestReason.isFallbackSubject(interestReason.getSubject());\n    }\n\n    boolean expressionMatch(String subscriptionName, String expressionStr, Reason reason) {\n        try {\n            Expression expression =\n                expressionParser.parseExpression(expressionStr);\n            var result = expression.getValue(evaluationContext,\n                exprRootObject(reason),\n                Boolean.class);\n            return BooleanUtils.isTrue(result);\n        } catch (ParseException | EvaluationException e) {\n            log.debug(\"Failed to parse or evaluate expression for subscription [{}], skip it.\",\n                subscriptionName, Throwables.getRootCause(e));\n            return false;\n        }\n    }\n\n    Map<String, Object> exprRootObject(Reason reason) {\n        var map = new HashMap<String, Object>(3, 1);\n        map.put(\"props\", defaultIfNull(reason.getSpec().getAttributes(), new ReasonAttributes()));\n        map.put(\"subject\", reason.getSpec().getSubject());\n        map.put(\"author\", reason.getSpec().getAuthor());\n        return Collections.unmodifiableMap(map);\n    }\n\n    static boolean subjectMatch(Subscription subscription, Reason.Subject reasonSubject) {\n        Assert.notNull(subscription, \"The subscription must not be null\");\n        Assert.notNull(reasonSubject, \"The reasonSubject must not be null\");\n        final var sourceSubject = subscription.getSpec().getReason().getSubject();\n\n        var matchSubject = new Subscription.ReasonSubject();\n        matchSubject.setKind(reasonSubject.getKind());\n        matchSubject.setApiVersion(reasonSubject.getApiVersion());\n\n        if (StringUtils.isBlank(sourceSubject.getName())) {\n            return sourceSubject.equals(matchSubject);\n        }\n        matchSubject.setName(reasonSubject.getName());\n        return sourceSubject.equals(matchSubject);\n    }\n\n    boolean isNotDisabled(Subscription subscription) {\n        return !subscription.getSpec().isDisabled();\n    }\n\n    EvaluationContext createEvaluationContext() {\n        return SimpleEvaluationContext.forPropertyAccessors(\n                DataBindingPropertyAccessor.forReadOnlyAccess(),\n                new MapAccessor(),\n                new JsonPropertyAccessor()\n            )\n            .withConversionService(DefaultConversionService.getSharedInstance())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/Subscriber.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.Optional;\nimport org.springframework.util.Assert;\nimport run.halo.app.infra.AnonymousUserConst;\n\npublic record Subscriber(UserIdentity identity, String subscriptionName) {\n    public Subscriber {\n        Assert.notNull(identity, \"The subscriber must not be null\");\n        Assert.hasText(subscriptionName, \"The subscription name must not be blank\");\n    }\n\n    public String name() {\n        return identity.name();\n    }\n\n    public String username() {\n        return identity.isAnonymous() ? AnonymousUserConst.PRINCIPAL : identity.name();\n    }\n\n    public boolean isAnonymous() {\n        return identity.isAnonymous();\n    }\n\n    public Optional<String> getEmail() {\n        return identity.getEmail();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/SubscriberEmailResolver.java",
    "content": "package run.halo.app.notification;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Subscription;\n\n/**\n * <p>{@link SubscriberEmailResolver} used to resolve email from {@link Subscription.Subscriber}\n * .</p>\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface SubscriberEmailResolver {\n\n    Mono<String> resolve(Subscription.Subscriber subscriber);\n\n    /**\n     * Creates an email subscriber from email.\n     *\n     * @param email email\n     * @return email subscriber\n     */\n    Subscription.Subscriber ofEmail(String email);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/SubscriptionService.java",
    "content": "package run.halo.app.notification;\n\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.ListOptions;\n\npublic interface SubscriptionService {\n\n    /**\n     * <p>List subscriptions by page one by one.only consume one page then next page will be\n     * loaded.</p>\n     * <p>Note that: result can not be used to delete the subscription, it is only used to query the\n     * subscription.</p>\n     */\n    Flux<Subscription> listByPerPage(String reasonType);\n\n    Mono<Void> remove(Subscription.Subscriber subscriber,\n        Subscription.InterestReason interestReasons);\n\n    Mono<Void> remove(Subscription.Subscriber subscriber);\n\n    Mono<Subscription> remove(Subscription subscription);\n\n    Flux<Subscription> removeBy(ListOptions listOptions);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/SubscriptionServiceImpl.java",
    "content": "package run.halo.app.notification;\n\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.extension.index.query.Queries.startsWith;\n\nimport java.time.Duration;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.ReactiveExtensionPaginatedOperator;\n\n@Component\n@RequiredArgsConstructor\npublic class SubscriptionServiceImpl implements SubscriptionService {\n    private final ReactiveExtensionClient client;\n    private final ReactiveExtensionPaginatedOperator paginatedOperator;\n\n    @Override\n    public Mono<Void> remove(Subscription.Subscriber subscriber,\n        Subscription.InterestReason interestReason) {\n        Assert.notNull(subscriber, \"The subscriber must not be null\");\n        Assert.notNull(interestReason, \"The interest reason must not be null\");\n        var reasonType = interestReason.getReasonType();\n        var expression = interestReason.getExpression();\n        var subject = interestReason.getSubject();\n\n        var listOptions = new ListOptions();\n        var fieldQuery = and(isNull(\"metadata.deletionTimestamp\"),\n            equal(\"spec.subscriber\", subscriber.toString()),\n            equal(\"spec.reason.reasonType\", reasonType));\n\n        if (subject != null) {\n            fieldQuery = and(fieldQuery, reasonSubjectMatch(subject));\n        }\n        if (StringUtils.isNotBlank(expression)) {\n            fieldQuery = and(fieldQuery, equal(\"spec.reason.expression\", expression));\n        }\n        listOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n        return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions).then();\n    }\n\n    @Override\n    public Mono<Void> remove(Subscription.Subscriber subscriber) {\n        var listOptions = new ListOptions();\n        var fieldQuery = and(isNull(\"metadata.deletionTimestamp\"),\n            equal(\"spec.subscriber\", subscriber.toString()));\n        listOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n        return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions)\n            .then();\n    }\n\n    @Override\n    public Mono<Subscription> remove(Subscription subscription) {\n        return client.delete(subscription)\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> attemptToDelete(subscription.getMetadata().getName()));\n    }\n\n    @Override\n    public Flux<Subscription> removeBy(ListOptions listOptions) {\n        return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions);\n    }\n\n    @Override\n    public Flux<Subscription> listByPerPage(String reasonType) {\n        final var listOptions = new ListOptions();\n        var fieldQuery = and(isNull(\"metadata.deletionTimestamp\"),\n            equal(\"spec.reason.reasonType\", reasonType));\n        listOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n        return paginatedOperator.list(Subscription.class, listOptions);\n    }\n\n    private Mono<Subscription> attemptToDelete(String subscriptionName) {\n        return Mono.defer(() -> client.fetch(Subscription.class, subscriptionName)\n                .flatMap(client::delete)\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    Condition reasonSubjectMatch(Subscription.ReasonSubject reasonSubject) {\n        Assert.notNull(reasonSubject, \"The reasonSubject must not be null\");\n        if (StringUtils.isNotBlank(reasonSubject.getName())) {\n            return equal(\"spec.reason.subject\", reasonSubject.toString());\n        }\n        var matchAllSubject = new Subscription.ReasonSubject();\n        matchAllSubject.setKind(reasonSubject.getKind());\n        matchAllSubject.setApiVersion(reasonSubject.getApiVersion());\n        return startsWith(\"spec.reason.subject\", matchAllSubject.toString());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/UserNotificationPreference.java",
    "content": "package run.halo.app.notification;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport java.util.HashMap;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.Getter;\nimport org.springframework.lang.NonNull;\n\n/**\n * Notification preference of user.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Getter\npublic class UserNotificationPreference {\n    private static final String DEFAULT_NOTIFIER = \"default-email-notifier\";\n\n    private final ReasonTypeNotifier reasonTypeNotifier = new ReasonTypeNotifier();\n\n    public static class ReasonTypeNotifier extends HashMap<String, NotifierSetting> {\n\n        /**\n         * Gets notifiers by reason type.\n         *\n         * @param reasonType reason type\n         * @return if key of reasonType not exists, return default notifier, otherwise return the\n         * notifiers\n         */\n        @NonNull\n        public Set<String> getNotifiers(String reasonType) {\n            var result = this.get(reasonType);\n            return result == null ? Set.of(DEFAULT_NOTIFIER)\n                : defaultIfNull(result.getNotifiers(), Set.of());\n        }\n    }\n\n    @Data\n    public static class NotifierSetting {\n        private Set<String> notifiers;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/UserNotificationPreferenceService.java",
    "content": "package run.halo.app.notification;\n\nimport reactor.core.publisher.Mono;\n\n/**\n * User notification preference service.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface UserNotificationPreferenceService {\n\n    Mono<UserNotificationPreference> getByUser(String username);\n\n    Mono<Void> saveByUser(String username,\n        UserNotificationPreference userNotificationPreference);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/UserNotificationPreferenceServiceImpl.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.HashMap;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * User notification preference service implementation.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class UserNotificationPreferenceServiceImpl implements UserNotificationPreferenceService {\n\n    public static final String NOTIFICATION_PREFERENCE = \"notification\";\n\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<UserNotificationPreference> getByUser(String username) {\n        var configName = buildUserPreferenceConfigMapName(username);\n        return client.fetch(ConfigMap.class, configName)\n            .map(config -> {\n                if (config.getData() == null) {\n                    return new UserNotificationPreference();\n                }\n                String s = config.getData().get(NOTIFICATION_PREFERENCE);\n                if (StringUtils.isNotBlank(s)) {\n                    return JsonUtils.jsonToObject(s, UserNotificationPreference.class);\n                }\n                return new UserNotificationPreference();\n            })\n            .defaultIfEmpty(new UserNotificationPreference());\n    }\n\n    @Override\n    public Mono<Void> saveByUser(String username,\n        UserNotificationPreference userNotificationPreference) {\n        var configName = buildUserPreferenceConfigMapName(username);\n        return client.fetch(ConfigMap.class, configName)\n            .switchIfEmpty(Mono.defer(() -> {\n                var configMap = new ConfigMap();\n                configMap.setMetadata(new Metadata());\n                configMap.getMetadata().setName(configName);\n                return client.create(configMap);\n            }))\n            .flatMap(config -> {\n                if (config.getData() == null) {\n                    config.setData(new HashMap<>());\n                }\n                config.getData().put(NOTIFICATION_PREFERENCE,\n                    JsonUtils.objectToJson(userNotificationPreference));\n                return client.update(config);\n            })\n            .then();\n    }\n\n    static String buildUserPreferenceConfigMapName(String username) {\n        return \"user-preferences-\" + username;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/UserNotificationQuery.java",
    "content": "package run.halo.app.notification;\n\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.web.server.ServerWebExchange;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.router.SortableRequest;\n\n/**\n * Notification query object for authenticated user.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic class UserNotificationQuery extends SortableRequest {\n\n    private final String username;\n\n    public UserNotificationQuery(ServerWebExchange exchange, String username) {\n        super(exchange);\n        this.username = username;\n    }\n\n    /**\n     * Build a list options from the query object.\n     */\n    @Override\n    public ListOptions toListOptions() {\n        var listOptions =\n            labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());\n        var builder = ListOptions.builder(listOptions);\n        if (StringUtils.isNotBlank(username)) {\n            builder.andQuery(equal(\"spec.recipient\", username));\n        }\n        return builder.build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/UserNotificationService.java",
    "content": "package run.halo.app.notification;\n\nimport java.util.List;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Notification;\nimport run.halo.app.extension.ListResult;\n\n/**\n * Notification service.\n *\n * @author guqing\n * @since 2.10.0\n */\npublic interface UserNotificationService {\n\n    /**\n     * List notifications for the authenticated user.\n     *\n     * @param query query object\n     * @return a page result of notifications\n     */\n    Mono<ListResult<Notification>> listByUser(String username, UserNotificationQuery query);\n\n    /**\n     * Mark the specified notification as read.\n     *\n     * @param name notification name\n     * @return read notification\n     */\n    Mono<Notification> markAsRead(String username, String name);\n\n    /**\n     * Mark the specified notifications as read.\n     *\n     * @param names the names of notifications\n     * @return the names of read notification that has been marked as read\n     */\n    Flux<String> markSpecifiedAsRead(String username, List<String> names);\n\n    Mono<Notification> deleteByName(String username, String name);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/endpoint/ConsoleNotifierEndpoint.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.notification.NotifierConfigStore;\n\n/**\n * Custom notifier endpoint.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class ConsoleNotifierEndpoint implements CustomEndpoint {\n\n    private final NotifierConfigStore notifierConfigStore;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"NotifierV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"/notifiers/{name}/sender-config\", this::fetchSenderConfig,\n                builder -> builder.operationId(\"FetchSenderConfig\")\n                    .description(\"Fetch sender config of notifier\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Notifier name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder().implementation(Object.class))\n            )\n            .POST(\"/notifiers/{name}/sender-config\", this::saveSenderConfig,\n                builder -> builder.operationId(\"SaveSenderConfig\")\n                    .description(\"Save sender config of notifier\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Notifier name\")\n                        .required(true)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder().implementation(Object.class))\n                        )\n                    )\n                    .response(responseBuilder().implementation(Void.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> fetchSenderConfig(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return notifierConfigStore.fetchSenderConfig(name)\n            .flatMap(config -> ServerResponse.ok().bodyValue(config));\n    }\n\n    private Mono<ServerResponse> saveSenderConfig(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return request.bodyToMono(ObjectNode.class)\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"Request body must not be empty.\"))\n            )\n            .flatMap(jsonNode -> notifierConfigStore.saveSenderConfig(name, jsonNode))\n            .then(ServerResponse.ok().build());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/endpoint/EmailConfigValidationEndpoint.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.security.Principal;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.mail.MailException;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.notification.EmailSenderHelper;\n\n/**\n * Validation endpoint for email config.\n *\n * @author guqing\n * @since 2.14.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class EmailConfigValidationEndpoint implements CustomEndpoint {\n    private static final String EMAIL_SUBJECT = \"测试邮件 - 验证邮箱连通性\";\n    private static final String EMAIL_BODY = \"\"\"\n        你好！<br/>\n        这是一封测试邮件，旨在验证您的邮箱发件配置是否正确。<br/>\n        此邮件由系统自动发送，请勿回复。<br/>\n        祝好\n        \"\"\";\n\n    private final EmailSenderHelper emailSenderHelper;\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"NotifierV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"/notifiers/default-email-notifier/verify-connection\",\n                this::verifyEmailSenderConfig,\n                builder -> builder.operationId(\"VerifyEmailSenderConfig\")\n                    .description(\"Verify email sender config.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .implementation(ValidationRequest.class)\n                    )\n                    .response(responseBuilder().implementation(Void.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> verifyEmailSenderConfig(ServerRequest request) {\n        return request.bodyToMono(ValidationRequest.class)\n            .switchIfEmpty(\n                Mono.error(new ServerWebInputException(\"Required request body is missing.\"))\n            )\n            .flatMap(validationRequest -> getCurrentUserEmail()\n                .flatMap(recipient -> {\n                    var mailSender = emailSenderHelper.createJavaMailSender(validationRequest);\n                    var message = emailSenderHelper.createMimeMessagePreparator(validationRequest,\n                        recipient, EMAIL_SUBJECT, EMAIL_BODY, EMAIL_BODY);\n                    try {\n                        mailSender.send(message);\n                    } catch (MailException e) {\n                        String errorMsg =\n                            \"Failed to send email, please check your email configuration.\";\n                        log.error(errorMsg, e);\n                        throw new ServerWebInputException(errorMsg, null, e);\n                    }\n                    return ServerResponse.ok().build();\n                })\n            );\n    }\n\n    Mono<String> getCurrentUserEmail() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Principal::getName)\n            .flatMap(username -> client.fetch(User.class, username))\n            .flatMap(user -> {\n                var email = user.getSpec().getEmail();\n                if (StringUtils.isBlank(email)) {\n                    return Mono.error(new ServerWebInputException(\n                        \"Your email is missing, please set it in your profile.\"));\n                }\n                return Mono.just(email);\n            });\n    }\n\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    @Schema(name = \"EmailConfigValidationRequest\")\n    static class ValidationRequest extends EmailSenderHelper.EmailSenderConfig {\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"console.api.notification.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/endpoint/SubscriptionRouter.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.net.URI;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ExternalUrlSupplier;\n\n/**\n * A router for {@link Subscription}.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class SubscriptionRouter {\n\n    public static final String UNSUBSCRIBE_PATTERN =\n        \"/apis/api.notification.halo.run/v1alpha1/subscriptions/{name}/unsubscribe\";\n\n    private final ExternalUrlSupplier externalUrlSupplier;\n    private final ReactiveExtensionClient client;\n\n    @Bean\n    RouterFunction<ServerResponse> notificationSubscriptionRouter() {\n        final var tag = \"NotificationV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(UNSUBSCRIBE_PATTERN, this::unsubscribe, builder -> {\n                builder.operationId(\"Unsubscribe\")\n                    .tag(tag)\n                    .description(\"Unsubscribe a subscription\")\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Subscription name\")\n                        .required(true)\n                    ).parameter(parameterBuilder()\n                        .in(ParameterIn.QUERY)\n                        .name(\"token\")\n                        .description(\"Unsubscribe token\")\n                        .required(true)\n                    )\n                    .response(responseBuilder().implementation(String.class))\n                    .build();\n            })\n            .build();\n    }\n\n    Mono<ServerResponse> unsubscribe(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var token = request.queryParam(\"token\").orElse(\"\");\n        return client.fetch(Subscription.class, name)\n            .filter(subscription -> {\n                var unsubscribeToken = subscription.getSpec().getUnsubscribeToken();\n                return StringUtils.equals(token, unsubscribeToken);\n            })\n            .flatMap(client::delete)\n            .then(Mono.defer(() -> ServerResponse.ok()\n                .contentType(MediaType.TEXT_PLAIN)\n                .bodyValue(\"Unsubscribe successfully.\"))\n            );\n    }\n\n    /**\n     * Gets unsubscribe url from the given subscription.\n     *\n     * @param subscription subscription must not be null\n     * @return unsubscribe url\n     */\n    public String getUnsubscribeUrl(Subscription subscription) {\n        var name = subscription.getMetadata().getName();\n        var token = subscription.getSpec().getUnsubscribeToken();\n        var externalUrl = defaultIfNull(externalUrlSupplier.getRaw(), URI.create(\"/\"));\n        return UriComponentsBuilder.fromUriString(externalUrl.toString())\n            .path(UNSUBSCRIBE_PATTERN)\n            .queryParam(\"token\", token)\n            .build(name)\n            .toString();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/endpoint/UserNotificationEndpoint.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport java.util.List;\nimport java.util.function.Supplier;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.extension.notification.Notification;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.notification.UserNotificationQuery;\nimport run.halo.app.notification.UserNotificationService;\n\n/**\n * Custom notification endpoint to managing notification for authenticated user.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class UserNotificationEndpoint implements CustomEndpoint {\n\n    private final UserNotificationService notificationService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        return SpringdocRouteBuilder.route()\n            .nest(RequestPredicates.path(\"/userspaces/{username}\"), userspaceScopedApis())\n            .build();\n    }\n\n    Supplier<RouterFunction<ServerResponse>> userspaceScopedApis() {\n        var tag = \"NotificationV1alpha1Uc\";\n        return () -> SpringdocRouteBuilder.route()\n            .GET(\"/notifications\", this::listNotification,\n                builder -> {\n                    builder.operationId(\"ListUserNotifications\")\n                        .description(\"List notifications for the authenticated user.\")\n                        .tag(tag)\n                        .parameter(parameterBuilder()\n                            .in(ParameterIn.PATH)\n                            .name(\"username\")\n                            .description(\"Username\")\n                            .required(true)\n                        )\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(Notification.class))\n                        );\n                    UserNotificationQuery.buildParameters(builder);\n                }\n            )\n            .PUT(\"/notifications/{name}/mark-as-read\", this::markNotificationAsRead,\n                builder -> builder.operationId(\"MarkNotificationAsRead\")\n                    .description(\"Mark the specified notification as read.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"username\")\n                        .description(\"Username\")\n                        .required(true)\n                    )\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Notification name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder().implementation(Notification.class))\n            )\n            .PUT(\"/notifications/-/mark-specified-as-read\", this::markNotificationsAsRead,\n                builder -> builder.operationId(\"MarkNotificationsAsRead\")\n                    .description(\"Mark the specified notifications as read.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"username\")\n                        .description(\"Username\")\n                        .required(true)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder()\n                                .implementation(MarkSpecifiedRequest.class))\n                        )\n                    )\n                    .response(responseBuilder().implementationArray(String.class))\n            )\n            .DELETE(\"/notifications/{name}\", this::deleteNotification,\n                builder -> builder.operationId(\"DeleteSpecifiedNotification\")\n                    .description(\"Delete the specified notification.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"username\")\n                        .description(\"Username\")\n                        .required(true)\n                    )\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Notification name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder().implementation(Notification.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> deleteNotification(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        var username = request.pathVariable(\"username\");\n        return notificationService.deleteByName(username, name)\n            .flatMap(notification -> ServerResponse.ok().bodyValue(notification));\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"api.notification.halo.run/v1alpha1\");\n    }\n\n    record MarkSpecifiedRequest(List<String> names) {\n    }\n\n    private Mono<ServerResponse> listNotification(ServerRequest request) {\n        var username = request.pathVariable(\"username\");\n        var query = new UserNotificationQuery(request.exchange(), username);\n        return notificationService.listByUser(username, query)\n            .flatMap(notifications -> ServerResponse.ok().bodyValue(notifications));\n    }\n\n    private Mono<ServerResponse> markNotificationAsRead(ServerRequest request) {\n        var username = request.pathVariable(\"username\");\n        var name = request.pathVariable(\"name\");\n        return notificationService.markAsRead(username, name)\n            .flatMap(notification -> ServerResponse.ok().bodyValue(notification));\n    }\n\n    Mono<ServerResponse> markNotificationsAsRead(ServerRequest request) {\n        var username = request.pathVariable(\"username\");\n        return request.bodyToMono(MarkSpecifiedRequest.class)\n            .flatMapMany(\n                requestBody -> notificationService.markSpecifiedAsRead(username, requestBody.names))\n            .collectList()\n            .flatMap(names -> ServerResponse.ok().bodyValue(names));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpoint.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.experimental.Accessors;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport reactor.util.function.Tuples;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.extension.notification.NotifierDescriptor;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.extension.Comparators;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.notification.UserNotificationPreference;\nimport run.halo.app.notification.UserNotificationPreferenceService;\n\n/**\n * Endpoint for user notification preferences.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class UserNotificationPreferencesEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n    private final UserNotificationPreferenceService userNotificationPreferenceService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        return SpringdocRouteBuilder.route()\n            .nest(RequestPredicates.path(\"/userspaces/{username}\"), userspaceScopedApis())\n            .build();\n    }\n\n    Supplier<RouterFunction<ServerResponse>> userspaceScopedApis() {\n        var tag = \"NotificationV1alpha1Uc\";\n        return () -> SpringdocRouteBuilder.route()\n            .GET(\"/notification-preferences\", this::listNotificationPreferences,\n                builder -> builder.operationId(\"ListUserNotificationPreferences\")\n                    .description(\"List notification preferences for the authenticated user.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"username\")\n                        .description(\"Username\")\n                        .required(true)\n                    )\n                    .response(responseBuilder()\n                        .implementation(ReasonTypeNotifierMatrix.class)\n                    )\n            )\n            .POST(\"/notification-preferences\", this::saveNotificationPreferences,\n                builder -> builder.operationId(\"SaveUserNotificationPreferences\")\n                    .description(\"Save notification preferences for the authenticated user.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"username\")\n                        .description(\"Username\")\n                        .required(true)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .implementation(ReasonTypeNotifierCollectionRequest.class)\n                    )\n                    .response(responseBuilder().implementation(ReasonTypeNotifierMatrix.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> saveNotificationPreferences(ServerRequest request) {\n        var username = request.pathVariable(\"username\");\n        return request.bodyToMono(ReasonTypeNotifierCollectionRequest.class)\n            .flatMap(requestBody -> {\n                var reasonTypNotifiers = requestBody.reasonTypeNotifiers();\n                return userNotificationPreferenceService.getByUser(username)\n                    .flatMap(preference -> {\n                        var reasonTypeNotifierMap = preference.getReasonTypeNotifier();\n                        reasonTypeNotifierMap.clear();\n                        reasonTypNotifiers.forEach(reasonTypeNotifierRequest -> {\n                            var reasonType = reasonTypeNotifierRequest.getReasonType();\n                            var notifiers = reasonTypeNotifierRequest.getNotifiers();\n                            var notifierSetting = new UserNotificationPreference.NotifierSetting();\n                            notifierSetting.setNotifiers(\n                                notifiers == null ? Set.of() : Set.copyOf(notifiers));\n                            reasonTypeNotifierMap.put(reasonType, notifierSetting);\n                        });\n                        return userNotificationPreferenceService.saveByUser(username, preference);\n                    });\n            })\n            .then(Mono.defer(() -> listReasonTypeNotifierMatrix(username)\n                .flatMap(result -> ServerResponse.ok().bodyValue(result)))\n            );\n    }\n\n    private Mono<ServerResponse> listNotificationPreferences(ServerRequest request) {\n        var username = request.pathVariable(\"username\");\n        return listReasonTypeNotifierMatrix(username)\n            .flatMap(matrix -> ServerResponse.ok().bodyValue(matrix));\n    }\n\n    @NonNull\n    private static <T> Map<String, Integer> toNameIndexMap(List<T> collection,\n        Function<T, String> nameGetter) {\n        Map<String, Integer> indexMap = new HashMap<>();\n        for (int i = 0; i < collection.size(); i++) {\n            var item = collection.get(i);\n            indexMap.put(nameGetter.apply(item), i);\n        }\n        return indexMap;\n    }\n\n    Mono<ReasonTypeNotifierMatrix> listReasonTypeNotifierMatrix(String username) {\n        var listOptions = ListOptions.builder()\n            .labelSelector()\n            .notExists(MetadataUtil.HIDDEN_LABEL)\n            .end()\n            .build();\n        return client.listAll(ReasonType.class, listOptions, ExtensionUtil.defaultSort())\n            .map(ReasonTypeInfo::from)\n            .collectList()\n            .flatMap(reasonTypes -> client.list(NotifierDescriptor.class, null,\n                    Comparators.defaultComparator())\n                .map(notifier -> new NotifierInfo(notifier.getMetadata().getName(),\n                    notifier.getSpec().getDisplayName(),\n                    notifier.getSpec().getDescription())\n                )\n                .collectList()\n                .map(notifiers -> {\n                    var matrix = new ReasonTypeNotifierMatrix()\n                        .setReasonTypes(reasonTypes)\n                        .setNotifiers(notifiers)\n                        .setStateMatrix(new boolean[reasonTypes.size()][notifiers.size()]);\n                    return Tuples.of(reasonTypes, matrix);\n                })\n            )\n            .flatMap(tuple2 -> {\n                var reasonTypes = tuple2.getT1();\n                var matrix = tuple2.getT2();\n\n                var reasonTypeIndexMap = toNameIndexMap(reasonTypes, ReasonTypeInfo::name);\n                var notifierIndexMap = toNameIndexMap(matrix.getNotifiers(), NotifierInfo::name);\n                var stateMatrix = matrix.getStateMatrix();\n\n                return userNotificationPreferenceService.getByUser(username)\n                    .doOnNext(preference -> {\n                        var reasonTypeNotifierMap = preference.getReasonTypeNotifier();\n                        for (ReasonTypeInfo reasonType : reasonTypes) {\n                            var reasonTypeIndex = reasonTypeIndexMap.get(reasonType.name());\n                            var notifierNames =\n                                reasonTypeNotifierMap.getNotifiers(reasonType.name());\n                            for (String notifierName : notifierNames) {\n                                // Skip if the notifier enabled in the user preference does not\n                                // exist to avoid null index\n                                if (!notifierIndexMap.containsKey(notifierName)) {\n                                    continue;\n                                }\n                                var notifierIndex = notifierIndexMap.get(notifierName);\n                                stateMatrix[reasonTypeIndex][notifierIndex] = true;\n                            }\n                        }\n                    })\n                    .thenReturn(matrix);\n            });\n    }\n\n    @Data\n    @Accessors(chain = true)\n    static class ReasonTypeNotifierMatrix {\n        private List<ReasonTypeInfo> reasonTypes;\n        private List<NotifierInfo> notifiers;\n        private boolean[][] stateMatrix;\n    }\n\n    record ReasonTypeInfo(String name, String displayName, String description,\n                          Set<String> uiPermissions) {\n\n        public static ReasonTypeInfo from(ReasonType reasonType) {\n            var uiPermissions = Optional.of(MetadataUtil.nullSafeAnnotations(reasonType))\n                .map(annotations -> annotations.get(Role.UI_PERMISSIONS_ANNO))\n                .filter(StringUtils::isNotBlank)\n                .map(uiPermissionStr -> JsonUtils.jsonToObject(uiPermissionStr,\n                    new TypeReference<Set<String>>() {\n                    })\n                )\n                .orElse(Set.of());\n            return new ReasonTypeInfo(reasonType.getMetadata().getName(),\n                reasonType.getSpec().getDisplayName(),\n                reasonType.getSpec().getDescription(),\n                uiPermissions);\n        }\n    }\n\n    record NotifierInfo(String name, String displayName, String description) {\n    }\n\n    record ReasonTypeNotifierCollectionRequest(\n        @Schema(requiredMode = REQUIRED) List<ReasonTypeNotifierRequest> reasonTypeNotifiers) {\n    }\n\n    @Data\n    static class ReasonTypeNotifierRequest {\n        private String reasonType;\n        private List<String> notifiers;\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"api.notification.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/notification/endpoint/UserNotifierEndpoint.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport lombok.RequiredArgsConstructor;\nimport org.springdoc.core.fn.builders.schema.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.notification.NotifierConfigStore;\n\n/**\n * Notifier endpoint for user center.\n *\n * @author guqing\n * @since 2.10.0\n */\n@Component\n@RequiredArgsConstructor\npublic class UserNotifierEndpoint implements CustomEndpoint {\n\n    private final NotifierConfigStore notifierConfigStore;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"NotifierV1alpha1Uc\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"/notifiers/{name}/receiver-config\", this::fetchReceiverConfig,\n                builder -> builder.operationId(\"FetchReceiverConfig\")\n                    .description(\"Fetch receiver config of notifier\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Notifier name\")\n                        .required(true)\n                    )\n                    .response(responseBuilder().implementation(Object.class))\n            )\n            .POST(\"/notifiers/{name}/receiver-config\", this::saveReceiverConfig,\n                builder -> builder.operationId(\"SaveReceiverConfig\")\n                    .description(\"Save receiver config of notifier\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .description(\"Notifier name\")\n                        .required(true)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(Builder.schemaBuilder().implementation(Object.class))\n                        )\n                    )\n                    .response(responseBuilder().implementation(Void.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> fetchReceiverConfig(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return notifierConfigStore.fetchReceiverConfig(name)\n            .flatMap(config -> ServerResponse.ok().bodyValue(config));\n    }\n\n    private Mono<ServerResponse> saveReceiverConfig(ServerRequest request) {\n        var name = request.pathVariable(\"name\");\n        return request.bodyToMono(ObjectNode.class)\n            .switchIfEmpty(Mono.error(\n                () -> new ServerWebInputException(\"Request body must not be empty.\"))\n            )\n            .flatMap(jsonNode -> notifierConfigStore.saveReceiverConfig(name, jsonNode))\n            .then(ServerResponse.ok().build());\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"api.notification.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/AggregatedRouterFunction.java",
    "content": "package run.halo.app.plugin;\n\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.endpoint.console.CustomEndpointsBuilder;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.infra.SecureServerRequest;\n\n/**\n * Aggregated router function built from all custom endpoints.\n *\n * @author johnniang\n */\npublic class AggregatedRouterFunction implements RouterFunction<ServerResponse> {\n\n    private final RouterFunction<ServerResponse> aggregated;\n\n    public AggregatedRouterFunction(ObjectProvider<CustomEndpoint> customEndpoints) {\n        var builder = new CustomEndpointsBuilder();\n        customEndpoints.orderedStream()\n            .forEach(builder::add);\n        this.aggregated = builder.build();\n    }\n\n    @Override\n    public Mono<HandlerFunction<ServerResponse>> route(ServerRequest request) {\n        return aggregated.route(new SecureServerRequest(request));\n    }\n\n    @Override\n    public void accept(RouterFunctions.Visitor visitor) {\n        this.aggregated.accept(visitor);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/BuiltInPluginsInitializer.java",
    "content": "package run.halo.app.plugin;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\nimport org.springframework.core.io.support.ResourcePatternResolver;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.infra.ExtensionInitializedEvent;\nimport run.halo.app.infra.exception.PluginAlreadyExistsException;\n\n@Slf4j\n@Component\nclass BuiltInPluginsInitializer implements ApplicationListener<ExtensionInitializedEvent> {\n\n    private static final String PRESETS_LOCATION_PATTERN = \"classpath:/plugins/built-in/*.jar\";\n\n    private final ExtensionClient client;\n\n    private final PluginService pluginService;\n\n    private ResourcePatternResolver resourcePatternResolver;\n\n    private PluginFinder pluginFinder;\n\n    BuiltInPluginsInitializer(ExtensionClient client, PluginService pluginService) {\n        this.client = client;\n        this.pluginService = pluginService;\n        this.resourcePatternResolver = new PathMatchingResourcePatternResolver();\n        this.pluginFinder = new YamlPluginFinder();\n    }\n\n    /**\n     * Only for testing purpose.\n     *\n     * @param resourcePatternResolver resource pattern resolver\n     */\n    void setResourcePatternResolver(ResourcePatternResolver resourcePatternResolver) {\n        Assert.notNull(resourcePatternResolver, \"ResourcePatternResolver must not be null\");\n        this.resourcePatternResolver = resourcePatternResolver;\n    }\n\n    /**\n     * Only for testing purpose.\n     *\n     * @param pluginFinder plugin finder\n     */\n    void setPluginFinder(PluginFinder pluginFinder) {\n        this.pluginFinder = pluginFinder;\n    }\n\n    @Override\n    public void onApplicationEvent(ExtensionInitializedEvent event) {\n        try {\n            for (var resource : resourcePatternResolver.getResources(PRESETS_LOCATION_PATTERN)) {\n                var filename = resource.getFilename();\n                if (filename == null) {\n                    continue;\n                }\n                var pluginPath = Path.of(resource.getURI());\n                var preflightPlugin = pluginFinder.find(pluginPath);\n                var pluginName = preflightPlugin.getMetadata().getName();\n\n                log.info(\"Try to installing built-in plugin '{}'...\", pluginName);\n                var plugin = pluginService.install(pluginPath)\n                    .doOnNext(created -> {\n                        log.info(\"Built-in plugin '{}' has been installed.\",\n                            created.getMetadata().getName());\n                    })\n                    .onErrorResume(PluginAlreadyExistsException.class, e -> {\n                        log.info(\"Built-in plugin '{}' already installed, trying to upgrade...\",\n                            pluginName);\n                        return pluginService.upgrade(pluginName, pluginPath)\n                            .doOnNext(updated -> log.info(\"Built-in plugin '{}' has been upgraded.\",\n                                updated.getMetadata().getName()));\n                    })\n                    .blockOptional(Duration.ofSeconds(10)).orElseThrow(\n                        () -> new IllegalStateException(\n                            \"Failed to install or upgrade built-in plugin '\" + pluginName + \"'\"\n                        )\n                    );\n                // try to update metadata to add system reserved label and finalizer\n                var metadata = plugin.getMetadata();\n                metadata.setDeletionTimestamp(null);\n                if (metadata.getLabels() == null) {\n                    metadata.setLabels(new HashMap<>());\n                }\n                metadata.getLabels().put(Plugin.SYSTEM_RESERVED_LABEL_KEY, Boolean.TRUE.toString());\n                if (metadata.getFinalizers() == null) {\n                    metadata.setFinalizers(new HashSet<>());\n                }\n                metadata.getFinalizers().add(Plugin.BUILT_IN_KEEPER_FINALIZER);\n                client.update(plugin);\n            }\n        } catch (FileNotFoundException ignored) {\n            // should never happen\n            log.warn(\"No built-in plugins found.\");\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DefaultDevelopmentPluginRepository.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.pf4j.DevelopmentPluginRepository;\nimport org.springframework.util.CollectionUtils;\n\n/**\n * <p>A {@link org.pf4j.PluginRepository} implementation that can add fixed plugin paths for\n * development {@link org.pf4j.RuntimeMode#DEVELOPMENT}.</p>\n * <p>change {@link #deletePluginPath(Path)} to a no-op method.</p>\n * Note: This class is not thread-safe.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class DefaultDevelopmentPluginRepository extends DevelopmentPluginRepository {\n    private final List<Path> fixedPaths = new ArrayList<>();\n\n    public DefaultDevelopmentPluginRepository(Path... pluginsRoots) {\n        super(pluginsRoots);\n    }\n\n    public DefaultDevelopmentPluginRepository(List<Path> pluginsRoots) {\n        super(pluginsRoots);\n    }\n\n    public void setFixedPaths(List<Path> paths) {\n        if (CollectionUtils.isEmpty(paths)) {\n            return;\n        }\n        fixedPaths.clear();\n        fixedPaths.addAll(paths);\n    }\n\n    @Override\n    public List<Path> getPluginPaths() {\n        List<Path> paths = new ArrayList<>(fixedPaths);\n        paths.addAll(super.getPluginPaths());\n        return paths;\n    }\n\n    @Override\n    public boolean deletePluginPath(Path pluginPath) {\n        // If the plugin path is not included in the fixed paths,\n        // return false and give another repository a chance.\n        //\n        // Meanwhile, there is no need to physically delete the plugin here.\n        return fixedPaths.remove(pluginPath);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.PluginRuntimeException;\nimport org.springframework.beans.factory.support.DefaultBeanNameGenerator;\nimport org.springframework.beans.factory.support.DefaultListableBeanFactory;\nimport org.springframework.boot.env.PropertySourceLoader;\nimport org.springframework.boot.env.YamlPropertySourceLoader;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.context.event.ContextClosedEvent;\nimport org.springframework.context.event.ContextRefreshedEvent;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.core.ResolvableType;\nimport org.springframework.core.env.PropertySource;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.ResourceLoader;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.util.StopWatch;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.Exceptions;\nimport run.halo.app.core.endpoint.WebSocketEndpoint;\nimport run.halo.app.core.endpoint.WebSocketEndpointManager;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.plugin.event.HaloPluginBeforeStopEvent;\nimport run.halo.app.plugin.event.HaloPluginStartedEvent;\nimport run.halo.app.plugin.event.HaloPluginStoppedEvent;\nimport run.halo.app.plugin.event.SpringPluginStartedEvent;\nimport run.halo.app.plugin.event.SpringPluginStoppedEvent;\nimport run.halo.app.plugin.event.SpringPluginStoppingEvent;\nimport run.halo.app.search.SearchService;\nimport run.halo.app.theme.DefaultTemplateNameResolver;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.finders.FinderRegistry;\n\n@Slf4j\npublic class DefaultPluginApplicationContextFactory implements PluginApplicationContextFactory {\n\n    private final SpringPluginManager pluginManager;\n\n    public DefaultPluginApplicationContextFactory(SpringPluginManager pluginManager) {\n        this.pluginManager = pluginManager;\n    }\n\n    /**\n     * Create and refresh application context. Make sure the plugin has already loaded\n     * before.\n     *\n     * @param pluginId plugin id\n     * @return refresh application context for the plugin.\n     */\n    @Override\n    public ApplicationContext create(String pluginId) {\n        log.debug(\"Preparing to create application context for plugin {}\", pluginId);\n\n        var sw = new StopWatch(\"CreateApplicationContextFor\" + pluginId);\n        sw.start(\"Create\");\n\n        var pluginWrapper = pluginManager.getPlugin(pluginId);\n        var classLoader = pluginWrapper.getPluginClassLoader();\n\n\n        /*\n         * Manually creating a BeanFactory and setting the plugin's ClassLoader is necessary\n         * to ensure that conditional annotations (e.g., @ConditionalOnClass) within the plugin\n         * context can correctly load classes.\n         * When PluginApplicationContext is created, its constructor initializes an\n         * AnnotatedBeanDefinitionReader, which in turn creates a ConditionEvaluator.\n         * ConditionEvaluator is responsible for evaluating conditional annotations such as\n         * @ConditionalOnClass.\n         * It relies on the BeanDefinitionRegistry's ClassLoader to load the classes specified in\n         * the annotations.\n         * Without explicitly setting the plugin's ClassLoader, the default application\n         * ClassLoader is used, which fails to load classes from the plugin.\n         * Therefore, a custom DefaultListableBeanFactory with the plugin ClassLoader is required\n         * to resolve this issue.\n         */\n        var beanFactory = new DefaultListableBeanFactory();\n        beanFactory.setBeanClassLoader(classLoader);\n\n        var context = new PluginApplicationContext(pluginId, pluginManager, beanFactory);\n        context.setBeanNameGenerator(DefaultBeanNameGenerator.INSTANCE);\n        context.registerShutdownHook();\n        context.setParent(pluginManager.getSharedContext());\n\n        var resourceLoader = new DefaultResourceLoader(classLoader);\n        context.setResourceLoader(resourceLoader);\n        sw.stop();\n\n        sw.start(\"LoadPropertySources\");\n        var mutablePropertySources = context.getEnvironment().getPropertySources();\n\n        resolvePropertySources(pluginId, resourceLoader)\n            .forEach(mutablePropertySources::addLast);\n        sw.stop();\n\n        sw.start(\"RegisterBeans\");\n        beanFactory.registerSingleton(\"pluginWrapper\", pluginWrapper);\n        context.registerBean(AggregatedRouterFunction.class);\n\n        if (pluginWrapper.getPlugin() instanceof SpringPlugin springPlugin) {\n            beanFactory.registerSingleton(\"pluginContext\", springPlugin.getPluginContext());\n        }\n\n        var rootContext = pluginManager.getRootContext();\n        rootContext.getBeanProvider(ViewNameResolver.class)\n            .ifAvailable(viewNameResolver -> {\n                var templateNameResolver =\n                    new DefaultTemplateNameResolver(viewNameResolver, context);\n                beanFactory.registerSingleton(\"templateNameResolver\", templateNameResolver);\n            });\n\n        rootContext.getBeanProvider(ReactiveExtensionClient.class)\n            .ifUnique(client -> {\n                context.registerBean(\"reactiveSettingFetcher\", DefaultReactiveSettingFetcher.class);\n                context.registerBean(\"settingFetcher\", DefaultSettingFetcher.class);\n            });\n\n        rootContext.getBeanProvider(PluginRequestMappingHandlerMapping.class)\n            .ifAvailable(handlerMapping -> {\n                var handlerMappingManager =\n                    new PluginHandlerMappingManager(pluginId, handlerMapping);\n                beanFactory.registerSingleton(\"pluginHandlerMappingManager\", handlerMappingManager);\n            });\n\n        context.registerBean(PluginControllerManager.class);\n        beanFactory.registerSingleton(\"springPluginStoppedEventAdapter\",\n            new SpringPluginStoppedEventAdapter(pluginId));\n        beanFactory.registerSingleton(\"haloPluginEventBridge\", new HaloPluginEventBridge());\n\n        rootContext.getBeanProvider(FinderRegistry.class)\n            .ifAvailable(finderRegistry -> {\n                var finderManager = new FinderManager(pluginId, finderRegistry);\n                beanFactory.registerSingleton(\"finderManager\", finderManager);\n            });\n\n        rootContext.getBeanProvider(WebSocketEndpointManager.class)\n            .ifUnique(manager -> beanFactory.registerSingleton(\"pluginWebSocketEndpointManager\",\n                new PluginWebSocketEndpointManager(manager)));\n\n        rootContext.getBeanProvider(PluginRouterFunctionRegistry.class)\n            .ifUnique(registry -> {\n                var pluginRouterFunctionManager = new PluginRouterFunctionManager(registry);\n                beanFactory.registerSingleton(\n                    \"pluginRouterFunctionManager\",\n                    pluginRouterFunctionManager\n                );\n            });\n\n        rootContext.getBeanProvider(SearchService.class)\n            .ifUnique(searchService ->\n                beanFactory.registerSingleton(\"searchService\", searchService)\n            );\n\n        sw.stop();\n\n        sw.start(\"LoadComponents\");\n        var classNames = pluginManager.getExtensionClassNames(pluginId);\n        classNames.stream()\n            .map(className -> {\n                try {\n                    return classLoader.loadClass(className);\n                } catch (ClassNotFoundException e) {\n                    throw new PluginRuntimeException(String.format(\"\"\"\n                        Failed to load class %s for plugin %s.\\\n                        \"\"\", className, pluginId), e);\n                }\n            })\n            .forEach(clazzName -> context.registerBean(clazzName));\n        sw.stop();\n        log.debug(\"Created application context for plugin {}\", pluginId);\n\n        log.debug(\"Refreshing application context for plugin {}\", pluginId);\n        sw.start(\"Refresh\");\n\n        // Set the context ClassLoader to the plugin ClassLoader to ensure that\n        // any class loading operations performed by the context (e.g., initializing\n        // bean definitions, loading class resources during static initialization)\n        // use the correct ClassLoader.\n        var previous = Thread.currentThread().getContextClassLoader();\n        try {\n            Thread.currentThread().setContextClassLoader(classLoader);\n            context.refresh();\n        } finally {\n            // reset the class loader to previous one to prevent resource leak\n            Thread.currentThread().setContextClassLoader(previous);\n        }\n        sw.stop();\n        log.debug(\"Refreshed application context for plugin {}\", pluginId);\n        if (log.isDebugEnabled()) {\n            log.debug(\"\\n{}\", sw.prettyPrint(TimeUnit.MILLISECONDS));\n        }\n        return context;\n    }\n\n    private static class FinderManager {\n\n        private final String pluginId;\n\n        private final FinderRegistry finderRegistry;\n\n        private FinderManager(String pluginId, FinderRegistry finderRegistry) {\n            this.pluginId = pluginId;\n            this.finderRegistry = finderRegistry;\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextClosedEvent ignored) {\n            this.finderRegistry.unregister(this.pluginId);\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextRefreshedEvent event) {\n            this.finderRegistry.register(this.pluginId, event.getApplicationContext());\n        }\n\n    }\n\n    private static class PluginWebSocketEndpointManager {\n\n        private final WebSocketEndpointManager manager;\n\n        private List<WebSocketEndpoint> endpoints;\n\n        private PluginWebSocketEndpointManager(WebSocketEndpointManager manager) {\n            this.manager = manager;\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextRefreshedEvent event) {\n            var context = event.getApplicationContext();\n            this.endpoints = context.getBeanProvider(WebSocketEndpoint.class)\n                .orderedStream()\n                .toList();\n            manager.register(this.endpoints);\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextClosedEvent ignored) {\n            manager.unregister(this.endpoints);\n        }\n    }\n\n    private static class PluginRouterFunctionManager {\n\n        private final PluginRouterFunctionRegistry routerFunctionRegistry;\n\n        private Collection<RouterFunction<ServerResponse>> routerFunctions;\n\n        private PluginRouterFunctionManager(PluginRouterFunctionRegistry routerFunctionRegistry) {\n            this.routerFunctionRegistry = routerFunctionRegistry;\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextClosedEvent ignored) {\n            if (routerFunctions != null) {\n                routerFunctionRegistry.unregister(routerFunctions);\n            }\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextRefreshedEvent event) {\n            var routerFunctions = event.getApplicationContext()\n                .<RouterFunction<ServerResponse>>getBeanProvider(\n                    ResolvableType.forClassWithGenerics(RouterFunction.class, ServerResponse.class)\n                )\n                .orderedStream()\n                .toList();\n            routerFunctionRegistry.register(routerFunctions);\n            this.routerFunctions = routerFunctions;\n        }\n    }\n\n\n    private static class PluginHandlerMappingManager {\n        private final String pluginId;\n\n        private final PluginRequestMappingHandlerMapping handlerMapping;\n\n        private PluginHandlerMappingManager(String pluginId,\n            PluginRequestMappingHandlerMapping handlerMapping) {\n            this.pluginId = pluginId;\n            this.handlerMapping = handlerMapping;\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextRefreshedEvent event) {\n            var context = event.getApplicationContext();\n            context.getBeansWithAnnotation(Controller.class)\n                .values()\n                .forEach(controller ->\n                    handlerMapping.registerHandlerMethods(this.pluginId, controller)\n                );\n        }\n\n        @EventListener\n        public void onApplicationEvent(ContextClosedEvent ignored) {\n            handlerMapping.unregister(this.pluginId);\n        }\n    }\n\n    private class SpringPluginStoppedEventAdapter\n        implements ApplicationListener<ContextClosedEvent> {\n\n        private final String pluginId;\n\n        private SpringPluginStoppedEventAdapter(String pluginId) {\n            this.pluginId = pluginId;\n        }\n\n        @Override\n        public void onApplicationEvent(ContextClosedEvent event) {\n            var plugin = pluginManager.getPlugin(pluginId).getPlugin();\n            if (plugin instanceof SpringPlugin springPlugin) {\n                event.getApplicationContext()\n                    .publishEvent(new SpringPluginStoppedEvent(this, springPlugin));\n            }\n        }\n    }\n\n    private class HaloPluginEventBridge {\n\n        @EventListener\n        public void onApplicationEvent(SpringPluginStartedEvent event) {\n            var pluginContext = event.getSpringPlugin().getPluginContext();\n            var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());\n\n            pluginManager.getRootContext()\n                .publishEvent(new HaloPluginStartedEvent(this, pluginWrapper));\n        }\n\n        @EventListener\n        public void onApplicationEvent(SpringPluginStoppingEvent event) {\n            var pluginContext = event.getSpringPlugin().getPluginContext();\n            var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());\n            pluginManager.getRootContext()\n                .publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper));\n        }\n\n        @EventListener\n        public void onApplicationEvent(SpringPluginStoppedEvent event) {\n            var pluginContext = event.getSpringPlugin().getPluginContext();\n            var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());\n            pluginManager.getRootContext()\n                .publishEvent(new HaloPluginStoppedEvent(this, pluginWrapper));\n        }\n\n    }\n\n    private List<PropertySource<?>> resolvePropertySources(String pluginId,\n        ResourceLoader resourceLoader) {\n        var haloProperties = pluginManager.getRootContext()\n            .getBeanProvider(HaloProperties.class)\n            .getIfAvailable();\n        if (haloProperties == null) {\n            return List.of();\n        }\n\n        var propertySourceLoader = new YamlPropertySourceLoader();\n        var propertySources = new ArrayList<PropertySource<?>>();\n        var configsPath = haloProperties.getWorkDir().resolve(\"plugins\").resolve(\"configs\");\n        // resolve user defined config\n        Stream.of(\n                configsPath.resolve(pluginId + \".yaml\"),\n                configsPath.resolve(pluginId + \".yml\")\n            )\n            .map(path -> resourceLoader.getResource(path.toUri().toString()))\n            .forEach(resource -> {\n                var sources =\n                    loadPropertySources(\"user-defined-config\", resource, propertySourceLoader);\n                propertySources.addAll(sources);\n            });\n\n        // resolve default config\n        Stream.of(\n                CLASSPATH_URL_PREFIX + \"/config.yaml\",\n                CLASSPATH_URL_PREFIX + \"/config.yml\"\n            )\n            .map(resourceLoader::getResource)\n            .forEach(resource -> {\n                var sources = loadPropertySources(\"default-config\", resource, propertySourceLoader);\n                propertySources.addAll(sources);\n            });\n        return propertySources;\n    }\n\n    private List<PropertySource<?>> loadPropertySources(String propertySourceName,\n        Resource resource,\n        PropertySourceLoader propertySourceLoader) {\n        if (log.isDebugEnabled()) {\n            log.debug(\"Loading property sources from {}\", resource);\n        }\n        if (!resource.exists()) {\n            return List.of();\n        }\n        try {\n            return propertySourceLoader.load(propertySourceName, resource);\n        } catch (IOException e) {\n            throw Exceptions.propagate(e);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DefaultPluginGetter.java",
    "content": "package run.halo.app.plugin;\n\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.infra.exception.NotFoundException;\n\n/**\n * Default implementation of {@link PluginGetter}.\n *\n * @author guqing\n * @since 2.17.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DefaultPluginGetter implements PluginGetter {\n    private final ExtensionClient client;\n\n    @Override\n    public Plugin getPlugin(String name) {\n        if (StringUtils.isBlank(name)) {\n            throw new IllegalArgumentException(\"Plugin name must not be blank\");\n        }\n        return client.fetch(Plugin.class, name)\n            .orElseThrow(() -> new NotFoundException(\"Plugin not found\"));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistry.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.Collection;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SecureServerRequest;\nimport run.halo.app.infra.exception.PluginRuntimeIncompatibleException;\n\n/**\n * A composite {@link RouterFunction} implementation for plugin.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class DefaultPluginRouterFunctionRegistry\n    implements RouterFunction<ServerResponse>, PluginRouterFunctionRegistry {\n\n    private final Collection<RouterFunction<ServerResponse>> routerFunctions;\n\n    public DefaultPluginRouterFunctionRegistry() {\n        this.routerFunctions = new CopyOnWriteArraySet<>();\n    }\n\n    @Override\n    @NonNull\n    public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {\n        var secureRequest = new SecureServerRequest(request);\n        return Flux.fromIterable(this.routerFunctions)\n            .concatMap(routerFunction -> {\n                // wrap the handler function\n                return routerFunction.route(secureRequest)\n                    .map(hf -> (HandlerFunction<ServerResponse>)\n                        serverRequest -> {\n                            try {\n                                return hf.handle(secureRequest);\n                            } catch (LinkageError le) {\n                                return Mono.error(new PluginRuntimeIncompatibleException(le));\n                            }\n                        }\n                    );\n            })\n            .next();\n    }\n\n    @Override\n    public void accept(@NonNull RouterFunctions.Visitor visitor) {\n        this.routerFunctions.forEach(routerFunction -> routerFunction.accept(visitor));\n    }\n\n    @Override\n    public void register(Collection<RouterFunction<ServerResponse>> routerFunctions) {\n        this.routerFunctions.addAll(routerFunctions);\n    }\n\n    @Override\n    public void unregister(Collection<RouterFunction<ServerResponse>> routerFunctions) {\n        this.routerFunctions.removeAll(routerFunctions);\n    }\n\n    /**\n     * Only for testing.\n     *\n     * @return maintained router functions.\n     */\n    Collection<RouterFunction<ServerResponse>> getRouterFunctions() {\n        return routerFunctions;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DefaultReactiveSettingFetcher.java",
    "content": "package run.halo.app.plugin;\n\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jspecify.annotations.Nullable;\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionMatcher;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.infra.utils.SystemConfigUtils;\nimport tools.jackson.core.JacksonException;\nimport tools.jackson.databind.json.JsonMapper;\n\n/**\n * A default implementation of {@link ReactiveSettingFetcher}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\nclass DefaultReactiveSettingFetcher\n    implements ReactiveSettingFetcher, Reconciler<Reconciler.Request>, ApplicationContextAware {\n\n    private static final Duration TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private final JsonMapper mapper = JsonMapper.builder().build();\n\n    private final ObjectMapper v2Mapper = JsonUtils.mapper();\n\n    private final ReactiveExtensionClient client;\n\n    private final Mono<ConfigMap> configMapCache;\n\n    private final AtomicBoolean isCacheInvalidated = new AtomicBoolean(false);\n\n    /**\n     * The application context of the plugin.\n     */\n    private ApplicationContext applicationContext;\n\n    private final String pluginName;\n\n    private final String configMapName;\n\n    public DefaultReactiveSettingFetcher(\n        PluginContext pluginContext, ReactiveExtensionClient client\n    ) {\n        this.client = client;\n        this.pluginName = pluginContext.getName();\n        this.configMapName = pluginContext.getConfigMapName();\n        this.configMapCache = Mono.defer(() -> {\n            if (StringUtils.isBlank(configMapName)) {\n                return Mono.empty();\n            }\n            return client.fetch(ConfigMap.class, configMapName);\n        }).cacheInvalidateIf(cm -> isCacheInvalidated.getAndSet(false));\n    }\n\n    @Override\n    public <T> Mono<T> fetch(String group, Class<T> clazz) {\n        return getSettingValue(group)\n            .map(n -> mapper.convertValue(n, clazz));\n    }\n\n    @Override\n    public Mono<JsonNode> get(String group) {\n        return getValues().mapNotNull(m -> m.get(group))\n            .switchIfEmpty(Mono.fromSupplier(v2Mapper::createObjectNode));\n    }\n\n    @Override\n    public Mono<tools.jackson.databind.JsonNode> getSettingValue(String group) {\n        return getSettingValues().mapNotNull(m -> m.get(group));\n    }\n\n    @Override\n    public Mono<Map<String, JsonNode>> getValues() {\n        return configMapCache.mapNotNull(ConfigMap::getData)\n            .map(this::toJackson2JsonNodeMap)\n            .defaultIfEmpty(Map.of());\n    }\n\n    @Override\n    public Mono<Map<String, tools.jackson.databind.JsonNode>> getSettingValues() {\n        return configMapCache.mapNotNull(ConfigMap::getData)\n            .map(this::toJackson3JsonNodeMap)\n            .defaultIfEmpty(Map.of());\n    }\n\n    private Map<String, tools.jackson.databind.JsonNode> toJackson3JsonNodeMap(\n        @Nullable Map<String, String> data\n    ) {\n        if (data == null) {\n            return Map.of();\n        }\n        return data.entrySet().stream().collect(Collectors.toMap(\n            Map.Entry::getKey,\n            e -> toJackson3JsonNode(e.getValue())\n        ));\n    }\n\n    private Map<String, JsonNode> toJackson2JsonNodeMap(@Nullable Map<String, String> data) {\n        if (data == null) {\n            return Map.of();\n        }\n        return data.entrySet().stream().collect(Collectors.toMap(\n            Map.Entry::getKey,\n            e -> toJackson2JsonNode(e.getValue())\n        ));\n    }\n\n    private tools.jackson.databind.JsonNode toJackson3JsonNode(\n        @Nullable String json) {\n        if (StringUtils.isBlank(json)) {\n            return mapper.missingNode();\n        }\n        try {\n            return mapper.readTree(json);\n        } catch (JacksonException ex) {\n            log.error(\"Failed to parse plugin [{}] config json: [{}]\", pluginName, json, ex);\n            return mapper.missingNode();\n        }\n    }\n\n    private JsonNode toJackson2JsonNode(String json) {\n        if (StringUtils.isBlank(json)) {\n            return v2Mapper.missingNode();\n        }\n        try {\n            return v2Mapper.readTree(json);\n        } catch (JsonProcessingException e) {\n            // ignore\n            log.error(\"Failed to parse plugin [{}] config json: [{}]\", pluginName, json, e);\n        }\n        return JsonNodeFactory.instance.missingNode();\n    }\n\n    static String buildCacheKey(String pluginName) {\n        return \"plugin-\" + pluginName + \"-configmap\";\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        return client.fetch(ConfigMap.class, configMapName)\n            .filter(Predicate.not(ExtensionUtil::isDeleted))\n            .flatMap(cm -> {\n                // get data snapshot\n                var snapshot = SystemConfigUtils.getDataSnapshot(cm);\n                if (SystemConfigUtils.populateChecksum(cm)) {\n                    // if config map is changed\n                    SystemConfigUtils.updateDataSnapshot(cm);\n                    return client.update(cm).then(Mono.fromCallable(() -> {\n                        this.isCacheInvalidated.set(true);\n                        applicationContext.publishEvent(PluginConfigUpdatedEvent.builder()\n                            .source(this)\n                            .oldConfig(toJackson2JsonNodeMap(snapshot))\n                            .newConfig(toJackson2JsonNodeMap(cm.getData()))\n                            .oldSettingValues(toJackson3JsonNodeMap(snapshot))\n                            .newSettingValues(toJackson3JsonNodeMap(cm.getData()))\n                            .build());\n                        return null;\n                    }));\n                }\n                return Mono.<Result>empty();\n            }).blockOptional(TIMEOUT).orElse(null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        if (StringUtils.isBlank(configMapName)) {\n            // Disable the controller if the config map name is not set\n            return builder\n                .extension(new ConfigMap())\n                .syncAllOnStart(false)\n                .onAddMatcher(extension -> false)\n                .onUpdateMatcher(extension -> false)\n                .onDeleteMatcher(extension -> false)\n                .build();\n        }\n        ExtensionMatcher matcher =\n            extension -> Objects.equals(extension.getMetadata().getName(), configMapName);\n        return builder\n            .extension(new ConfigMap())\n            .syncAllOnStart(true)\n            .syncAllListOptions(ListOptions.builder()\n                .fieldQuery(equal(\"metadata.name\", configMapName))\n                .build())\n            .onAddMatcher(matcher)\n            .onUpdateMatcher(matcher)\n            .onDeleteMatcher(matcher)\n            .build();\n    }\n\n    @Override\n    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n        this.applicationContext = applicationContext;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DefaultSettingFetcher.java",
    "content": "package run.halo.app.plugin;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport org.springframework.lang.NonNull;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * <p>A value fetcher for plugin form configuration.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\nclass DefaultSettingFetcher implements SettingFetcher {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private final ReactiveSettingFetcher delegateFetcher;\n\n    public DefaultSettingFetcher(ReactiveSettingFetcher reactiveSettingFetcher) {\n        this.delegateFetcher = reactiveSettingFetcher;\n    }\n\n    @NonNull\n    @Override\n    public <T> Optional<T> fetch(String group, Class<T> clazz) {\n        return delegateFetcher.fetch(group, clazz)\n            .blockOptional(BLOCKING_TIMEOUT);\n    }\n\n    @NonNull\n    @Override\n    public JsonNode get(String group) {\n        return Objects.requireNonNull(delegateFetcher.get(group).block(BLOCKING_TIMEOUT));\n    }\n\n    @Override\n    public tools.jackson.databind.JsonNode getSettingValue(String group) {\n        return delegateFetcher.getSettingValue(group).block(BLOCKING_TIMEOUT);\n    }\n\n    /**\n     * Get values from {@link ConfigMap}.\n     *\n     * @return a unmodifiable map of values(non-null).\n     */\n    @NonNull\n    @Override\n    public Map<String, JsonNode> getValues() {\n        return Objects.requireNonNull(delegateFetcher.getValues().block(BLOCKING_TIMEOUT));\n    }\n\n    @Override\n    public Map<String, tools.jackson.databind.JsonNode> getSettingValues() {\n        return delegateFetcher.getSettingValues().block(BLOCKING_TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DefaultSpringPlugin.java",
    "content": "package run.halo.app.plugin;\n\nimport org.jspecify.annotations.NonNull;\nimport org.pf4j.Plugin;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport run.halo.app.plugin.event.SpringPluginStartedEvent;\nimport run.halo.app.plugin.event.SpringPluginStartingEvent;\nimport run.halo.app.plugin.event.SpringPluginStoppingEvent;\n\n/**\n * Default implementation of {@link SpringPlugin}.\n *\n * @author johnniang\n * @since 2.22.0\n */\nclass DefaultSpringPlugin extends Plugin implements SpringPlugin {\n\n    private ApplicationContext context;\n\n    private Plugin delegate;\n\n    private final PluginApplicationContextFactory contextFactory;\n\n    private final PluginContext pluginContext;\n\n    public DefaultSpringPlugin(PluginApplicationContextFactory contextFactory,\n        PluginContext pluginContext) {\n        this.contextFactory = contextFactory;\n        this.pluginContext = pluginContext;\n    }\n\n    @Override\n    public void start() {\n        log.info(\"Preparing starting plugin {}\", pluginContext.getName());\n        var pluginId = pluginContext.getName();\n        var previous = Thread.currentThread().getContextClassLoader();\n        try {\n            // initialize context\n            this.context = contextFactory.create(pluginId);\n            Thread.currentThread().setContextClassLoader(this.context.getClassLoader());\n            log.info(\"Application context {} for plugin {} is created\", this.context, pluginId);\n\n            var pluginOpt = context.getBeanProvider(Plugin.class)\n                .stream()\n                .findFirst();\n            log.info(\"Before publishing plugin starting event for plugin {}\", pluginId);\n            context.publishEvent(new SpringPluginStartingEvent(this, this));\n            log.info(\"After publishing plugin starting event for plugin {}\", pluginId);\n            if (pluginOpt.isPresent()) {\n                this.delegate = pluginOpt.get();\n                log.info(\"Starting {} for plugin {}\", this.delegate, pluginId);\n                this.delegate.start();\n                log.info(\"Started {} for plugin {}\", this.delegate, pluginId);\n            }\n            log.info(\"Before publishing plugin started event for plugin {}\", pluginId);\n            context.publishEvent(new SpringPluginStartedEvent(this, this));\n            log.info(\"After publishing plugin started event for plugin {}\", pluginId);\n        } catch (Throwable t) {\n            // try to stop plugin for cleaning resources if something went wrong\n            log.error(\n                \"Cleaning up plugin resources for plugin {} due to not being able to start plugin.\",\n                pluginId);\n            this.stop();\n            // propagate exception to invoker.\n            throw t;\n        } finally {\n            Thread.currentThread().setContextClassLoader(previous);\n        }\n    }\n\n    @Override\n    public void stop() {\n        var previous = Thread.currentThread().getContextClassLoader();\n        try {\n            if (context != null) {\n                Thread.currentThread().setContextClassLoader(context.getClassLoader());\n                log.info(\"Before publishing plugin stopping event for plugin {}\",\n                    pluginContext.getName());\n                context.publishEvent(new SpringPluginStoppingEvent(this, this));\n                log.info(\"After publishing plugin stopping event for plugin {}\",\n                    pluginContext.getName());\n            }\n            if (this.delegate != null) {\n                log.info(\"Stopping {} for plugin {}\", this.delegate, pluginContext.getName());\n                this.delegate.stop();\n                log.info(\"Stopped {} for plugin {}\", this.delegate, pluginContext.getName());\n            }\n        } finally {\n            Thread.currentThread().setContextClassLoader(previous);\n            if (context instanceof ConfigurableApplicationContext configurableContext) {\n                log.info(\"Closing plugin context for plugin {}\", pluginContext.getName());\n                configurableContext.close();\n                log.info(\"Closed plugin context for plugin {}\", pluginContext.getName());\n            }\n            // reset application context\n            log.info(\"Reset plugin context for plugin {}\", pluginContext.getName());\n            context = null;\n        }\n    }\n\n    @Override\n    public void delete() {\n        if (delegate != null) {\n            delegate.delete();\n        }\n        this.delegate = null;\n    }\n\n    @Override\n    @NonNull\n    public ApplicationContext getApplicationContext() {\n        if (context == null) {\n            throw new IllegalStateException(\"\"\"\n                Plugin has not been started or has already been stopped; \\\n                ApplicationContext is not available.\n                \"\"\");\n        }\n        return context;\n    }\n\n    @Override\n    @NonNull\n    public PluginContext getPluginContext() {\n        return pluginContext;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/DevPluginLoader.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport org.pf4j.DevelopmentPluginLoader;\nimport org.pf4j.PluginDescriptor;\nimport org.pf4j.PluginManager;\n\npublic class DevPluginLoader extends DevelopmentPluginLoader {\n\n    private final PluginProperties pluginProperties;\n\n    public DevPluginLoader(\n        PluginManager pluginManager,\n        PluginProperties pluginProperties\n    ) {\n        super(pluginManager);\n        this.pluginProperties = pluginProperties;\n    }\n\n    @Override\n    public ClassLoader loadPlugin(Path pluginPath, PluginDescriptor pluginDescriptor) {\n        var classesDirectories = pluginProperties.getClassesDirectories();\n        if (classesDirectories != null) {\n            classesDirectories.forEach(\n                classesDirectory -> pluginClasspath.addClassesDirectories(classesDirectory)\n            );\n        }\n        var libDirectories = pluginProperties.getLibDirectories();\n        if (libDirectories != null) {\n            libDirectories.forEach(\n                libDirectory -> pluginClasspath.addJarsDirectories(libDirectory)\n            );\n        }\n        return super.loadPlugin(pluginPath, pluginDescriptor);\n    }\n\n    @Override\n    public boolean isApplicable(Path pluginPath) {\n        // Currently we only support a plugin loading from directory in dev mode.\n        return Files.isDirectory(pluginPath);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/HaloPluginManager.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Stack;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.CompoundPluginLoader;\nimport org.pf4j.CompoundPluginRepository;\nimport org.pf4j.DefaultPluginManager;\nimport org.pf4j.DefaultPluginRepository;\nimport org.pf4j.DefaultPluginStatusProvider;\nimport org.pf4j.ExtensionFactory;\nimport org.pf4j.ExtensionFinder;\nimport org.pf4j.JarPluginLoader;\nimport org.pf4j.JarPluginRepository;\nimport org.pf4j.PluginDescriptorFinder;\nimport org.pf4j.PluginFactory;\nimport org.pf4j.PluginLoader;\nimport org.pf4j.PluginRepository;\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginStateEvent;\nimport org.pf4j.PluginStateListener;\nimport org.pf4j.PluginStatusProvider;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.data.util.Lazy;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.plugin.event.PluginStartedEvent;\n\n/**\n * PluginManager to hold the main ApplicationContext.\n * It provides methods for managing the plugin lifecycle.\n *\n * @author guqing\n * @author johnniang\n * @since 2.0.0\n */\n@Slf4j\nclass HaloPluginManager extends DefaultPluginManager\n    implements SpringPluginManager, InitializingBean {\n\n    private final ApplicationContext rootContext;\n\n    private Lazy<ApplicationContext> sharedContext;\n\n    private final PluginProperties pluginProperties;\n\n    private final PluginsRootGetter pluginsRootGetter;\n\n    private final SystemVersionSupplier systemVersionSupplier;\n\n    public HaloPluginManager(ApplicationContext rootContext,\n        PluginProperties pluginProperties,\n        SystemVersionSupplier systemVersionSupplier,\n        PluginsRootGetter pluginsRootGetter) {\n        this.pluginProperties = pluginProperties;\n        this.rootContext = rootContext;\n        this.pluginsRootGetter = pluginsRootGetter;\n        this.systemVersionSupplier = systemVersionSupplier;\n    }\n\n    @Override\n    protected void initialize() {\n        // Leave the implementation empty because the super#initialize eagerly initializes\n        // components before properties set.\n    }\n\n    @Override\n    public void afterPropertiesSet() throws Exception {\n        super.runtimeMode = pluginProperties.getRuntimeMode();\n        this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext));\n        setExactVersionAllowed(pluginProperties.isExactVersionAllowed());\n        setSystemVersion(systemVersionSupplier.get().toStableVersion().toString());\n\n        super.initialize();\n        // the listener must be after the super#initialize\n        addPluginStateListener(new PluginStartedListener());\n    }\n\n    @Override\n    protected ExtensionFactory createExtensionFactory() {\n        return new SpringExtensionFactory(this);\n    }\n\n    @Override\n    protected ExtensionFinder createExtensionFinder() {\n        var finder = new SpringComponentsFinder(this);\n        addPluginStateListener(finder);\n        return finder;\n    }\n\n    @Override\n    protected PluginFactory createPluginFactory() {\n        var contextFactory = new DefaultPluginApplicationContextFactory(this);\n        var pluginGetter = rootContext.getBean(PluginGetter.class);\n        return new SpringPluginFactory(contextFactory, pluginGetter);\n    }\n\n    @Override\n    protected PluginDescriptorFinder createPluginDescriptorFinder() {\n        return new YamlPluginDescriptorFinder();\n    }\n\n    @Override\n    protected PluginLoader createPluginLoader() {\n        var compoundLoader = new CompoundPluginLoader();\n        compoundLoader.add(new DevPluginLoader(this, this.pluginProperties), this::isDevelopment);\n        compoundLoader.add(new JarPluginLoader(this));\n        return compoundLoader;\n    }\n\n    @Override\n    protected PluginStatusProvider createPluginStatusProvider() {\n        if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) {\n            return new PropertyPluginStatusProvider(pluginProperties);\n        }\n        // Only plugins root is writeable\n        return new DefaultPluginStatusProvider(pluginsRootGetter.get());\n    }\n\n    @Override\n    protected PluginRepository createPluginRepository() {\n        var developmentPluginRepository =\n            new DefaultDevelopmentPluginRepository(getPluginsRoots());\n        developmentPluginRepository\n            .setFixedPaths(pluginProperties.getFixedPluginPath());\n        return new CompoundPluginRepository()\n            .add(developmentPluginRepository, this::isDevelopment)\n            .add(new JarPluginRepository(getPluginsRoots()))\n            .add(new DefaultPluginRepository(getPluginsRoots()));\n    }\n\n    @Override\n    protected List<Path> createPluginsRoot() {\n        return List.of(pluginsRootGetter.get());\n    }\n\n    @Override\n    public void startPlugins() {\n        throw new UnsupportedOperationException(\n            \"The operation of starting all plugins is not supported.\"\n        );\n    }\n\n    @Override\n    public void stopPlugins() {\n        throw new UnsupportedOperationException(\n            \"The operation of stopping all plugins is not supported.\"\n        );\n    }\n\n    @Override\n    public ApplicationContext getRootContext() {\n        return rootContext;\n    }\n\n    @Override\n    public ApplicationContext getSharedContext() {\n        return sharedContext.get();\n    }\n\n    @Override\n    public List<PluginWrapper> getDependents(String pluginId) {\n        if (getPlugin(pluginId) == null) {\n            return List.of();\n        }\n\n        var dependents = new ArrayList<PluginWrapper>();\n        var stack = new Stack<String>();\n        dependencyResolver.getDependents(pluginId).forEach(stack::push);\n        while (!stack.isEmpty()) {\n            var dependent = stack.pop();\n            var pluginWrapper = getPlugin(dependent);\n            if (pluginWrapper != null) {\n                dependents.add(pluginWrapper);\n                dependencyResolver.getDependents(dependent).forEach(stack::push);\n            }\n        }\n        return dependents;\n    }\n\n    @Override\n    public List<PluginWrapper> startedPlugins() {\n        return List.copyOf(super.getStartedPlugins())\n            .stream()\n            // Make sure the plugin is really started\n            .filter(p -> p.getPluginState().isStarted())\n            .toList();\n    }\n\n    /**\n     * Listener for plugin started event.\n     *\n     * @author johnniang\n     * @since 2.17.0\n     */\n    private static class PluginStartedListener implements PluginStateListener {\n\n        @Override\n        public void pluginStateChanged(PluginStateEvent event) {\n            if (PluginState.STARTED.equals(event.getPluginState())) {\n                var plugin = event.getPlugin().getPlugin();\n                if (plugin instanceof SpringPlugin springPlugin) {\n                    try {\n                        springPlugin.getApplicationContext()\n                            .publishEvent(new PluginStartedEvent(this));\n                    } catch (Throwable t) {\n                        var pluginId = event.getPlugin().getPluginId();\n                        log.warn(\"Error while publishing plugin started event for plugin {}\",\n                            pluginId, t);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/HaloSharedEventDelegator.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.Objects;\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * The event that delegates a shared event in core into all started plugins.\n *\n * @author johnniang\n * @since 2.17\n */\n@Getter\nclass HaloSharedEventDelegator extends ApplicationEvent {\n\n    private final ApplicationEvent delegate;\n\n    public HaloSharedEventDelegator(Object source, ApplicationEvent delegate) {\n        super(source);\n        this.delegate = delegate;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        HaloSharedEventDelegator that = (HaloSharedEventDelegator) o;\n        return Objects.equals(delegate, that.delegate);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(delegate);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/OptionalDependentResolver.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.pf4j.PluginDependency;\nimport org.pf4j.PluginDescriptor;\nimport org.pf4j.util.DirectedGraph;\n\n/**\n * <p>Pass in a list of started plugin names to resolve dependency relationships, and return a\n * list of plugin names that depend on the specified plugin.</p>\n * <p>Do not consider the problem of circular dependency, because all the plugins that have been\n * started must not have this problem.</p>\n */\npublic class OptionalDependentResolver {\n    private final DirectedGraph<String> dependentsGraph;\n    private final List<PluginDescriptor> plugins;\n\n    public OptionalDependentResolver(List<PluginDescriptor> startedPlugins) {\n        this.plugins = startedPlugins;\n        this.dependentsGraph = new DirectedGraph<>();\n        resolve();\n    }\n\n    private void resolve() {\n        // populate graphs\n        for (PluginDescriptor plugin : plugins) {\n            addPlugin(plugin);\n        }\n    }\n\n    public List<String> getOptionalDependents(String pluginId) {\n        return new ArrayList<>(dependentsGraph.getNeighbors(pluginId));\n    }\n\n    private void addPlugin(PluginDescriptor descriptor) {\n        String pluginId = descriptor.getPluginId();\n        List<PluginDependency> dependencies = descriptor.getDependencies();\n        if (dependencies.isEmpty()) {\n            dependentsGraph.addVertex(pluginId);\n        } else {\n            boolean edgeAdded = false;\n            for (PluginDependency dependency : dependencies) {\n                if (dependency.isOptional()) {\n                    edgeAdded = true;\n                    dependentsGraph.addEdge(dependency.getPluginId(), pluginId);\n                }\n            }\n\n            // Register the plugin without dependencies, if all of its dependencies are optional.\n            if (!edgeAdded) {\n                dependentsGraph.addVertex(pluginId);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.List;\nimport java.util.concurrent.locks.StampedLock;\nimport org.springframework.beans.factory.support.DefaultListableBeanFactory;\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.context.annotation.AnnotationConfigApplicationContext;\nimport org.springframework.core.ResolvableType;\nimport org.springframework.core.annotation.AnnotationUtils;\nimport org.springframework.util.Assert;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.util.MultiValueMap;\nimport run.halo.app.extension.GroupVersionKind;\n\n/**\n * The generic IOC container for plugins.\n * The plugin-classes loaded through the same plugin-classloader will be put into the same\n * {@link PluginApplicationContext} for bean creation.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class PluginApplicationContext extends AnnotationConfigApplicationContext {\n\n    private final GvkExtensionMapping gvkExtensionMapping = new GvkExtensionMapping();\n\n    private final String pluginId;\n\n    private final SpringPluginManager pluginManager;\n\n    public PluginApplicationContext(String pluginId, SpringPluginManager pluginManager,\n        DefaultListableBeanFactory beanFactory) {\n        super(beanFactory);\n        this.pluginId = pluginId;\n        this.pluginManager = pluginManager;\n    }\n\n    public String getPluginId() {\n        return pluginId;\n    }\n\n    /**\n     * Gets the gvk-extension mapping.\n     * It is thread safe\n     *\n     * @param gvk the group-kind-version\n     * @param extensionName extension resources name\n     */\n    public void addExtensionMapping(GroupVersionKind gvk, String extensionName) {\n        gvkExtensionMapping.addExtensionMapping(gvk, extensionName);\n    }\n\n    /**\n     * Gets the extension names by gvk.\n     * It is thread safe\n     *\n     * @param gvk the group-kind-version\n     * @return a immutable list of extension names\n     */\n    public List<String> getExtensionNames(GroupVersionKind gvk) {\n        return List.copyOf(gvkExtensionMapping.getExtensionNames(gvk));\n    }\n\n    public MultiValueMap<GroupVersionKind, String> extensionNamesMapping() {\n        return gvkExtensionMapping.extensionNamesMapping();\n    }\n\n    static class GvkExtensionMapping {\n        private final StampedLock sl = new StampedLock();\n        private final MultiValueMap<GroupVersionKind, String> extensionNamesMapping =\n            new LinkedMultiValueMap<>();\n\n        public void addAllExtensionMapping(GroupVersionKind gvk, List<String> extensionNames) {\n            long stamp = sl.writeLock();\n            try {\n                extensionNamesMapping.addAll(gvk, extensionNames);\n            } finally {\n                sl.unlockWrite(stamp);\n            }\n        }\n\n        public void addExtensionMapping(GroupVersionKind gvk, String extensionName) {\n            long stamp = sl.writeLock();\n            try {\n                extensionNamesMapping.add(gvk, extensionName);\n            } finally {\n                sl.unlockWrite(stamp);\n            }\n        }\n\n        public List<String> getExtensionNames(GroupVersionKind gvk) {\n            Assert.notNull(gvk, \"The gvk must not be null\");\n            long stamp = sl.tryOptimisticRead();\n            List<String> values = extensionNamesMapping.get(gvk);\n            if (!sl.validate(stamp)) {\n                // Check if another write lock occurs after the optimistic read lock\n                // If so, escalate lock to a pessimistic lock\n                stamp = sl.readLock();\n                try {\n                    return extensionNamesMapping.get(gvk);\n                } finally {\n                    sl.unlockRead(stamp);\n                }\n            }\n            return values;\n        }\n\n        public MultiValueMap<GroupVersionKind, String> extensionNamesMapping() {\n            return new LinkedMultiValueMap<>(extensionNamesMapping);\n        }\n\n        public void clear() {\n            extensionNamesMapping.clear();\n        }\n    }\n\n    @Override\n    protected void publishEvent(Object event, ResolvableType typeHint) {\n        if (event instanceof ApplicationEvent applicationEvent\n            && AnnotationUtils.findAnnotation(event.getClass(), SharedEvent.class) != null) {\n            // publish event via root context\n            var delegateEvent = new PluginSharedEventDelegator(this, applicationEvent);\n            pluginManager.getRootContext().publishEvent(delegateEvent);\n            return;\n        }\n        // unwrap event if needed\n        var originalEvent = event;\n        if (event instanceof HaloSharedEventDelegator delegator) {\n            originalEvent = delegator.getDelegate();\n        }\n        super.publishEvent(originalEvent, typeHint);\n    }\n\n    @Override\n    protected void onClose() {\n        // For subclasses: do nothing by default.\n        super.onClose();\n        gvkExtensionMapping.clear();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginApplicationContextFactory.java",
    "content": "package run.halo.app.plugin;\n\nimport org.springframework.context.ApplicationContext;\n\npublic interface PluginApplicationContextFactory {\n\n    /**\n     * Create and refresh application context.\n     *\n     * @param pluginId plugin id\n     * @return refresh application context for the plugin.\n     */\n    ApplicationContext create(String pluginId);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java",
    "content": "package run.halo.app.plugin;\n\nimport static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResource;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.time.Instant;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.PluginManager;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.reactive.accept.RequestedContentTypeResolver;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemVersionSupplier;\n\n/**\n * Plugin autoconfiguration for Spring Boot.\n *\n * @author guqing\n * @see PluginProperties\n */\n@Slf4j\n@Configuration\n@EnableConfigurationProperties(PluginProperties.class)\npublic class PluginAutoConfiguration {\n\n    @Bean\n    public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping(\n        @Qualifier(\"webFluxContentTypeResolver\")\n        RequestedContentTypeResolver requestedContentTypeResolver\n    ) {\n        PluginRequestMappingHandlerMapping mapping = new PluginRequestMappingHandlerMapping();\n        mapping.setContentTypeResolver(requestedContentTypeResolver);\n        mapping.setOrder(-1);\n        return mapping;\n    }\n\n    @Bean\n    public SpringPluginManager pluginManager(ApplicationContext context,\n        SystemVersionSupplier systemVersionSupplier,\n        PluginProperties pluginProperties,\n        PluginsRootGetter pluginsRootGetter) throws FileNotFoundException, URISyntaxException {\n        return new HaloPluginManager(\n            context, pluginProperties, systemVersionSupplier, pluginsRootGetter\n        );\n    }\n\n    @Bean\n    public RouterFunction<ServerResponse> pluginJsBundleRoute(PluginManager pluginManager,\n        WebProperties webProperties) {\n        var cacheProperties = webProperties.getResources().getCache();\n        return RouterFunctions.route()\n            .GET(\"/plugins/{name}/assets/console/{*resource}\", request -> {\n                String pluginName = request.pathVariable(\"name\");\n                String fileName = request.pathVariable(\"resource\");\n\n                var jsBundle = getJsBundleResource(pluginManager, pluginName, fileName);\n                if (jsBundle == null || !jsBundle.exists()) {\n                    return ServerResponse.notFound().build();\n                }\n                var useLastModified = cacheProperties.isUseLastModified();\n                var bodyBuilder = ServerResponse.ok()\n                    .cacheControl(cacheProperties.getCachecontrol().toHttpCacheControl());\n                try {\n                    if (useLastModified) {\n                        var lastModified = Instant.ofEpochMilli(jsBundle.lastModified());\n                        return request.checkNotModified(lastModified)\n                            .switchIfEmpty(Mono.defer(() -> bodyBuilder.lastModified(lastModified)\n                                .body(BodyInserters.fromResource(jsBundle))));\n                    }\n                    return bodyBuilder.body(BodyInserters.fromResource(jsBundle));\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            })\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java",
    "content": "package run.halo.app.plugin;\n\nimport java.time.Duration;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.exception.SchemeNotFoundException;\nimport run.halo.app.plugin.event.HaloPluginBeforeStopEvent;\n\n/**\n * Synchronization listener executed by the plugin before it is stopped.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\nclass PluginBeforeStopSyncListener {\n\n    private static final Duration CLEANUP_TIMEOUT = Duration.ofMinutes(1);\n\n    private final ReactiveExtensionClient client;\n\n    public PluginBeforeStopSyncListener(ReactiveExtensionClient client) {\n        this.client = client;\n    }\n\n    @EventListener\n    void onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) {\n        var pluginWrapper = event.getPlugin();\n        var p = pluginWrapper.getPlugin();\n        if (!(p instanceof SpringPlugin springPlugin)) {\n            return;\n        }\n        var applicationContext = springPlugin.getApplicationContext();\n        if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) {\n            return;\n        }\n        cleanUpPluginExtensionResources(pluginApplicationContext).block(CLEANUP_TIMEOUT);\n    }\n\n    private Mono<Void> cleanUpPluginExtensionResources(PluginApplicationContext context) {\n        var gvkExtensionNames = context.extensionNamesMapping();\n        return Flux.fromIterable(gvkExtensionNames.entrySet())\n            .flatMap(entry -> Flux.fromIterable(entry.getValue())\n                .flatMap(extensionName -> client.fetch(entry.getKey(), extensionName)\n                    .onErrorComplete(SchemeNotFoundException.class)\n                    .filter(e -> !ExtensionUtil.hasDoNotOverwriteLabel(e))\n                    .flatMap(client::delete)\n                    .retryWhen(Retry.backoff(10, Duration.ofMillis(100))\n                        .filter(OptimisticLockingFailureException.class::isInstance)\n                    )\n                )\n                .flatMap(e -> waitForDeleted(e.groupVersionKind(), e.getMetadata().getName())))\n            .then();\n    }\n\n    private Mono<Void> waitForDeleted(GroupVersionKind gvk, String name) {\n        return client.fetch(gvk, name)\n            .flatMap(e -> {\n                if (log.isDebugEnabled()) {\n                    log.debug(\"Wait for {}/{} deleted\", gvk, name);\n                }\n                return Mono.error(new IllegalStateException(\"Wait for extension deleted\"));\n            })\n            .retryWhen(Retry.backoff(10, Duration.ofMillis(100))\n                .filter(IllegalStateException.class::isInstance)\n            )\n            .then()\n            .doOnSuccess(v -> {\n                if (log.isDebugEnabled()) {\n                    log.debug(\"{}/{} was deleted successfully.\", gvk, name);\n                }\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginConst.java",
    "content": "package run.halo.app.plugin;\n\n/**\n * Plugin constants.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface PluginConst {\n    /**\n     * Plugin metadata labels key.\n     */\n    String PLUGIN_NAME_LABEL_NAME = \"plugin.halo.run/plugin-name\";\n\n    String SYSTEM_PLUGIN_NAME = \"system\";\n\n    String RELOAD_ANNO = \"plugin.halo.run/reload\";\n\n    String REQUEST_TO_UNLOAD_LABEL = \"plugin.halo.run/request-to-unload\";\n\n    String PLUGIN_PATH = \"plugin.halo.run/plugin-path\";\n\n    String RUNTIME_MODE_ANNO = \"plugin.halo.run/runtime-mode\";\n\n    static String assetsRoutePrefix(String pluginName) {\n        return \"/plugins/\" + pluginName + \"/assets/\";\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginControllerManager.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.springframework.core.ResolvableType.forClassWithGenerics;\n\nimport java.util.concurrent.ConcurrentHashMap;\nimport org.springframework.context.event.EventListener;\nimport reactor.core.Disposable;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.plugin.event.SpringPluginStartedEvent;\nimport run.halo.app.plugin.event.SpringPluginStoppingEvent;\n\npublic class PluginControllerManager {\n\n    private final ConcurrentHashMap<String, Controller> controllers;\n\n    private final ExtensionClient client;\n\n    public PluginControllerManager(ExtensionClient client) {\n        this.client = client;\n        controllers = new ConcurrentHashMap<>();\n    }\n\n    @EventListener\n    public void onApplicationEvent(SpringPluginStartedEvent event) {\n        event.getSpringPlugin().getApplicationContext()\n            .<Reconciler<Reconciler.Request>>getBeanProvider(\n                forClassWithGenerics(Reconciler.class, Reconciler.Request.class))\n            .orderedStream()\n            .forEach(this::start);\n    }\n\n    @EventListener\n    public void onApplicationEvent(SpringPluginStoppingEvent event) throws Exception {\n        controllers.values()\n            .forEach(Disposable::dispose);\n        controllers.clear();\n    }\n\n    private void start(Reconciler<Reconciler.Request> reconciler) {\n        var builder = new ControllerBuilder(reconciler, client);\n        var controller = reconciler.setupWith(builder);\n        controllers.put(reconciler.getClass().getName(), controller);\n        controller.start();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginDevelopmentInitializer.java",
    "content": "package run.halo.app.plugin;\n\nimport static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;\n\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.PluginManager;\nimport org.springframework.boot.context.event.ApplicationReadyEvent;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\npublic class PluginDevelopmentInitializer implements ApplicationListener<ApplicationReadyEvent> {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private final PluginManager pluginManager;\n\n    private final PluginProperties pluginProperties;\n\n    private final ReactiveExtensionClient extensionClient;\n\n    public PluginDevelopmentInitializer(PluginManager pluginManager,\n        PluginProperties pluginProperties, ReactiveExtensionClient extensionClient) {\n        this.pluginManager = pluginManager;\n        this.pluginProperties = pluginProperties;\n        this.extensionClient = extensionClient;\n    }\n\n    @Override\n    public void onApplicationEvent(@NonNull ApplicationReadyEvent ignored) {\n        if (!pluginManager.isDevelopment()) {\n            return;\n        }\n        createFixedPluginIfNecessary();\n    }\n\n    private void createFixedPluginIfNecessary() {\n        for (Path path : pluginProperties.getFixedPluginPath()) {\n            Plugin plugin = new YamlPluginFinder().find(path);\n            extensionClient.fetch(Plugin.class, plugin.getMetadata().getName())\n                .flatMap(persistent -> {\n                    plugin.getMetadata().setVersion(persistent.getMetadata().getVersion());\n                    nullSafeAnnotations(plugin).put(PluginConst.RUNTIME_MODE_ANNO, \"dev\");\n                    return extensionClient.update(plugin);\n                })\n                .switchIfEmpty(Mono.defer(() -> {\n                    nullSafeAnnotations(plugin).put(PluginConst.RUNTIME_MODE_ANNO, \"dev\");\n                    return extensionClient.create(plugin);\n                }))\n                .retryWhen(Retry.backoff(10, Duration.ofMillis(100))\n                    .filter(t -> t instanceof OptimisticLockingFailureException))\n                .block(BLOCKING_TIMEOUT);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginExtensionLoaderUtils.java",
    "content": "package run.halo.app.plugin;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.net.URLClassLoader;\nimport java.util.Objects;\nimport java.util.function.Predicate;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\nimport org.springframework.data.util.Predicates;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.extension.Unstructured;\n\n@Slf4j\npublic class PluginExtensionLoaderUtils {\n    static final String EXTENSION_LOCATION_PATTERN = \"classpath:extensions/*.{ext:yaml|yml}\";\n\n    public static Predicate<Unstructured> isSetting(String settingName) {\n        if (StringUtils.isBlank(settingName)) {\n            return Predicates.isFalse();\n        }\n        var settingGk = Setting.GVK.groupKind();\n        return unstructured -> {\n            var gk = unstructured.groupVersionKind().groupKind();\n            var name = unstructured.getMetadata().getName();\n            return Objects.equals(settingName, name) && Objects.equals(settingGk, gk);\n        };\n    }\n\n    public static Resource[] lookupExtensions(ClassLoader classLoader) {\n        if (log.isDebugEnabled()) {\n            log.debug(\"Trying to lookup extensions from {}\", classLoader);\n        }\n        if (classLoader instanceof URLClassLoader urlClassLoader) {\n            var urls = urlClassLoader.getURLs();\n            // The parent class loader must be null here because we don't want to\n            // get any resources from parent class loader.\n            classLoader = new URLClassLoader(urls, null);\n        }\n        var resolver = new PathMatchingResourcePatternResolver(classLoader);\n        try {\n            var resources = resolver.getResources(EXTENSION_LOCATION_PATTERN);\n            if (log.isDebugEnabled()) {\n                log.debug(\"Looked up {} resources(s) from {}\", resources.length, classLoader);\n            }\n            return resources;\n        } catch (FileNotFoundException ignored) {\n            // Ignore the exception only if extensions folder was not found.\n        } catch (IOException e) {\n            throw new RuntimeException(String.format(\"\"\"\n                Failed to get extension resources while resolving plugin setting \\\n                in class loader %s.\\\n                \"\"\", classLoader), e);\n        }\n        return new Resource[] {};\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginFinder.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Path;\nimport org.jspecify.annotations.NonNull;\nimport org.pf4j.PluginRuntimeException;\nimport run.halo.app.core.extension.Plugin;\n\n/**\n * The plugin finder to find plugin manifest from given plugin path.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface PluginFinder {\n\n    /**\n     * Finds plugin manifest by given plugin path.\n     *\n     * @param pluginPath the plugin path\n     * @return the found plugin\n     * @throws PluginRuntimeException if any error occurs during finding plugin\n     */\n    @NonNull\n    Plugin find(@NonNull Path pluginPath);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginGetter.java",
    "content": "package run.halo.app.plugin;\n\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.infra.exception.NotFoundException;\n\n/**\n * An interface to get {@link Plugin} by name.\n *\n * @author guqing\n * @since 2.17.0\n */\n@FunctionalInterface\npublic interface PluginGetter {\n\n    /**\n     * Get plugin by name.\n     *\n     * @param name plugin name must not be null\n     * @return plugin\n     * @throws IllegalArgumentException if plugin name is null\n     * @throws NotFoundException    if plugin not found\n     */\n    Plugin getPlugin(String name);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginNotFoundException.java",
    "content": "package run.halo.app.plugin;\n\nimport run.halo.app.infra.exception.NotFoundException;\n\n/**\n * Exception for plugin not found.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class PluginNotFoundException extends NotFoundException {\n    public PluginNotFoundException(String message) {\n        super(message);\n    }\n\n    public PluginNotFoundException(Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginProperties.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport lombok.Data;\nimport org.pf4j.RuntimeMode;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n/**\n * Properties for plugin.\n *\n * @author guqing\n * @see PluginAutoConfiguration\n */\n@Data\n@ConfigurationProperties(prefix = \"halo.plugin\")\npublic class PluginProperties {\n    public static final String GRADLE_LIBS_DIR = \"build/libs\";\n\n    /**\n     * Auto start plugin when main app is ready.\n     */\n    private boolean autoStartPlugin = true;\n\n    /**\n     * The default plugin path is obtained through file scanning.\n     * In the development mode, you can specify the plugin path as the project directory.\n     */\n    private List<Path> fixedPluginPath = new ArrayList<>();\n\n    /**\n     * Plugins disabled by default.\n     */\n    private String[] disabledPlugins = new String[0];\n\n    /**\n     * Plugins enabled by default, prior to `disabledPlugins`.\n     */\n    private String[] enabledPlugins = new String[0];\n\n    /**\n     * Set to true to allow requires expression to be exactly x.y.z. The default is false, meaning\n     * that using an exact version x.y.z will implicitly mean the same as >=x.y.z.\n     */\n    private boolean exactVersionAllowed = false;\n\n    /**\n     * Extended Plugin Class Directory.\n     */\n    private List<String> classesDirectories = new ArrayList<>();\n\n    /**\n     * Extended Plugin Jar Directory.\n     */\n    private List<String> libDirectories = new ArrayList<>(List.of(GRADLE_LIBS_DIR));\n\n    /**\n     * Runtime Mode：development/deployment.\n     */\n    private RuntimeMode runtimeMode = RuntimeMode.DEPLOYMENT;\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginRequestMappingHandlerMapping.java",
    "content": "package run.halo.app.plugin;\n\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport org.springframework.aop.support.AopUtils;\nimport org.springframework.core.MethodIntrospector;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.util.Assert;\nimport org.springframework.util.ClassUtils;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.reactive.result.method.RequestMappingInfo;\nimport org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;\nimport org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;\nimport run.halo.app.extension.GroupVersion;\n\n/**\n * An extension of {@link RequestMappingInfoHandlerMapping} that creates\n * {@link RequestMappingInfo} instances from class-level and method-level\n * {@link RequestMapping} annotations used by plugin.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class PluginRequestMappingHandlerMapping extends RequestMappingHandlerMapping {\n\n    private final MultiValueMap<String, RequestMappingInfo> pluginMappingInfo =\n        new LinkedMultiValueMap<>();\n\n    @Override\n    protected void initHandlerMethods() {\n        // Parent method will scan beans in the ApplicationContext\n        // detect and register handler methods.\n        // but this is superfluous for this class.\n    }\n\n    /**\n     * Register handler methods according to the plugin id and the controller(annotated\n     * {@link Controller}) bean.\n     *\n     * @param pluginId plugin id to be registered\n     * @param handler controller bean\n     */\n    public void registerHandlerMethods(String pluginId, Object handler) {\n        Class<?> handlerType = (handler instanceof String beanName\n            ? obtainApplicationContext().getType(beanName) : handler.getClass());\n\n        if (handlerType != null) {\n            final Class<?> userType = ClassUtils.getUserClass(handlerType);\n            Map<Method, RequestMappingInfo> methods = MethodIntrospector.selectMethods(userType,\n                (MethodIntrospector.MetadataLookup<RequestMappingInfo>)\n                    method -> getPluginMappingForMethod(pluginId, method, userType));\n            if (logger.isTraceEnabled()) {\n                logger.trace(formatMappings(userType, methods));\n            } else if (mappingsLogger.isDebugEnabled()) {\n                mappingsLogger.debug(formatMappings(userType, methods));\n            }\n            methods.forEach((method, mapping) -> {\n                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);\n                registerHandlerMethod(handler, invocableMethod, mapping);\n                pluginMappingInfo.add(pluginId, mapping);\n            });\n        }\n    }\n\n    private String formatMappings(Class<?> userType, Map<Method, RequestMappingInfo> methods) {\n        String packageName = ClassUtils.getPackageName(userType);\n        String formattedType = (StringUtils.hasText(packageName)\n            ? Arrays.stream(packageName.split(\"\\\\.\"))\n            .map(packageSegment -> packageSegment.substring(0, 1))\n            .collect(Collectors.joining(\".\", \"\", \".\" + userType.getSimpleName())) :\n            userType.getSimpleName());\n        Function<Method, String> methodFormatter =\n            method -> Arrays.stream(method.getParameterTypes())\n                .map(Class::getSimpleName)\n                .collect(Collectors.joining(\",\", \"(\", \")\"));\n        return methods.entrySet().stream()\n            .map(e -> {\n                Method method = e.getKey();\n                return e.getValue() + \": \" + method.getName() + methodFormatter.apply(method);\n            })\n            .collect(Collectors.joining(\"\\n\\t\", \"\\n\\t\" + formattedType + \":\" + \"\\n\\t\", \"\"));\n    }\n\n    /**\n     * Remove handler methods and mapping based on plugin id.\n     *\n     * @param pluginId plugin id\n     */\n    public void unregister(String pluginId) {\n        Assert.notNull(pluginId, \"The pluginId must not be null.\");\n        if (!pluginMappingInfo.containsKey(pluginId)) {\n            return;\n        }\n        pluginMappingInfo.remove(pluginId).forEach(this::unregisterMapping);\n    }\n\n    protected List<RequestMappingInfo> getMappings(String pluginId) {\n        List<RequestMappingInfo> requestMappingInfos = pluginMappingInfo.get(pluginId);\n        if (requestMappingInfos == null) {\n            return Collections.emptyList();\n        }\n        return List.copyOf(requestMappingInfos);\n    }\n\n    protected RequestMappingInfo getPluginMappingForMethod(String pluginId,\n        Method method, Class<?> handlerType) {\n        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);\n        if (info != null) {\n            ApiVersion apiVersion = handlerType.getAnnotation(ApiVersion.class);\n            if (apiVersion == null) {\n                return info;\n            }\n            info = RequestMappingInfo.paths(buildPrefix(pluginId, apiVersion.value())).build()\n                .combine(info);\n        }\n        return info;\n    }\n\n    protected String buildPrefix(String pluginName, String apiVersion) {\n        GroupVersion groupVersion = GroupVersion.parseAPIVersion(apiVersion);\n        if (StringUtils.hasText(groupVersion.group())) {\n            // apis/{group}/{version}\n            return String.format(\"/apis/%s/%s\", groupVersion.group(), groupVersion.version());\n        }\n        // apis/api.plugin.halo.run/{version}/plugins/{pluginName}\n        return String.format(\"/apis/api.plugin.halo.run/%s/plugins/%s\", groupVersion.version(),\n            pluginName);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginRouterFunctionRegistry.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.Collection;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\npublic interface PluginRouterFunctionRegistry {\n    void register(Collection<RouterFunction<ServerResponse>> routerFunctions);\n\n    void unregister(Collection<RouterFunction<ServerResponse>> routerFunctions);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginService.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Plugin;\n\npublic interface PluginService {\n\n    Mono<Void> installPresetPlugins();\n\n    /**\n     * Installs a plugin from a temporary Jar path.\n     *\n     * @param path is temporary jar path. Do not set the plugin home at here.\n     * @return created plugin.\n     */\n    Mono<Plugin> install(Path path);\n\n    Mono<Plugin> upgrade(String name, Path path);\n\n    /**\n     * <p>Reload a plugin by name.</p>\n     * Note that this method will set <code>spec.enabled</code> to true it means that the plugin\n     * will be started.\n     *\n     * @param name plugin name\n     * @return an updated plugin reloaded from plugin path\n     * @throws ServerWebInputException if plugin not found by the given name\n     * @see Plugin.PluginSpec#setEnabled(Boolean)\n     */\n    Mono<Plugin> reload(String name);\n\n    /**\n     * Uglify js bundle from all enabled plugins to a single js bundle string.\n     *\n     * @return uglified js bundle\n     */\n    Flux<DataBuffer> uglifyJsBundle();\n\n    /**\n     * Uglify css bundle from all enabled plugins to a single css bundle string.\n     *\n     * @return uglified css bundle\n     */\n    Flux<DataBuffer> uglifyCssBundle();\n\n    /**\n     * <p>Generate js/css bundle version for cache control.</p>\n     * This method will list all enabled plugins version and sign it to a string.\n     *\n     * @return signed js/css bundle version by all enabled plugins version.\n     */\n    Mono<String> generateBundleVersion();\n\n    /**\n     * Retrieves the JavaScript bundle for all enabled plugins.\n     *\n     * <p>This method combines the JavaScript bundles of all enabled plugins into a single bundle\n     * and returns a representation of this bundle as a resource.\n     * If the JavaScript bundle already exists and is up-to-date, the existing resource is\n     * returned; otherwise, a new JavaScript bundle is generated.\n     *\n     * <p>Note: This method may perform IO operations and could potentially block, so it should be\n     * used in a non-blocking environment.\n     *\n     * @param version The version of the CSS bundle to retrieve.\n     * @return A {@code Mono<Resource>} object representing the JavaScript bundle. When this\n     * {@code Mono} is subscribed to, it emits the JavaScript bundle resource if successful, or\n     * an error signal if an error occurs.\n     */\n    Mono<Resource> getJsBundle(String version);\n\n    /**\n     * Retrieves the CSS bundle for all enabled plugins.\n     *\n     * <p>This method combines the CSS bundles of all enabled plugins into a single bundle and\n     * returns a representation of this bundle as a resource.\n     * If the CSS bundle already exists and is up-to-date, the existing resource is returned;\n     * otherwise, a new CSS bundle is generated.\n     *\n     * <p>Note: This method may perform IO operations and could potentially block, so it should be\n     * used in a non-blocking environment.\n     *\n     * @param version The version of the CSS bundle to retrieve.\n     * @return A {@code Mono<Resource>} object representing the CSS bundle. When this {@code Mono\n     * } is subscribed to, it emits the CSS bundle resource if successful, or an error signal if\n     * an error occurs.\n     */\n    Mono<Resource> getCssBundle(String version);\n\n    /**\n     * Enables or disables a plugin by name.\n     *\n     * @param pluginName plugin name\n     * @param requestToEnable request to enable or disable\n     * @param wait wait for plugin to be enabled or disabled\n     * @return updated plugin\n     */\n    Mono<Plugin> changeState(String pluginName, boolean requestToEnable, boolean wait);\n\n    /**\n     * Gets required dependencies of the given plugin.\n     *\n     * @param plugin the plugin to get dependencies from\n     * {@link Plugin.PluginSpec#getPluginDependencies()}\n     * @param predicate the predicate to filter by {@link PluginWrapper},such as enabled or disabled\n     * @return plugin names of required dependencies\n     */\n    List<String> getRequiredDependencies(Plugin plugin,\n        Predicate<PluginWrapper> predicate);\n\n    /**\n     * Get started plugin names.\n     *\n     * @return started plugin names\n     */\n    Flux<String> getStartedPluginNames();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginServiceImpl.java",
    "content": "package run.halo.app.plugin;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\nimport static java.nio.file.StandardOpenOption.CREATE;\nimport static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;\nimport static org.pf4j.PluginState.STARTED;\nimport static run.halo.app.plugin.PluginConst.RELOAD_ANNO;\n\nimport com.github.zafarkhaja.semver.Version;\nimport com.google.common.hash.HashCode;\nimport com.google.common.hash.Hashing;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.pf4j.DependencyResolver;\nimport org.pf4j.PluginDependency;\nimport org.pf4j.PluginDescriptor;\nimport org.pf4j.PluginWrapper;\nimport org.reactivestreams.Publisher;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.WritableResource;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.core.io.buffer.DefaultDataBufferFactory;\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.FileSystemUtils;\nimport org.springframework.util.StreamUtils;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.infra.exception.PluginAlreadyExistsException;\nimport run.halo.app.infra.exception.PluginDependenciesNotEnabledException;\nimport run.halo.app.infra.exception.PluginDependencyException;\nimport run.halo.app.infra.exception.PluginDependentsNotDisabledException;\nimport run.halo.app.infra.exception.PluginInstallationException;\nimport run.halo.app.infra.exception.UnsatisfiedAttributeValueException;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.infra.utils.VersionUtils;\nimport run.halo.app.plugin.resources.BundleResourceUtils;\n\n@Slf4j\n@Component\npublic class PluginServiceImpl implements PluginService, InitializingBean, DisposableBean {\n\n    private static final String PRESET_LOCATION_PREFIX = \"classpath:/presets/plugins/\";\n    private static final String PRESETS_LOCATION_PATTERN = PRESET_LOCATION_PREFIX + \"*.jar\";\n\n    private final ReactiveExtensionClient client;\n\n    private final SystemVersionSupplier systemVersion;\n\n    private final PluginsRootGetter pluginsRootGetter;\n\n    private final SpringPluginManager pluginManager;\n\n    private final BundleCache jsBundleCache;\n\n    private final BundleCache cssBundleCache;\n\n    private Path tempDir;\n\n    private final Scheduler scheduler = Schedulers.boundedElastic();\n\n    private Clock clock = Clock.systemUTC();\n\n    public PluginServiceImpl(ReactiveExtensionClient client,\n        SystemVersionSupplier systemVersion,\n        PluginsRootGetter pluginsRootGetter,\n        SpringPluginManager pluginManager) {\n        this.client = client;\n        this.systemVersion = systemVersion;\n        this.pluginsRootGetter = pluginsRootGetter;\n        this.pluginManager = pluginManager;\n\n        this.jsBundleCache = new BundleCache(\".js\");\n        this.cssBundleCache = new BundleCache(\".css\");\n    }\n\n    /**\n     * The method is only for testing.\n     *\n     * @param clock new clock\n     */\n    void setClock(Clock clock) {\n        Assert.notNull(clock, \"Clock must not be null\");\n        this.clock = clock;\n    }\n\n    @Override\n    public Mono<Void> installPresetPlugins() {\n        return getPresetJars()\n            .flatMap(path -> this.install(path)\n                .onErrorResume(PluginAlreadyExistsException.class, e -> Mono.empty())\n                .flatMap(plugin -> FileUtils.deleteFileSilently(path)\n                    .thenReturn(plugin)\n                )\n            )\n            .flatMap(this::enablePlugin)\n            .subscribeOn(Schedulers.boundedElastic())\n            .then();\n    }\n\n    private Mono<Plugin> enablePlugin(Plugin plugin) {\n        plugin.getSpec().setEnabled(true);\n        return client.update(plugin)\n            .onErrorResume(OptimisticLockingFailureException.class,\n                e -> enablePlugin(plugin.getMetadata().getName())\n            );\n    }\n\n    private Mono<Plugin> enablePlugin(String name) {\n        return Mono.defer(() -> client.get(Plugin.class, name)\n                .flatMap(plugin -> {\n                    plugin.getSpec().setEnabled(true);\n                    return client.update(plugin);\n                })\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    @Override\n    public Mono<Plugin> install(Path path) {\n        return findPluginManifest(path)\n            .doOnNext(plugin -> {\n                // validate the plugin version\n                satisfiesRequiresVersion(plugin);\n                checkDependencies(plugin);\n            })\n            .flatMap(pluginInPath ->\n                client.fetch(Plugin.class, pluginInPath.getMetadata().getName())\n                    .flatMap(oldPlugin -> Mono.<Plugin>error(\n                        new PluginAlreadyExistsException(oldPlugin.getMetadata().getName())))\n                    .switchIfEmpty(Mono.defer(\n                        () -> copyToPluginHome(pluginInPath)\n                            .flatMap(this::findPluginManifest)\n                            .doOnNext(p -> {\n                                // Disable auto enable after installation\n                                p.getSpec().setEnabled(false);\n                            })\n                            .flatMap(client::create))\n                    ));\n    }\n\n    private void checkDependencies(Plugin plugin) {\n        var resolvedPlugins = new ArrayList<>(pluginManager.getResolvedPlugins());\n        var pluginDescriptors = new ArrayList<PluginDescriptor>(resolvedPlugins.size() + 1);\n\n        resolvedPlugins.stream()\n            .map(PluginWrapper::getDescriptor)\n            .forEach(pluginDescriptors::add);\n\n        var pluginDescriptor = YamlPluginDescriptorFinder.convert(plugin);\n        pluginDescriptors.add(pluginDescriptor);\n\n        var deptResolver = new DependencyResolver(pluginManager.getVersionManager());\n        var result = deptResolver.resolve(pluginDescriptors);\n        if (result.hasCyclicDependency()) {\n            throw new PluginDependencyException.CyclicException();\n        }\n        var notFoundDependencies = result.getNotFoundDependencies();\n        if (!CollectionUtils.isEmpty(notFoundDependencies)) {\n            throw new PluginDependencyException.NotFoundException(notFoundDependencies);\n        }\n\n        var wrongVersionDependencies = result.getWrongVersionDependencies();\n        if (!CollectionUtils.isEmpty(wrongVersionDependencies)) {\n            throw new PluginDependencyException.WrongVersionsException(wrongVersionDependencies);\n        }\n    }\n\n    @Override\n    public Mono<Plugin> upgrade(String name, Path path) {\n        return findPluginManifest(path)\n            .doOnNext(plugin -> {\n                satisfiesRequiresVersion(plugin);\n                checkDependencies(plugin);\n            })\n            .flatMap(pluginInPath -> {\n                // pre-check the plugin in the path\n                Assert.notNull(pluginInPath.statusNonNull().getLoadLocation(),\n                    \"plugin.status.load-location must not be null\");\n                if (!Objects.equals(name, pluginInPath.getMetadata().getName())) {\n                    return Mono.error(new ServerWebInputException(\n                        \"The provided plugin \" + pluginInPath.getMetadata().getName()\n                            + \" didn't match the given plugin \" + name));\n                }\n\n                // check if the plugin exists\n                return client.fetch(Plugin.class, name)\n                    .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                        \"The given plugin with name \" + name + \" was not found.\")))\n                    // copy plugin into plugin home\n                    .flatMap(oldPlugin -> copyToPluginHome(pluginInPath).thenReturn(oldPlugin))\n                    .doOnNext(oldPlugin -> updatePlugin(oldPlugin, pluginInPath))\n                    .flatMap(client::update);\n            });\n    }\n\n    @Override\n    public Mono<Plugin> reload(String name) {\n        return client.get(Plugin.class, name)\n            .flatMap(oldPlugin -> {\n                if (oldPlugin.getStatus() == null\n                    || oldPlugin.getStatus().getLoadLocation() == null) {\n                    return Mono.error(new IllegalStateException(\n                        \"Load location of plugin has not been populated.\"));\n                }\n                var loadLocation = oldPlugin.getStatus().getLoadLocation();\n                var loadPath = Path.of(loadLocation);\n                return findPluginManifest(loadPath)\n                    .doOnNext(newPlugin -> updatePlugin(oldPlugin, newPlugin))\n                    .thenReturn(oldPlugin);\n            })\n            .flatMap(client::update);\n    }\n\n    @Override\n    public Flux<DataBuffer> uglifyJsBundle() {\n        var startedPlugins = pluginManager.startedPlugins();\n        var dataBufferFactory = DefaultDataBufferFactory.sharedInstance;\n        var end = Mono.fromSupplier(\n            () -> {\n                var sb = new StringBuilder(\"this.enabledPlugins = [\");\n                var iterator = startedPlugins.iterator();\n                while (iterator.hasNext()) {\n                    var plugin = iterator.next();\n                    sb.append(\"\"\"\n                        {\"name\":\"%s\",\"version\":\"%s\"}\\\n                        \"\"\"\n                        .formatted(\n                            plugin.getPluginId(),\n                            plugin.getDescriptor().getVersion()\n                        )\n                    );\n                    if (iterator.hasNext()) {\n                        sb.append(',');\n                    }\n                }\n                sb.append(']');\n                return dataBufferFactory.wrap(sb.toString().getBytes(StandardCharsets.UTF_8));\n            });\n        var body = Flux.fromIterable(startedPlugins)\n            .sort(Comparator.comparing(PluginWrapper::getPluginId))\n            .flatMapSequential(pluginWrapper -> {\n                var pluginId = pluginWrapper.getPluginId();\n                return getBundleResource(pluginId, BundleResourceUtils.JS_BUNDLE)\n                    .flatMapMany(resource -> {\n                        var head = Mono.<DataBuffer>fromSupplier(\n                            () -> dataBufferFactory.wrap(\n                                (\"// Generated from plugin \" + pluginId + \"\\n\").getBytes()\n                            ));\n                        var content = DataBufferUtils.read(\n                            resource, dataBufferFactory, StreamUtils.BUFFER_SIZE\n                        );\n                        var tail = Mono.fromSupplier(() -> dataBufferFactory.wrap(\"\\n\".getBytes()));\n                        return Flux.concat(head, content, tail);\n                    });\n            });\n        return Flux.concat(body, end);\n    }\n\n    @Override\n    public Flux<DataBuffer> uglifyCssBundle() {\n        return Flux.fromIterable(pluginManager.startedPlugins())\n            .sort(Comparator.comparing(PluginWrapper::getPluginId))\n            .flatMapSequential(pluginWrapper -> {\n                var pluginId = pluginWrapper.getPluginId();\n                var dataBufferFactory = DefaultDataBufferFactory.sharedInstance;\n                return getBundleResource(pluginId, BundleResourceUtils.CSS_BUNDLE)\n                    .flatMapMany(resource -> {\n                        var head = Mono.<DataBuffer>fromSupplier(() -> dataBufferFactory.wrap(\n                            (\"/* Generated from plugin \" + pluginId + \" */\\n\").getBytes()\n                        ));\n                        var content = DataBufferUtils.read(\n                            resource, dataBufferFactory, StreamUtils.BUFFER_SIZE);\n                        var tail = Mono.fromSupplier(() -> dataBufferFactory.wrap(\"\\n\".getBytes()));\n                        return Flux.concat(head, content, tail);\n                    });\n            });\n    }\n\n    private Mono<Resource> getBundleResource(String pluginName, String bundleName) {\n        return Mono.fromSupplier(\n                () -> BundleResourceUtils.getJsBundleResource(pluginManager, pluginName, bundleName)\n            )\n            .filter(Resource::isReadable);\n    }\n\n    @Override\n    public Mono<String> generateBundleVersion() {\n        if (pluginManager.isDevelopment()) {\n            return Mono.just(String.valueOf(clock.instant().toEpochMilli()));\n        }\n        return Flux.fromIterable(pluginManager.startedPlugins())\n            .sort(Comparator.comparing(PluginWrapper::getPluginId))\n            .map(pw -> pw.getPluginId() + ':' + pw.getDescriptor().getVersion())\n            .collect(Collectors.joining())\n            .map(Hashing.sha256()::hashUnencodedChars)\n            .map(HashCode::toString);\n    }\n\n    @Override\n    public Mono<Resource> getJsBundle(String version) {\n        return jsBundleCache.computeIfAbsent(version, this.uglifyJsBundle());\n    }\n\n    @Override\n    public Mono<Resource> getCssBundle(String version) {\n        return cssBundleCache.computeIfAbsent(version, this.uglifyCssBundle());\n    }\n\n    @Override\n    public Mono<Plugin> changeState(String pluginName, boolean requestToEnable, boolean wait) {\n        var updatedPlugin = Mono.defer(() -> client.get(Plugin.class, pluginName))\n            .flatMap(plugin -> {\n                if (!Objects.equals(requestToEnable, plugin.getSpec().getEnabled())) {\n                    // preflight check\n                    if (requestToEnable) {\n                        // make sure the dependencies are enabled\n                        var notStartedDependencies = getRequiredDependencies(plugin,\n                            pw -> pw == null || !Objects.equals(STARTED, pw.getPluginState())\n                        );\n                        if (!CollectionUtils.isEmpty(notStartedDependencies)) {\n                            return Mono.error(\n                                new PluginDependenciesNotEnabledException(notStartedDependencies)\n                            );\n                        }\n                    } else {\n                        // make sure the dependents are disabled\n                        var dependents = pluginManager.getDependents(pluginName);\n                        var notDisabledDependents = dependents.stream()\n                            .filter(\n                                dependent -> Objects.equals(STARTED, dependent.getPluginState())\n                            )\n                            .map(PluginWrapper::getPluginId)\n                            .toList();\n                        if (!CollectionUtils.isEmpty(notDisabledDependents)) {\n                            return Mono.error(\n                                new PluginDependentsNotDisabledException(notDisabledDependents)\n                            );\n                        }\n                    }\n\n                    plugin.getSpec().setEnabled(requestToEnable);\n                    log.debug(\"Updating plugin {} state to {}\", pluginName, requestToEnable);\n                    return client.update(plugin);\n                }\n                log.debug(\"Checking plugin {} state, no need to update\", pluginName);\n                return Mono.just(plugin);\n            });\n\n        if (wait) {\n            // if we want to wait the state of plugin to be updated\n            updatedPlugin = updatedPlugin\n                .flatMap(plugin -> {\n                    var phase = plugin.statusNonNull().getPhase();\n                    if (requestToEnable) {\n                        // if we request to enable the plugin\n                        if (!(Plugin.Phase.STARTED.equals(phase)\n                            || Plugin.Phase.FAILED.equals(phase))) {\n                            return Mono.error(UnexpectedPluginStateException::new);\n                        }\n                    } else {\n                        // if we request to disable the plugin\n                        if (Plugin.Phase.STARTED.equals(phase)) {\n                            return Mono.error(UnexpectedPluginStateException::new);\n                        }\n                    }\n                    return Mono.just(plugin);\n                })\n                .retryWhen(\n                    Retry.backoff(10, Duration.ofMillis(100))\n                        .filter(UnexpectedPluginStateException.class::isInstance)\n                        .doBeforeRetry(signal ->\n                            log.debug(\"Waiting for plugin {} to meet expected state\", pluginName)\n                        )\n                )\n                .doOnSuccess(plugin -> {\n                    log.info(\"Plugin {} met expected state {}\",\n                        pluginName, plugin.statusNonNull().getPhase());\n                });\n        }\n\n        return updatedPlugin;\n    }\n\n    @Override\n    public List<String> getRequiredDependencies(Plugin plugin,\n        Predicate<PluginWrapper> predicate) {\n        return getPluginDependency(plugin)\n            .stream()\n            .filter(dependency -> !dependency.isOptional())\n            .map(PluginDependency::getPluginId)\n            .filter(dependencyPlugin -> {\n                var pluginWrapper = pluginManager.getPlugin(dependencyPlugin);\n                return predicate.test(pluginWrapper);\n            })\n            .sorted()\n            .toList();\n    }\n\n    @Override\n    public Flux<String> getStartedPluginNames() {\n        return Flux.fromIterable(pluginManager.startedPlugins())\n            .map(PluginWrapper::getPluginId);\n    }\n\n    private static List<PluginDependency> getPluginDependency(Plugin plugin) {\n        return plugin.getSpec().getPluginDependencies().keySet()\n            .stream()\n            .map(PluginDependency::new)\n            .toList();\n    }\n\n    Mono<Plugin> findPluginManifest(Path path) {\n        return Mono.fromSupplier(\n                () -> {\n                    final var pluginFinder = new YamlPluginFinder();\n                    return pluginFinder.find(path);\n                })\n            .onErrorMap(e -> new PluginInstallationException(\"Failed to parse the plugin manifest\",\n                \"problemDetail.plugin.missingManifest\", null)\n            );\n    }\n\n\n    @Override\n    public void afterPropertiesSet() throws Exception {\n        this.tempDir = Files.createTempDirectory(\"halo-plugin-bundle\");\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        FileSystemUtils.deleteRecursively(this.tempDir);\n    }\n\n    /**\n     * Set temporary directory for plugin bundle.\n     *\n     * @param tempDir temporary directory.\n     */\n    void setTempDir(Path tempDir) {\n        this.tempDir = tempDir;\n    }\n\n    /**\n     * Copy plugin into plugin home.\n     *\n     * @param plugin is a staging plugin.\n     * @return new path in plugin home.\n     */\n    private Mono<Path> copyToPluginHome(Plugin plugin) {\n        return Mono.fromCallable(\n                () -> {\n                    var fileName = PluginUtils.generateFileName(plugin);\n                    var pluginRoot = pluginsRootGetter.get();\n                    try {\n                        Files.createDirectories(pluginRoot);\n                    } catch (IOException e) {\n                        throw Exceptions.propagate(e);\n                    }\n                    var pluginFilePath = pluginRoot.resolve(fileName);\n                    FileUtils.checkDirectoryTraversal(pluginRoot, pluginFilePath);\n                    // move the plugin jar file to the plugin root\n                    // replace the old plugin jar file if exists\n                    var path = Path.of(plugin.getStatus().getLoadLocation());\n                    FileUtils.copy(path, pluginFilePath, REPLACE_EXISTING);\n                    return pluginFilePath;\n                })\n            .subscribeOn(Schedulers.boundedElastic())\n            .doOnNext(loadLocation -> {\n                // reset load location and annotation PLUGIN_PATH\n                plugin.getStatus().setLoadLocation(loadLocation.toUri());\n                var annotations = plugin.getMetadata().getAnnotations();\n                if (annotations == null) {\n                    annotations = new HashMap<>();\n                    plugin.getMetadata().setAnnotations(annotations);\n                }\n                annotations.put(PluginConst.PLUGIN_PATH, loadLocation.toString());\n            });\n    }\n\n    private void satisfiesRequiresVersion(Plugin newPlugin) {\n        Assert.notNull(newPlugin, \"The plugin must not be null.\");\n        Version version = systemVersion.get();\n        // validate the plugin version\n        // only use the nominal system version to compare, the format is like MAJOR.MINOR.PATCH\n        String systemVersion = version.toStableVersion().toString();\n        String requires = newPlugin.getSpec().getRequires();\n        if (!VersionUtils.satisfiesRequires(systemVersion, requires)) {\n            throw new UnsatisfiedAttributeValueException(String.format(\n                \"Plugin requires a minimum system version of [%s], but the current version is \"\n                    + \"[%s].\",\n                requires, systemVersion),\n                \"problemDetail.plugin.version.unsatisfied.requires\",\n                new String[] {requires, systemVersion});\n        }\n    }\n\n    private Flux<Path> getPresetJars() {\n        var resolver = new PathMatchingResourcePatternResolver();\n        try {\n            var resources = resolver.getResources(PRESETS_LOCATION_PATTERN);\n            return Flux.fromArray(resources)\n                .mapNotNull(resource -> {\n                    var filename = resource.getFilename();\n                    if (StringUtils.isBlank(filename)) {\n                        return null;\n                    }\n                    var path = tempDir.resolve(filename);\n                    FileUtils.copyResource(resource, path);\n                    return path;\n                });\n        } catch (IOException e) {\n            log.debug(\"Failed to load preset plugins: {}\", e.getMessage());\n            return Flux.empty();\n        }\n    }\n\n    private static void updatePlugin(Plugin oldPlugin, Plugin newPlugin) {\n        var oldMetadata = oldPlugin.getMetadata();\n        var newMetadata = newPlugin.getMetadata();\n        // merge labels\n        if (!CollectionUtils.isEmpty(newMetadata.getLabels())) {\n            var labels = oldMetadata.getLabels();\n            if (labels == null) {\n                labels = new HashMap<>();\n                oldMetadata.setLabels(labels);\n            }\n            labels.putAll(newMetadata.getLabels());\n        }\n\n        var annotations = oldMetadata.getAnnotations();\n        if (annotations == null) {\n            annotations = new HashMap<>();\n            oldMetadata.setAnnotations(annotations);\n        }\n\n        // merge annotations\n        if (!CollectionUtils.isEmpty(newMetadata.getAnnotations())) {\n            annotations.putAll(newMetadata.getAnnotations());\n        }\n\n        // request to reload\n        annotations.put(RELOAD_ANNO,\n            newPlugin.getStatus().getLoadLocation().toString());\n\n        // apply spec and keep enabled request\n        var enabled = oldPlugin.getSpec().getEnabled();\n        oldPlugin.setSpec(newPlugin.getSpec());\n        oldPlugin.getSpec().setEnabled(enabled);\n    }\n\n    class BundleCache {\n\n        private final String suffix;\n\n        private final AtomicBoolean writing = new AtomicBoolean();\n\n        private volatile Resource resource;\n\n        BundleCache(String suffix) {\n            this.suffix = suffix;\n        }\n\n        Mono<Resource> computeIfAbsent(String version, Publisher<DataBuffer> content) {\n            var filename = buildBundleFilename(version, suffix);\n            if (isResourceMatch(resource, filename)) {\n                return Mono.just(resource);\n            }\n            return generateBundleVersion()\n                .flatMap(newVersion -> {\n                    var newFilename = buildBundleFilename(newVersion, suffix);\n                    if (isResourceMatch(this.resource, newFilename)) {\n                        // if the resource was not changed, just return it\n                        return Mono.just(resource);\n                    }\n                    if (writing.compareAndSet(false, true)) {\n                        return Mono.justOrEmpty(this.resource)\n                            // double check of the resource\n                            .filter(res -> isResourceMatch(res, newFilename))\n                            .switchIfEmpty(Mono.using(\n                                    () -> {\n                                        if (!Files.exists(tempDir)) {\n                                            Files.createDirectories(tempDir);\n                                        }\n                                        return tempDir.resolve(newFilename);\n                                    },\n                                    path -> DataBufferUtils.write(content, path,\n                                            CREATE, TRUNCATE_EXISTING)\n                                        .then(Mono.<Resource>fromSupplier(\n                                            () -> new FileSystemResource(path)\n                                        )),\n                                    path -> {\n                                        if (shouldCleanUp(path)) {\n                                            // clean up old resource\n                                            cleanUp(this.resource);\n                                        }\n                                    })\n                                .subscribeOn(scheduler)\n                                .doOnNext(newResource -> this.resource = newResource)\n                            )\n                            .doFinally(signalType -> writing.set(false));\n                    } else {\n                        return Mono.defer(() -> {\n                            if (this.writing.get()) {\n                                log.debug(\"Waiting for the bundle file {} to be written\", filename);\n                                return Mono.empty();\n                            }\n                            log.debug(\"Waited the bundle file {} to be written\", filename);\n                            return Mono.just(this.resource);\n                        }).repeatWhenEmpty(100, count -> {\n                            // retry after 100ms\n                            return count.delayElements(Duration.ofMillis(100));\n                        });\n                    }\n                });\n        }\n\n        private boolean shouldCleanUp(Path newPath) {\n            if (this.resource == null || !this.resource.exists()) {\n                return false;\n            }\n            try {\n                var oldPath = this.resource.getFile().toPath();\n                return !oldPath.equals(newPath);\n            } catch (IOException e) {\n                return false;\n            }\n        }\n\n        private static void cleanUp(Resource resource) {\n            if (resource instanceof WritableResource wr\n                && wr.isWritable()\n                && wr.isFile()) {\n                try {\n                    Files.deleteIfExists(wr.getFile().toPath());\n                } catch (IOException e) {\n                    log.warn(\"Failed to delete old bundle file {}\",\n                        wr.getFilename(), e);\n                }\n            }\n        }\n\n        private static boolean isResourceMatch(Resource resource, String filename) {\n            return resource != null\n                && resource.exists()\n                && resource.isFile()\n                && Objects.equals(filename, resource.getFilename());\n        }\n    }\n\n    private static String buildBundleFilename(String v, String suffix) {\n        Assert.notNull(v, \"Version must not be null\");\n        Assert.notNull(suffix, \"Suffix must not be null\");\n        return v + suffix;\n    }\n\n    private static class UnexpectedPluginStateException extends RuntimeException {\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginSharedEventDelegator.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.Objects;\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.lang.NonNull;\n\n/**\n * The event that delegates to another shared event published by a plugin.\n *\n * @author johnniang\n * @since 2.17\n */\n@Getter\nclass PluginSharedEventDelegator extends ApplicationEvent {\n\n    /**\n     * The delegate event.\n     */\n    private final ApplicationEvent delegate;\n\n    public PluginSharedEventDelegator(@NonNull Object source, @NonNull ApplicationEvent delegate) {\n        super(source);\n        this.delegate = delegate;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        PluginSharedEventDelegator that = (PluginSharedEventDelegator) o;\n        return Objects.equals(delegate, that.delegate);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(delegate);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginStartedListener.java",
    "content": "package run.halo.app.plugin;\n\nimport static run.halo.app.plugin.PluginConst.PLUGIN_NAME_LABEL_NAME;\nimport static run.halo.app.plugin.PluginExtensionLoaderUtils.isSetting;\nimport static run.halo.app.plugin.PluginExtensionLoaderUtils.lookupExtensions;\n\nimport java.time.Duration;\nimport java.util.HashMap;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\nimport run.halo.app.plugin.event.HaloPluginStartedEvent;\n\n/**\n * TODO Optimized Unstructured loading.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\nclass PluginStartedListener {\n\n    private static final Duration TIMEOUT = Duration.ofMinutes(1);\n\n    private final ReactiveExtensionClient client;\n\n    public PluginStartedListener(ReactiveExtensionClient extensionClient) {\n        this.client = extensionClient;\n    }\n\n    private Mono<Unstructured> createOrUpdate(Unstructured unstructured) {\n        var name = unstructured.getMetadata().getName();\n        return client.fetch(unstructured.groupVersionKind(), name)\n            .doOnNext(old -> unstructured.getMetadata().setVersion(old.getMetadata().getVersion()))\n            .map(ignored -> unstructured)\n            .flatMap(extension -> {\n                if (ExtensionUtil.hasDoNotOverwriteLabel(extension)) {\n                    log.debug(\"Skip updating extension {} due to do-not-overwrite label\",\n                        extension.getMetadata().getName());\n                    return Mono.just(extension);\n                }\n                return client.update(extension);\n            })\n            .switchIfEmpty(Mono.defer(() -> client.create(unstructured)));\n    }\n\n    @EventListener\n    void onApplicationEvent(HaloPluginStartedEvent event) {\n        var pluginWrapper = event.getPlugin();\n        var p = pluginWrapper.getPlugin();\n        if (!(p instanceof SpringPlugin springPlugin)) {\n            return;\n        }\n        var applicationContext = springPlugin.getApplicationContext();\n        if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) {\n            return;\n        }\n        var pluginName = pluginWrapper.getPluginId();\n\n        client.get(Plugin.class, pluginName)\n            .flatMap(plugin -> Flux.fromStream(\n                    () -> {\n                        log.debug(\"Collecting extensions for plugin {}\", pluginName);\n                        var resources = lookupExtensions(pluginWrapper.getPluginClassLoader());\n                        var loader = new YamlUnstructuredLoader(resources);\n                        var settingName = plugin.getSpec().getSettingName();\n                        // TODO The load method may be over memory consumption.\n                        return loader.load()\n                            .stream()\n                            .filter(isSetting(settingName).negate());\n                    })\n                .doOnNext(unstructured -> {\n                    var name = unstructured.getMetadata().getName();\n                    pluginApplicationContext\n                        .addExtensionMapping(unstructured.groupVersionKind(), name);\n                    var labels = unstructured.getMetadata().getLabels();\n                    if (labels == null) {\n                        labels = new HashMap<>();\n                        unstructured.getMetadata().setLabels(labels);\n                    }\n                    labels.put(PLUGIN_NAME_LABEL_NAME, plugin.getMetadata().getName());\n                })\n                .flatMap(this::createOrUpdate)\n                .then()\n            )\n            .block(TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginUtils.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.Objects;\nimport lombok.experimental.UtilityClass;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebInputException;\nimport run.halo.app.core.extension.Plugin;\n\n@UtilityClass\npublic class PluginUtils {\n\n    public static String generateFileName(Plugin plugin) {\n        Assert.notNull(plugin, \"The plugin must not be null.\");\n        Assert.notNull(plugin.getMetadata(), \"The plugin metadata must not be null.\");\n        Assert.notNull(plugin.getSpec(), \"The plugin spec must not be null.\");\n        String version = plugin.getSpec().getVersion();\n        if (StringUtils.isBlank(version)) {\n            throw new ServerWebInputException(\"The plugin version must not be blank.\");\n        }\n        return String.format(\"%s-%s.jar\", plugin.getMetadata().getName(), version);\n    }\n\n    /**\n     * Determine if the plugin is in development mode. Currently, we detect it from annotations.\n     *\n     * @param plugin is a manifest about plugin.\n     * @return true if the plugin  is in development mode; false otherwise.\n     */\n    public static boolean isDevelopmentMode(Plugin plugin) {\n        var annotations = plugin.getMetadata().getAnnotations();\n        return annotations != null\n            && Objects.equals(\"dev\", annotations.get(PluginConst.RUNTIME_MODE_ANNO));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PluginsRootGetterImpl.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Path;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.infra.properties.HaloProperties;\n\n/**\n * Default implementation of {@link PluginsRootGetter}.\n *\n * @author johnniang\n */\n@Component\npublic class PluginsRootGetterImpl implements PluginsRootGetter {\n\n    private final HaloProperties haloProperties;\n\n    public PluginsRootGetterImpl(HaloProperties haloProperties) {\n        this.haloProperties = haloProperties;\n    }\n\n    @Override\n    @NonNull\n    public Path get() {\n        return haloProperties.getWorkDir().resolve(\"plugins\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/PropertyPluginStatusProvider.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.pf4j.PluginStatusProvider;\nimport org.thymeleaf.util.ArrayUtils;\n\n/**\n * An implementation for PluginStatusProvider. The enabled plugins are read\n * from {@code halo.plugin.enabled-plugins} properties in <code>application.yaml</code>\n * and the disabled plugins are read from {@code halo.plugin.disabled-plugins}\n * in <code>application.yaml</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class PropertyPluginStatusProvider implements PluginStatusProvider {\n\n    private final List<String> enabledPlugins;\n    private final List<String> disabledPlugins;\n\n    public PropertyPluginStatusProvider(PluginProperties pluginProperties) {\n        this.enabledPlugins = pluginProperties.getEnabledPlugins() != null\n            ? Arrays.asList(pluginProperties.getEnabledPlugins()) : new ArrayList<>();\n        this.disabledPlugins = pluginProperties.getDisabledPlugins() != null\n            ? Arrays.asList(pluginProperties.getDisabledPlugins()) : new ArrayList<>();\n    }\n\n    public static boolean isPropertySet(PluginProperties pluginProperties) {\n        return !ArrayUtils.isEmpty(pluginProperties.getEnabledPlugins())\n            && !ArrayUtils.isEmpty(pluginProperties.getDisabledPlugins());\n    }\n\n    @Override\n    public boolean isPluginDisabled(String pluginId) {\n        if (disabledPlugins.contains(pluginId)) {\n            return true;\n        }\n        return !enabledPlugins.isEmpty() && !enabledPlugins.contains(pluginId);\n    }\n\n    @Override\n    public void disablePlugin(String pluginId) {\n        if (isPluginDisabled(pluginId)) {\n            return;\n        }\n        disabledPlugins.add(pluginId);\n        enabledPlugins.remove(pluginId);\n    }\n\n    @Override\n    public void enablePlugin(String pluginId) {\n        if (!isPluginDisabled(pluginId)) {\n            return;\n        }\n        enabledPlugins.add(pluginId);\n        disabledPlugins.remove(pluginId);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java",
    "content": "package run.halo.app.plugin;\n\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport org.springframework.cache.CacheManager;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.support.GenericApplicationContext;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport run.halo.app.content.PostContentService;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.DefaultSchemeManager;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.BackupRootGetter;\nimport run.halo.app.infra.ExternalLinkProcessor;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemInfoGetter;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.notification.NotificationReasonEmitter;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.security.LoginHandlerEnhancer;\nimport run.halo.app.security.authentication.CryptoService;\n\n/**\n * Utility for creating shared application context.\n *\n * @author guqing\n * @author johnniang\n * @since 2.12.0\n */\npublic enum SharedApplicationContextFactory {\n    ;\n\n    public static ApplicationContext create(ApplicationContext rootContext) {\n        // TODO Optimize creation timing\n        var sharedContext = new GenericApplicationContext();\n        sharedContext.registerShutdownHook();\n\n        var beanFactory = sharedContext.getBeanFactory();\n\n        // register shared object here\n        var extensionClient = rootContext.getBean(ExtensionClient.class);\n        var reactiveExtensionClient = rootContext.getBean(ReactiveExtensionClient.class);\n        beanFactory.registerSingleton(\"extensionClient\", extensionClient);\n        beanFactory.registerSingleton(\"reactiveExtensionClient\", reactiveExtensionClient);\n\n        DefaultSchemeManager defaultSchemeManager =\n            rootContext.getBean(DefaultSchemeManager.class);\n        beanFactory.registerSingleton(\"schemeManager\", defaultSchemeManager);\n        beanFactory.registerSingleton(\"externalUrlSupplier\",\n            rootContext.getBean(ExternalUrlSupplier.class));\n        beanFactory.registerSingleton(\"serverSecurityContextRepository\",\n            rootContext.getBean(ServerSecurityContextRepository.class));\n        beanFactory.registerSingleton(\"attachmentService\",\n            rootContext.getBean(AttachmentService.class));\n        beanFactory.registerSingleton(\"backupRootGetter\",\n            rootContext.getBean(BackupRootGetter.class));\n        beanFactory.registerSingleton(\"notificationReasonEmitter\",\n            rootContext.getBean(NotificationReasonEmitter.class));\n        beanFactory.registerSingleton(\"notificationCenter\",\n            rootContext.getBean(NotificationCenter.class));\n        beanFactory.registerSingleton(\"externalLinkProcessor\",\n            rootContext.getBean(ExternalLinkProcessor.class));\n        beanFactory.registerSingleton(\"postContentService\",\n            rootContext.getBean(PostContentService.class));\n        beanFactory.registerSingleton(\"cacheManager\",\n            rootContext.getBean(CacheManager.class));\n        beanFactory.registerSingleton(\"loginHandlerEnhancer\",\n            rootContext.getBean(LoginHandlerEnhancer.class));\n        rootContext.getBeanProvider(PluginsRootGetter.class)\n            .ifUnique(pluginsRootGetter ->\n                beanFactory.registerSingleton(\"pluginsRootGetter\", pluginsRootGetter)\n            );\n        beanFactory.registerSingleton(\"extensionGetter\",\n            rootContext.getBean(ExtensionGetter.class));\n        rootContext.getBeanProvider(CryptoService.class)\n            .ifUnique(\n                cryptoService -> beanFactory.registerSingleton(\"cryptoService\", cryptoService)\n            );\n        rootContext.getBeanProvider(RateLimiterRegistry.class)\n            .ifUnique(rateLimiterRegistry ->\n                beanFactory.registerSingleton(\"rateLimiterRegistry\", rateLimiterRegistry)\n            );\n\n        // Authentication plugins may need this RequestCache to handle successful login redirect\n        rootContext.getBeanProvider(ServerRequestCache.class)\n            .ifUnique(serverRequestCache ->\n                beanFactory.registerSingleton(\"serverRequestCache\", serverRequestCache)\n            );\n        rootContext.getBeanProvider(UserService.class)\n            .ifUnique(userService -> beanFactory.registerSingleton(\"userService\", userService));\n        rootContext.getBeanProvider(RoleService.class)\n            .ifUnique(roleService -> beanFactory.registerSingleton(\"roleService\", roleService));\n        rootContext.getBeanProvider(ReactiveUserDetailsService.class)\n            .ifUnique(userDetailsService ->\n                beanFactory.registerSingleton(\"userDetailsService\", userDetailsService)\n            );\n        rootContext.getBeanProvider(SystemInfoGetter.class)\n            .ifUnique(systemInfoGetter ->\n                beanFactory.registerSingleton(\"systemInfoGetter\", systemInfoGetter)\n            );\n        // TODO add more shared instance here\n\n        sharedContext.refresh();\n        return sharedContext;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/SharedEventDispatcher.java",
    "content": "package run.halo.app.plugin;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.context.Lifecycle;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.core.annotation.AnnotationUtils;\nimport org.springframework.stereotype.Component;\n\n@Component\n@RequiredArgsConstructor\npublic class SharedEventDispatcher {\n\n    private final SpringPluginManager pluginManager;\n\n    private final ApplicationEventPublisher publisher;\n\n    @EventListener(ApplicationEvent.class)\n    void onApplicationEvent(ApplicationEvent event) {\n        if (AnnotationUtils.findAnnotation(event.getClass(), SharedEvent.class) == null) {\n            return;\n        }\n        // we should copy the plugins list to avoid ConcurrentModificationException\n        var startedPlugins = pluginManager.startedPlugins();\n        // broadcast event to all started plugins except the publisher\n        for (var startedPlugin : startedPlugins) {\n            var plugin = startedPlugin.getPlugin();\n            if (!(plugin instanceof SpringPlugin springPlugin)) {\n                continue;\n            }\n            var context = springPlugin.getApplicationContext();\n            // make sure the context is running before publishing the event\n            if (context instanceof Lifecycle lifecycle && lifecycle.isRunning()) {\n                context.publishEvent(new HaloSharedEventDelegator(this, event));\n            }\n        }\n    }\n\n    @EventListener(PluginSharedEventDelegator.class)\n    void onApplicationEvent(PluginSharedEventDelegator event) {\n        publisher.publishEvent(event.getDelegate());\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java",
    "content": "package run.halo.app.plugin;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.AbstractExtensionFinder;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginStateEvent;\nimport org.pf4j.PluginWrapper;\nimport org.pf4j.processor.ExtensionStorage;\n\n/**\n * <p>The spring component finder. it will read {@code META-INF/plugin-components.idx} file in\n * plugin to obtain the class name that needs to be registered in the plugin IOC.</p>\n * <p>Reading index files directly is much faster than dynamically scanning class components when\n * the plugin is enabled.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\npublic class SpringComponentsFinder extends AbstractExtensionFinder {\n    public static final String EXTENSIONS_RESOURCE = \"META-INF/plugin-components.idx\";\n\n    public SpringComponentsFinder(PluginManager pluginManager) {\n        super(pluginManager);\n        entries = new ConcurrentHashMap<>();\n    }\n\n    @Override\n    public Map<String, Set<String>> readClasspathStorages() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Map<String, Set<String>> readPluginsStorages() {\n        throw new UnsupportedOperationException();\n    }\n\n    private Set<String> readPluginStorage(PluginWrapper pluginWrapper) {\n        var pluginId = pluginWrapper.getPluginId();\n        log.debug(\"Reading extensions storage from plugin '{}'\", pluginId);\n        var bucket = new HashSet<String>();\n        try {\n            log.debug(\"Read '{}'\", EXTENSIONS_RESOURCE);\n            var classLoader = pluginWrapper.getPluginClassLoader();\n            try (var resourceStream = classLoader.getResourceAsStream(EXTENSIONS_RESOURCE)) {\n                if (resourceStream == null) {\n                    log.debug(\"Cannot find '{}'\", EXTENSIONS_RESOURCE);\n                } else {\n                    collectExtensions(resourceStream, bucket);\n                }\n            }\n            debugExtensions(bucket);\n        } catch (IOException e) {\n            log.error(\"Failed to read components from \" + EXTENSIONS_RESOURCE, e);\n        }\n        return bucket;\n    }\n\n    private void collectExtensions(InputStream inputStream, Set<String> bucket) throws IOException {\n        try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {\n            ExtensionStorage.read(reader, bucket);\n        }\n    }\n\n    @Override\n    public void pluginStateChanged(PluginStateEvent event) {\n        var pluginState = event.getPluginState();\n        String pluginId = event.getPlugin().getPluginId();\n        if (pluginState == PluginState.UNLOADED) {\n            entries.remove(pluginId);\n        } else if (pluginState == PluginState.CREATED || pluginState == PluginState.RESOLVED) {\n            entries.computeIfAbsent(pluginId, id -> readPluginStorage(event.getPlugin()));\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java",
    "content": "package run.halo.app.plugin;\n\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.Comparator;\nimport java.util.Optional;\nimport java.util.stream.Stream;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.Extension;\nimport org.pf4j.ExtensionFactory;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.config.AutowireCapableBeanFactory;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.lang.Nullable;\n\n/**\n * <p>Basic implementation of an extension factory.</p>\n * <p>Uses Springs {@link AutowireCapableBeanFactory} to instantiate a given extension class.</p>\n * <p>All kinds of {@link Autowired} are supported (see example below). If no\n * {@link ApplicationContext} is\n * available (this is the case if either the related plugin is not a {@link BasePlugin} or the\n * given plugin manager is not a {@link HaloPluginManager}), standard Java reflection will be used\n * to instantiate an extension.</p>\n * <p>Creates a new extension instance every time a request is done.</p>\n * Example of supported autowire modes:\n * <pre>{@code\n *     @Extension\n *     public class Foo implements ExtensionPoint {\n *\n *         private final Bar bar;       // Constructor injection\n *         private Baz baz;             // Setter injection\n *         @Autowired\n *         private Qux qux;             // Field injection\n *\n *         @Autowired\n *         public Foo(final Bar bar) {\n *             this.bar = bar;\n *         }\n *\n *         @Autowired\n *         public void setBaz(final Baz baz) {\n *             this.baz = baz;\n *         }\n *     }\n * }</pre>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@RequiredArgsConstructor\npublic class SpringExtensionFactory implements ExtensionFactory {\n\n    /**\n     * The plugin manager is used for retrieving a plugin from a given extension class and as a\n     * fallback supplier of an application context.\n     */\n    protected final PluginManager pluginManager;\n\n    @Override\n    @Nullable\n    public <T> T create(Class<T> extensionClass) {\n        return getPluginApplicationContextBy(extensionClass)\n            .map(context -> context.getBean(extensionClass))\n            .orElseGet(() -> createWithoutSpring(extensionClass));\n    }\n\n    /**\n     * Creates an instance of the given class object by using standard Java reflection.\n     *\n     * @param extensionClass The class annotated with {@code @}{@link Extension}.\n     * @param <T> The type for that an instance should be created.\n     * @return an instantiated extension.\n     * @throws IllegalArgumentException if the given class object has no public constructor.\n     * @throws RuntimeException         if the called constructor cannot be instantiated with {@code\n     *                                  null}-parameters.\n     */\n    @SuppressWarnings(\"unchecked\")\n    protected <T> T createWithoutSpring(final Class<T> extensionClass)\n        throws IllegalArgumentException {\n        final Constructor<?> constructor =\n            getPublicConstructorWithShortestParameterList(extensionClass)\n                // An extension class is required to have at least one public constructor.\n                .orElseThrow(\n                    () -> new IllegalArgumentException(\"Extension class '\" + nameOf(extensionClass)\n                        + \"' must have at least one public constructor.\"));\n        try {\n            if (log.isTraceEnabled()) {\n                log.trace(\"Instantiate '\" + nameOf(extensionClass) + \"' by calling '\" + constructor\n                    + \"'with standard Java reflection.\");\n            }\n            // Creating the instance by calling the constructor with null-parameters (if there\n            // are any).\n            return (T) constructor.newInstance(nullParameters(constructor));\n        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {\n            // If one of these exceptions is thrown it it most likely because of NPE inside the\n            // called constructor and\n            // not the reflective call itself as we precisely searched for a fitting constructor.\n            log.error(ex.getMessage(), ex);\n            throw new RuntimeException(\n                \"Most likely this exception is thrown because the called constructor (\"\n                    + constructor + \")\"\n                    + \" cannot handle 'null' parameters. Original message was: \"\n                    + ex.getMessage(), ex);\n        }\n    }\n\n    private Optional<Constructor<?>> getPublicConstructorWithShortestParameterList(\n        final Class<?> extensionClass) {\n        return Stream.of(extensionClass.getConstructors())\n            .min(Comparator.comparing(Constructor::getParameterCount));\n    }\n\n    private Object[] nullParameters(final Constructor<?> constructor) {\n        return new Object[constructor.getParameterCount()];\n    }\n\n    protected <T> Optional<ApplicationContext> getPluginApplicationContextBy(\n        final Class<T> extensionClass) {\n        return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass))\n            .map(PluginWrapper::getPlugin)\n            .filter(SpringPlugin.class::isInstance)\n            .map(plugin -> (SpringPlugin) plugin)\n            .map(SpringPlugin::getApplicationContext);\n    }\n\n    private <T> String nameOf(final Class<T> clazz) {\n        return clazz.getName();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/SpringPlugin.java",
    "content": "package run.halo.app.plugin;\n\nimport org.jspecify.annotations.NonNull;\nimport org.springframework.context.ApplicationContext;\n\n/**\n * Spring based plugin.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic interface SpringPlugin {\n\n    /**\n     * Gets plugin context.\n     *\n     * @return plugin context\n     */\n    @NonNull\n    PluginContext getPluginContext();\n\n    /**\n     * Gets application context of the plugin.\n     *\n     * @return application context of the plugin\n     * @throws IllegalStateException if the application context is not ready yet\n     */\n    @NonNull\n    ApplicationContext getApplicationContext();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java",
    "content": "package run.halo.app.plugin;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.Plugin;\nimport org.pf4j.PluginFactory;\nimport org.pf4j.PluginWrapper;\n\n/**\n * The default implementation for PluginFactory.\n * <p>Get a {@link BasePlugin} instance from the {@link PluginApplicationContext}.</p>\n *\n * @author guqing\n * @author johnniang\n * @since 2.0.0\n */\n@Slf4j\npublic class SpringPluginFactory implements PluginFactory {\n\n    private final PluginApplicationContextFactory contextFactory;\n    private final PluginGetter pluginGetter;\n\n    public SpringPluginFactory(PluginApplicationContextFactory contextFactory,\n        PluginGetter pluginGetter) {\n        this.contextFactory = contextFactory;\n        this.pluginGetter = pluginGetter;\n    }\n\n    @Override\n    public Plugin create(PluginWrapper pluginWrapper) {\n        var plugin = pluginGetter.getPlugin(pluginWrapper.getPluginId());\n        var pluginContext = PluginContext.builder()\n            .name(pluginWrapper.getPluginId())\n            .configMapName(plugin.getSpec().getConfigMapName())\n            .version(pluginWrapper.getDescriptor().getVersion())\n            .runtimeMode(pluginWrapper.getRuntimeMode())\n            .build();\n        return new DefaultSpringPlugin(contextFactory, pluginContext);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/SpringPluginManager.java",
    "content": "package run.halo.app.plugin;\n\nimport java.util.List;\nimport org.jspecify.annotations.NonNull;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.context.ApplicationContext;\n\n/**\n * Plugin manager for Spring-based applications.\n *\n * @author johnniang\n * @since 2.12.0\n */\npublic interface SpringPluginManager extends PluginManager {\n\n    /**\n     * Gets the root application context.\n     *\n     * @return the root application context\n     */\n    @NonNull\n    ApplicationContext getRootContext();\n\n    /**\n     * Get the shared application context among plugins.\n     *\n     * @return the shared application context\n     */\n    @NonNull\n    ApplicationContext getSharedContext();\n\n    /**\n     * Get all dependents recursively.\n     *\n     * @param pluginId plugin id\n     * @return a list of plugin wrapper. The order of the list is from the farthest dependent to\n     * the nearest dependent.\n     * @since 2.16.0\n     */\n    @NonNull\n    List<PluginWrapper> getDependents(@NonNull String pluginId);\n\n    /**\n     * Gets all started plugins.\n     *\n     * @return a list of started plugins. Mutable.\n     * @apiNote The plugin inside this list may not be really started\n     */\n    @Override\n    List<PluginWrapper> getStartedPlugins();\n\n    /**\n     * Gets all really started plugins.\n     *\n     * @return a list of really started plugins. Immutable.\n     */\n    List<PluginWrapper> startedPlugins();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/YamlPluginDescriptorFinder.java",
    "content": "package run.halo.app.plugin;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.pf4j.DefaultPluginDescriptor;\nimport org.pf4j.PluginDependency;\nimport org.pf4j.PluginDescriptor;\nimport org.pf4j.PluginDescriptorFinder;\nimport org.pf4j.util.FileUtils;\nimport org.springframework.util.CollectionUtils;\nimport run.halo.app.core.extension.Plugin;\n\n/**\n * Find a plugin descriptor for a plugin path.\n *\n * @author guqing\n * @see DefaultPluginDescriptor\n * @since 2.0.0\n */\n@Slf4j\nclass YamlPluginDescriptorFinder implements PluginDescriptorFinder {\n\n    private final YamlPluginFinder yamlPluginFinder;\n\n    public YamlPluginDescriptorFinder() {\n        yamlPluginFinder = new YamlPluginFinder();\n    }\n\n    @Override\n    public boolean isApplicable(Path pluginPath) {\n        return Files.exists(pluginPath)\n            && (Files.isDirectory(pluginPath)\n            || FileUtils.isJarFile(pluginPath));\n    }\n\n    @Override\n    public PluginDescriptor find(Path pluginPath) {\n        Plugin plugin = yamlPluginFinder.find(pluginPath);\n        return convert(plugin);\n    }\n\n    public static PluginDescriptor convert(Plugin plugin) {\n        var pluginId = plugin.getMetadata().getName();\n        var spec = plugin.getSpec();\n        var author = spec.getAuthor();\n        var provider = (author == null ? StringUtils.EMPTY : author.getName());\n\n        var defaultPluginDescriptor = new DefaultPluginDescriptor(pluginId,\n            spec.getDescription(),\n            BasePlugin.class.getName(),\n            spec.getVersion(),\n            spec.getRequires(),\n            provider,\n            joinLicense(spec.getLicense()));\n        // add dependencies\n        spec.getPluginDependencies().forEach((pluginDepName, versionRequire) -> {\n            PluginDependency dependency =\n                new PluginDependency(String.format(\"%s@%s\", pluginDepName, versionRequire));\n            defaultPluginDescriptor.addDependency(dependency);\n        });\n        return defaultPluginDescriptor;\n    }\n\n    private static String joinLicense(List<Plugin.License> licenses) {\n        if (CollectionUtils.isEmpty(licenses)) {\n            return StringUtils.EMPTY;\n        }\n        return licenses.stream()\n            .map(Plugin.License::getName)\n            .collect(Collectors.joining(\",\"));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/YamlPluginFinder.java",
    "content": "package run.halo.app.plugin;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jspecify.annotations.NonNull;\nimport org.pf4j.DevelopmentPluginClasspath;\nimport org.pf4j.PluginRuntimeException;\nimport org.pf4j.util.FileUtils;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\n\n/**\n * <p>Reading plugin descriptor data from plugin.yaml.</p>\n * Example:\n * <pre>\n * apiVersion: v1alpha1\n * kind: Plugin\n * metadata:\n *   name: plugin-1\n *   labels:\n *     extensions.guqing.xyz/category: attachment\n * spec:\n *   # 'version' is a valid semantic version string (see semver.org).\n *   version: 0.0.1\n *   requires: \">=2.0.0\"\n *   author: guqing\n *   logo: example.com/logo.png\n *   pluginClass: xyz.guqing.plugin.potatoes.PotatoesApp\n *   pluginDependencies:\n *    \"plugin-2\": 1.0.0\n *   # 'homepage' usually links to the GitHub repository of the plugin\n *   homepage: example.com\n *   # 'displayName' explains what the plugin does in only a few words\n *   displayName: \"a name to show\"\n *   description: \"Tell me more about this plugin.\"\n *   license:\n *     - name: MIT\n * </pre>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\npublic class YamlPluginFinder implements PluginFinder {\n    static final DevelopmentPluginClasspath PLUGIN_CLASSPATH = new DevelopmentPluginClasspath();\n    public static final String DEFAULT_PROPERTIES_FILE_NAME = \"plugin.yaml\";\n    private final String propertiesFileName;\n\n    public YamlPluginFinder() {\n        this(DEFAULT_PROPERTIES_FILE_NAME);\n    }\n\n    public YamlPluginFinder(String propertiesFileName) {\n        this.propertiesFileName = propertiesFileName;\n    }\n\n    @Override\n    @NonNull\n    public Plugin find(@NonNull Path pluginPath) {\n        Plugin plugin = readPluginDescriptor(pluginPath);\n        if (plugin.getStatus() == null) {\n            Plugin.PluginStatus pluginStatus = new Plugin.PluginStatus();\n            pluginStatus.setPhase(Plugin.Phase.PENDING);\n            pluginStatus.setLoadLocation(pluginPath.toUri());\n            plugin.setStatus(pluginStatus);\n        }\n        MetadataUtil.nullSafeAnnotations(plugin)\n            .put(PluginConst.PLUGIN_PATH, pluginPath.toString());\n        return plugin;\n    }\n\n    protected Plugin readPluginDescriptor(Path pluginPath) {\n        Path propertiesPath = null;\n        try {\n            propertiesPath = getManifestPath(pluginPath, propertiesFileName);\n            if (propertiesPath == null) {\n                throw new PluginRuntimeException(\"Cannot find the plugin manifest path\");\n            }\n\n            log.debug(\"Lookup plugin descriptor in '{}'\", propertiesPath);\n            if (Files.notExists(propertiesPath)) {\n                throw new PluginRuntimeException(\"Cannot find '{}' path\", propertiesPath);\n            }\n            Resource propertyResource = new FileSystemResource(propertiesPath);\n            return unstructuredToPlugin(propertyResource);\n        } finally {\n            FileUtils.closePath(propertiesPath);\n        }\n    }\n\n    protected Plugin unstructuredToPlugin(Resource propertyResource) {\n        YamlUnstructuredLoader yamlUnstructuredLoader =\n            new YamlUnstructuredLoader(propertyResource);\n        List<Unstructured> unstructuredList = yamlUnstructuredLoader.load();\n        if (unstructuredList.size() != 1) {\n            throw new PluginRuntimeException(\"Unable to find plugin descriptor file '{}'\",\n                propertiesFileName);\n        }\n        Unstructured unstructured = unstructuredList.get(0);\n        return Unstructured.OBJECT_MAPPER.convertValue(unstructured,\n            Plugin.class);\n    }\n\n    protected Path getManifestPath(Path pluginPath, String propertiesFileName) {\n        if (Files.isDirectory(pluginPath)) {\n            for (String location : PLUGIN_CLASSPATH.getClassesDirectories()) {\n                var path = pluginPath.resolve(location).resolve(propertiesFileName);\n                Resource propertyResource = new FileSystemResource(path);\n                if (propertyResource.exists()) {\n                    return path;\n                }\n            }\n            throw new PluginRuntimeException(\n                \"Unable to find plugin descriptor file: \" + DEFAULT_PROPERTIES_FILE_NAME);\n        } else {\n            // it's a jar file\n            try {\n                return FileUtils.getPath(pluginPath, propertiesFileName);\n            } catch (IOException e) {\n                throw new PluginRuntimeException(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/event/HaloPluginBeforeStopEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport org.pf4j.PluginWrapper;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class HaloPluginBeforeStopEvent extends ApplicationEvent {\n    private final PluginWrapper plugin;\n\n    public HaloPluginBeforeStopEvent(Object source, PluginWrapper plugin) {\n        super(source);\n        this.plugin = plugin;\n    }\n\n    public PluginWrapper getPlugin() {\n        return plugin;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/event/HaloPluginStartedEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport lombok.Getter;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.util.Assert;\n\n/**\n * This event will be published to <b>application context</b> once plugin is started.\n *\n * @author guqing\n */\n@Getter\npublic class HaloPluginStartedEvent extends ApplicationEvent {\n\n    private final PluginWrapper plugin;\n\n    public HaloPluginStartedEvent(Object source, PluginWrapper plugin) {\n        super(source);\n        Assert.notNull(plugin, \"Plugin must not be null.\");\n        this.plugin = plugin;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/event/HaloPluginStoppedEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.context.ApplicationEvent;\n\n/**\n * This event will be published to <b>plugin application context</b> once plugin is stopped.\n *\n * @author guqing\n * @date 2021-11-02\n */\npublic class HaloPluginStoppedEvent extends ApplicationEvent {\n\n    private final PluginWrapper plugin;\n\n    public HaloPluginStoppedEvent(Object source, PluginWrapper plugin) {\n        super(source);\n        this.plugin = plugin;\n    }\n\n    public PluginWrapper getPlugin() {\n        return plugin;\n    }\n\n    public PluginState getPluginState() {\n        return plugin.getPluginState();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/event/SpringPluginStartedEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.plugin.SpringPlugin;\n\npublic class SpringPluginStartedEvent extends ApplicationEvent {\n\n    private final SpringPlugin springPlugin;\n\n    public SpringPluginStartedEvent(Object source, SpringPlugin springPlugin) {\n        super(source);\n        this.springPlugin = springPlugin;\n    }\n\n    public SpringPlugin getSpringPlugin() {\n        return springPlugin;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/event/SpringPluginStartingEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.plugin.SpringPlugin;\n\npublic class SpringPluginStartingEvent extends ApplicationEvent {\n\n    private final SpringPlugin springPlugin;\n\n    public SpringPluginStartingEvent(Object source, SpringPlugin springPlugin) {\n        super(source);\n        this.springPlugin = springPlugin;\n    }\n\n    public SpringPlugin getSpringPlugin() {\n        return springPlugin;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppedEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.plugin.SpringPlugin;\n\npublic class SpringPluginStoppedEvent extends ApplicationEvent {\n\n    private final SpringPlugin springPlugin;\n\n    public SpringPluginStoppedEvent(Object source, SpringPlugin springPlugin) {\n        super(source);\n        this.springPlugin = springPlugin;\n    }\n\n    public SpringPlugin getSpringPlugin() {\n        return springPlugin;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppingEvent.java",
    "content": "package run.halo.app.plugin.event;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.plugin.SpringPlugin;\n\npublic class SpringPluginStoppingEvent extends ApplicationEvent {\n\n    private final SpringPlugin springPlugin;\n\n    public SpringPluginStoppingEvent(Object source, SpringPlugin springPlugin) {\n        super(source);\n        this.springPlugin = springPlugin;\n    }\n\n    public SpringPlugin getSpringPlugin() {\n        return springPlugin;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/AbstractDefinitionGetter.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.beans.factory.DisposableBean;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\n\n@RequiredArgsConstructor\nabstract class AbstractDefinitionGetter<E extends Extension>\n    implements Reconciler<Reconciler.Request>, DisposableBean {\n\n    protected final ConcurrentMap<String, E> cache = new ConcurrentHashMap<>();\n\n    private final ExtensionClient client;\n\n    private final E watchType;\n\n    abstract void putCache(E definition);\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public Result reconcile(Request request) {\n        client.fetch((Class<E>) watchType.getClass(), request.name())\n            .ifPresent(this::putCache);\n        return Result.doNotRetry();\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder.extension(watchType)\n            .syncAllOnStart(true)\n            .build();\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        cache.clear();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport java.util.ArrayList;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.pf4j.ExtensionPoint;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.beans.factory.BeanFactory;\nimport org.springframework.core.annotation.AnnotationAwareOrderComparator;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting.ExtensionPointEnabled;\nimport run.halo.app.plugin.SpringPlugin;\nimport run.halo.app.plugin.SpringPluginManager;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class DefaultExtensionGetter implements ExtensionGetter {\n\n    private final SystemConfigFetcher systemConfigFetcher;\n\n    private final SpringPluginManager pluginManager;\n\n    private final BeanFactory beanFactory;\n\n    private final ExtensionDefinitionGetter extensionDefinitionGetter;\n\n    private final ExtensionPointDefinitionGetter extensionPointDefinitionGetter;\n\n    @Override\n    public <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPoint) {\n        return Flux.fromIterable(lookExtensions(extensionPoint))\n            .concatWith(\n                Flux.fromStream(() -> beanFactory.getBeanProvider(extensionPoint).orderedStream())\n            )\n            .sort(new AnnotationAwareOrderComparator());\n    }\n\n    @Override\n    public <T extends ExtensionPoint> List<T> getExtensionList(Class<T> extensionPoint) {\n        var extensions = new LinkedList<T>();\n        extensions.addAll(lookExtensions(extensionPoint));\n        extensions.addAll(beanFactory.getBeanProvider(extensionPoint).orderedStream().toList());\n        extensions.sort(new AnnotationAwareOrderComparator());\n        return extensions;\n    }\n\n    @Override\n    public <T extends ExtensionPoint> Mono<T> getEnabledExtension(Class<T> extensionPoint) {\n        return getEnabledExtensions(extensionPoint).next();\n    }\n\n    @Override\n    public <T extends ExtensionPoint> Flux<T> getEnabledExtensions(\n        Class<T> extensionPoint) {\n        return fetchExtensionPointDefinition(extensionPoint)\n            .flatMapMany(epd -> {\n                var epdName = epd.getMetadata().getName();\n                var type = epd.getSpec().getType();\n                if (type == ExtensionPointDefinition.ExtensionPointType.SINGLETON) {\n                    return getEnabledExtensions(epdName, extensionPoint).take(1);\n                }\n                // TODO If the type is sortable, may need to process the returned order.\n                return getEnabledExtensions(epdName, extensionPoint);\n            });\n    }\n\n    private <T extends ExtensionPoint> Flux<T> getEnabledExtensions(String epdName,\n        Class<T> extensionPoint) {\n        return systemConfigFetcher.fetch(ExtensionPointEnabled.GROUP, ExtensionPointEnabled.class)\n            .switchIfEmpty(Mono.fromSupplier(ExtensionPointEnabled::new))\n            .flatMapMany(enabled -> {\n                var extensionDefNames = enabled.getOrDefault(epdName, null);\n                if (extensionDefNames == null) {\n                    // get all extensions if not specified\n                    return Flux.defer(() -> getExtensions(extensionPoint));\n                }\n                var extensions = getExtensions(extensionPoint).cache();\n                return Flux.fromIterable(extensionDefNames)\n                    .flatMapSequential(extensionDefinitionGetter::get)\n                    .flatMapSequential(extensionDef -> {\n                        var className = extensionDef.getSpec().getClassName();\n                        return extensions.filter(\n                            extension -> Objects.equals(extension.getClass().getName(),\n                                className)\n                        );\n                    });\n            });\n    }\n\n    private Mono<ExtensionPointDefinition> fetchExtensionPointDefinition(\n        Class<? extends ExtensionPoint> extensionPoint) {\n        return extensionPointDefinitionGetter.getByClassName(extensionPoint.getName());\n    }\n\n    @NonNull\n    protected <T> List<T> lookExtensions(Class<T> type) {\n        List<T> beans = new ArrayList<>();\n        // avoid concurrent modification\n        var startedPlugins = pluginManager.startedPlugins();\n        for (PluginWrapper startedPlugin : startedPlugins) {\n            if (startedPlugin.getPlugin() instanceof SpringPlugin springPlugin) {\n                var pluginApplicationContext = springPlugin.getApplicationContext();\n                try {\n                    pluginApplicationContext.getBeansOfType(type)\n                        .forEach((name, bean) -> beans.add(bean));\n                } catch (Throwable e) {\n                    // Ignore\n                    log.error(\"Error while looking for extensions of type {}\", type, e);\n                }\n            } else {\n                var extensions = pluginManager.getExtensions(type, startedPlugin.getPluginId());\n                beans.addAll(extensions);\n            }\n        }\n        return beans;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinition.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * Extension definition.\n * An {@link ExtensionDefinition} is a type of metadata that provides additional information about\n * an extension. An extension is a way to add new functionality to an existing class, structure,\n * enumeration, or protocol type without needing to subclass it.\n *\n * @author guqing\n * @since 2.4.0\n */\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"plugin.halo.run\", version = \"v1alpha1\",\n    kind = \"ExtensionDefinition\", singular = \"extensiondefinition\",\n    plural = \"extensiondefinitions\")\npublic class ExtensionDefinition extends AbstractExtension {\n\n    @Schema(requiredMode = REQUIRED)\n    private ExtensionSpec spec;\n\n    @Data\n    public static class ExtensionSpec {\n        @Schema(requiredMode = REQUIRED)\n        private String className;\n\n        @Schema(requiredMode = REQUIRED)\n        private String extensionPointName;\n\n        @Schema(requiredMode = REQUIRED)\n        private String displayName;\n\n        private String description;\n\n        private String icon;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetter.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport reactor.core.publisher.Mono;\n\npublic interface ExtensionDefinitionGetter {\n\n    /**\n     * Gets extension definition by extension definition name.\n     *\n     * @param name extension definition name\n     */\n    Mono<ExtensionDefinition> get(String name);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionDefinitionGetterImpl.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ExtensionClient;\n\n@Component\npublic class ExtensionDefinitionGetterImpl\n    extends AbstractDefinitionGetter<ExtensionDefinition>\n    implements ExtensionDefinitionGetter {\n\n    public ExtensionDefinitionGetterImpl(ExtensionClient client) {\n        super(client, new ExtensionDefinition());\n    }\n\n    @Override\n    public Mono<ExtensionDefinition> get(String name) {\n        return Mono.fromSupplier(() -> cache.get(name));\n    }\n\n    @Override\n    void putCache(ExtensionDefinition definition) {\n        cache.put(definition.getMetadata().getName(), definition);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinition.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n/**\n * Extension point definition.\n * An {@link ExtensionPointDefinition} is a concept used in <code>Halo</code> to allow for the\n * dynamic extension of system. It defines a location within <code>Halo</code> where\n * additional functionality can be added through the use of plugins or extensions.\n *\n * @author guqing\n * @since 2.4.0\n */\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\n@GVK(group = \"plugin.halo.run\", version = \"v1alpha1\",\n    kind = \"ExtensionPointDefinition\", singular = \"extensionpointdefinition\",\n    plural = \"extensionpointdefinitions\")\npublic class ExtensionPointDefinition extends AbstractExtension {\n\n    @Schema(requiredMode = REQUIRED)\n    private ExtensionPointSpec spec;\n\n    @Data\n    public static class ExtensionPointSpec {\n        @Schema(requiredMode = REQUIRED)\n        private String className;\n\n        @Schema(requiredMode = REQUIRED)\n        private String displayName;\n\n        @Schema(requiredMode = REQUIRED)\n        private ExtensionPointType type;\n\n        private String description;\n\n        private String icon;\n    }\n\n    /**\n     * <p>Types of extension points include.</p>\n     * There are several types:\n     * <ul>\n     * <li>Singleton extension point: means that only one implementation class of the extension\n     * point can be enabled. It is generally used for global core extension points, such as global\n     * logging components. When using a singleton extension point, it is necessary to ensure that\n     * only one implementation class is enabled, otherwise unexpected issues may occur.</li>\n     * <li>Multi-instance extension point: means that there can be multiple implementation\n     * classes of the extension point enabled, and the execution order of each implementation\n     * class may be different. It is generally used for specific business logic extension points,\n     * such as the selection of data sources or the use of caches. When using a multi-instance\n     * extension point, it is necessary to consider the dependency relationship and execution\n     * order between each implementation class to ensure the correctness of the business logic.</li>\n     * <li>Ordered extension point: means that multiple implementation classes of the extension\n     * point can be enabled, but they need to be executed in a specified order. It is generally\n     * used in scenarios that require strict control of execution order, such as the execution\n     * order of message listeners. When using an ordered extension point, it is necessary to\n     * assign a priority for each implementation class to ensure that they can be executed in the\n     * correct order.</li>\n     * <li>Conditional extension point: means that multiple implementation classes of the extension\n     * point can be enabled, but they need to meet specific conditions to be executed. For\n     * example, some implementation classes can only be executed under specific operating systems\n     * or specific runtime environments. When using a conditional extension point, it is\n     * necessary to define appropriate conditions according to the actual scenario to ensure the\n     * correctness and availability of the extension point.</li>\n     * </ul>\n     * There are two kinds of definitions for the time being: SINGLETON and MULTI_INSTANCE.\n     */\n    public enum ExtensionPointType {\n        SINGLETON,\n        MULTI_INSTANCE;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetter.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport reactor.core.publisher.Mono;\n\npublic interface ExtensionPointDefinitionGetter {\n\n    /**\n     * Gets extension point definition by extension point class.\n     * <p>Retrieve by filedSelector: <code>spec.className</code></p>\n     *\n     * @param className extension point class name\n     */\n    Mono<ExtensionPointDefinition> getByClassName(String className);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionPointDefinitionGetterImpl.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ExtensionClient;\n\n@Component\npublic class ExtensionPointDefinitionGetterImpl\n    extends AbstractDefinitionGetter<ExtensionPointDefinition>\n    implements ExtensionPointDefinitionGetter {\n\n    public ExtensionPointDefinitionGetterImpl(ExtensionClient client) {\n        super(client, new ExtensionPointDefinition());\n    }\n\n    @Override\n    public Mono<ExtensionPointDefinition> getByClassName(String className) {\n        return Mono.fromSupplier(() -> cache.get(className));\n    }\n\n    @Override\n    void putCache(ExtensionPointDefinition definition) {\n        var className = definition.getSpec().getClassName();\n        cache.put(className, definition);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/resources/BundleResourceUtils.java",
    "content": "package run.halo.app.plugin.resources;\n\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport org.springframework.core.io.Resource;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.Assert;\nimport org.springframework.util.StringUtils;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.infra.utils.PathUtils;\n\n/**\n * Plugin bundle resources utils.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic abstract class BundleResourceUtils {\n    private static final String CONSOLE_BUNDLE_LOCATION = \"console\";\n    public static final String JS_BUNDLE = \"main.js\";\n    public static final String CSS_BUNDLE = \"style.css\";\n\n    /**\n     * Gets js bundle resource by plugin name in console location.\n     *\n     * @return js bundle resource if exists, otherwise null\n     */\n    @Nullable\n    public static Resource getJsBundleResource(PluginManager pluginManager, String pluginName,\n        String bundleName) {\n        Assert.hasText(pluginName, \"The pluginName must not be blank\");\n        Assert.hasText(bundleName, \"Bundle name must not be blank\");\n\n        DefaultResourceLoader resourceLoader = getResourceLoader(pluginManager, pluginName);\n        if (resourceLoader == null) {\n            return null;\n        }\n        String path = PathUtils.combinePath(CONSOLE_BUNDLE_LOCATION, bundleName);\n        String simplifyPath = StringUtils.cleanPath(path);\n        FileUtils.checkDirectoryTraversal(\"/\" + CONSOLE_BUNDLE_LOCATION, simplifyPath);\n        Resource resource = resourceLoader.getResource(simplifyPath);\n        return resource.exists() ? resource : null;\n    }\n\n    @Nullable\n    public static DefaultResourceLoader getResourceLoader(PluginManager pluginManager,\n        String pluginName) {\n        Assert.notNull(pluginManager, \"Plugin manager must not be null\");\n        PluginWrapper plugin = pluginManager.getPlugin(pluginName);\n        if (plugin == null) {\n            return null;\n        }\n        return new DefaultResourceLoader(plugin.getPluginClassLoader());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java",
    "content": "package run.halo.app.plugin.resources;\n\nimport static org.springframework.http.MediaType.ALL;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.util.List;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.pf4j.PluginManager;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.ResourceLoader;\nimport org.springframework.http.CacheControl;\nimport org.springframework.http.server.PathContainer;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.reactive.resource.NoResourceFoundException;\nimport org.springframework.web.util.pattern.PathPatternParser;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;\nimport run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.plugin.PluginConst;\n\n/**\n * <p>Plugin's reverse proxy router factory.</p>\n * <p>It creates a {@link RouterFunction} based on the ReverseProxy rule configured by\n * the plugin.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\n@AllArgsConstructor\npublic class ReverseProxyRouterFunctionFactory {\n\n    private final PluginManager pluginManager;\n    private final ApplicationContext applicationContext;\n    private final WebProperties webProperties;\n\n    /**\n     * <p>Create {@link RouterFunction} according to the {@link ReverseProxy} custom resource\n     * configuration of the plugin.</p>\n     * <p>Note that: returns {@code Null} if the plugin does not have a {@link ReverseProxy} custom\n     * resource.</p>\n     *\n     * @param pluginName plugin name(nullable if system)\n     * @return A reverse proxy RouterFunction handle(nullable)\n     */\n    @Nullable\n    public RouterFunction<ServerResponse> create(ReverseProxy reverseProxy, String pluginName) {\n        return createReverseProxyRouterFunction(reverseProxy, nullSafePluginName(pluginName));\n    }\n\n    @Nullable\n    private RouterFunction<ServerResponse> createReverseProxyRouterFunction(\n        ReverseProxy reverseProxy, @NonNull String pluginName) {\n        Assert.notNull(reverseProxy, \"The reverseProxy must not be null.\");\n        var rules = getReverseProxyRules(reverseProxy);\n        var cacheProperties = webProperties.getResources().getCache();\n        var useLastModified = cacheProperties.isUseLastModified();\n        var cacheControl = cacheProperties.getCachecontrol().toHttpCacheControl();\n        if (cacheControl == null) {\n            cacheControl = CacheControl.empty();\n        }\n        var finalCacheControl = cacheControl;\n        return rules.stream().map(rule -> {\n            String routePath = buildRoutePath(pluginName, rule);\n            log.debug(\"Plugin [{}] registered reverse proxy route path [{}]\", pluginName,\n                routePath);\n            return RouterFunctions.route(GET(routePath).and(accept(ALL)),\n                request -> {\n                    var resource = loadResourceByFileRule(pluginName, rule, request);\n                    if (!resource.exists()) {\n                        return Mono.error(new NoResourceFoundException(request.uri(), routePath));\n                    }\n                    if (!useLastModified) {\n                        return ServerResponse.ok()\n                            .cacheControl(finalCacheControl)\n                            .body(BodyInserters.fromResource(resource));\n                    }\n                    Instant lastModified;\n                    try {\n                        lastModified = Instant.ofEpochMilli(resource.lastModified());\n                    } catch (IOException e) {\n                        if (e instanceof FileNotFoundException) {\n                            return Mono.error(\n                                new NoResourceFoundException(request.uri(), routePath)\n                            );\n                        }\n                        return Mono.error(e);\n                    }\n                    return request.checkNotModified(lastModified)\n                        .switchIfEmpty(Mono.defer(\n                            () -> ServerResponse.ok()\n                                .cacheControl(finalCacheControl)\n                                .lastModified(lastModified)\n                                .body(BodyInserters.fromResource(resource)))\n                        );\n                });\n        }).reduce(RouterFunction::and).orElse(null);\n    }\n\n    private String nullSafePluginName(String pluginName) {\n        return pluginName == null ? PluginConst.SYSTEM_PLUGIN_NAME : pluginName;\n    }\n\n    private List<ReverseProxyRule> getReverseProxyRules(ReverseProxy reverseProxy) {\n        return reverseProxy.getRules();\n    }\n\n    public static String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) {\n        return PathUtils.combinePath(PluginConst.assetsRoutePrefix(pluginId),\n            reverseProxyRule.path());\n    }\n\n    /**\n     * <p>File load rule: if the directory is configured but the file name is not configured, it\n     * means access through wildcards. Otherwise, if only the file name is configured, this\n     * method only returns the file pointed to by the rule.</p>\n     * <p>You should only use {@link Resource#getInputStream()} to get resource content instead of\n     * {@link Resource#getFile()},the resource is loaded from the plugin jar file using a\n     * specific plugin class loader; if you use {@link Resource#getFile()}, you cannot get the\n     * file.</p>\n     * <p>Note that a returned Resource handle does not imply an existing resource; you need to\n     * invoke {@link Resource#exists()} to check for existence</p>\n     *\n     * @param pluginName plugin to load file by name\n     * @param rule reverse proxy rule\n     * @param request client request\n     * @return a Resource handle for the specified resource location by the plugin(never null);\n     */\n    @NonNull\n    private Resource loadResourceByFileRule(String pluginName, ReverseProxyRule rule,\n        ServerRequest request) {\n        Assert.notNull(rule.file(), \"File rule must not be null.\");\n        FileReverseProxyProvider file = rule.file();\n        String directory = file.directory();\n\n        // Decision file name\n        String filename;\n        String configuredFilename = file.filename();\n        if (StringUtils.isNotBlank(configuredFilename)) {\n            filename = configuredFilename;\n        } else {\n            String routePath = buildRoutePath(pluginName, rule);\n            PathContainer pathContainer = PathPatternParser.defaultInstance.parse(routePath)\n                .extractPathWithinPattern(PathContainer.parsePath(request.path()));\n            filename = pathContainer.value();\n        }\n\n        String filePath = PathUtils.combinePath(directory, filename);\n        return getResourceLoader(pluginName).getResource(filePath);\n    }\n\n    private ResourceLoader getResourceLoader(String pluginName) {\n        if (PluginConst.SYSTEM_PLUGIN_NAME.equals(pluginName)) {\n            return applicationContext;\n        }\n        DefaultResourceLoader resourceLoader =\n            BundleResourceUtils.getResourceLoader(pluginManager, pluginName);\n        if (resourceLoader == null) {\n            throw new NotFoundException(\"Plugin [\" + pluginName + \"] not found.\");\n        }\n        return resourceLoader;\n    }\n}\n\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistry.java",
    "content": "package run.halo.app.plugin.resources;\n\nimport com.google.common.collect.LinkedHashMultimap;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.locks.StampedLock;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.plugin.PluginRouterFunctionRegistry;\n\n/**\n * A registry for {@link RouterFunction} of plugin.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class ReverseProxyRouterFunctionRegistry {\n\n    private final PluginRouterFunctionRegistry pluginRouterFunctionRegistry;\n\n    private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;\n    private final StampedLock lock = new StampedLock();\n    private final Map<String, RouterFunction<ServerResponse>> proxyNameRouterFunctionRegistry =\n        new HashMap<>();\n    private final LinkedHashMultimap<String, String> pluginIdReverseProxyMap =\n        LinkedHashMultimap.create();\n\n    public ReverseProxyRouterFunctionRegistry(\n        PluginRouterFunctionRegistry pluginRouterFunctionRegistry,\n        ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) {\n        this.pluginRouterFunctionRegistry = pluginRouterFunctionRegistry;\n        this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;\n    }\n\n    /**\n     * Register reverse proxy router function.\n     *\n     * @param pluginId plugin id\n     * @param reverseProxy reverse proxy\n     */\n    public void register(String pluginId, ReverseProxy reverseProxy) {\n        Assert.notNull(pluginId, \"The plugin id must not be null.\");\n        final String proxyName = reverseProxy.getMetadata().getName();\n        long stamp = lock.writeLock();\n        try {\n            pluginIdReverseProxyMap.put(pluginId, proxyName);\n            var routerFunction = reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId);\n            if (routerFunction != null) {\n                proxyNameRouterFunctionRegistry.put(proxyName, routerFunction);\n                pluginRouterFunctionRegistry.register(Set.of(routerFunction));\n            }\n        } finally {\n            lock.unlockWrite(stamp);\n        }\n    }\n\n    /**\n     * Only for test.\n     */\n    int reverseProxySize(String pluginId) {\n        Set<String> names = pluginIdReverseProxyMap.get(pluginId);\n        return names.size();\n    }\n\n    /**\n     * Remove reverse proxy router function by pluginId and reverse proxy name.\n     */\n    public void remove(String pluginId, String reverseProxyName) {\n        long stamp = lock.writeLock();\n        try {\n            pluginIdReverseProxyMap.remove(pluginId, reverseProxyName);\n            var removedRouterFunction = proxyNameRouterFunctionRegistry.remove(reverseProxyName);\n            if (removedRouterFunction != null) {\n                pluginRouterFunctionRegistry.unregister(Set.of(removedRouterFunction));\n            }\n        } finally {\n            lock.unlockWrite(stamp);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/HaloDocumentEventsListener.java",
    "content": "package run.halo.app.search;\n\nimport java.time.Duration;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.search.event.HaloDocumentAddRequestEvent;\nimport run.halo.app.search.event.HaloDocumentDeleteRequestEvent;\nimport run.halo.app.search.event.HaloDocumentRebuildRequestEvent;\n\n@Component\npublic class HaloDocumentEventsListener {\n\n    private final ExtensionGetter extensionGetter;\n\n    private int bufferSize;\n\n    public HaloDocumentEventsListener(ExtensionGetter extensionGetter) {\n        this.extensionGetter = extensionGetter;\n        this.bufferSize = 200;\n    }\n\n    /**\n     * Only for testing.\n     *\n     * @param bufferSize new buffer size for rebuilding indices\n     */\n    void setBufferSize(int bufferSize) {\n        this.bufferSize = bufferSize;\n    }\n\n    @EventListener\n    @Async\n    void onApplicationEvent(HaloDocumentRebuildRequestEvent event) {\n        getSearchEngine()\n            .doOnNext(SearchEngine::deleteAll)\n            .flatMap(searchEngine -> extensionGetter.getExtensions(HaloDocumentsProvider.class)\n                .flatMap(HaloDocumentsProvider::fetchAll)\n                .buffer(this.bufferSize)\n                .doOnNext(searchEngine::addOrUpdate)\n                .then())\n            .blockOptional(Duration.ofMinutes(1));\n    }\n\n    @EventListener\n    @Async\n    void onApplicationEvent(HaloDocumentAddRequestEvent event) {\n        getSearchEngine()\n            .doOnNext(searchEngine -> searchEngine.addOrUpdate(event.getDocuments()))\n            .then()\n            .blockOptional(Duration.ofMinutes(1));\n    }\n\n    @EventListener\n    @Async\n    void onApplicationEvent(HaloDocumentDeleteRequestEvent event) {\n        getSearchEngine()\n            .doOnNext(searchEngine -> searchEngine.deleteDocument(event.getDocIds()))\n            .then()\n            .blockOptional(Duration.ofMinutes(1));\n    }\n\n    private Mono<SearchEngine> getSearchEngine() {\n        return extensionGetter.getEnabledExtension(SearchEngine.class)\n            .filter(SearchEngine::available)\n            .switchIfEmpty(Mono.error(SearchEngineUnavailableException::new));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/IndexEndpoint.java",
    "content": "package run.halo.app.search;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\n\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\n\n@Component\npublic class IndexEndpoint implements CustomEndpoint {\n\n    private static final String API_VERSION = \"api.halo.run/v1alpha1\";\n\n    private final SearchService searchService;\n\n    public IndexEndpoint(SearchService searchService) {\n        this.searchService = searchService;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"IndexV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"/indices/-/search\", this::indicesSearch,\n                builder -> builder.operationId(\"IndicesSearch\")\n                    .tag(tag)\n                    .description(\"Search indices.\")\n                    .requestBody(requestBodyBuilder().implementation(SearchOption.class)\n                        .description(\"\"\"\n                            Please note that the \"filterPublished\", \"filterExposed\" and \\\n                            \"filterRecycled\" fields are ignored in this endpoint.\\\n                            \"\"\")\n                    )\n                    .response(responseBuilder().implementation(SearchResult.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> indicesSearch(ServerRequest serverRequest) {\n        return serverRequest.bodyToMono(SearchOption.class)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Request body required.\")))\n            .flatMap(this::performSearch)\n            .flatMap(result -> ServerResponse.ok().bodyValue(result));\n    }\n\n    private Mono<SearchResult> performSearch(SearchOption option) {\n        option.setFilterExposed(true);\n        option.setFilterPublished(true);\n        option.setFilterRecycled(false);\n        return searchService.search(option);\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(API_VERSION);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/IndicesEndpoint.java",
    "content": "package run.halo.app.search;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.search.event.HaloDocumentRebuildRequestEvent;\n\n@Component\n@Slf4j\npublic class IndicesEndpoint implements CustomEndpoint {\n\n    private static final String API_VERSION = \"api.console.halo.run/v1alpha1\";\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    public IndicesEndpoint(ApplicationEventPublisher eventPublisher) {\n        this.eventPublisher = eventPublisher;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"IndicesV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"/indices/-/rebuild\", this::rebuildIndices,\n                builder -> builder.operationId(\"RebuildAllIndices\")\n                    .tag(tag)\n                    .description(\"Rebuild all indices\")\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> rebuildIndices(ServerRequest serverRequest) {\n        return Mono.fromRunnable(\n            () -> eventPublisher.publishEvent(new HaloDocumentRebuildRequestEvent(this))\n        ).then(ServerResponse.accepted().build());\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(API_VERSION);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/SearchEngineUnavailableException.java",
    "content": "package run.halo.app.search;\n\nimport org.springframework.web.server.ServerWebInputException;\n\n/**\n * Search engine unavailable exception.\n *\n * @author johnniang\n */\npublic class SearchEngineUnavailableException extends ServerWebInputException {\n\n    public SearchEngineUnavailableException() {\n        super(\"Search Engine is unavailable.\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/SearchServiceImpl.java",
    "content": "package run.halo.app.search;\n\nimport org.springframework.stereotype.Service;\nimport org.springframework.validation.Validator;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.infra.exception.RequestBodyValidationException;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n@Service\npublic class SearchServiceImpl implements SearchService {\n\n    private final Validator validator;\n\n    private final ExtensionGetter extensionGetter;\n\n    public SearchServiceImpl(Validator validator, ExtensionGetter extensionGetter) {\n        this.validator = validator;\n        this.extensionGetter = extensionGetter;\n    }\n\n    @Override\n    public Mono<SearchResult> search(SearchOption option) {\n        // validate the option\n        var errors = validator.validateObject(option);\n        if (errors.hasErrors()) {\n            return Mono.error(new RequestBodyValidationException(errors));\n        }\n        return extensionGetter.getEnabledExtension(SearchEngine.class)\n            .filter(SearchEngine::available)\n            .switchIfEmpty(Mono.error(SearchEngineUnavailableException::new))\n            .flatMap(searchEngine -> Mono.fromSupplier(() ->\n                searchEngine.search(option)\n            ).subscribeOn(Schedulers.boundedElastic()));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java",
    "content": "package run.halo.app.search.lucene;\n\nimport static org.apache.lucene.document.Field.Store.YES;\nimport static org.apache.lucene.index.IndexWriterConfig.OpenMode.CREATE_OR_APPEND;\nimport static org.apache.lucene.search.BooleanClause.Occur.FILTER;\nimport static org.apache.lucene.search.BooleanClause.Occur.MUST;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.lucene.analysis.Analyzer;\nimport org.apache.lucene.analysis.charfilter.HTMLStripCharFilterFactory;\nimport org.apache.lucene.analysis.cjk.CJKBigramFilterFactory;\nimport org.apache.lucene.analysis.cjk.CJKWidthCharFilterFactory;\nimport org.apache.lucene.analysis.cjk.CJKWidthFilterFactory;\nimport org.apache.lucene.analysis.core.LowerCaseFilterFactory;\nimport org.apache.lucene.analysis.custom.CustomAnalyzer;\nimport org.apache.lucene.analysis.standard.StandardTokenizerFactory;\nimport org.apache.lucene.document.Document;\nimport org.apache.lucene.document.FieldType;\nimport org.apache.lucene.document.LongField;\nimport org.apache.lucene.document.StoredField;\nimport org.apache.lucene.document.StringField;\nimport org.apache.lucene.document.TextField;\nimport org.apache.lucene.index.DocValuesType;\nimport org.apache.lucene.index.IndexNotFoundException;\nimport org.apache.lucene.index.IndexWriter;\nimport org.apache.lucene.index.IndexWriterConfig;\nimport org.apache.lucene.index.Term;\nimport org.apache.lucene.queryparser.flexible.core.QueryNodeException;\nimport org.apache.lucene.queryparser.flexible.standard.StandardQueryParser;\nimport org.apache.lucene.search.BooleanQuery;\nimport org.apache.lucene.search.FuzzyQuery;\nimport org.apache.lucene.search.IndexSearcher;\nimport org.apache.lucene.search.SearcherManager;\nimport org.apache.lucene.search.Sort;\nimport org.apache.lucene.search.TermInSetQuery;\nimport org.apache.lucene.search.TermQuery;\nimport org.apache.lucene.search.highlight.Highlighter;\nimport org.apache.lucene.search.highlight.InvalidTokenOffsetsException;\nimport org.apache.lucene.search.highlight.QueryTermScorer;\nimport org.apache.lucene.search.highlight.SimpleHTMLFormatter;\nimport org.apache.lucene.store.Directory;\nimport org.apache.lucene.store.FSDirectory;\nimport org.apache.lucene.util.BytesRef;\nimport org.apache.lucene.util.IOUtils;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.StopWatch;\nimport org.springframework.util.StringUtils;\nimport reactor.core.Exceptions;\nimport run.halo.app.search.HaloDocument;\nimport run.halo.app.search.SearchEngine;\nimport run.halo.app.search.SearchOption;\nimport run.halo.app.search.SearchResult;\n\n@Slf4j\npublic class LuceneSearchEngine implements SearchEngine, InitializingBean, DisposableBean {\n\n    private final Path indexRootDir;\n\n    private final Converter<HaloDocument, Document> haloDocumentConverter =\n        new HaloDocumentConverter();\n\n    private final Converter<Document, HaloDocument> documentConverter =\n        new DocumentConverter();\n\n    private Analyzer analyzer;\n\n    private volatile SearcherManager searcherManager;\n\n    private Directory directory;\n\n    public LuceneSearchEngine(Path indexRootDir) {\n        this.indexRootDir = indexRootDir;\n    }\n\n    @Override\n    public boolean available() {\n        return true;\n    }\n\n    @Override\n    public void addOrUpdate(Iterable<HaloDocument> haloDocs) {\n        var docs = new LinkedList<Document>();\n        var terms = new LinkedList<BytesRef>();\n        haloDocs.forEach(haloDoc -> {\n            var doc = this.haloDocumentConverter.convert(haloDoc);\n            terms.add(new BytesRef(haloDoc.getId()));\n            docs.add(doc);\n        });\n        var deleteQuery = new TermInSetQuery(\"id\", terms);\n\n        var writerConfig = new IndexWriterConfig(this.analyzer)\n            .setOpenMode(CREATE_OR_APPEND);\n        synchronized (this) {\n            try (var indexWriter = new IndexWriter(this.directory, writerConfig)) {\n                indexWriter.updateDocuments(deleteQuery, docs);\n            } catch (IOException e) {\n                throw Exceptions.propagate(e);\n            } finally {\n                this.refreshSearcherManager();\n            }\n        }\n    }\n\n    @Override\n    public void deleteDocument(Iterable<String> haloDocIds) {\n        var terms = new LinkedList<BytesRef>();\n        haloDocIds.forEach(haloDocId -> terms.add(new BytesRef(haloDocId)));\n        var deleteQuery = new TermInSetQuery(\"id\", terms);\n        var writerConfig = new IndexWriterConfig(this.analyzer)\n            .setOpenMode(CREATE_OR_APPEND);\n        synchronized (this) {\n            try (var indexWriter = new IndexWriter(this.directory, writerConfig)) {\n                indexWriter.deleteDocuments(deleteQuery);\n            } catch (IOException e) {\n                throw Exceptions.propagate(e);\n            } finally {\n                this.refreshSearcherManager();\n            }\n        }\n    }\n\n    @Override\n    public void deleteAll() {\n        var writerConfig = new IndexWriterConfig(this.analyzer)\n            .setOpenMode(CREATE_OR_APPEND);\n        synchronized (this) {\n            try (var indexWriter = new IndexWriter(this.directory, writerConfig)) {\n                indexWriter.deleteAll();\n            } catch (IOException e) {\n                throw Exceptions.propagate(e);\n            } finally {\n                this.refreshSearcherManager();\n            }\n        }\n    }\n\n    @Override\n    public SearchResult search(SearchOption option) {\n        IndexSearcher searcher = null;\n        var sm = obtainSearcherManager();\n        if (sm.isEmpty()) {\n            // indicate the index is empty\n            var emptyResult = new SearchResult();\n            emptyResult.setKeyword(option.getKeyword());\n            emptyResult.setLimit(option.getLimit());\n            emptyResult.setTotal(0L);\n            emptyResult.setHits(List.of());\n            return emptyResult;\n        }\n        try {\n            searcher = searcherManager.acquire();\n            var queryParser = new StandardQueryParser(analyzer);\n            queryParser.setMultiFields(new String[] {\"title\", \"description\", \"content\"});\n            queryParser.setFieldsBoost(Map.of(\"title\", 1.0f, \"description\", 0.5f, \"content\", 0.2f));\n            queryParser.setFuzzyMinSim(FuzzyQuery.defaultMaxEdits);\n            queryParser.setFuzzyPrefixLength(FuzzyQuery.defaultPrefixLength);\n\n            var keyword = option.getKeyword();\n            var query = queryParser.parse(keyword, null);\n            var queryBuilder = new BooleanQuery.Builder()\n                .add(query, MUST);\n\n            var filterExposed = option.getFilterExposed();\n            if (filterExposed != null) {\n                queryBuilder.add(\n                    new TermQuery(new Term(\"exposed\", filterExposed.toString())), FILTER\n                );\n            }\n            var filterRecycled = option.getFilterRecycled();\n            if (filterRecycled != null) {\n                queryBuilder.add(\n                    new TermQuery(new Term(\"recycled\", filterRecycled.toString())), FILTER\n                );\n            }\n            var filterPublished = option.getFilterPublished();\n            if (filterPublished != null) {\n                queryBuilder.add(\n                    new TermQuery(new Term(\"published\", filterPublished.toString())), FILTER\n                );\n            }\n\n            Optional.ofNullable(option.getIncludeTypes())\n                .filter(types -> !types.isEmpty())\n                .ifPresent(types -> {\n                    var typeTerms = types.stream()\n                        .distinct()\n                        .map(BytesRef::new)\n                        .toList();\n                    queryBuilder.add(new TermInSetQuery(\"type\", typeTerms), FILTER);\n                });\n\n            Optional.ofNullable(option.getIncludeOwnerNames())\n                .filter(ownerNames -> !ownerNames.isEmpty())\n                .ifPresent(ownerNames -> {\n                    var ownerTerms = ownerNames.stream()\n                        .distinct()\n                        .map(BytesRef::new)\n                        .toList();\n                    queryBuilder.add(new TermInSetQuery(\"ownerName\", ownerTerms), FILTER);\n                });\n\n            Optional.ofNullable(option.getIncludeTagNames())\n                .filter(tagNames -> !tagNames.isEmpty())\n                .ifPresent(tagNames -> tagNames\n                    .stream()\n                    .distinct()\n                    .forEach(tagName ->\n                        queryBuilder.add(new TermQuery(new Term(\"tag\", tagName)), FILTER)\n                    ));\n\n            Optional.ofNullable(option.getIncludeCategoryNames())\n                .filter(categoryNames -> !categoryNames.isEmpty())\n                .ifPresent(categoryNames -> categoryNames\n                    .stream()\n                    .distinct()\n                    .forEach(categoryName ->\n                        queryBuilder.add(new TermQuery(new Term(\"category\", categoryName)), FILTER)\n                    ));\n\n            var finalQuery = queryBuilder.build();\n            var limit = option.getLimit();\n\n            var stopWatch = new StopWatch(\"SearchWatch\");\n            stopWatch.start(\"search \" + keyword);\n            var hits = searcher.search(finalQuery, limit, Sort.RELEVANCE);\n            stopWatch.stop();\n            var formatter =\n                new SimpleHTMLFormatter(option.getHighlightPreTag(), option.getHighlightPostTag());\n            var queryScorer = new QueryTermScorer(query);\n            var highlighter = new Highlighter(formatter, queryScorer);\n\n            var haloDocs = new ArrayList<HaloDocument>(hits.scoreDocs.length);\n            for (var hit : hits.scoreDocs) {\n                var doc = searcher.storedFields().document(hit.doc);\n                var haloDoc = documentConverter.convert(doc);\n\n                var title = doc.get(\"title\");\n                var hlTitle = highlighter.getBestFragment(this.analyzer, \"title\", title);\n                if (!StringUtils.hasText(hlTitle)) {\n                    hlTitle = title;\n                }\n\n                var description = doc.get(\"description\");\n                String hlDescription = null;\n                if (description != null) {\n                    hlDescription =\n                        highlighter.getBestFragment(this.analyzer, \"description\", description);\n                }\n\n                var content = doc.get(\"content\");\n                var hlContent = highlighter.getBestFragment(this.analyzer, \"content\", content);\n\n                haloDoc.setTitle(hlTitle);\n                haloDoc.setDescription(hlDescription);\n                haloDoc.setContent(hlContent);\n                haloDocs.add(haloDoc);\n            }\n            var result = new SearchResult();\n            result.setHits(haloDocs);\n            result.setTotal(hits.totalHits.value());\n            result.setKeyword(keyword);\n            result.setLimit(limit);\n            result.setProcessingTimeMillis(stopWatch.getTotalTimeMillis());\n            return result;\n        } catch (IOException | QueryNodeException | InvalidTokenOffsetsException e) {\n            throw new RuntimeException(e);\n        } finally {\n            if (searcher != null) {\n                try {\n                    searcherManager.release(searcher);\n                } catch (IOException e) {\n                    log.error(\"Failed to release searcher\", e);\n                }\n            }\n        }\n    }\n\n    @Override\n    public void afterPropertiesSet() throws Exception {\n        this.analyzer = CustomAnalyzer.builder()\n            .withTokenizer(StandardTokenizerFactory.class)\n            .addCharFilter(HTMLStripCharFilterFactory.NAME)\n            .addCharFilter(CJKWidthCharFilterFactory.NAME)\n            .addTokenFilter(LowerCaseFilterFactory.NAME)\n            .addTokenFilter(CJKWidthFilterFactory.NAME)\n            .addTokenFilter(CJKBigramFilterFactory.NAME)\n            .build();\n        this.directory = FSDirectory.open(this.indexRootDir);\n        log.info(\"Initialized lucene search engine\");\n    }\n\n    Optional<SearcherManager> obtainSearcherManager() {\n        if (this.searcherManager != null) {\n            return Optional.of(this.searcherManager);\n        }\n        synchronized (this) {\n            // double check\n            if (this.searcherManager != null) {\n                return Optional.of(this.searcherManager);\n            }\n            try {\n                this.searcherManager = new SearcherManager(this.directory, null);\n                return Optional.of(this.searcherManager);\n            } catch (IndexNotFoundException e) {\n                log.warn(\"Index not ready for creating searcher manager\");\n            } catch (IOException e) {\n                log.error(\"Failed to create searcher manager\", e);\n            }\n            return Optional.empty();\n        }\n    }\n\n    private void refreshSearcherManager() {\n        this.obtainSearcherManager().ifPresent(sm -> {\n            try {\n                sm.maybeRefreshBlocking();\n            } catch (IOException e) {\n                log.warn(\"Failed to refresh searcher\", e);\n            }\n        });\n    }\n\n    Directory getDirectory() {\n        return directory;\n    }\n\n    Analyzer getAnalyzer() {\n        return analyzer;\n    }\n\n    void setDirectory(Directory directory) {\n        this.directory = directory;\n    }\n\n    void setSearcherManager(SearcherManager searcherManager) {\n        this.searcherManager = searcherManager;\n    }\n\n    void setAnalyzer(Analyzer analyzer) {\n        this.analyzer = analyzer;\n    }\n\n    Converter<HaloDocument, Document> getHaloDocumentConverter() {\n        return haloDocumentConverter;\n    }\n\n    Converter<Document, HaloDocument> getDocumentConverter() {\n        return documentConverter;\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        var closers = new ArrayList<Closeable>(4);\n        if (this.analyzer != null) {\n            closers.add(this.analyzer);\n        }\n        if (this.searcherManager != null) {\n            closers.add(this.searcherManager);\n        }\n        if (this.directory != null) {\n            closers.add(this.directory);\n        }\n        IOUtils.close(closers);\n        this.analyzer = null;\n        this.searcherManager = null;\n        this.directory = null;\n        log.info(\"Destroyed lucene search engine\");\n    }\n\n    private static class HaloDocumentConverter implements Converter<HaloDocument, Document> {\n\n        @Override\n        @NonNull\n        public Document convert(HaloDocument haloDoc) {\n            var doc = new Document();\n            doc.add(new StringField(\"id\", haloDoc.getId(), YES));\n            doc.add(new StringField(\"name\", haloDoc.getMetadataName(), YES));\n            doc.add(new StringField(\"type\", haloDoc.getType(), YES));\n            doc.add(new StringField(\"ownerName\", haloDoc.getOwnerName(), YES));\n            var categories = haloDoc.getCategories();\n            if (categories != null) {\n                categories.forEach(category -> doc.add(new StringField(\"category\", category, YES)));\n            }\n            var tags = haloDoc.getTags();\n            if (tags != null) {\n                tags.forEach(tag -> doc.add(new StringField(\"tag\", tag, YES)));\n            }\n\n            doc.add(new TextField(\"title\", haloDoc.getTitle(), YES));\n            if (haloDoc.getDescription() != null) {\n                doc.add(new TextField(\"description\", haloDoc.getDescription(), YES));\n            }\n            doc.add(new TextField(\"content\", haloDoc.getContent(), YES));\n            doc.add(new StringField(\"recycled\", Boolean.toString(haloDoc.isRecycled()), YES));\n            doc.add(new StringField(\"exposed\", Boolean.toString(haloDoc.isExposed()), YES));\n            doc.add(new StringField(\"published\", Boolean.toString(haloDoc.isPublished()), YES));\n\n            var annotations = haloDoc.getAnnotations();\n            if (annotations != null) {\n                try (var baos = new ByteArrayOutputStream();\n                     var oos = new ObjectOutputStream(baos)) {\n                    oos.writeObject(annotations);\n                    var type = new FieldType();\n                    type.setStored(true);\n                    type.setTokenized(false);\n                    type.setDocValuesType(DocValuesType.BINARY);\n                    type.freeze();\n                    doc.add(new StoredField(\"annotations\", new BytesRef(baos.toByteArray()), type));\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n\n            var creationTimestamp = haloDoc.getCreationTimestamp();\n            doc.add(new LongField(\"creationTimestamp\", creationTimestamp.toEpochMilli(), YES));\n            var updateTimestamp = haloDoc.getUpdateTimestamp();\n            if (updateTimestamp != null) {\n                doc.add(new LongField(\"updateTimestamp\", updateTimestamp.toEpochMilli(), YES));\n            }\n            doc.add(new StringField(\"permalink\", haloDoc.getPermalink(), YES));\n            return doc;\n        }\n    }\n\n    private static class DocumentConverter implements Converter<Document, HaloDocument> {\n\n        @Override\n        @NonNull\n        public HaloDocument convert(Document doc) {\n            var haloDoc = new HaloDocument();\n            haloDoc.setId(doc.get(\"id\"));\n            haloDoc.setType(doc.get(\"type\"));\n            haloDoc.setMetadataName(doc.get(\"name\"));\n            haloDoc.setTitle(doc.get(\"title\"));\n            haloDoc.setDescription(doc.get(\"description\"));\n            haloDoc.setPermalink(doc.get(\"permalink\"));\n            haloDoc.setOwnerName(doc.get(\"ownerName\"));\n            haloDoc.setCategories(List.of(doc.getValues(\"category\")));\n            haloDoc.setTags(List.of(doc.getValues(\"tag\")));\n\n            haloDoc.setRecycled(getBooleanValue(doc, \"recycled\", false));\n            haloDoc.setPublished(getBooleanValue(doc, \"published\", false));\n            haloDoc.setExposed(getBooleanValue(doc, \"exposed\", false));\n\n            var annotationsBytesRef = doc.getBinaryValue(\"annotations\");\n            if (annotationsBytesRef != null) {\n                try (var bais = new ByteArrayInputStream(annotationsBytesRef.bytes);\n                     var ois = new ObjectInputStream(bais)) {\n                    @SuppressWarnings(\"unchecked\")\n                    var annotations = (Map<String, String>) ois.readObject();\n                    haloDoc.setAnnotations(annotations);\n                } catch (IOException | ClassNotFoundException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n\n            var creationTimestamp = doc.getField(\"creationTimestamp\").numericValue().longValue();\n            haloDoc.setCreationTimestamp(Instant.ofEpochMilli(creationTimestamp));\n            var updateTimestampField = doc.getField(\"updateTimestamp\");\n            if (updateTimestampField != null) {\n                var updateTimestamp = updateTimestampField.numericValue().longValue();\n                haloDoc.setUpdateTimestamp(Instant.ofEpochMilli(updateTimestamp));\n            }\n            // handle content later\n            return haloDoc;\n        }\n\n        private static boolean getBooleanValue(Document doc, String fieldName,\n            boolean defaultValue) {\n            var boolStr = doc.get(fieldName);\n            return boolStr == null ? defaultValue : Boolean.parseBoolean(boolStr);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/post/PostEventsListener.java",
    "content": "package run.halo.app.search.post;\n\nimport static run.halo.app.search.post.PostHaloDocumentsProvider.POST_DOCUMENT_TYPE;\nimport static run.halo.app.search.post.PostHaloDocumentsProvider.convert;\n\nimport java.util.List;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.event.post.PostDeletedEvent;\nimport run.halo.app.event.post.PostUpdatedEvent;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.search.event.HaloDocumentAddRequestEvent;\nimport run.halo.app.search.event.HaloDocumentDeleteRequestEvent;\n\n@Component\npublic class PostEventsListener {\n\n    private final ApplicationEventPublisher publisher;\n\n    private final PostService postService;\n\n    private final ReactiveExtensionClient client;\n\n    public PostEventsListener(\n        ApplicationEventPublisher publisher,\n        PostService postService,\n        ReactiveExtensionClient client) {\n        this.publisher = publisher;\n        this.postService = postService;\n        this.client = client;\n    }\n\n    @EventListener\n    Mono<Void> onApplicationEvent(PostUpdatedEvent event) {\n        return addOrUpdateOrDelete(event.getName());\n    }\n\n    @EventListener\n    void onApplicationEvent(PostDeletedEvent event) {\n        delete(event.getName());\n    }\n\n    private Mono<Void> addOrUpdateOrDelete(String postName) {\n        return client.fetch(Post.class, postName)\n            .flatMap(post -> {\n                if (ExtensionUtil.isDeleted(post)) {\n                    // if the post is deleted permanently, delete it.\n                    return Mono.fromRunnable(() -> delete(postName));\n                }\n                // convert the post into halo document and add it to the search engine.\n                return postService.getReleaseContent(post)\n                    .map(content -> convert(post, content))\n                    .doOnNext(haloDoc -> publisher.publishEvent(\n                        new HaloDocumentAddRequestEvent(this, List.of(haloDoc))\n                    ));\n            })\n            .then();\n    }\n\n    private void delete(String postName) {\n        publisher.publishEvent(\n            new HaloDocumentDeleteRequestEvent(this, List.of(POST_DOCUMENT_TYPE + '-' + postName))\n        );\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/search/post/PostHaloDocumentsProvider.java",
    "content": "package run.halo.app.search.post;\n\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.ReactiveExtensionPaginatedOperator;\nimport run.halo.app.search.HaloDocument;\nimport run.halo.app.search.HaloDocumentsProvider;\n\n@Component\npublic class PostHaloDocumentsProvider implements HaloDocumentsProvider {\n\n    public static final String POST_DOCUMENT_TYPE = \"post.content.halo.run\";\n\n    private final ReactiveExtensionPaginatedOperator paginatedOperator;\n\n    private final PostService postService;\n\n    public PostHaloDocumentsProvider(ReactiveExtensionPaginatedOperator paginatedOperator,\n        PostService postService) {\n        this.paginatedOperator = paginatedOperator;\n        this.postService = postService;\n    }\n\n    @Override\n    public Flux<HaloDocument> fetchAll() {\n        // make sure the posts are published, public visible and not deleted.\n        var options = new ListOptions();\n        var noteDeleted = Queries.isNull(\"metadata.deletionTimestamp\");\n        options.setFieldSelector(FieldSelector.of(noteDeleted));\n        // get content\n        return paginatedOperator.list(Post.class, options)\n            .flatMap(post -> postService.getReleaseContent(post)\n                .switchIfEmpty(Mono.fromSupplier(() -> ContentWrapper.builder()\n                    .content(\"\")\n                    .raw(\"\")\n                    .rawType(\"\")\n                    .build()))\n                .map(contentWrapper -> convert(post, contentWrapper))\n            );\n    }\n\n    @Override\n    public String getType() {\n        return POST_DOCUMENT_TYPE;\n    }\n\n    /**\n     * Converts post to HaloDocument.\n     *\n     * @param post post detail\n     * @param content post content\n     * @return halo document\n     */\n    public static HaloDocument convert(Post post, ContentWrapper content) {\n        var haloDoc = new HaloDocument();\n        var spec = post.getSpec();\n        haloDoc.setMetadataName(post.getMetadata().getName());\n        haloDoc.setType(POST_DOCUMENT_TYPE);\n        haloDoc.setId(POST_DOCUMENT_TYPE + '-' + post.getMetadata().getName());\n        haloDoc.setTitle(spec.getTitle());\n        haloDoc.setDescription(post.getStatus().getExcerpt());\n        haloDoc.setPublished(Post.isPublished(post.getMetadata()));\n        haloDoc.setRecycled(Post.isRecycled(post.getMetadata()));\n        haloDoc.setExposed(Post.isPublic(spec));\n        haloDoc.setContent(content.getContent());\n        haloDoc.setTags(spec.getTags());\n        haloDoc.setCategories(spec.getCategories());\n        haloDoc.setOwnerName(spec.getOwner());\n        haloDoc.setUpdateTimestamp(spec.getPublishTime());\n        haloDoc.setCreationTimestamp(post.getMetadata().getCreationTimestamp());\n        haloDoc.setPermalink(post.getStatus().getPermalink());\n        return haloDoc;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/AuthProviderService.java",
    "content": "package run.halo.app.security;\n\nimport java.util.List;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.AuthProvider;\n\n/**\n * A service for {@link AuthProvider}.\n *\n * @author guqing\n * @since 2.4.0\n */\npublic interface AuthProviderService {\n\n    Mono<AuthProvider> enable(String name);\n\n    Mono<AuthProvider> disable(String name);\n\n    Mono<List<ListedAuthProvider>> listAll();\n\n    /**\n     * Return a list of enabled AuthProviders sorted by priority.\n     */\n    Flux<AuthProvider> getEnabledProviders();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/AuthProviderServiceImpl.java",
    "content": "package run.halo.app.security;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.experimental.Accessors;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.AuthProvider;\nimport run.halo.app.core.extension.UserConnection;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * A default implementation of {@link AuthProviderService}.\n *\n * @author guqing\n * @since 2.4.0\n */\n@Component\n@RequiredArgsConstructor\npublic class AuthProviderServiceImpl implements AuthProviderService {\n    private final ReactiveExtensionClient client;\n    private final ObjectProvider<SystemConfigFetcher> environmentFetcherProvider;\n\n    @Override\n    public Mono<AuthProvider> enable(String name) {\n        return client.get(AuthProvider.class, name)\n            .flatMap(authProvider -> updateAuthProviderEnabled(name, true)\n                .thenReturn(authProvider)\n            );\n    }\n\n    @Override\n    public Mono<AuthProvider> disable(String name) {\n        return client.get(AuthProvider.class, name)\n            // privileged auth provider cannot be disabled\n            .filter(authProvider -> !privileged(authProvider))\n            .flatMap(authProvider -> updateAuthProviderEnabled(name, false)\n                .thenReturn(authProvider)\n            );\n    }\n\n    @Override\n    public Mono<List<ListedAuthProvider>> listAll() {\n        var listOptions = ListOptions.builder()\n            .andQuery(ExtensionUtil.notDeleting())\n            .build();\n        var allProvidersMono =\n            client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())\n                .map(this::convertTo)\n                .collectList()\n                .subscribeOn(Schedulers.boundedElastic());\n\n        var boundProvidersMono = listMyConnections()\n            .map(connection -> connection.getSpec().getRegistrationId())\n            .collect(Collectors.toSet())\n            .subscribeOn(Schedulers.boundedElastic());\n\n        return Mono.zip(allProvidersMono, boundProvidersMono, fetchProviderStates())\n            .map(tuple3 -> {\n                var allProviders = tuple3.getT1();\n                var boundProviderNames = tuple3.getT2();\n                var stateMap = tuple3.getT3().stream()\n                    .collect(Collectors.toMap(SystemSetting.AuthProviderState::getName,\n                        Function.identity()));\n                return allProviders.stream()\n                    .peek(authProvider -> {\n                        authProvider.setIsBound(\n                            boundProviderNames.contains(authProvider.getName()));\n                        authProvider.setEnabled(false);\n                        // set enabled state and priority\n                        var state = stateMap.get(authProvider.getName());\n                        if (state != null) {\n                            authProvider.setEnabled(state.isEnabled());\n                            authProvider.setPriority(state.getPriority());\n                        }\n                    })\n                    .sorted(Comparator.comparingInt(ListedAuthProvider::getPriority)\n                        .thenComparing(ListedAuthProvider::getName))\n                    .toList();\n            });\n    }\n\n    @Override\n    public Flux<AuthProvider> getEnabledProviders() {\n        return fetchProviderStates().flatMapMany(states -> {\n            var namePriorityMap = states.stream()\n                // filter enabled providers\n                .filter(SystemSetting.AuthProviderState::isEnabled)\n                .collect(Collectors.toMap(SystemSetting.AuthProviderState::getName,\n                    SystemSetting.AuthProviderState::getPriority));\n\n            var listOptions = ListOptions.builder()\n                .andQuery(Queries.in(\"metadata.name\", namePriorityMap.keySet()))\n                .andQuery(ExtensionUtil.notDeleting())\n                .build();\n            return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())\n                .map(provider -> new AuthProviderWithPriority()\n                    .setAuthProvider(provider)\n                    .setPriority(namePriorityMap.getOrDefault(\n                        provider.getMetadata().getName(), 0)\n                    )\n                )\n                .sort(AuthProviderWithPriority::compareTo)\n                .map(AuthProviderWithPriority::getAuthProvider);\n        });\n    }\n\n    @Data\n    @Accessors(chain = true)\n    static class AuthProviderWithPriority implements Comparable<AuthProviderWithPriority> {\n        private AuthProvider authProvider;\n        private int priority;\n\n        public String getName() {\n            return authProvider.getMetadata().getName();\n        }\n\n        @Override\n        public int compareTo(@NonNull AuthProviderWithPriority o) {\n            return Comparator.comparingInt(AuthProviderWithPriority::getPriority)\n                .thenComparing(AuthProviderWithPriority::getName)\n                .compare(this, o);\n        }\n    }\n\n    private Mono<List<SystemSetting.AuthProviderState>> fetchProviderStates() {\n        return getSystemConfigMap()\n            .map(AuthProviderServiceImpl::getAuthProviderConfig)\n            .map(SystemSetting.AuthProvider::getStates)\n            .defaultIfEmpty(List.of())\n            .subscribeOn(Schedulers.boundedElastic());\n    }\n\n    Flux<UserConnection> listMyConnections() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(securityContext -> securityContext.getAuthentication().getName())\n            .flatMapMany(username -> {\n                var listOptions = ListOptions.builder()\n                    .andQuery(Queries.equal(\"spec.username\", username))\n                    .andQuery(ExtensionUtil.notDeleting())\n                    .build();\n                return client.listAll(UserConnection.class, listOptions,\n                    ExtensionUtil.defaultSort());\n            });\n    }\n\n    private ListedAuthProvider convertTo(AuthProvider authProvider) {\n        return ListedAuthProvider.builder()\n            .name(authProvider.getMetadata().getName())\n            .displayName(authProvider.getSpec().getDisplayName())\n            .logo(authProvider.getSpec().getLogo())\n            .website(authProvider.getSpec().getWebsite())\n            .description(authProvider.getSpec().getDescription())\n            .authenticationUrl(authProvider.getSpec().getAuthenticationUrl())\n            .helpPage(authProvider.getSpec().getHelpPage())\n            .bindingUrl(authProvider.getSpec().getBindingUrl())\n            .unbindingUrl(authProvider.getSpec().getUnbindUrl())\n            .supportsBinding(supportsBinding(authProvider))\n            .authType(authProvider.getSpec().getAuthType())\n            .isBound(false)\n            .enabled(false)\n            .privileged(privileged(authProvider))\n            .build();\n    }\n\n    private static boolean supportsBinding(AuthProvider authProvider) {\n        return BooleanUtils.TRUE.equals(MetadataUtil.nullSafeLabels(authProvider)\n            .get(AuthProvider.AUTH_BINDING_LABEL));\n    }\n\n    private boolean privileged(AuthProvider authProvider) {\n        return BooleanUtils.TRUE.equals(MetadataUtil.nullSafeLabels(authProvider)\n            .get(AuthProvider.PRIVILEGED_LABEL));\n    }\n\n    @NonNull\n    private static SystemSetting.AuthProvider getAuthProviderConfig(ConfigMap configMap) {\n        if (configMap.getData() == null) {\n            configMap.setData(new HashMap<>());\n        }\n        final Map<String, String> data = configMap.getData();\n        String providerGroup = data.get(SystemSetting.AuthProvider.GROUP);\n\n        SystemSetting.AuthProvider authProvider;\n        if (StringUtils.isBlank(providerGroup)) {\n            authProvider = new SystemSetting.AuthProvider();\n        } else {\n            authProvider =\n                JsonUtils.jsonToObject(providerGroup, SystemSetting.AuthProvider.class);\n        }\n        if (authProvider.getStates() == null) {\n            authProvider.setStates(new ArrayList<>());\n        }\n\n        return authProvider;\n    }\n\n    private Mono<ConfigMap> updateAuthProviderEnabled(String name, boolean enabled) {\n        return Mono.defer(() -> getSystemConfigMap()\n                .flatMap(configMap -> {\n                    var providerConfig = getAuthProviderConfig(configMap);\n                    var stateToFoundOpt = providerConfig.getStates()\n                        .stream()\n                        .filter(state -> state.getName().equals(name))\n                        .findFirst();\n                    if (stateToFoundOpt.isEmpty()) {\n                        var state = new SystemSetting.AuthProviderState()\n                            .setName(name)\n                            .setEnabled(enabled);\n                        providerConfig.getStates().add(state);\n                    } else {\n                        stateToFoundOpt.get().setEnabled(enabled);\n                    }\n\n                    configMap.getData().put(SystemSetting.AuthProvider.GROUP,\n                        JsonUtils.objectToJson(providerConfig));\n\n                    return client.update(configMap);\n                })\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    private Mono<ConfigMap> getSystemConfigMap() {\n        var systemFetcher = environmentFetcherProvider.getIfUnique();\n        if (systemFetcher == null) {\n            return Mono.error(\n                new IllegalStateException(\"No SystemConfigurableEnvironmentFetcher found\"));\n        }\n        return systemFetcher.getConfigMap();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/CorsConfigurer.java",
    "content": "package run.halo.app.security;\n\nimport com.google.common.net.HttpHeaders;\nimport java.util.List;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.reactive.CorsConfigurationSource;\nimport org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.properties.SecurityProperties;\nimport run.halo.app.security.authentication.SecurityConfigurer;\n\n@Component\n@Order(0)\npublic class CorsConfigurer implements SecurityConfigurer {\n\n    private final SecurityProperties.CorsOptions corsOptions;\n\n    public CorsConfigurer(HaloProperties haloProperties) {\n        corsOptions = haloProperties.getSecurity().getCorsOptions();\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        http.cors(spec -> {\n            if (corsOptions.isDisabled()) {\n                spec.disable();\n                return;\n            }\n            spec.configurationSource(apiCorsConfigSource());\n        });\n    }\n\n    CorsConfigurationSource apiCorsConfigSource() {\n        var source = new UrlBasedCorsConfigurationSource();\n        // additional CORS configuration\n        this.corsOptions.getConfigs().forEach(corsConfig -> source.registerCorsConfiguration(\n            corsConfig.getPathPattern(), corsConfig.getConfig().toCorsConfiguration()\n        ));\n\n        // default CORS configuration\n        var configuration = new CorsConfiguration();\n        configuration.setAllowedOriginPatterns(List.of(\"*\"));\n        configuration.setAllowedHeaders(\n            List.of(HttpHeaders.AUTHORIZATION, HttpHeaders.CONTENT_TYPE, HttpHeaders.ACCEPT,\n                \"X-XSRF-TOKEN\", HttpHeaders.COOKIE));\n        configuration.setAllowCredentials(true);\n        configuration.setAllowedMethods(List.of(\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"));\n        source.registerCorsConfiguration(\"/api/**\", configuration);\n        source.registerCorsConfiguration(\"/apis/**\", configuration);\n        return source;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/CsrfConfigurer.java",
    "content": "package run.halo.app.security;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;\n\nimport org.springframework.core.annotation.Order;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;\nimport org.springframework.security.web.server.csrf.CsrfWebFilter;\nimport org.springframework.security.web.server.csrf.XorServerCsrfTokenRequestAttributeHandler;\nimport org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.authentication.SecurityConfigurer;\nimport run.halo.app.security.authentication.pat.PatAuthenticationConverter;\n\n@Component\n@Order(0)\nclass CsrfConfigurer implements SecurityConfigurer {\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var csrfMatcher = new AndServerWebExchangeMatcher(\n            CsrfWebFilter.DEFAULT_CSRF_MATCHER,\n            new NegatedServerWebExchangeMatcher(pathMatchers(\n                \"/api/**\",\n                \"/apis/**\",\n                \"/actuator/**\",\n                \"/system/setup\"\n            )),\n            new NegatedServerWebExchangeMatcher(patAuthMatcher())\n        );\n        http.csrf(csrfSpec -> csrfSpec\n            .csrfTokenRepository(new CookieServerCsrfTokenRepository())\n            .csrfTokenRequestHandler(new XorServerCsrfTokenRequestAttributeHandler())\n            .requireCsrfProtectionMatcher(csrfMatcher));\n    }\n\n    private static ServerWebExchangeMatcher patAuthMatcher() {\n        var patConverter = new PatAuthenticationConverter();\n        return exchange -> patConverter.convert(exchange)\n            .flatMap(a -> ServerWebExchangeMatcher.MatchResult.match())\n            .switchIfEmpty(Mono.defer(ServerWebExchangeMatcher.MatchResult::notMatch));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/DefaultServerAuthenticationEntryPoint.java",
    "content": "package run.halo.app.security;\n\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.web.server.ServerAuthenticationEntryPoint;\nimport org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.utils.HaloUtils;\n\n/**\n * Default authentication entry point.\n * See <a href=\"https://datatracker.ietf.org/doc/html/rfc7235#section-4.1\">\n * https://datatracker.ietf.org/doc/html/rfc7235#section-4.1</a>\n * for more.\n *\n * @author johnniang\n */\npublic class DefaultServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {\n\n    private final ServerWebExchangeMatcher xhrMatcher = exchange -> {\n        if (HaloUtils.isXhr(exchange.getRequest().getHeaders())) {\n            return MatchResult.match();\n        }\n        return MatchResult.notMatch();\n    };\n\n    private final RedirectServerAuthenticationEntryPoint redirectEntryPoint;\n\n    public DefaultServerAuthenticationEntryPoint(ServerRequestCache serverRequestCache) {\n        var entryPoint =\n            new RedirectServerAuthenticationEntryPoint(\"/login?authentication_required\");\n        entryPoint.setRequestCache(serverRequestCache);\n        this.redirectEntryPoint = entryPoint;\n    }\n\n    @Override\n    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {\n        return xhrMatcher.matches(exchange)\n            .filter(MatchResult::isMatch)\n            .switchIfEmpty(\n                Mono.defer(() -> this.redirectEntryPoint.commence(exchange, ex).then(Mono.empty()))\n            )\n            .flatMap(match -> Mono.defer(\n                () -> {\n                    var response = exchange.getResponse();\n                    var wwwAuthenticate = \"FormLogin realm=\\\"console\\\"\";\n                    response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);\n                    response.setStatusCode(HttpStatus.UNAUTHORIZED);\n                    return response.setComplete();\n                }).then(Mono.empty())\n            );\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/DefaultSuperAdminInitializer.java",
    "content": "package run.halo.app.security;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.RoleBinding;\nimport run.halo.app.core.extension.RoleBinding.RoleRef;\nimport run.halo.app.core.extension.RoleBinding.Subject;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.User.UserSpec;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class DefaultSuperAdminInitializer implements SuperAdminInitializer {\n\n    private final ReactiveExtensionClient client;\n    private final PasswordEncoder passwordEncoder;\n\n    @Override\n    public Mono<Void> initialize(InitializationParam param) {\n        return client.fetch(User.class, param.getUsername())\n            .switchIfEmpty(Mono.defer(() -> client.create(\n                    createAdmin(param.getUsername(), param.getPassword(), param.getEmail())))\n                .flatMap(admin -> {\n                    var binding = bindAdminAndSuperRole(admin);\n                    return client.create(binding).thenReturn(admin);\n                })\n            )\n            .then();\n    }\n\n    RoleBinding bindAdminAndSuperRole(User admin) {\n        String adminUserName = admin.getMetadata().getName();\n        var metadata = new Metadata();\n        String name =\n            String.join(\"-\", adminUserName, SUPER_ROLE_NAME, \"binding\");\n        metadata.setName(name);\n        var roleRef = new RoleRef();\n        roleRef.setName(SUPER_ROLE_NAME);\n        roleRef.setApiGroup(Role.GROUP);\n        roleRef.setKind(Role.KIND);\n\n        var subject = new Subject();\n        subject.setName(adminUserName);\n        subject.setApiGroup(admin.groupVersionKind().group());\n        subject.setKind(admin.groupVersionKind().kind());\n\n        var roleBinding = new RoleBinding();\n        roleBinding.setMetadata(metadata);\n        roleBinding.setRoleRef(roleRef);\n        roleBinding.setSubjects(List.of(subject));\n\n        return roleBinding;\n    }\n\n    User createAdmin(String username, String password, String email) {\n        var metadata = new Metadata();\n        metadata.setName(username);\n        metadata.setFinalizers(Set.of(MetadataUtil.SYSTEM_FINALIZER));\n\n        var spec = new UserSpec();\n        spec.setDisplayName(\"Administrator\");\n        spec.setDisabled(false);\n        spec.setRegisteredAt(Instant.now());\n        spec.setTwoFactorAuthEnabled(false);\n        spec.setEmail(email);\n        spec.setPassword(passwordEncoder.encode(password));\n\n        var user = new User();\n        user.setMetadata(metadata);\n        user.setSpec(spec);\n        return user;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/DefaultUserDetailService.java",
    "content": "package run.halo.app.security;\n\nimport static java.util.Objects.requireNonNullElse;\nimport static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME;\nimport static run.halo.app.security.authorization.AuthorityUtils.AUTHENTICATED_ROLE_NAME;\nimport static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX;\n\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.infra.exception.UserNotFoundException;\nimport run.halo.app.security.authentication.login.HaloUser;\nimport run.halo.app.security.authentication.twofactor.TwoFactorUtils;\n\n@Slf4j\npublic class DefaultUserDetailService\n    implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {\n\n    private final UserService userService;\n\n    private final RoleService roleService;\n\n    /**\n     * Indicates whether two-factor authentication is disabled.\n     */\n    @Setter\n    private boolean twoFactorAuthDisabled;\n\n    public DefaultUserDetailService(UserService userService, RoleService roleService) {\n        this.userService = userService;\n        this.roleService = roleService;\n    }\n\n    @Override\n    public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {\n        return userService.updatePassword(user.getUsername(), newPassword)\n            .map(u -> withNewPassword(user, newPassword));\n    }\n\n    @Override\n    public Mono<UserDetails> findByUsername(String username) {\n        var getUser = Mono.defer(() -> {\n            var isEmail = username.contains(\"@\");\n            if (isEmail) {\n                log.debug(\"Try to authenticate by email: {}\", username);\n                return userService.findUserByVerifiedEmail(username);\n            } else {\n                log.debug(\"Try to authenticate by username: {}\", username);\n                return userService.getUser(username);\n            }\n        });\n        return getUser.switchIfEmpty(Mono.error(() -> new UserNotFoundException(username)))\n            .onErrorMap(UserNotFoundException.class,\n                ignored -> new BadCredentialsException(\"Invalid Credentials\"))\n            .flatMap(user -> {\n                var name = user.getMetadata().getName();\n                var userBuilder = User.withUsername(name)\n                    .password(user.getSpec().getPassword())\n                    .disabled(requireNonNullElse(user.getSpec().getDisabled(), false));\n                var setAuthorities = roleService.getRolesByUsername(name)\n                    // every authenticated user should have authenticated and anonymous roles.\n                    .concatWithValues(AUTHENTICATED_ROLE_NAME, ANONYMOUS_ROLE_NAME)\n                    .map(roleName -> new SimpleGrantedAuthority(ROLE_PREFIX + roleName))\n                    .distinct()\n                    .collectList()\n                    .doOnNext(userBuilder::authorities);\n\n                return setAuthorities.then(Mono.fromSupplier(() -> {\n                    var twoFactorAuthSettings = TwoFactorUtils.getTwoFactorAuthSettings(user);\n                    return new HaloUser.Builder(userBuilder.build())\n                        .twoFactorAuthEnabled(\n                            (!twoFactorAuthDisabled) && twoFactorAuthSettings.isAvailable()\n                        )\n                        .totpEncryptedSecret(user.getSpec().getTotpEncryptedSecret())\n                        .build();\n                }));\n            });\n    }\n\n    private UserDetails withNewPassword(UserDetails userDetails, String newPassword) {\n        return User.withUserDetails(userDetails)\n            .password(newPassword)\n            .build();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/ExceptionSecurityConfigurer.java",
    "content": "package run.halo.app.security;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.anyExchange;\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;\n\nimport java.util.ArrayList;\nimport org.springframework.context.MessageSource;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;\nimport org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;\nimport org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;\nimport org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;\nimport org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;\nimport org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.security.authentication.SecurityConfigurer;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthenticationEntryPoint;\n\n@Component\n@Order(0)\npublic class ExceptionSecurityConfigurer implements SecurityConfigurer {\n\n    private final MessageSource messageSource;\n\n    private final ServerResponse.Context context;\n\n    private final ServerRequestCache serverRequestCache;\n\n    public ExceptionSecurityConfigurer(MessageSource messageSource,\n        ServerResponse.Context context,\n        ServerRequestCache serverRequestCache) {\n        this.messageSource = messageSource;\n        this.context = context;\n        this.serverRequestCache = serverRequestCache;\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        http.exceptionHandling(exception -> {\n            var accessDeniedHandlers =\n                new ArrayList<ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry>(\n                    3\n                );\n            accessDeniedHandlers.add(\n                new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(\n                    new AuthenticationConverterServerWebExchangeMatcher(\n                        new ServerBearerTokenAuthenticationConverter()\n                    ),\n                    new BearerTokenServerAccessDeniedHandler()\n                ));\n            accessDeniedHandlers.add(\n                new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(\n                    pathMatchers(HttpMethod.GET, \"/login\", \"/signup\"),\n                    new RedirectAccessDeniedHandler(\"/uc\")\n                ));\n            accessDeniedHandlers.add(\n                new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(\n                    anyExchange(),\n                    new HttpStatusServerAccessDeniedHandler(HttpStatus.FORBIDDEN)\n                )\n            );\n\n            var entryPoints =\n                new ArrayList<DelegatingServerAuthenticationEntryPoint.DelegateEntry>(2);\n            entryPoints.add(new DelegatingServerAuthenticationEntryPoint.DelegateEntry(\n                TwoFactorAuthenticationEntryPoint.MATCHER,\n                new TwoFactorAuthenticationEntryPoint(messageSource, context)\n            ));\n            entryPoints.add(new DelegatingServerAuthenticationEntryPoint.DelegateEntry(\n                anyExchange(),\n                new DefaultServerAuthenticationEntryPoint(serverRequestCache)\n            ));\n\n            exception.authenticationEntryPoint(\n                    new DelegatingServerAuthenticationEntryPoint(entryPoints)\n                )\n                .accessDeniedHandler(\n                    new ServerWebExchangeDelegatingServerAccessDeniedHandler(accessDeniedHandlers)\n                );\n        });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/HaloRedirectAuthenticationSuccessHandler.java",
    "content": "package run.halo.app.security;\n\nimport static run.halo.app.security.HaloServerRequestCache.uriInApplication;\n\nimport java.net.URI;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.server.DefaultServerRedirectStrategy;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;\nimport reactor.core.publisher.Mono;\n\n/**\n * This class is responsible for handling the redirection after a successful authentication.\n * It checks if a valid 'redirect_uri' query parameter is present in the request. If it is,\n * the user is redirected to the specified URI. Otherwise, the user is redirected to a default\n * location.\n *\n * @author johnniang\n */\n@Slf4j\npublic class HaloRedirectAuthenticationSuccessHandler\n    implements ServerAuthenticationSuccessHandler {\n\n    private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();\n\n    private final URI location;\n\n    public HaloRedirectAuthenticationSuccessHandler(String location) {\n        this.location = URI.create(location);\n    }\n\n    @Override\n    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,\n        Authentication authentication) {\n        var request = webFilterExchange.getExchange().getRequest();\n        var redirectUriQuery = request.getQueryParams()\n            .getFirst(\"redirect_uri\");\n        if (redirectUriQuery == null || redirectUriQuery.isBlank()) {\n            return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), location);\n        }\n        var redirectUri = uriInApplication(request, URI.create(redirectUriQuery));\n        if (log.isDebugEnabled()) {\n            log.debug(\n                \"Redirecting to: {} after switching to {}\",\n                redirectUri, authentication.getName()\n            );\n        }\n        return redirectStrategy.sendRedirect(\n            webFilterExchange.getExchange(), URI.create(redirectUri)\n        );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/HaloServerRequestCache.java",
    "content": "package run.halo.app.security;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;\n\nimport java.net.URI;\nimport java.util.Collections;\nimport java.util.Objects;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.server.RequestPath;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;\nimport org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebSession;\nimport reactor.core.publisher.Mono;\n\n/**\n * Halo server request cache implementation for saving redirect URI from query.\n *\n * @author johnniang\n */\npublic class HaloServerRequestCache extends WebSessionServerRequestCache {\n\n    /**\n     * Currently, we have no idea to customize the sessionAttributeName in\n     * WebSessionServerRequestCache, so we have to copy the attr into here.\n     */\n    private static final String DEFAULT_SAVED_REQUEST_ATTR = \"SPRING_SECURITY_SAVED_REQUEST\";\n\n    private static final String REDIRECT_URI_QUERY = \"redirect_uri\";\n\n    private final String sessionAttrName = DEFAULT_SAVED_REQUEST_ATTR;\n\n    public HaloServerRequestCache() {\n        super();\n        setSaveRequestMatcher(createDefaultRequestMatcher());\n    }\n\n    @Override\n    public Mono<Void> saveRequest(ServerWebExchange exchange) {\n        var redirectUriQuery = exchange.getRequest().getQueryParams().getFirst(REDIRECT_URI_QUERY);\n        if (StringUtils.isNotBlank(redirectUriQuery)) {\n            // the query value is decoded, so we don't need to decode it again\n            var redirectUri = URI.create(redirectUriQuery);\n            return saveRedirectUri(exchange, redirectUri);\n        }\n        return super.saveRequest(exchange);\n    }\n\n    @Override\n    public Mono<URI> getRedirectUri(ServerWebExchange exchange) {\n        return super.getRedirectUri(exchange);\n    }\n\n    @Override\n    public Mono<ServerHttpRequest> removeMatchingRequest(ServerWebExchange exchange) {\n        return getRedirectUri(exchange)\n            .flatMap(redirectUri -> {\n                if (redirectUri.getFragment() != null) {\n                    var redirectUriInApplication =\n                        uriInApplication(exchange.getRequest(), redirectUri, false);\n                    var uriInApplication =\n                        uriInApplication(exchange.getRequest(), exchange.getRequest().getURI());\n                    // compare the path and query only\n                    if (!Objects.equals(redirectUriInApplication, uriInApplication)) {\n                        return Mono.empty();\n                    }\n                    // remove the exchange\n                    return exchange.getSession().map(WebSession::getAttributes)\n                        .doOnNext(attributes -> attributes.remove(this.sessionAttrName))\n                        .thenReturn(exchange.getRequest());\n                }\n                return super.removeMatchingRequest(exchange);\n            });\n    }\n\n    private Mono<Void> saveRedirectUri(ServerWebExchange exchange, URI redirectUri) {\n        var redirectUriInApplication = uriInApplication(exchange.getRequest(), redirectUri);\n        return exchange.getSession()\n            .map(WebSession::getAttributes)\n            .doOnNext(attributes -> attributes.put(this.sessionAttrName, redirectUriInApplication))\n            .then();\n    }\n\n    public static String uriInApplication(ServerHttpRequest request, URI uri) {\n        return uriInApplication(request, uri, true);\n    }\n\n    public static String uriInApplication(\n        ServerHttpRequest request, URI uri, boolean appendFragment\n    ) {\n        var path = RequestPath.parse(uri, request.getPath().contextPath().value());\n        var query = uri.getRawQuery();\n        var fragment = uri.getRawFragment();\n        return path.pathWithinApplication().value()\n            + (query == null ? \"\" : \"?\" + query)\n            + (fragment == null || !appendFragment ? \"\" : \"#\" + fragment);\n    }\n\n    private static ServerWebExchangeMatcher createDefaultRequestMatcher() {\n        var get = pathMatchers(HttpMethod.GET, \"/**\");\n        var notFavicon = new NegatedServerWebExchangeMatcher(\n            pathMatchers(\n                \"/favicon.*\", \"/login/**\", \"/signup/**\", \"/password-reset/**\", \"/challenges/**\",\n                \"/oauth2/**\", \"/social/**\"\n            ));\n        var html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);\n        html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));\n        return new AndServerWebExchangeMatcher(get, notFavicon, html);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/HaloUserDetails.java",
    "content": "package run.halo.app.security;\n\nimport org.springframework.security.core.userdetails.UserDetails;\n\npublic interface HaloUserDetails extends UserDetails {\n\n    /**\n     * Checks if two-factor authentication is enabled.\n     *\n     * @return true if two-factor authentication is enabled, false otherwise.\n     */\n    boolean isTwoFactorAuthEnabled();\n\n    /**\n     * Gets the encrypted secret of TOTP.\n     *\n     * @return encrypted secret of TOTP.\n     */\n    String getTotpEncryptedSecret();\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java",
    "content": "package run.halo.app.security;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;\n\nimport java.net.URI;\nimport java.util.Set;\nimport lombok.Getter;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.web.server.DefaultServerRedirectStrategy;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.InitializationStateGetter;\n\n/**\n * A web filter that will redirect user to set up page if system is not initialized.\n *\n * @author guqing\n * @since 2.5.2\n */\n@Component\nclass InitializeRedirectionWebFilter implements WebFilter {\n    private final URI location = URI.create(\"/system/setup\");\n    private final ServerWebExchangeMatcher redirectMatcher;\n\n    private final InitializationStateGetter initializationStateGetter;\n\n    @Getter\n    private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();\n\n    InitializeRedirectionWebFilter(InitializationStateGetter initializationStateGetter) {\n        this.initializationStateGetter = initializationStateGetter;\n\n        var html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);\n        html.setIgnoredMediaTypes(Set.of(MediaType.ALL));\n        this.redirectMatcher = new AndServerWebExchangeMatcher(\n            pathMatchers(HttpMethod.GET, \"/\", \"/console/**\", \"/uc/**\", \"/login\", \"/signup\"),\n            html\n        );\n    }\n\n    @Override\n    @NonNull\n    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {\n        return redirectMatcher.matches(exchange)\n            .flatMap(matched -> {\n                if (!matched.isMatch()) {\n                    return chain.filter(exchange);\n                }\n                return initializationStateGetter.userInitialized()\n                    .defaultIfEmpty(false)\n                    .flatMap(initialized -> {\n                        if (initialized) {\n                            return chain.filter(exchange);\n                        }\n                        // Redirect to set up page if system is not initialized.\n                        return redirectStrategy.sendRedirect(exchange, location);\n                    });\n            });\n    }\n\n    public void setRedirectStrategy(ServerRedirectStrategy redirectStrategy) {\n        Assert.notNull(redirectStrategy, \"redirectStrategy cannot be null\");\n        this.redirectStrategy = redirectStrategy;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/ListedAuthProvider.java",
    "content": "package run.halo.app.security;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Builder;\nimport lombok.Data;\nimport run.halo.app.core.extension.AuthProvider;\n\n/**\n * A listed value object for {@link run.halo.app.core.extension.AuthProvider}.\n *\n * @author guqing\n * @since 2.4.0\n */\n@Data\n@Builder\npublic class ListedAuthProvider {\n    @Schema(requiredMode = REQUIRED)\n    String name;\n\n    @Schema(requiredMode = REQUIRED)\n    String displayName;\n\n    String description;\n\n    String logo;\n\n    String website;\n\n    String authenticationUrl;\n\n    String helpPage;\n\n    String bindingUrl;\n\n    String unbindingUrl;\n\n    AuthProvider.AuthType authType;\n\n    Boolean isBound;\n\n    Boolean enabled;\n\n    int priority;\n\n    Boolean supportsBinding;\n\n    Boolean privileged;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/LoginHandlerEnhancerImpl.java",
    "content": "package run.halo.app.security;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.UserLoginOrLogoutProcessing;\nimport run.halo.app.security.authentication.oauth2.OAuth2LoginHandlerEnhancer;\nimport run.halo.app.security.authentication.rememberme.RememberMeRequestCache;\nimport run.halo.app.security.authentication.rememberme.RememberMeServices;\nimport run.halo.app.security.authentication.rememberme.WebSessionRememberMeRequestCache;\nimport run.halo.app.security.device.DeviceService;\n\n/**\n * A default implementation for {@link LoginHandlerEnhancer} to handle device management and\n * remember me.\n *\n * @author guqing\n * @since 2.17.0\n */\n@Component\n@RequiredArgsConstructor\npublic class LoginHandlerEnhancerImpl implements LoginHandlerEnhancer {\n\n    private final RememberMeServices rememberMeServices;\n\n    private final DeviceService deviceService;\n\n    private final RememberMeRequestCache rememberMeRequestCache =\n        new WebSessionRememberMeRequestCache();\n\n    private final OAuth2LoginHandlerEnhancer oauth2LoginHandlerEnhancer;\n\n    private final UserLoginOrLogoutProcessing userLoginOrLogoutProcessing;\n\n    @Override\n    public Mono<Void> onLoginSuccess(ServerWebExchange exchange,\n        Authentication successfulAuthentication) {\n        return Mono.when(\n            rememberMeServices.loginSuccess(exchange, successfulAuthentication),\n            deviceService.loginSuccess(exchange, successfulAuthentication),\n            rememberMeRequestCache.removeRememberMe(exchange),\n            oauth2LoginHandlerEnhancer.loginSuccess(exchange, successfulAuthentication),\n            userLoginOrLogoutProcessing.loginProcessing(successfulAuthentication.getName())\n        );\n    }\n\n    @Override\n    public Mono<Void> onLoginFailure(ServerWebExchange exchange,\n        AuthenticationException exception) {\n        return rememberMeServices.loginFail(exchange);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/LogoutSecurityConfigurer.java",
    "content": "package run.halo.app.security;\n\nimport static run.halo.app.security.authentication.WebExchangeMatchers.ignoringMediaTypeAll;\n\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.web.server.DefaultServerRedirectStrategy;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;\nimport org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;\nimport org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;\nimport org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.UserLoginOrLogoutProcessing;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.infra.actuator.GlobalInfoService;\nimport run.halo.app.security.authentication.SecurityConfigurer;\nimport run.halo.app.theme.router.ModelConst;\n\n@Component\n@RequiredArgsConstructor\n@Order(0)\nclass LogoutSecurityConfigurer implements SecurityConfigurer {\n\n    private final ApplicationContext applicationContext;\n\n    private final UserLoginOrLogoutProcessing userLoginOrLogoutProcessing;\n\n    private final ServerRequestCache serverRequestCache = new HaloServerRequestCache();\n\n    private final ServerSecurityContextRepository securityContextRepository;\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        http.logout(logout -> logout\n            .logoutHandler(getLogoutHandler())\n            .logoutSuccessHandler(new LogoutSuccessHandler())\n        );\n    }\n\n    private ServerLogoutHandler getLogoutHandler() {\n        var defaultLogoutHandler = new SecurityContextServerLogoutHandler();\n        defaultLogoutHandler.setSecurityContextRepository(securityContextRepository);\n        var logoutHandlers = new ArrayList<ServerLogoutHandler>();\n        logoutHandlers.add(defaultLogoutHandler);\n        applicationContext.getBeanProvider(ServerLogoutHandler.class)\n            .forEach(logoutHandlers::add);\n        if (logoutHandlers.size() == 1) {\n            return logoutHandlers.getFirst();\n        }\n        return new DelegatingServerLogoutHandler(logoutHandlers);\n    }\n\n    @Bean\n    RouterFunction<ServerResponse> logoutPage(\n        UserService userService,\n        GlobalInfoService globalInfoService\n    ) {\n        return RouterFunctions.route()\n            .GET(\"/logout\", request -> {\n                var user = ReactiveSecurityContextHolder.getContext()\n                    .map(SecurityContext::getAuthentication)\n                    .map(Authentication::getName)\n                    .flatMap(userService::getUser);\n                var exchange = request.exchange();\n                var contextPath = exchange.getRequest().getPath().contextPath().value();\n\n                return ServerResponse.ok().render(\"logout\", Map.of(\n                    \"globalInfo\", globalInfoService.getGlobalInfo(),\n                    \"action\", contextPath + \"/logout\",\n                    \"user\", user\n                ));\n            })\n            .before(request -> {\n                request.exchange().getAttributes().put(ModelConst.NO_CACHE, true);\n                return request;\n            })\n            .filter((request, next) ->\n                // Save request before handling the logout\n                serverRequestCache.saveRequest(request.exchange()).then(next.handle(request))\n            )\n            .build();\n    }\n\n\n    private class LogoutSuccessHandler implements ServerLogoutSuccessHandler {\n\n        private final ServerLogoutSuccessHandler defaultHandler;\n\n        public LogoutSuccessHandler() {\n            var redirectHandler = new RequestCacheRedirectLogoutSuccessHandler();\n            redirectHandler.setRequestCache(serverRequestCache);\n            this.defaultHandler = redirectHandler;\n        }\n\n        @Override\n        public Mono<Void> onLogoutSuccess(WebFilterExchange exchange,\n            Authentication authentication) {\n            return userLoginOrLogoutProcessing.logoutProcessing(authentication.getName())\n                .then(ignoringMediaTypeAll(MediaType.APPLICATION_JSON)\n                    .matches(exchange.getExchange())\n                    .filter(ServerWebExchangeMatcher.MatchResult::isMatch)\n                    .switchIfEmpty(Mono.defer(() ->\n                        defaultHandler.onLogoutSuccess(exchange, authentication).then(Mono.empty())\n                    ))\n                    .flatMap(match -> {\n                        var response = exchange.getExchange().getResponse();\n                        response.setStatusCode(HttpStatus.NO_CONTENT);\n                        return response.setComplete();\n                    })\n                );\n        }\n    }\n\n    private static class RequestCacheRedirectLogoutSuccessHandler\n        implements ServerLogoutSuccessHandler {\n\n        private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();\n\n        private URI location = URI.create(\"/login?logout\");\n\n        private ServerRequestCache requestCache = new WebSessionServerRequestCache();\n\n        public RequestCacheRedirectLogoutSuccessHandler() {\n        }\n\n        public RequestCacheRedirectLogoutSuccessHandler(String location) {\n            this.location = URI.create(location);\n        }\n\n        public void setRequestCache(@NonNull ServerRequestCache requestCache) {\n            Assert.notNull(requestCache, \"requestCache cannot be null\");\n            this.requestCache = requestCache;\n        }\n\n        @Override\n        public Mono<Void> onLogoutSuccess(\n            WebFilterExchange exchange, Authentication authentication\n        ) {\n            return this.requestCache.getRedirectUri(exchange.getExchange())\n                .defaultIfEmpty(this.location)\n                .flatMap(location ->\n                    this.redirectStrategy.sendRedirect(exchange.getExchange(), location)\n                );\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/RedirectAccessDeniedHandler.java",
    "content": "package run.halo.app.security;\n\nimport java.net.URI;\nimport org.springframework.security.access.AccessDeniedException;\nimport org.springframework.security.web.server.DefaultServerRedirectStrategy;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * Redirect access denied handler.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class RedirectAccessDeniedHandler implements ServerAccessDeniedHandler {\n\n    private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();\n\n    private final URI redirectUri;\n\n    public RedirectAccessDeniedHandler(String redirectUri) {\n        this.redirectUri = URI.create(redirectUri);\n    }\n\n    @Override\n    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {\n        return redirectStrategy.sendRedirect(exchange, redirectUri);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/SecurityWebFiltersConfigurer.java",
    "content": "package run.halo.app.security;\n\nimport static org.springframework.security.config.web.server.SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION;\nimport static org.springframework.security.config.web.server.SecurityWebFiltersOrder.AUTHENTICATION;\nimport static org.springframework.security.config.web.server.SecurityWebFiltersOrder.FIRST;\nimport static org.springframework.security.config.web.server.SecurityWebFiltersOrder.FORM_LOGIN;\nimport static org.springframework.security.config.web.server.SecurityWebFiltersOrder.HTTP_BASIC;\nimport static org.springframework.security.config.web.server.SecurityWebFiltersOrder.LAST;\nimport static org.springframework.security.config.web.server.SecurityWebFiltersOrder.OAUTH2_AUTHORIZATION_CODE;\n\nimport lombok.Setter;\nimport org.pf4j.ExtensionPoint;\nimport org.springframework.core.annotation.AnnotationAwareOrderComparator;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.web.server.WebFilterChainProxy;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.security.authentication.SecurityConfigurer;\n\n@Component\n// Specific an order here to control the order or security configurer initialization\n@Order(100)\npublic class SecurityWebFiltersConfigurer implements SecurityConfigurer {\n\n    private final ExtensionGetter extensionGetter;\n\n    public SecurityWebFiltersConfigurer(ExtensionGetter extensionGetter) {\n        this.extensionGetter = extensionGetter;\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        http\n            .addFilterAt(\n                new SecurityWebFilterChainProxy(BeforeSecurityWebFilter.class),\n                FIRST\n            )\n            .addFilterAt(\n                new SecurityWebFilterChainProxy(HttpBasicSecurityWebFilter.class),\n                HTTP_BASIC\n            )\n            .addFilterAt(\n                new SecurityWebFilterChainProxy(FormLoginSecurityWebFilter.class),\n                FORM_LOGIN\n            )\n            .addFilterAt(\n                new SecurityWebFilterChainProxy(AuthenticationSecurityWebFilter.class),\n                AUTHENTICATION\n            )\n            .addFilterAt(\n                new SecurityWebFilterChainProxy(AnonymousAuthenticationSecurityWebFilter.class),\n                ANONYMOUS_AUTHENTICATION\n            )\n            .addFilterAt(\n                new SecurityWebFilterChainProxy(OAuth2AuthorizationCodeSecurityWebFilter.class),\n                OAUTH2_AUTHORIZATION_CODE\n            )\n            .addFilterAt(\n                new SecurityWebFilterChainProxy(AfterSecurityWebFilter.class),\n                LAST\n            )\n        ;\n    }\n\n    public class SecurityWebFilterChainProxy implements WebFilter {\n\n        @Setter\n        private WebFilterChainProxy.WebFilterChainDecorator filterChainDecorator;\n\n        private final Class<? extends ExtensionPoint> extensionPointClass;\n\n        public SecurityWebFilterChainProxy(Class<? extends ExtensionPoint> extensionPointClass) {\n            this.extensionPointClass = extensionPointClass;\n            this.filterChainDecorator = new WebFilterChainProxy.DefaultWebFilterChainDecorator();\n        }\n\n        @Override\n        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n            return extensionGetter.getExtensions(this.extensionPointClass)\n                .sort(AnnotationAwareOrderComparator.INSTANCE)\n                .cast(WebFilter.class)\n                .collectList()\n                .map(filters -> filterChainDecorator.decorate(chain, filters))\n                .flatMap(decoratedChain -> decoratedChain.filter(exchange));\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/SuperAdminInitializer.java",
    "content": "package run.halo.app.security;\n\nimport lombok.Builder;\nimport lombok.Data;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\n/**\n * Super admin initializer.\n *\n * @author guqing\n * @since 2.9.0\n */\npublic interface SuperAdminInitializer {\n\n    String SUPER_ROLE_NAME = AuthorityUtils.SUPER_ROLE_NAME;\n\n    /**\n     * Initialize super admin.\n     *\n     * @param param super admin initialization param\n     */\n    Mono<Void> initialize(InitializationParam param);\n\n    @Data\n    @Builder\n    class InitializationParam {\n        private String username;\n        private String password;\n        private String email;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/SecurityConfigurer.java",
    "content": "package run.halo.app.security.authentication;\n\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\n\npublic interface SecurityConfigurer {\n\n    void configure(ServerHttpSecurity http);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/WebExchangeMatchers.java",
    "content": "package run.halo.app.security.authentication;\n\nimport java.util.Set;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\n\npublic enum WebExchangeMatchers {\n    ;\n\n    public static ServerWebExchangeMatcher ignoringMediaTypeAll(MediaType... matchingMediaTypes) {\n        var matcher = new MediaTypeServerWebExchangeMatcher(matchingMediaTypes);\n        matcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));\n        return matcher;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/exception/TooManyRequestsException.java",
    "content": "package run.halo.app.security.authentication.exception;\n\nimport org.springframework.lang.Nullable;\nimport org.springframework.security.core.AuthenticationException;\nimport run.halo.app.infra.exception.RateLimitExceededException;\n\n/**\n * Too many requests exception while authenticating. Because\n * {@link RateLimitExceededException} is not a subclass of\n * {@link AuthenticationException}, we need to create a new exception class to map it.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class TooManyRequestsException extends AuthenticationException {\n\n    public TooManyRequestsException(@Nullable Throwable throwable) {\n        super(\"Too many requests.\", throwable);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/exception/TwoFactorAuthException.java",
    "content": "package run.halo.app.security.authentication.exception;\n\nimport org.springframework.security.core.AuthenticationException;\n\npublic class TwoFactorAuthException extends AuthenticationException {\n\n    public TwoFactorAuthException(String msg, Throwable cause) {\n        super(msg, cause);\n    }\n\n    public TwoFactorAuthException(String msg) {\n        super(msg);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/impl/RsaKeyService.java",
    "content": "package run.halo.app.security.authentication.impl;\n\nimport static com.nimbusds.jose.jwk.KeyOperation.SIGN;\nimport static com.nimbusds.jose.jwk.KeyOperation.VERIFY;\nimport static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWSAlgorithm;\nimport com.nimbusds.jose.jwk.JWK;\nimport com.nimbusds.jose.jwk.KeyUse;\nimport com.nimbusds.jose.jwk.RSAKey;\nimport com.nimbusds.jose.jwk.gen.RSAKeyGenerator;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.InvalidKeyException;\nimport java.security.KeyFactory;\nimport java.security.KeyPair;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.PublicKey;\nimport java.security.interfaces.RSAPublicKey;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.Set;\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.Cipher;\nimport javax.crypto.IllegalBlockSizeException;\nimport javax.crypto.NoSuchPaddingException;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.security.crypto.codec.Hex;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authentication.login.InvalidEncryptedMessageException;\n\n@Slf4j\npublic class RsaKeyService implements CryptoService, InitializingBean {\n\n    public static final String TRANSFORMATION = \"RSA/ECB/PKCS1Padding\";\n\n    public static final String ALGORITHM = \"RSA\";\n\n    private final Path keysRoot;\n\n    private KeyPair keyPair;\n\n    private String keyId;\n\n    private JWK jwk;\n\n    public RsaKeyService(Path dir) {\n        this.keysRoot = dir;\n    }\n\n    @Override\n    public void afterPropertiesSet() throws JOSEException {\n        this.keyPair = this.getRsaKeyPairOrCreate();\n        this.keyId = sha256(keyPair.getPrivate().getEncoded());\n        this.jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())\n            .privateKey(keyPair.getPrivate())\n            .keyUse(KeyUse.SIGNATURE)\n            .keyOperations(Set.of(SIGN, VERIFY))\n            .keyIDFromThumbprint()\n            .algorithm(JWSAlgorithm.RS256)\n            .build();\n    }\n\n    private KeyPair getRsaKeyPairOrCreate() {\n        var privKeyPath = keysRoot.resolve(\"pat_id_rsa\");\n        var pubKeyPath = keysRoot.resolve(\"pat_id_rsa.pub\");\n        try {\n            if (Files.exists(privKeyPath) && Files.exists(pubKeyPath)) {\n                log.debug(\"Skip initializing RSA Keys for PAT due to existence.\");\n\n                var keyFactory = KeyFactory.getInstance(ALGORITHM);\n\n                var privKeyBytes = Files.readAllBytes(privKeyPath);\n                var privKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);\n                var privKey = keyFactory.generatePrivate(privKeySpec);\n\n                var pubKeyBytes = Files.readAllBytes(pubKeyPath);\n                var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);\n                var pubKey = keyFactory.generatePublic(pubKeySpec);\n\n                return new KeyPair(pubKey, privKey);\n            }\n\n            if (Files.notExists(keysRoot)) {\n                Files.createDirectories(keysRoot);\n            }\n            Files.createFile(privKeyPath);\n            Files.createFile(pubKeyPath);\n\n            log.info(\"Generating RSA keys for PAT.\");\n            var rsaKey = new RSAKeyGenerator(4096).generate();\n            var pubKey = rsaKey.toRSAPublicKey();\n            var privKey = rsaKey.toRSAPrivateKey();\n            Files.write(privKeyPath, privKey.getEncoded(), TRUNCATE_EXISTING);\n            Files.write(pubKeyPath, pubKey.getEncoded(), TRUNCATE_EXISTING);\n            log.info(\"Wrote RSA keys for PAT into {} and {}\", privKeyPath, pubKeyPath);\n            return new KeyPair(pubKey, privKey);\n        } catch (JOSEException | IOException\n                 | InvalidKeySpecException | NoSuchAlgorithmException e) {\n            throw new RuntimeException(\"Failed to generate or read RSA key pair\", e);\n        }\n    }\n\n    @Override\n    public Mono<byte[]> decrypt(byte[] encryptedMessage) {\n        return Mono.just(this.keyPair)\n            .map(KeyPair::getPrivate)\n            .flatMap(privateKey -> {\n                try {\n                    var cipher = Cipher.getInstance(TRANSFORMATION);\n                    cipher.init(Cipher.DECRYPT_MODE, privateKey);\n                    return Mono.just(cipher.doFinal(encryptedMessage));\n                } catch (NoSuchAlgorithmException\n                         | NoSuchPaddingException\n                         | InvalidKeyException e) {\n                    return Mono.error(new RuntimeException(\n                        \"Failed to read private key or the key was invalid.\", e\n                    ));\n                } catch (IllegalBlockSizeException | BadPaddingException e) {\n                    return Mono.error(new InvalidEncryptedMessageException(\n                        \"Invalid encrypted message.\"\n                    ));\n                }\n            })\n            .subscribeOn(Schedulers.boundedElastic());\n    }\n\n    @Override\n    public Mono<byte[]> readPublicKey() {\n        return Mono.just(keyPair)\n            .map(KeyPair::getPublic)\n            .map(PublicKey::getEncoded);\n    }\n\n    @Override\n    public String getKeyId() {\n        return this.keyId;\n    }\n\n    @Override\n    public JWK getJwk() {\n        return this.jwk;\n    }\n\n    private static String sha256(byte[] data) {\n        try {\n            var md = MessageDigest.getInstance(\"SHA-256\");\n            return new String(Hex.encode(md.digest(data)));\n        } catch (NoSuchAlgorithmException e) {\n            // should never happen\n            throw new RuntimeException(\"Cannot obtain SHA-256 algorithm for message digest.\", e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/login/HaloUser.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport java.util.Collection;\nimport java.util.Objects;\nimport org.springframework.security.core.CredentialsContainer;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.util.Assert;\nimport run.halo.app.security.HaloUserDetails;\n\npublic class HaloUser implements HaloUserDetails, CredentialsContainer {\n\n    private final UserDetails delegate;\n\n    private final boolean twoFactorAuthEnabled;\n\n    private String totpEncryptedSecret;\n\n    public HaloUser(UserDetails delegate,\n        boolean twoFactorAuthEnabled,\n        String totpEncryptedSecret) {\n        Assert.notNull(delegate, \"Delegate user must not be null\");\n        this.delegate = delegate;\n        this.twoFactorAuthEnabled = twoFactorAuthEnabled;\n        this.totpEncryptedSecret = totpEncryptedSecret;\n    }\n\n    @Override\n    public Collection<? extends GrantedAuthority> getAuthorities() {\n        return delegate.getAuthorities();\n    }\n\n    @Override\n    public String getPassword() {\n        return delegate.getPassword();\n    }\n\n    @Override\n    public String getUsername() {\n        return delegate.getUsername();\n    }\n\n    @Override\n    public boolean isAccountNonExpired() {\n        return delegate.isAccountNonExpired();\n    }\n\n    @Override\n    public boolean isAccountNonLocked() {\n        return delegate.isAccountNonLocked();\n    }\n\n    @Override\n    public boolean isCredentialsNonExpired() {\n        return delegate.isCredentialsNonExpired();\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return delegate.isEnabled();\n    }\n\n    @Override\n    public void eraseCredentials() {\n        if (delegate instanceof CredentialsContainer container) {\n            container.eraseCredentials();\n        }\n        this.totpEncryptedSecret = null;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj instanceof HaloUser user) {\n            return Objects.equals(this.delegate, user.delegate);\n        }\n        return false;\n    }\n\n    @Override\n    public int hashCode() {\n        return this.delegate.hashCode();\n    }\n\n    @Override\n    public boolean isTwoFactorAuthEnabled() {\n        return this.twoFactorAuthEnabled;\n    }\n\n    @Override\n    public String getTotpEncryptedSecret() {\n        return this.totpEncryptedSecret;\n    }\n\n    public static class Builder {\n\n        private final UserDetails user;\n\n        private boolean twoFactorAuthEnabled;\n\n        private String totpEncryptedSecret;\n\n        public Builder(UserDetails user) {\n            this.user = user;\n        }\n\n        public Builder twoFactorAuthEnabled(boolean twoFactorAuthEnabled) {\n            this.twoFactorAuthEnabled = twoFactorAuthEnabled;\n            return this;\n        }\n\n        public Builder totpEncryptedSecret(String totpEncryptedSecret) {\n            this.totpEncryptedSecret = totpEncryptedSecret;\n            return this;\n        }\n\n        public HaloUserDetails build() {\n            return new HaloUser(user, twoFactorAuthEnabled, totpEncryptedSecret);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/login/InvalidEncryptedMessageException.java",
    "content": "package run.halo.app.security.authentication.login;\n\n/**\n * InvalidEncryptedMessageException indicates the encrypted message is invalid.\n *\n * @author johnniang\n */\npublic class InvalidEncryptedMessageException extends RuntimeException {\n\n    public InvalidEncryptedMessageException(String message) {\n        super(message);\n    }\n\n    public InvalidEncryptedMessageException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/login/LoginAuthenticationConverter.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport io.github.resilience4j.ratelimiter.RequestNotPermitted;\nimport io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;\nimport java.util.Base64;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.utils.IpAddressUtils;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authentication.exception.TooManyRequestsException;\n\n@Slf4j\npublic class LoginAuthenticationConverter extends ServerFormLoginAuthenticationConverter {\n\n    private final CryptoService cryptoService;\n\n    private final RateLimiterRegistry rateLimiterRegistry;\n\n    public LoginAuthenticationConverter(CryptoService cryptoService,\n        RateLimiterRegistry rateLimiterRegistry) {\n        this.cryptoService = cryptoService;\n        this.rateLimiterRegistry = rateLimiterRegistry;\n    }\n\n    @Override\n    public Mono<Authentication> convert(ServerWebExchange exchange) {\n        return super.convert(exchange)\n            // validate the password\n            .<Authentication>flatMap(token -> {\n                if (token.getCredentials() == null) {\n                    return Mono.error(new BadCredentialsException(\"Empty credentials.\"));\n                }\n                var credentials = (String) token.getCredentials();\n                byte[] credentialsBytes;\n                try {\n                    credentialsBytes = Base64.getDecoder().decode(credentials);\n                } catch (IllegalArgumentException e) {\n                    // the credentials are not in valid Base64 scheme\n                    return Mono.error(new BadCredentialsException(\"Invalid Base64 scheme.\"));\n                }\n                return cryptoService.decrypt(credentialsBytes)\n                    .onErrorMap(InvalidEncryptedMessageException.class,\n                        error -> new BadCredentialsException(\"Invalid credential.\", error))\n                    .map(decryptedCredentials -> new UsernamePasswordAuthenticationToken(\n                        token.getPrincipal(),\n                        new String(decryptedCredentials, UTF_8)));\n            })\n            .transformDeferred(createIpBasedRateLimiter(exchange))\n            // We have to remap the exception to an AuthenticationException\n            // for using in failure handler\n            .onErrorMap(RequestNotPermitted.class, TooManyRequestsException::new);\n    }\n\n    private <T> RateLimiterOperator<T> createIpBasedRateLimiter(ServerWebExchange exchange) {\n        var clientIp = IpAddressUtils.getClientIp(exchange.getRequest());\n        var rateLimiter = rateLimiterRegistry.rateLimiter(\"authentication-from-ip-\" + clientIp,\n            \"authentication\");\n        if (log.isDebugEnabled()) {\n            var metrics = rateLimiter.getMetrics();\n            log.debug(\n                \"Authentication with Rate Limiter: {}, available permissions: {}, number of \"\n                    + \"waiting threads: {}\",\n                rateLimiter, metrics.getAvailablePermissions(),\n                metrics.getNumberOfWaitingThreads());\n        }\n        return RateLimiterOperator.of(rateLimiter);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/login/LoginSecurityConfigurer.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport io.micrometer.observation.ObservationRegistry;\nimport org.springframework.context.MessageSource;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.authentication.ObservationReactiveAuthenticationManager;\nimport org.springframework.security.authentication.ReactiveAuthenticationManager;\nimport org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;\nimport org.springframework.security.config.web.server.SecurityWebFiltersOrder;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.AuthenticationWebFilter;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.security.HaloUserDetails;\nimport run.halo.app.security.LoginHandlerEnhancer;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authentication.SecurityConfigurer;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;\n\n@Component\n@Order(0)\npublic class LoginSecurityConfigurer implements SecurityConfigurer {\n\n    private final ObservationRegistry observationRegistry;\n\n    private final ReactiveUserDetailsService userDetailsService;\n\n    private final ReactiveUserDetailsPasswordService passwordService;\n\n    private final PasswordEncoder passwordEncoder;\n\n    private final ServerSecurityContextRepository securityContextRepository;\n\n    private final CryptoService cryptoService;\n\n    private final ExtensionGetter extensionGetter;\n    private final ServerResponse.Context context;\n    private final MessageSource messageSource;\n    private final RateLimiterRegistry rateLimiterRegistry;\n\n    private final LoginHandlerEnhancer loginHandlerEnhancer;\n\n    public LoginSecurityConfigurer(ObservationRegistry observationRegistry,\n        ReactiveUserDetailsService userDetailsService,\n        ReactiveUserDetailsPasswordService passwordService, PasswordEncoder passwordEncoder,\n        ServerSecurityContextRepository securityContextRepository, CryptoService cryptoService,\n        ExtensionGetter extensionGetter, ServerResponse.Context context,\n        MessageSource messageSource, RateLimiterRegistry rateLimiterRegistry,\n        LoginHandlerEnhancer loginHandlerEnhancer) {\n        this.observationRegistry = observationRegistry;\n        this.userDetailsService = userDetailsService;\n        this.passwordService = passwordService;\n        this.passwordEncoder = passwordEncoder;\n        this.securityContextRepository = securityContextRepository;\n        this.cryptoService = cryptoService;\n        this.extensionGetter = extensionGetter;\n        this.context = context;\n        this.messageSource = messageSource;\n        this.rateLimiterRegistry = rateLimiterRegistry;\n        this.loginHandlerEnhancer = loginHandlerEnhancer;\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var filter = new AuthenticationWebFilter(authenticationManager()) {\n            @Override\n            protected Mono<Void> onAuthenticationSuccess(Authentication authentication,\n                WebFilterExchange webFilterExchange) {\n                // check if 2FA is enabled after authenticating successfully.\n                if (authentication.getPrincipal() instanceof HaloUserDetails userDetails\n                    && userDetails.isTwoFactorAuthEnabled()) {\n                    authentication = new TwoFactorAuthentication(authentication);\n                }\n                return super.onAuthenticationSuccess(authentication, webFilterExchange);\n            }\n        };\n        var requiresMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, \"/login\");\n        var handler = new UsernamePasswordHandler(context, messageSource, loginHandlerEnhancer);\n        var authConverter = new LoginAuthenticationConverter(cryptoService, rateLimiterRegistry);\n        filter.setRequiresAuthenticationMatcher(requiresMatcher);\n        filter.setAuthenticationFailureHandler(handler);\n        filter.setAuthenticationSuccessHandler(handler);\n        filter.setServerAuthenticationConverter(authConverter);\n        filter.setSecurityContextRepository(securityContextRepository);\n\n        http.addFilterAt(filter, SecurityWebFiltersOrder.FORM_LOGIN);\n    }\n\n    ReactiveAuthenticationManager authenticationManager() {\n        var manager = new UsernamePasswordDelegatingAuthenticationManager(extensionGetter,\n            defaultAuthenticationManager());\n        return new ObservationReactiveAuthenticationManager(observationRegistry, manager);\n    }\n\n    ReactiveAuthenticationManager defaultAuthenticationManager() {\n        var manager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);\n        manager.setPasswordEncoder(passwordEncoder);\n        manager.setUserDetailsPasswordService(passwordService);\n        return manager;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilder.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport java.util.Base64;\nimport lombok.Data;\nimport org.springdoc.core.fn.builders.apiresponse.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.security.authentication.CryptoService;\n\npublic class PublicKeyRouteBuilder {\n\n    private final CryptoService cryptoService;\n\n    public PublicKeyRouteBuilder(CryptoService cryptoService) {\n        this.cryptoService = cryptoService;\n    }\n\n    /**\n     * Builds public key router function.\n     *\n     * @return public key router function.\n     */\n    public RouterFunction<ServerResponse> build() {\n        return SpringdocRouteBuilder.route()\n            .GET(\"/login/public-key\", request -> cryptoService.readPublicKey()\n                    .flatMap(publicKey -> {\n                        var base64Format = Base64.getEncoder().encodeToString(publicKey);\n                        var response = new PublicKeyResponse();\n                        response.setBase64Format(base64Format);\n                        return ServerResponse.ok()\n                            .bodyValue(response);\n                    }),\n                builder -> builder.operationId(\"GetPublicKey\")\n                    .description(\"Read public key for encrypting password.\")\n                    .tag(\"Login\")\n                    .response(Builder.responseBuilder()\n                        .implementation(PublicKeyResponse.class))).build();\n    }\n\n    @Data\n    public static class PublicKeyResponse {\n\n        private String base64Format;\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordDelegatingAuthenticationManager.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.authentication.ReactiveAuthenticationManager;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.AuthenticationException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n@Slf4j\npublic class UsernamePasswordDelegatingAuthenticationManager\n    implements ReactiveAuthenticationManager {\n\n    private final ExtensionGetter extensionGetter;\n\n    private final ReactiveAuthenticationManager defaultAuthenticationManager;\n\n    public UsernamePasswordDelegatingAuthenticationManager(ExtensionGetter extensionGetter,\n        ReactiveAuthenticationManager defaultAuthenticationManager) {\n        this.extensionGetter = extensionGetter;\n        this.defaultAuthenticationManager = defaultAuthenticationManager;\n    }\n\n    @Override\n    public Mono<Authentication> authenticate(Authentication authentication) {\n        return extensionGetter\n            .getEnabledExtensions(UsernamePasswordAuthenticationManager.class)\n            .next()\n            .flatMap(authenticationManager -> authenticationManager.authenticate(authentication)\n                .doOnError(t -> log.error(\n                    \"failed to authenticate with {}, fallback to default username password \"\n                        + \"authentication.\", authenticationManager.getClass(), t)\n                )\n                .onErrorResume(\n                    t -> !(t instanceof AuthenticationException),\n                    t -> Mono.empty()\n                )\n            )\n            .switchIfEmpty(\n                Mono.defer(() -> defaultAuthenticationManager.authenticate(authentication))\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordHandler.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport static org.springframework.http.HttpStatus.UNAUTHORIZED;\nimport static org.springframework.http.MediaType.APPLICATION_JSON;\nimport static run.halo.app.infra.exception.Exceptions.createErrorResponse;\nimport static run.halo.app.security.authentication.WebExchangeMatchers.ignoringMediaTypeAll;\n\nimport java.net.URI;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.MessageSource;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.security.authentication.DisabledException;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.core.CredentialsContainer;\nimport org.springframework.security.web.server.DefaultServerRedirectStrategy;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;\nimport org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;\nimport org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.web.ErrorResponse;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.LoginHandlerEnhancer;\nimport run.halo.app.security.authentication.exception.TooManyRequestsException;\nimport run.halo.app.security.authentication.rememberme.RememberMeRequestCache;\nimport run.halo.app.security.authentication.rememberme.WebSessionRememberMeRequestCache;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;\n\n@Slf4j\npublic class UsernamePasswordHandler implements ServerAuthenticationSuccessHandler,\n    ServerAuthenticationFailureHandler {\n\n    private final ServerResponse.Context context;\n\n    private final MessageSource messageSource;\n\n    private final LoginHandlerEnhancer loginHandlerEnhancer;\n\n    private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();\n\n    @Setter\n    private RememberMeRequestCache rememberMeRequestCache = new WebSessionRememberMeRequestCache();\n\n    private final ServerAuthenticationSuccessHandler defaultSuccessHandler =\n        new RedirectServerAuthenticationSuccessHandler(\"/uc\");\n\n    public UsernamePasswordHandler(ServerResponse.Context context, MessageSource messageSource,\n        LoginHandlerEnhancer loginHandlerEnhancer) {\n        this.context = context;\n        this.messageSource = messageSource;\n        this.loginHandlerEnhancer = loginHandlerEnhancer;\n    }\n\n    @Override\n    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange,\n        AuthenticationException exception) {\n        var exchange = webFilterExchange.getExchange();\n        return loginHandlerEnhancer.onLoginFailure(exchange, exception)\n            .then(ignoringMediaTypeAll(APPLICATION_JSON)\n                .matches(exchange)\n                .filter(ServerWebExchangeMatcher.MatchResult::isMatch)\n                .switchIfEmpty(Mono.defer(\n                    () -> {\n                        var location = URI.create(\"/login?error&method=local\");\n                        if (exception instanceof DisabledException) {\n                            location = URI.create(\"/login?error=account-disabled&method=local\");\n                        }\n                        if (exception instanceof BadCredentialsException) {\n                            location = URI.create(\"/login?error=invalid-credential&method=local\");\n                        }\n                        if (exception instanceof TooManyRequestsException) {\n                            location = URI.create(\"/login?error=rate-limit-exceeded&method=local\");\n                        }\n                        return redirectStrategy.sendRedirect(exchange, location);\n                    }).then(Mono.empty())\n                )\n                .flatMap(matchResult -> handleAuthenticationException(exception, exchange)));\n    }\n\n    @Override\n    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,\n        Authentication authentication) {\n        if (authentication instanceof TwoFactorAuthentication) {\n            return rememberMeRequestCache.saveRememberMe(webFilterExchange.getExchange())\n                // Do not use RedirectServerAuthenticationSuccessHandler to redirect\n                // because it will use request cache to redirect\n                .then(redirectStrategy.sendRedirect(webFilterExchange.getExchange(),\n                    URI.create(\"/challenges/two-factor/totp\"))\n                );\n        }\n\n        if (authentication instanceof CredentialsContainer container) {\n            container.eraseCredentials();\n        }\n\n        ServerWebExchangeMatcher xhrMatcher = exchange -> {\n            if (exchange.getRequest().getHeaders().getOrEmpty(\"X-Requested-With\")\n                .contains(\"XMLHttpRequest\")) {\n                return ServerWebExchangeMatcher.MatchResult.match();\n            }\n            return ServerWebExchangeMatcher.MatchResult.notMatch();\n        };\n\n        var exchange = webFilterExchange.getExchange();\n        return loginHandlerEnhancer.onLoginSuccess(webFilterExchange.getExchange(), authentication)\n            .then(xhrMatcher.matches(exchange)\n                .filter(ServerWebExchangeMatcher.MatchResult::isMatch)\n                .switchIfEmpty(Mono.defer(\n                    () -> defaultSuccessHandler.onAuthenticationSuccess(webFilterExchange,\n                            authentication)\n                        .then(Mono.empty())))\n                .flatMap(isXhr -> ServerResponse.ok()\n                    .bodyValue(authentication.getPrincipal())\n                    .flatMap(response -> response.writeTo(exchange, context))));\n    }\n\n    private Mono<Void> handleAuthenticationException(Throwable exception,\n        ServerWebExchange exchange) {\n        var errorResponse = createErrorResponse(exception, UNAUTHORIZED, exchange, messageSource);\n        return writeErrorResponse(errorResponse, exchange);\n    }\n\n    private Mono<Void> writeErrorResponse(ErrorResponse errorResponse,\n        ServerWebExchange exchange) {\n        return ServerResponse.status(errorResponse.getStatusCode())\n            .contentType(APPLICATION_JSON)\n            .bodyValue(errorResponse.getBody())\n            .flatMap(response -> response.writeTo(exchange, context));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/oauth2/DefaultOAuth2LoginHandlerEnhancer.java",
    "content": "package run.halo.app.security.authentication.oauth2;\n\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.UserConnectionService;\n\n/**\n * Default implementation of {@link OAuth2LoginHandlerEnhancer}.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Slf4j\n@Component\npublic class DefaultOAuth2LoginHandlerEnhancer implements OAuth2LoginHandlerEnhancer {\n\n    private final UserConnectionService connectionService;\n\n    @Setter\n    private OAuth2AuthenticationTokenCache oauth2TokenCache =\n        new WebSessionOAuth2AuthenticationTokenCache();\n\n    private final AuthenticationTrustResolver authenticationTrustResolver =\n        new AuthenticationTrustResolverImpl();\n\n    public DefaultOAuth2LoginHandlerEnhancer(UserConnectionService connectionService) {\n        this.connectionService = connectionService;\n    }\n\n    @Override\n    public Mono<Void> loginSuccess(ServerWebExchange exchange, Authentication authentication) {\n        if (!authenticationTrustResolver.isFullyAuthenticated(authentication)) {\n            // Should never happen\n            // Remove token directly if not fully authenticated\n            return oauth2TokenCache.removeToken(exchange).then();\n        }\n        return oauth2TokenCache.getToken(exchange)\n            .flatMap(oauth2Token -> {\n                var oauth2User = oauth2Token.getPrincipal();\n                var username = authentication.getName();\n                var registrationId = oauth2Token.getAuthorizedClientRegistrationId();\n                return connectionService.updateUserConnectionIfPresent(registrationId, oauth2User)\n                    .doOnNext(connection -> {\n                        if (log.isDebugEnabled()) {\n                            log.debug(\n                                \"User connection already exists, skip creating. connection: [{}]\",\n                                connection\n                            );\n                        }\n                    })\n                    .switchIfEmpty(Mono.defer(() -> connectionService.createUserConnection(\n                        username,\n                        registrationId,\n                        oauth2User\n                    )))\n                    .then(oauth2TokenCache.removeToken(exchange));\n            });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/oauth2/MapOAuth2AuthenticationFilter.java",
    "content": "package run.halo.app.security.authentication.oauth2;\n\nimport static run.halo.app.security.authentication.oauth2.HaloOAuth2AuthenticationToken.authenticated;\n\nimport java.net.URI;\nimport lombok.Setter;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.core.context.SecurityContextImpl;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.web.server.DefaultServerRedirectStrategy;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;\nimport org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.UserConnectionService;\nimport run.halo.app.security.LoginHandlerEnhancer;\n\n/**\n * A filter to map OAuth2 authentication to authenticated user.\n *\n * @author johnniang\n * @since 2.20.0\n */\nclass MapOAuth2AuthenticationFilter implements WebFilter {\n\n    private static final String PRE_AUTHENTICATION =\n        MapOAuth2AuthenticationFilter.class.getName() + \".PRE_AUTHENTICATION\";\n\n    private final UserConnectionService connectionService;\n\n    private final ServerSecurityContextRepository securityContextRepository;\n\n    @Setter\n    private OAuth2AuthenticationTokenCache authenticationCache =\n        new WebSessionOAuth2AuthenticationTokenCache();\n\n    private final ReactiveUserDetailsService userDetailsService;\n\n    private final ServerLogoutHandler logoutHandler;\n\n    private final LoginHandlerEnhancer loginHandlerEnhancer;\n\n    private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();\n\n    @Setter\n    private AuthenticationTrustResolver authenticationTrustResolver\n        = new AuthenticationTrustResolverImpl();\n\n    public MapOAuth2AuthenticationFilter(\n        ServerSecurityContextRepository securityContextRepository,\n        UserConnectionService connectionService,\n        ReactiveUserDetailsService userDetailsService,\n        LoginHandlerEnhancer loginHandlerEnhancer) {\n        this.connectionService = connectionService;\n        this.securityContextRepository = securityContextRepository;\n        this.userDetailsService = userDetailsService;\n        this.loginHandlerEnhancer = loginHandlerEnhancer;\n        var logoutHandler = new SecurityContextServerLogoutHandler();\n        logoutHandler.setSecurityContextRepository(securityContextRepository);\n        this.logoutHandler = logoutHandler;\n    }\n\n    @Override\n    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .filter(authenticationTrustResolver::isAuthenticated)\n            .doOnNext(\n                // cache the pre-authentication\n                authentication -> exchange.getAttributes().put(PRE_AUTHENTICATION, authentication)\n            )\n            .then(chain.filter(exchange))\n            .then(Mono.defer(() -> ReactiveSecurityContextHolder.getContext()\n                .map(SecurityContext::getAuthentication)\n                .filter(OAuth2AuthenticationToken.class::isInstance)\n                .cast(OAuth2AuthenticationToken.class)\n                .flatMap(oauth2Token -> {\n                    var registrationId = oauth2Token.getAuthorizedClientRegistrationId();\n                    var oauth2User = oauth2Token.getPrincipal();\n                    // check the connection\n                    return connectionService.updateUserConnectionIfPresent(\n                            registrationId, oauth2User\n                        )\n                        .switchIfEmpty(Mono.defer(() -> {\n                            var preAuthenticationObject = exchange.getAttribute(PRE_AUTHENTICATION);\n                            if (preAuthenticationObject instanceof Authentication preAuth\n                                && authenticationTrustResolver.isAuthenticated(preAuth)) {\n                                // check the authentication again\n                                // try to bind the user automatically\n                                return connectionService.createUserConnection(\n                                    preAuth.getName(), registrationId, oauth2User\n                                );\n                            }\n                            // save the OAuth2Authentication into session\n                            return authenticationCache.saveToken(exchange, oauth2Token)\n                                .then(Mono.defer(() -> {\n                                    var webFilterExchange = new WebFilterExchange(exchange, chain);\n                                    // clear the security context\n                                    return logoutHandler.logout(webFilterExchange, oauth2Token);\n                                }))\n                                .then(Mono.defer(() -> redirectStrategy.sendRedirect(exchange,\n                                    URI.create(\"/login?oauth2_bind\")\n                                )))\n                                // skip handling\n                                .then(Mono.empty());\n                        }))\n                        // user bound and remap the authentication\n                        .flatMap(connection ->\n                            userDetailsService.findByUsername(connection.getSpec().getUsername())\n                        )\n                        .map(userDetails -> authenticated(userDetails, oauth2Token))\n                        .flatMap(haloOAuth2Token -> {\n                            var securityContext = new SecurityContextImpl(haloOAuth2Token);\n                            return securityContextRepository.save(exchange, securityContext)\n                                .then(\n                                    loginHandlerEnhancer.onLoginSuccess(exchange, haloOAuth2Token)\n                                );\n                            // because this happens after the filter, there is no need to\n                            // write SecurityContext to the context\n                        });\n                })\n                .then())\n            );\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/oauth2/OAuth2AuthenticationTokenCache.java",
    "content": "package run.halo.app.security.authentication.oauth2;\n\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * OAuth2 authentication token cache. Saving OAuth2AuthenticationToken is mainly for further binding\n * to Halo user.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface OAuth2AuthenticationTokenCache {\n\n    /**\n     * Save OAuth2AuthenticationToken into cache.\n     *\n     * @param exchange Server web exchange\n     * @param oauth2Token OAuth2AuthenticationToken\n     * @return empty\n     */\n    Mono<Void> saveToken(ServerWebExchange exchange, OAuth2AuthenticationToken oauth2Token);\n\n    /**\n     * Get OAuth2AuthenticationToken from cache.\n     *\n     * @param exchange Server web exchange\n     * @return an {@link OAuth2AuthenticationToken} if present, empty otherwise\n     */\n    Mono<OAuth2AuthenticationToken> getToken(ServerWebExchange exchange);\n\n    /**\n     * Remove OAuth2AuthenticationToken from cache.\n     *\n     * @param exchange Server web exchange\n     * @return empty\n     */\n    Mono<Void> removeToken(ServerWebExchange exchange);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/oauth2/OAuth2LoginHandlerEnhancer.java",
    "content": "package run.halo.app.security.authentication.oauth2;\n\nimport org.springframework.security.core.Authentication;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * OAuth2 login handler enhancer.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface OAuth2LoginHandlerEnhancer {\n\n    Mono<Void> loginSuccess(ServerWebExchange exchange, Authentication authentication);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/oauth2/OAuth2SecurityConfigurer.java",
    "content": "package run.halo.app.security.authentication.oauth2;\n\nimport org.springframework.core.annotation.Order;\nimport org.springframework.security.config.web.server.SecurityWebFiltersOrder;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.user.service.UserConnectionService;\nimport run.halo.app.security.LoginHandlerEnhancer;\nimport run.halo.app.security.authentication.SecurityConfigurer;\n\n/**\n * OAuth2 security configurer.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\n@Order(0)\nclass OAuth2SecurityConfigurer implements SecurityConfigurer {\n\n    private final ServerSecurityContextRepository securityContextRepository;\n\n    private final UserConnectionService connectionService;\n\n    private final ReactiveUserDetailsService userDetailsService;\n\n    private final LoginHandlerEnhancer loginHandlerEnhancer;\n\n    public OAuth2SecurityConfigurer(ServerSecurityContextRepository securityContextRepository,\n        UserConnectionService connectionService, ReactiveUserDetailsService userDetailsService,\n        LoginHandlerEnhancer loginHandlerEnhancer) {\n        this.securityContextRepository = securityContextRepository;\n        this.connectionService = connectionService;\n        this.userDetailsService = userDetailsService;\n        this.loginHandlerEnhancer = loginHandlerEnhancer;\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var mapOAuth2Filter = new MapOAuth2AuthenticationFilter(\n            securityContextRepository, connectionService, userDetailsService, loginHandlerEnhancer\n        );\n        http.addFilterBefore(mapOAuth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/oauth2/WebSessionOAuth2AuthenticationTokenCache.java",
    "content": "package run.halo.app.security.authentication.oauth2;\n\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * WebSession cache implementation of {@link OAuth2AuthenticationTokenCache}.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class WebSessionOAuth2AuthenticationTokenCache implements OAuth2AuthenticationTokenCache {\n\n    private static final String SESSION_ATTRIBUTE_KEY =\n        OAuth2AuthenticationTokenCache.class + \".OAUTH2_TOKEN\";\n\n    @Override\n    public Mono<Void> saveToken(ServerWebExchange exchange, OAuth2AuthenticationToken oauth2Token) {\n        return exchange.getSession()\n            .doOnNext(session -> {\n                session.getAttributes().put(SESSION_ATTRIBUTE_KEY, oauth2Token);\n            })\n            .then();\n    }\n\n    @Override\n    public Mono<OAuth2AuthenticationToken> getToken(ServerWebExchange exchange) {\n        return exchange.getSession()\n            .mapNotNull(session -> session.getAttribute(SESSION_ATTRIBUTE_KEY))\n            .filter(OAuth2AuthenticationToken.class::isInstance)\n            .cast(OAuth2AuthenticationToken.class);\n    }\n\n    @Override\n    public Mono<Void> removeToken(ServerWebExchange exchange) {\n        return exchange.getSession()\n            .doOnNext(session -> session.getAttributes().remove(SESSION_ATTRIBUTE_KEY))\n            .then();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/pat/PatAuthenticationConverter.java",
    "content": "package run.halo.app.security.authentication.pat;\n\nimport static run.halo.app.security.PersonalAccessToken.PAT_TOKEN_PREFIX;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;\nimport org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * PAT authentication converter.\n *\n * @author johnniang\n * @since 2.20.4\n */\npublic class PatAuthenticationConverter extends ServerBearerTokenAuthenticationConverter {\n\n    @Override\n    public Mono<Authentication> convert(ServerWebExchange exchange) {\n        return super.convert(exchange)\n            .cast(BearerTokenAuthenticationToken.class)\n            .map(BearerTokenAuthenticationToken::getToken)\n            .filter(token -> StringUtils.startsWith(token, PAT_TOKEN_PREFIX))\n            .map(token -> StringUtils.removeStart(token, PAT_TOKEN_PREFIX))\n            .map(BearerTokenAuthenticationToken::new);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/pat/PatAuthenticationManager.java",
    "content": "package run.halo.app.security.authentication.pat;\n\nimport static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSource;\nimport static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME;\nimport static run.halo.app.security.authorization.AuthorityUtils.AUTHENTICATED_ROLE_NAME;\nimport static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX;\n\nimport com.nimbusds.jwt.JWTClaimNames;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.Objects;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.security.authentication.DisabledException;\nimport org.springframework.security.authentication.ReactiveAuthenticationManager;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\nimport org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;\nimport org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.security.PersonalAccessToken;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\nclass PatAuthenticationManager implements ReactiveAuthenticationManager {\n\n    /**\n     * Minimal duration gap of personal access token update.\n     */\n    private static final Duration MIN_UPDATE_GAP = Duration.ofMinutes(1);\n\n    private final JwtReactiveAuthenticationManager delegate;\n\n    private final ReactiveExtensionClient client;\n\n    private final CryptoService cryptoService;\n\n    private Clock clock;\n\n    public PatAuthenticationManager(ReactiveExtensionClient client, CryptoService cryptoService) {\n        this.client = client;\n        this.cryptoService = cryptoService;\n        this.delegate = getDelegate();\n        this.clock = Clock.systemDefaultZone();\n    }\n\n    private JwtReactiveAuthenticationManager getDelegate() {\n        var jwtDecoder = withJwkSource(signedJWT -> Flux.just(cryptoService.getJwk()))\n            .build();\n        return new JwtReactiveAuthenticationManager(jwtDecoder);\n    }\n\n    /**\n     * Set new clock. Only for testing.\n     *\n     * @param clock new clock\n     */\n    void setClock(Clock clock) {\n        this.clock = clock;\n    }\n\n    @Override\n    public Mono<Authentication> authenticate(Authentication authentication) {\n        return delegate.authenticate(authentication)\n            .cast(JwtAuthenticationToken.class)\n            .flatMap(this::checkAndRebuild);\n    }\n\n    private Mono<JwtAuthenticationToken> checkAndRebuild(JwtAuthenticationToken jat) {\n        var jwt = jat.getToken();\n        var patName = jwt.getClaimAsString(\"pat_name\");\n        var jwtId = jwt.getClaimAsString(JWTClaimNames.JWT_ID);\n        if (patName == null || jwtId == null) {\n            // Not a valid PAT\n            return Mono.error(new InvalidBearerTokenException(\"Missing claim pat_name or jti\"));\n        }\n        return client.fetch(PersonalAccessToken.class, patName)\n            .switchIfEmpty(\n                Mono.error(() -> new DisabledException(\"Personal access token has been deleted.\"))\n            )\n            .flatMap(pat -> patChecks(pat, jwtId).and(updateLastUsed(patName)).thenReturn(pat))\n            .map(pat -> {\n                // Make sure the authorities modifiable\n                var authorities = new ArrayList<>(jat.getAuthorities());\n                authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + ANONYMOUS_ROLE_NAME));\n                authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + AUTHENTICATED_ROLE_NAME));\n                var roles = pat.getSpec().getRoles();\n                if (roles != null) {\n                    roles.stream()\n                        .map(role -> AuthorityUtils.ROLE_PREFIX + role)\n                        .map(SimpleGrantedAuthority::new)\n                        .forEach(authorities::add);\n                }\n                return new JwtAuthenticationToken(jat.getToken(), authorities, jat.getName());\n            });\n    }\n\n    private Mono<Void> updateLastUsed(String patName) {\n        // we try our best to update the last used timestamp.\n\n        // the now should be outside the retry cycle because we don't want a fresh timestamp at\n        // every retry.\n        var now = clock.instant();\n        return Mono.defer(\n                // we have to obtain a fresh PAT and retry the update.\n                () -> client.fetch(PersonalAccessToken.class, patName)\n                    .filter(pat -> {\n                        var lastUsed = pat.getSpec().getLastUsed();\n                        if (lastUsed == null) {\n                            return true;\n                        }\n                        var diff = Duration.between(lastUsed, now);\n                        return !diff.minus(MIN_UPDATE_GAP).isNegative();\n                    })\n                    .doOnNext(pat -> pat.getSpec().setLastUsed(now))\n                    .flatMap(client::update)\n            )\n            .retryWhen(Retry.backoff(3, Duration.ofMillis(50))\n                .filter(OptimisticLockingFailureException.class::isInstance))\n            .onErrorComplete()\n            .then();\n    }\n\n    private Mono<Void> patChecks(PersonalAccessToken pat, String tokenId) {\n        if (ExtensionUtil.isDeleted(pat)) {\n            return Mono.error(\n                new InvalidBearerTokenException(\"Personal access token is being deleted.\"));\n        }\n        var spec = pat.getSpec();\n        if (!Objects.equals(spec.getTokenId(), tokenId)) {\n            return Mono.error(new InvalidBearerTokenException(\n                \"Token ID does not match the token ID of personal access token.\"));\n        }\n        if (spec.isRevoked()) {\n            return Mono.error(new InvalidBearerTokenException(\"Token has been revoked.\"));\n        }\n        return Mono.empty();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/pat/PatEndpoint.java",
    "content": "package run.halo.app.security.authentication.pat;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.security.PersonalAccessToken;\n\n@Component\nclass PatEndpoint implements CustomEndpoint {\n\n    private final UserScopedPatHandler patHandler;\n\n    public PatEndpoint(UserScopedPatHandler patHandler) {\n        this.patHandler = patHandler;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = PersonalAccessToken.KIND + \"V1alpha1Uc\";\n        return route().nest(path(\"/personalaccesstokens\"),\n                () -> route()\n                    .POST(patHandler::create,\n                        builder -> builder\n                            .tag(tag)\n                            .operationId(\"GeneratePat\")\n                            .description(\"Generate a PAT.\")\n                            .requestBody(requestBodyBuilder()\n                                .required(true)\n                                .implementation(PersonalAccessToken.class))\n                            .response(responseBuilder().implementation(PersonalAccessToken.class))\n                    )\n                    .GET(patHandler::list,\n                        builder -> builder\n                            .tag(tag)\n                            .operationId(\"ObtainPats\")\n                            .description(\"Obtain PAT list.\")\n                            .response(responseBuilder()\n                                .implementationArray(PersonalAccessToken.class)\n                            )\n                    )\n                    .GET(\"/{name}\", patHandler::get,\n                        builder -> builder\n                            .tag(tag)\n                            .operationId(\"ObtainPat\")\n                            .description(\"Obtain a PAT.\")\n                            .parameter(parameterBuilder()\n                                .in(ParameterIn.PATH)\n                                .required(true)\n                                .name(\"name\")))\n                    .PUT(\"/{name}/actions/revocation\",\n                        patHandler::revoke,\n                        builder -> builder.tag(tag)\n                            .operationId(\"RevokePat\")\n                            .description(\"Revoke a PAT\")\n                            .parameter(parameterBuilder()\n                                .in(ParameterIn.PATH)\n                                .required(true)\n                                .name(\"name\"))\n                    )\n                    .PUT(\"/{name}/actions/restoration\",\n                        patHandler::restore,\n                        builder -> builder.tag(tag)\n                            .operationId(\"RestorePat\")\n                            .description(\"Restore a PAT.\")\n                            .parameter(parameterBuilder()\n                                .in(ParameterIn.PATH)\n                                .required(true)\n                                .name(\"name\")\n                            )\n                    )\n                    .DELETE(\"/{name}\",\n                        patHandler::delete,\n                        builder -> builder.tag(tag)\n                            .operationId(\"DeletePat\")\n                            .description(\"Delete a PAT\")\n                            .parameter(parameterBuilder()\n                                .in(ParameterIn.PATH)\n                                .required(true)\n                                .name(\"name\")\n                            ))\n                    .build()\n            )\n            .build();\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.security.halo.run/v1alpha1\");\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/pat/PatSecurityConfigurer.java",
    "content": "package run.halo.app.security.authentication.pat;\n\nimport org.springframework.core.annotation.Order;\nimport org.springframework.security.config.web.server.SecurityWebFiltersOrder;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;\nimport org.springframework.security.web.server.authentication.AuthenticationWebFilter;\nimport org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authentication.SecurityConfigurer;\n\n/**\n * PAT security configurer.\n *\n * @author johnniang\n * @since 2.20.4\n */\n@Component\n// Specific an order here to control the order or security configurer initialization\n@Order(0)\nclass PatSecurityConfigurer implements SecurityConfigurer {\n\n    private final ReactiveExtensionClient client;\n\n    private final CryptoService cryptoService;\n\n    public PatSecurityConfigurer(ReactiveExtensionClient client, CryptoService cryptoService) {\n        this.client = client;\n        this.cryptoService = cryptoService;\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var filter =\n            new AuthenticationWebFilter(new PatAuthenticationManager(client, cryptoService));\n        filter.setServerAuthenticationConverter(new PatAuthenticationConverter());\n        var entryPoint = new BearerTokenServerAuthenticationEntryPoint();\n        var failureHandler = new ServerAuthenticationEntryPointFailureHandler(entryPoint);\n        filter.setAuthenticationFailureHandler(failureHandler);\n        http.addFilterAt(filter, SecurityWebFiltersOrder.AUTHENTICATION);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/pat/UserScopedPatHandler.java",
    "content": "package run.halo.app.security.authentication.pat;\n\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\n\npublic interface UserScopedPatHandler {\n\n    Mono<ServerResponse> create(ServerRequest request);\n\n    Mono<ServerResponse> list(ServerRequest request);\n\n    Mono<ServerResponse> get(ServerRequest request);\n\n    Mono<ServerResponse> revoke(ServerRequest request);\n\n    Mono<ServerResponse> delete(ServerRequest request);\n\n    Mono<ServerResponse> restore(ServerRequest request);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/pat/UserScopedPatHandlerImpl.java",
    "content": "package run.halo.app.security.authentication.pat;\n\nimport static run.halo.app.extension.Comparators.compareCreationTimestamp;\n\nimport java.util.HashMap;\nimport java.util.Objects;\nimport java.util.function.Predicate;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.PatService;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.exception.AccessDeniedException;\nimport run.halo.app.security.PersonalAccessToken;\n\n@Service\nclass UserScopedPatHandlerImpl implements UserScopedPatHandler {\n\n    private static final String ACCESS_TOKEN_ANNO_NAME = \"security.halo.run/access-token\";\n\n    private final ReactiveExtensionClient client;\n\n    private final PatService patService;\n\n    private final AuthenticationTrustResolver authTrustResolver =\n        new AuthenticationTrustResolverImpl();\n\n    public UserScopedPatHandlerImpl(ReactiveExtensionClient client,\n        PatService patService) {\n        this.client = client;\n        this.patService = patService;\n    }\n\n    private Mono<Authentication> mustBeAuthenticated(Mono<Authentication> authentication) {\n        return authentication.filter(authTrustResolver::isAuthenticated)\n            // Non-username-password authentication could not access the API at any time.\n            .switchIfEmpty(Mono.error(AccessDeniedException::new));\n    }\n\n    @Override\n    public Mono<ServerResponse> create(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .transform(this::mustBeAuthenticated)\n            .flatMap(auth -> request.bodyToMono(PersonalAccessToken.class))\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Missing request body.\")))\n            .flatMap(patService::create)\n            .flatMap(pat -> patService.generateToken(pat)\n                .doOnNext(token -> {\n                    if (pat.getMetadata().getAnnotations() == null) {\n                        pat.getMetadata().setAnnotations(new HashMap<>());\n                    }\n                    pat.getMetadata().getAnnotations()\n                        .put(ACCESS_TOKEN_ANNO_NAME, token);\n                })\n                .thenReturn(pat)\n            )\n            .flatMap(pat -> ServerResponse.ok().bodyValue(pat));\n    }\n\n    @Override\n    public Mono<ServerResponse> list(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .flatMap(auth -> {\n                Predicate<PersonalAccessToken> predicate =\n                    pat -> Objects.equals(auth.getName(), pat.getSpec().getUsername());\n                var pats = client.list(PersonalAccessToken.class, predicate,\n                    compareCreationTimestamp(false));\n                return ServerResponse.ok().body(pats, PersonalAccessToken.class);\n            });\n    }\n\n    @Override\n    public Mono<ServerResponse> get(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .flatMap(auth -> {\n                var name = request.pathVariable(\"name\");\n                return patService.get(name, auth.getName());\n            })\n            .flatMap(pat -> ServerResponse.ok().bodyValue(pat));\n    }\n\n    @Override\n    public Mono<ServerResponse> revoke(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .flatMap(auth -> {\n                var name = request.pathVariable(\"name\");\n                return patService.revoke(name, auth.getName());\n            })\n            .flatMap(revokedPat -> ServerResponse.ok().bodyValue(revokedPat));\n    }\n\n    @Override\n    public Mono<ServerResponse> delete(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .flatMap(auth -> {\n                var name = request.pathVariable(\"name\");\n                return patService.delete(name, auth.getName());\n            })\n            .flatMap(pat -> ServerResponse.ok().bodyValue(pat));\n    }\n\n    @Override\n    public Mono<ServerResponse> restore(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .transform(this::mustBeAuthenticated)\n            .flatMap(auth -> {\n                var name = request.pathVariable(\"name\");\n                return patService.restore(name, auth.getName());\n            })\n            .flatMap(pat -> ServerResponse.ok().bodyValue(pat));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/CookieSignatureKeyResolver.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport reactor.core.publisher.Mono;\n\n@FunctionalInterface\npublic interface CookieSignatureKeyResolver {\n    Mono<String> resolveSigningKey();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/DefaultCookieSignatureKeyResolver.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.authentication.CryptoService;\n\n@Component\n@RequiredArgsConstructor\npublic class DefaultCookieSignatureKeyResolver implements CookieSignatureKeyResolver {\n    private final CryptoService cryptoService;\n\n    @Override\n    public Mono<String> resolveSigningKey() {\n        return Mono.fromSupplier(cryptoService::getKeyId);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/PersistentRememberMeTokenRepository.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport java.time.Instant;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;\nimport reactor.core.publisher.Mono;\n\npublic interface PersistentRememberMeTokenRepository {\n    Mono<Void> createNewToken(PersistentRememberMeToken token);\n\n    Mono<Void> updateToken(String series, String tokenValue, Instant lastUsed);\n\n    Mono<PersistentRememberMeToken> getTokenForSeries(String seriesId);\n\n    Mono<Void> removeUserTokens(String username);\n\n    Mono<Void> removeToken(@NonNull String series);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/PersistentRememberMeTokenRepositoryImpl.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Date;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.RememberMeToken;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.ReactiveExtensionPaginatedOperatorImpl;\n\n/**\n * Extension based persistent remember me token repository implementation.\n *\n * @see RememberMeToken\n */\n@Component\n@RequiredArgsConstructor\npublic class PersistentRememberMeTokenRepositoryImpl\n    implements PersistentRememberMeTokenRepository {\n    private final ReactiveExtensionClient client;\n    private final ReactiveExtensionPaginatedOperatorImpl paginatedOperator;\n\n    @Override\n    public Mono<Void> createNewToken(PersistentRememberMeToken token) {\n        var rememberMeToken = new RememberMeToken();\n        var metadata = new Metadata();\n        rememberMeToken.setMetadata(metadata);\n        metadata.setGenerateName(\"token-\");\n        var creationTime = Instant.ofEpochMilli(token.getDate().getTime());\n        metadata.setCreationTimestamp(creationTime);\n\n        rememberMeToken.setSpec(new RememberMeToken.Spec());\n        rememberMeToken.getSpec()\n            .setUsername(token.getUsername())\n            .setSeries(token.getSeries())\n            .setTokenValue(token.getTokenValue());\n        return client.create(rememberMeToken).then();\n    }\n\n    @Override\n    public Mono<Void> updateToken(String series, String tokenValue, Instant lastUsed) {\n        return Mono.defer(() -> getTokenExtensionForSeries(series)\n                .flatMap(token -> {\n                    token.getSpec().setTokenValue(tokenValue)\n                        .setLastUsed(lastUsed);\n                    return client.update(token);\n                })\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance))\n            .then();\n    }\n\n    @Override\n    public Mono<PersistentRememberMeToken> getTokenForSeries(String seriesId) {\n        return getTokenExtensionForSeries(seriesId)\n            .map(token -> new PersistentRememberMeToken(\n                token.getSpec().getUsername(),\n                token.getSpec().getSeries(),\n                token.getSpec().getTokenValue(),\n                new Date(token.getMetadata().getCreationTimestamp().toEpochMilli())\n            ));\n    }\n\n    @Override\n    public Mono<Void> removeUserTokens(String username) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(equal(\"spec.username\", username)));\n        return paginatedOperator.deleteInitialBatch(RememberMeToken.class, listOptions).then();\n    }\n\n    @Override\n    public Mono<Void> removeToken(@NonNull String series) {\n        return getTokenExtensionForSeries(series)\n            .flatMap(client::delete)\n            .then();\n    }\n\n    private Mono<RememberMeToken> getTokenExtensionForSeries(String seriesId) {\n        var listOptions = ListOptions.builder()\n            .fieldQuery(and(equal(\"spec.series\", seriesId),\n                isNull(\"metadata.deletionTimestamp\")\n            ))\n            .build();\n        return client.listBy(RememberMeToken.class, listOptions, PageRequestImpl.ofSize(1))\n            .flatMap(result -> Mono.justOrEmpty(ListResult.first(result)));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/PersistentTokenBasedRememberMeServices.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport java.security.SecureRandom;\nimport java.time.Instant;\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.Date;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.web.authentication.rememberme.CookieTheftException;\nimport org.springframework.security.web.authentication.rememberme.InvalidCookieException;\nimport org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;\nimport org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;\nimport org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>{@link RememberMeServices} implementation based on Barry Jaspan's <a href=\n * \"https://web.archive.org/web/20180819014446/http://jaspan\n * .com/improved_persistent_login_cookie_best_practice\">Improved\n * Persistent Login Cookie Best Practice</a>.</p>\n * <p>There is a slight modification to the described approach, in that the username is not\n * stored as part of the cookie but obtained from the persistent store via an\n * implementation of {@link PersistentTokenRepository}. The latter should place a unique\n * constraint on the series identifier, so that it is impossible for the same identifier\n * to be allocated to two different users.</p>\n * <p>User management such as changing passwords, removing users and setting user status\n * should be combined with maintenance of the user's persistent tokens.</p>\n * <p>\n * Note that while this class will use the date a token was created to check whether a\n * presented cookie is older than the configured <tt>tokenValiditySeconds</tt> property\n * and deny authentication in this case, it will not delete these tokens from storage. A\n * suitable batch process should be run periodically to remove expired tokens from the\n * database.\n * </p>\n *\n * @author guqing\n * @see\n * <a href=\"https://github.com/spring-projects/spring-security/blob/902aff451f2f4d3f2ce44659bbef3645bf320ece/web/src/main/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServices.java#L61\">PersistentTokenBasedRememberMeServices</a>\n * @since 2.17.0\n */\n@Slf4j\n@Setter\n@Component\npublic class PersistentTokenBasedRememberMeServices extends TokenBasedRememberMeServices\n    implements RememberMeServices {\n\n    public static final String REMEMBER_ME_SERIES_REQUEST_NAME = \"remember-me-series\";\n\n    public static final int DEFAULT_SERIES_LENGTH = 16;\n\n    public static final int DEFAULT_TOKEN_LENGTH = 16;\n\n    private final SecureRandom random;\n\n    private final int seriesLength = DEFAULT_SERIES_LENGTH;\n\n    private final int tokenLength = DEFAULT_TOKEN_LENGTH;\n\n    private final PersistentRememberMeTokenRepository tokenRepository;\n\n    public PersistentTokenBasedRememberMeServices(\n        CookieSignatureKeyResolver cookieSignatureKeyResolver,\n        ReactiveUserDetailsService userDetailsService,\n        RememberMeCookieResolver rememberMeCookieResolver,\n        PersistentRememberMeTokenRepository tokenRepository) {\n        super(cookieSignatureKeyResolver, userDetailsService, rememberMeCookieResolver);\n        this.random = new SecureRandom();\n        this.tokenRepository = tokenRepository;\n    }\n\n    @Override\n    protected Mono<UserDetails> processAutoLoginCookie(String[] cookieTokens,\n        ServerWebExchange exchange) {\n        if (cookieTokens.length != 2) {\n            throw new InvalidCookieException(\n                \"Cookie token did not contain \" + 2 + \" tokens, but contained '\"\n                    + Arrays.asList(cookieTokens) + \"'\");\n        }\n        String presentedSeries = cookieTokens[0];\n        String presentedToken = cookieTokens[1];\n        return this.tokenRepository.getTokenForSeries(presentedSeries)\n            // No series match, so we can't authenticate using this cookie\n            .switchIfEmpty(Mono.error(new RememberMeAuthenticationException(\n                \"No persistent token found for series id: \" + presentedSeries))\n            )\n            .flatMap(token -> {\n                // We have a match for this user/series combination\n                if (!presentedToken.equals(token.getTokenValue())) {\n                    // Token doesn't match series value. Delete all logins for this user and throw\n                    // an exception to warn them.\n                    return this.tokenRepository.removeUserTokens(token.getUsername())\n                        .then(Mono.error(new CookieTheftException(\n                            \"Invalid remember-me token (Series/token) mismatch. Implies previous \"\n                                + \"cookie theft\"\n                                + \" attack.\")));\n                }\n\n                if (isTokenExpired(token)) {\n                    return Mono.error(\n                        new RememberMeAuthenticationException(\"Remember-me login has expired\"));\n                }\n\n                // Token also matches, so login is valid. Update the token value, keeping the\n                // *same* series number.\n                log.debug(\"Refreshing persistent login token for user '{}', series '{}'\",\n                    token.getUsername(), token.getSeries());\n                var newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),\n                    token.getTokenValue(), new Date());\n                return Mono.just(newToken);\n            })\n            .flatMap(newToken -> updateToken(newToken)\n                .doOnSuccess(unused -> addCookie(newToken, exchange))\n                .onErrorMap(ex -> {\n                    log.error(\"Failed to update token: \", ex);\n                    return new RememberMeAuthenticationException(\n                        \"Autologin failed due to data access problem\");\n                })\n                .then(getUserDetailsService().findByUsername(newToken.getUsername()))\n            );\n    }\n\n    private boolean isTokenExpired(PersistentRememberMeToken token) {\n        return isTokenExpired(token.getDate().getTime() + getTokenValidityMillis());\n    }\n\n    private Mono<Void> updateToken(PersistentRememberMeToken newToken) {\n        return this.tokenRepository.updateToken(newToken.getSeries(),\n            newToken.getTokenValue(), dateToInstant(newToken.getDate()));\n    }\n\n    Instant dateToInstant(Date date) {\n        return Instant.ofEpochMilli(date.getTime());\n    }\n\n    /**\n     * Creates a new persistent login token with a new series number, stores the data in\n     * the persistent token repository and adds the corresponding cookie to the response.\n     */\n    @Override\n    protected Mono<Void> onLoginSuccess(ServerWebExchange exchange,\n        Authentication successfulAuthentication) {\n        String username = successfulAuthentication.getName();\n        log.debug(\"Creating new persistent login for user {}\", username);\n        PersistentRememberMeToken persistentToken =\n            new PersistentRememberMeToken(username, generateSeriesData(),\n                generateTokenData(), new Date());\n        return this.tokenRepository.createNewToken(persistentToken)\n            .doOnSuccess(unused -> addCookie(persistentToken, exchange))\n            .onErrorResume(Throwable.class, ex -> {\n                log.error(\"Failed to save persistent token \", ex);\n                return Mono.empty();\n            });\n    }\n\n    @Override\n    protected Mono<Void> onLogout(WebFilterExchange exchange, Authentication authentication) {\n        if (authentication != null) {\n            return this.tokenRepository.removeUserTokens(authentication.getName());\n        }\n        return Mono.empty();\n    }\n\n    private void addCookie(PersistentRememberMeToken token, ServerWebExchange exchange) {\n        setCookie(new String[] {token.getSeries(), token.getTokenValue()}, exchange);\n        exchange.getAttributes().put(REMEMBER_ME_SERIES_REQUEST_NAME, token.getSeries());\n    }\n\n    protected String generateSeriesData() {\n        byte[] newSeries = new byte[this.seriesLength];\n        this.random.nextBytes(newSeries);\n        return new String(Base64.getEncoder().encode(newSeries));\n    }\n\n    protected String generateTokenData() {\n        byte[] newToken = new byte[this.tokenLength];\n        this.random.nextBytes(newToken);\n        return new String(Base64.getEncoder().encode(newToken));\n    }\n\n    private long getTokenValidityMillis() {\n        return rememberMeCookieResolver.getCookieMaxAge().toMillis();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberMeAuthenticationManager.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport lombok.NonNull;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.context.MessageSource;\nimport org.springframework.context.MessageSourceAware;\nimport org.springframework.context.support.MessageSourceAccessor;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.security.authentication.ReactiveAuthenticationManager;\nimport org.springframework.security.authentication.RememberMeAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.SpringSecurityMessageSource;\nimport org.springframework.util.Assert;\nimport reactor.core.publisher.Mono;\n\n@RequiredArgsConstructor\npublic class RememberMeAuthenticationManager implements ReactiveAuthenticationManager,\n    InitializingBean, MessageSourceAware {\n\n    private final CookieSignatureKeyResolver cookieSignatureKeyResolver;\n    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();\n\n    @Override\n    public Mono<Authentication> authenticate(Authentication authentication) {\n        if (authentication instanceof RememberMeAuthenticationToken rememberMeAuthenticationToken) {\n            return doAuthenticate(rememberMeAuthenticationToken);\n        }\n        return Mono.empty();\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        Assert.notNull(this.messages, \"A message source must be set\");\n    }\n\n    @Override\n    public void setMessageSource(@NonNull MessageSource messageSource) {\n        this.messages = new MessageSourceAccessor(messageSource);\n    }\n\n    private Mono<Authentication> doAuthenticate(RememberMeAuthenticationToken token) {\n        return cookieSignatureKeyResolver.resolveSigningKey()\n            .flatMap(key -> {\n                if (key.hashCode() != token.getKeyHash()) {\n                    return Mono.error(new BadCredentialsException(badCredentialMessage()));\n                }\n                return Mono.just(token);\n            });\n    }\n\n    private String badCredentialMessage() {\n        return this.messages.getMessage(\"RememberMeAuthenticationProvider.incorrectKey\",\n            \"The presented RememberMeAuthenticationToken does not contain the expected key\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberMeConfigurer.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.security.config.web.server.SecurityWebFiltersOrder;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.web.server.authentication.AuthenticationWebFilter;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.security.authentication.SecurityConfigurer;\n\n@Component\n@RequiredArgsConstructor\n@Order(0)\npublic class RememberMeConfigurer implements SecurityConfigurer {\n\n    private final RememberMeServices rememberMeServices;\n\n    private final ServerSecurityContextRepository securityContextRepository;\n\n    private final CookieSignatureKeyResolver cookieSignatureKeyResolver;\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var authManager = new RememberMeAuthenticationManager(cookieSignatureKeyResolver);\n        var filter = new AuthenticationWebFilter(authManager);\n        filter.setSecurityContextRepository(securityContextRepository);\n        filter.setAuthenticationFailureHandler(\n            (exchange, exception) -> rememberMeServices.loginFail(exchange.getExchange())\n        );\n        filter.setServerAuthenticationConverter(rememberMeServices::autoLogin);\n        filter.setRequiresAuthenticationMatcher(\n            exchange -> ReactiveSecurityContextHolder.getContext()\n                .flatMap(securityContext -> MatchResult.notMatch())\n                .switchIfEmpty(MatchResult.match())\n        );\n        http.addFilterAt(filter, SecurityWebFiltersOrder.AUTHENTICATION);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberMeCookieResolver.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport java.time.Duration;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ServerWebExchange;\n\npublic interface RememberMeCookieResolver {\n\n    @Nullable\n    HttpCookie resolveRememberMeCookie(ServerWebExchange exchange);\n\n    void setRememberMeCookie(ServerWebExchange exchange, String value);\n\n    void expireCookie(ServerWebExchange exchange);\n\n    String getCookieName();\n\n    Duration getCookieMaxAge();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberMeCookieResolverImpl.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport java.time.Duration;\nimport lombok.Getter;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.http.ResponseCookie;\nimport org.springframework.lang.Nullable;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebExchange;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@Getter\n@Component\npublic class RememberMeCookieResolverImpl implements RememberMeCookieResolver {\n    public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = \"remember-me\";\n\n    private final String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;\n\n    private final Duration cookieMaxAge;\n\n    public RememberMeCookieResolverImpl(HaloProperties haloProperties) {\n        this.cookieMaxAge = haloProperties.getSecurity().getRememberMe().getTokenValidity();\n    }\n\n    @Override\n    @Nullable\n    public HttpCookie resolveRememberMeCookie(ServerWebExchange exchange) {\n        return exchange.getRequest().getCookies().getFirst(getCookieName());\n    }\n\n    @Override\n    public void setRememberMeCookie(ServerWebExchange exchange, String value) {\n        Assert.notNull(value, \"'value' is required\");\n        exchange.getResponse().getCookies()\n            .set(getCookieName(), initCookie(exchange, value).build());\n    }\n\n    @Override\n    public void expireCookie(ServerWebExchange exchange) {\n        ResponseCookie cookie = initCookie(exchange, \"\").maxAge(0).build();\n        exchange.getResponse().getCookies().set(this.cookieName, cookie);\n    }\n\n    private ResponseCookie.ResponseCookieBuilder initCookie(ServerWebExchange exchange,\n        String value) {\n        return ResponseCookie.from(this.cookieName, value)\n            .path(exchange.getRequest().getPath().contextPath().value() + \"/\")\n            .maxAge(getCookieMaxAge())\n            .httpOnly(true)\n            .secure(\"https\".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()))\n            .sameSite(\"Lax\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberMeRequestCache.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * An interface for caching remember-me parameter in request for further handling. Especially\n * useful for two-factor authentication.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface RememberMeRequestCache {\n\n    /**\n     * Save remember-me parameter or form into cache.\n     *\n     * @param exchange exchange\n     * @return empty to return\n     */\n    Mono<Void> saveRememberMe(ServerWebExchange exchange);\n\n    /**\n     * Check if remember-me parameter exists in cache.\n     *\n     * @param exchange exchange\n     * @return true if remember-me exists, false otherwise\n     */\n    Mono<Boolean> isRememberMe(ServerWebExchange exchange);\n\n    /**\n     * Remove remember-me parameter from cache.\n     *\n     * @param exchange exchange\n     * @return empty to return\n     */\n    Mono<Void> removeRememberMe(ServerWebExchange exchange);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberMeServices.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport org.springframework.security.core.Authentication;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\npublic interface RememberMeServices {\n\n    Mono<Authentication> autoLogin(ServerWebExchange exchange);\n\n    Mono<Void> loginFail(ServerWebExchange exchange);\n\n    Mono<Void> loginSuccess(ServerWebExchange exchange, Authentication successfulAuthentication);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberMeTokenRevoker.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport java.time.Duration;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.event.user.PasswordChangedEvent;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * Remember me token revoker.\n * <p>\n * Listen to password changed event and revoke remember me token.\n * </p>\n * Maybe you should consider revoke remember me token when user logout or username changed.\n *\n * @author guqing\n * @since 2.17.0\n */\n@Component\n@RequiredArgsConstructor\npublic class RememberMeTokenRevoker {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private final PersistentRememberMeTokenRepository tokenRepository;\n\n    @Async\n    @EventListener(PasswordChangedEvent.class)\n    public void onPasswordChanged(PasswordChangedEvent event) {\n        tokenRepository.removeUserTokens(event.getUsername())\n            .block(BLOCKING_TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/RememberTokenCleaner.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.extension.index.query.Queries.lessThan;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.RememberMeToken;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.ReactiveExtensionPaginatedOperator;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * A cleaner for remember me tokens that cleans up expired tokens periodically.\n *\n * @author guqing\n * @since 2.17.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class RememberTokenCleaner {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private final ReactiveExtensionPaginatedOperator paginatedOperator;\n    private final RememberMeCookieResolver rememberMeCookieResolver;\n\n    /**\n     * Clean up expired tokens every day at 3:00 AM.\n     */\n    @Scheduled(cron = \"0 0 3 * * ?\")\n    public void cleanUpExpiredTokens() {\n        paginatedOperator.deleteInitialBatch(RememberMeToken.class, getExpiredTokensListOptions())\n            .then().block(BLOCKING_TIMEOUT);\n        log.info(\"Expired remember me tokens have been cleaned up.\");\n    }\n\n    ListOptions getExpiredTokensListOptions() {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            and(isNull(\"metadata.deletionTimestamp\"),\n                lessThan(\"metadata.creationTimestamp\", getExpirationThreshold().toString())\n            )\n        ));\n        return listOptions;\n    }\n\n    protected Instant getExpirationThreshold() {\n        return Instant.now().minus(getTokenValidity());\n    }\n\n    protected Duration getTokenValidity() {\n        return rememberMeCookieResolver.getCookieMaxAge();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/TokenBasedRememberMeServices.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.time.Instant;\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.Date;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.Setter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.authentication.AccountStatusException;\nimport org.springframework.security.authentication.AccountStatusUserDetailsChecker;\nimport org.springframework.security.authentication.RememberMeAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;\nimport org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.core.userdetails.UserDetailsChecker;\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\nimport org.springframework.security.crypto.codec.Hex;\nimport org.springframework.security.crypto.codec.Utf8;\nimport org.springframework.security.web.authentication.rememberme.CookieTheftException;\nimport org.springframework.security.web.authentication.rememberme.InvalidCookieException;\nimport org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>An {@link org.springframework.security.core.userdetails.UserDetailsService} is required\n * by this implementation, so that it can construct a valid <code>Authentication</code>\n * from the returned {@link org.springframework.security.core.userdetails.UserDetails}.</p>\n * <p>This is also necessary so that the user's password is available and can be checked as\n * part of the encoded cookie.</p>\n * <p>The cookie encoded by this implementation adopts the following form:\n * <pre>\n * username + \":\" + expiryTime + \":\" + algorithmName + \":\"\n *   + algorithmHex(username + \":\" + expiryTime + \":\" + password + \":\" + key)\n * </pre>\n * </p>\n *\n * @see org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices\n */\n@Slf4j\n@Setter\n@Getter\n@RequiredArgsConstructor\npublic class TokenBasedRememberMeServices implements ServerLogoutHandler, RememberMeServices {\n\n    public static final String DEFAULT_ALGORITHM = \"SHA-256\";\n\n    private static final String DELIMITER = \":\";\n\n    protected final CookieSignatureKeyResolver cookieSignatureKeyResolver;\n\n    private final ReactiveUserDetailsService userDetailsService;\n\n    protected final RememberMeCookieResolver rememberMeCookieResolver;\n\n    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();\n\n    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();\n\n    private RememberMeRequestCache rememberMeRequestCache = new WebSessionRememberMeRequestCache();\n\n    private static boolean equals(String expected, String actual) {\n        byte[] expectedBytes = bytesUtf8(expected);\n        byte[] actualBytes = bytesUtf8(actual);\n        return MessageDigest.isEqual(expectedBytes, actualBytes);\n    }\n\n    private static byte[] bytesUtf8(String s) {\n        return (s != null) ? Utf8.encode(s) : null;\n    }\n\n    @Override\n    public Mono<Authentication> autoLogin(ServerWebExchange exchange) {\n        var rememberMeCookie = rememberMeCookieResolver.resolveRememberMeCookie(exchange);\n        if (rememberMeCookie == null) {\n            return Mono.empty();\n        }\n        log.debug(\"Remember-me cookie detected\");\n        return Mono.defer(\n                () -> {\n                    String[] cookieTokens = decodeCookie(rememberMeCookie.getValue());\n                    return processAutoLoginCookie(cookieTokens, exchange);\n                })\n            .flatMap(user -> {\n                this.userDetailsChecker.check(user);\n                log.debug(\"Remember-me cookie accepted\");\n                return createSuccessfulAuthentication(exchange, user);\n            })\n            .onErrorResume(ex -> handleError(exchange, ex));\n    }\n\n    private Mono<Authentication> handleError(ServerWebExchange exchange, Throwable ex) {\n        cancelCookie(exchange);\n        if (ex instanceof CookieTheftException) {\n            log.error(\"Cookie theft detected\", ex);\n            return Mono.error(ex);\n        } else if (ex instanceof UsernameNotFoundException) {\n            log.debug(\"Remember-me login was valid but corresponding user not found.\", ex);\n        } else if (ex instanceof InvalidCookieException) {\n            log.debug(\"Invalid remember-me cookie: {}\", ex.getMessage());\n        } else if (ex instanceof AccountStatusException) {\n            log.debug(\"Invalid UserDetails: {}\", ex.getMessage());\n        } else if (ex instanceof RememberMeAuthenticationException) {\n            log.debug(ex.getMessage());\n        }\n        return Mono.empty();\n    }\n\n    protected void cancelCookie(ServerWebExchange exchange) {\n        rememberMeCookieResolver.expireCookie(exchange);\n    }\n\n    protected Mono<UserDetails> processAutoLoginCookie(String[] cookieTokens,\n        ServerWebExchange exchange) {\n        if (!isValidCookieTokensLength(cookieTokens)) {\n            throw new InvalidCookieException(\n                \"Cookie token did not contain 3 or 4 tokens, but contained '\" + Arrays.asList(\n                    cookieTokens) + \"'\");\n        }\n\n        long tokenExpiryTime = getTokenExpiryTime(cookieTokens);\n        if (isTokenExpired(tokenExpiryTime)) {\n            throw new InvalidCookieException(\n                \"Cookie token[1] has expired (expired on '\" + new Date(tokenExpiryTime)\n                    + \"'; current time is '\" + new Date() + \"')\");\n        }\n\n        // Check the user exists. Defer lookup until after expiry time checked, to\n        // possibly avoid expensive database call.\n        return getUserDetailsService().findByUsername(cookieTokens[0])\n            .switchIfEmpty(Mono.error(new UsernameNotFoundException(\"User '\" + cookieTokens[0]\n                + \"' not found\")))\n            .flatMap(userDetails -> {\n                // Check signature of token matches remaining details. Must do this after user\n                // lookup, as we need the DAO-derived password. If efficiency was a major issue,\n                // just add in a UserCache implementation, but recall that this method is usually\n                // only called once per HttpSession - if the token is valid, it will cause\n                // SecurityContextHolder population, whilst if invalid, will cause the cookie to\n                // be cancelled.\n                String actualTokenSignature;\n                String actualAlgorithm = DEFAULT_ALGORITHM;\n                // If the cookie value contains the algorithm, we use that algorithm to check the\n                // signature\n                if (cookieTokens.length == 4) {\n                    actualTokenSignature = cookieTokens[3];\n                    actualAlgorithm = cookieTokens[2];\n                } else {\n                    actualTokenSignature = cookieTokens[2];\n                }\n                return makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),\n                    userDetails.getPassword(), actualAlgorithm)\n                    .doOnNext(expectedTokenSignature -> {\n                        if (!equals(expectedTokenSignature, actualTokenSignature)) {\n                            throw new InvalidCookieException(\n                                \"Cookie contained signature '\" + actualTokenSignature\n                                    + \"' but expected '\"\n                                    + expectedTokenSignature + \"'\");\n                        }\n                    })\n                    .thenReturn(userDetails);\n            });\n    }\n\n    protected boolean isTokenExpired(long tokenExpiryTime) {\n        return tokenExpiryTime < System.currentTimeMillis();\n    }\n\n    private long getTokenExpiryTime(String[] cookieTokens) {\n        try {\n            return Long.parseLong(cookieTokens[1]);\n        } catch (NumberFormatException nfe) {\n            throw new InvalidCookieException(\n                \"Cookie token[1] did not contain a valid number (contained '\" + cookieTokens[1]\n                    + \"')\");\n        }\n    }\n\n    protected Mono<Authentication> createSuccessfulAuthentication(ServerWebExchange exchange,\n        UserDetails user) {\n        return getKey()\n            .map(key -> new RememberMeAuthenticationToken(key, user,\n                this.authoritiesMapper.mapAuthorities(user.getAuthorities()))\n            );\n    }\n\n    private boolean isValidCookieTokensLength(String[] cookieTokens) {\n        return cookieTokens.length == 3 || cookieTokens.length == 4;\n    }\n\n    @Override\n    public Mono<Void> loginFail(ServerWebExchange exchange) {\n        log.debug(\"Interactive login attempt was unsuccessful.\");\n        cancelCookie(exchange);\n        return rememberMeRequestCache.saveRememberMe(exchange);\n    }\n\n    @Override\n    public Mono<Void> loginSuccess(ServerWebExchange exchange,\n        Authentication successfulAuthentication) {\n        return rememberMeRequestCache.isRememberMe(exchange)\n            .filter(Boolean::booleanValue)\n            .switchIfEmpty(Mono.fromRunnable(() -> {\n                log.debug(\"Remember-me login not requested.\");\n            }))\n            .flatMap(rememberMe -> onLoginSuccess(exchange, successfulAuthentication));\n    }\n\n    protected Mono<Void> onLoginSuccess(ServerWebExchange exchange,\n        Authentication successfulAuthentication) {\n        return Mono.defer(() -> retrieveUsernamePassword(successfulAuthentication))\n            .flatMap(pair -> {\n                var username = pair.username();\n                var password = pair.password();\n                var expiryTimeMs = calculateExpireTime(exchange, successfulAuthentication);\n                return makeTokenSignature(expiryTimeMs, username, password, DEFAULT_ALGORITHM)\n                    .doOnNext(signatureValue -> {\n                        setCookie(\n                            new String[] {username, Long.toString(expiryTimeMs), DEFAULT_ALGORITHM,\n                                signatureValue},\n                            exchange);\n                        if (log.isDebugEnabled()) {\n                            log.debug(\"Added remember-me cookie for user '{}', expiry: '{}'\",\n                                username,\n                                new Date(expiryTimeMs));\n                        }\n                    });\n            })\n            .then();\n    }\n\n    private Mono<UsernamePassword> retrieveUsernamePassword(\n        Authentication successfulAuthentication) {\n        return Mono.defer(() -> {\n            String username = retrieveUserName(successfulAuthentication);\n            String password = retrievePassword(successfulAuthentication);\n            // If unable to find a username and password, just abort as\n            // TokenBasedRememberMeServices is\n            // unable to construct a valid token in this case.\n            if (!StringUtils.hasLength(username)) {\n                log.debug(\"Unable to retrieve username\");\n                return Mono.empty();\n            }\n            if (!StringUtils.hasLength(password)) {\n                return getUserDetailsService().findByUsername(username)\n                    .flatMap(user -> {\n                        String existingPassword = user.getPassword();\n                        if (!StringUtils.hasLength(existingPassword)) {\n                            log.debug(\"Unable to obtain password for user: {}\", username);\n                            return Mono.empty();\n                        }\n                        return Mono.just(new UsernamePassword(username, existingPassword));\n                    });\n            }\n            return Mono.just(new UsernamePassword(username, password));\n        });\n    }\n\n    void setCookie(String[] cookieTokens, ServerWebExchange exchange) {\n        String cookieValue = encodeCookie(cookieTokens);\n        rememberMeCookieResolver.setRememberMeCookie(exchange, cookieValue);\n    }\n\n    protected long calculateExpireTime(ServerWebExchange exchange,\n        Authentication authentication) {\n        var tokenLifetime = rememberMeCookieResolver.getCookieMaxAge().toSeconds();\n        return Instant.now().plusSeconds(tokenLifetime).toEpochMilli();\n    }\n\n    protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {\n        int paddingCount = 4 - (cookieValue.length() % 4);\n        if (paddingCount < 4) {\n            char[] padding = new char[paddingCount];\n            Arrays.fill(padding, '=');\n            cookieValue += new String(padding);\n        }\n        String cookieAsPlainText;\n        try {\n            cookieAsPlainText = new String(Base64.getDecoder().decode(cookieValue.getBytes()));\n        } catch (IllegalArgumentException ex) {\n            throw new InvalidCookieException(\n                \"Cookie token was not Base64 encoded; value was '\" + cookieValue + \"'\");\n        }\n        String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);\n        for (int i = 0; i < tokens.length; i++) {\n            tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8);\n        }\n        return tokens;\n    }\n\n    /**\n     * Inverse operation of decodeCookie.\n     *\n     * @param cookieTokens the tokens to be encoded.\n     * @return base64 encoding of the tokens concatenated with the \":\" delimiter.\n     */\n    protected String encodeCookie(String[] cookieTokens) {\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < cookieTokens.length; i++) {\n            sb.append(URLEncoder.encode(cookieTokens[i], StandardCharsets.UTF_8));\n            if (i < cookieTokens.length - 1) {\n                sb.append(DELIMITER);\n            }\n        }\n        String value = sb.toString();\n        sb = new StringBuilder(new String(Base64.getEncoder().encode(value.getBytes())));\n        while (sb.charAt(sb.length() - 1) == '=') {\n            sb.deleteCharAt(sb.length() - 1);\n        }\n        return sb.toString();\n    }\n\n    protected Mono<String> makeTokenSignature(long tokenExpiryTime, String username,\n        String password, String algorithm) {\n        return getKey()\n            .handle((key, sink) -> {\n                String data = username + \":\" + tokenExpiryTime + \":\" + password + \":\" + key;\n                try {\n                    MessageDigest digest = MessageDigest.getInstance(algorithm);\n                    sink.next(new String(Hex.encode(digest.digest(data.getBytes()))));\n                } catch (NoSuchAlgorithmException ex) {\n                    sink.error(\n                        new IllegalStateException(\"No \" + algorithm + \" algorithm available!\"));\n                }\n            });\n    }\n\n    protected String retrieveUserName(Authentication authentication) {\n        if (isInstanceOfUserDetails(authentication)) {\n            return ((UserDetails) authentication.getPrincipal()).getUsername();\n        }\n        return authentication.getPrincipal().toString();\n    }\n\n    protected String retrievePassword(Authentication authentication) {\n        if (isInstanceOfUserDetails(authentication)) {\n            return ((UserDetails) authentication.getPrincipal()).getPassword();\n        }\n        if (authentication.getCredentials() != null) {\n            return authentication.getCredentials().toString();\n        }\n        return null;\n    }\n\n    private boolean isInstanceOfUserDetails(Authentication authentication) {\n        return authentication.getPrincipal() instanceof UserDetails;\n    }\n\n    protected Mono<String> getKey() {\n        return cookieSignatureKeyResolver.resolveSigningKey();\n    }\n\n    @Override\n    public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {\n        if (log.isDebugEnabled()) {\n            log.debug(\"Logout of user {}\", (authentication != null) ? authentication.getName()\n                : \"Unknown\");\n        }\n        return loginFail(exchange.getExchange()).then(onLogout(exchange, authentication));\n    }\n\n    protected Mono<Void> onLogout(WebFilterExchange exchange, Authentication authentication) {\n        return Mono.empty();\n    }\n\n    record UsernamePassword(String username, String password) {\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/rememberme/WebSessionRememberMeRequestCache.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebSession;\nimport reactor.core.publisher.Mono;\n\n/**\n * An implementation of {@link RememberMeRequestCache} that stores remember-me parameter in\n * {@link WebSession}.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class WebSessionRememberMeRequestCache implements RememberMeRequestCache {\n\n    private static final String SESSION_ATTRIBUTE_NAME =\n        RememberMeRequestCache.class + \".REMEMBER_ME\";\n\n    private static final String DEFAULT_PARAMETER = \"remember-me\";\n\n    @Override\n    public Mono<Void> saveRememberMe(ServerWebExchange exchange) {\n        return resolveFromQuery(exchange)\n            .switchIfEmpty(resolveFromForm(exchange))\n            .flatMap(rememberMe -> exchange.getSession().doOnNext(\n                session -> session.getAttributes().put(SESSION_ATTRIBUTE_NAME, rememberMe))\n            )\n            .then();\n    }\n\n    @Override\n    public Mono<Boolean> isRememberMe(ServerWebExchange exchange) {\n        return resolveFromQuery(exchange)\n            .switchIfEmpty(resolveFromForm(exchange))\n            .switchIfEmpty(resolveFromSession(exchange))\n            .defaultIfEmpty(false);\n    }\n\n    @Override\n    public Mono<Void> removeRememberMe(ServerWebExchange exchange) {\n        return exchange.getSession()\n            .doOnNext(session -> session.getAttributes().remove(SESSION_ATTRIBUTE_NAME))\n            .then();\n    }\n\n    private Mono<Boolean> resolveFromQuery(ServerWebExchange exchange) {\n        return Mono.justOrEmpty(exchange.getRequest().getQueryParams().getFirst(DEFAULT_PARAMETER))\n            .map(Boolean::parseBoolean);\n    }\n\n    private Mono<Boolean> resolveFromForm(ServerWebExchange exchange) {\n        return exchange.getFormData()\n            .mapNotNull(form -> form.getFirst(DEFAULT_PARAMETER))\n            .map(Boolean::parseBoolean);\n    }\n\n    private Mono<Boolean> resolveFromSession(ServerWebExchange exchange) {\n        return exchange.getSession()\n            .mapNotNull(session -> session.getAttribute(SESSION_ATTRIBUTE_NAME))\n            .filter(Boolean.class::isInstance)\n            .cast(Boolean.class);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TotpAuthenticationSuccessHandler.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;\nimport org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.LoginHandlerEnhancer;\n\n@Slf4j\npublic class TotpAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {\n\n    private final LoginHandlerEnhancer loginEnhancer;\n\n    private final ServerAuthenticationSuccessHandler successHandler;\n\n    public TotpAuthenticationSuccessHandler(LoginHandlerEnhancer loginEnhancer,\n        ServerRequestCache serverRequestCache) {\n        this.loginEnhancer = loginEnhancer;\n        var successHandler = new RedirectServerAuthenticationSuccessHandler(\"/uc\");\n        successHandler.setRequestCache(serverRequestCache);\n        this.successHandler = successHandler;\n    }\n\n    @Override\n    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,\n        Authentication authentication) {\n        return loginEnhancer.onLoginSuccess(webFilterExchange.getExchange(), authentication)\n            .then(successHandler.onAuthenticationSuccess(webFilterExchange, authentication));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthEndpoint.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\n\nimport jakarta.validation.constraints.NotBlank;\nimport jakarta.validation.constraints.NotNull;\nimport java.net.URI;\nimport lombok.Data;\nimport org.springframework.security.authentication.AnonymousAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.stereotype.Component;\nimport org.springframework.validation.Validator;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.ValidationUtils;\nimport run.halo.app.infra.exception.AccessDeniedException;\nimport run.halo.app.infra.exception.RequestBodyValidationException;\nimport run.halo.app.security.authentication.twofactor.totp.TotpAuthService;\n\n@Component\npublic class TwoFactorAuthEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n\n    private final UserService userService;\n\n    private final TotpAuthService totpAuthService;\n\n    private final Validator validator;\n\n    private final PasswordEncoder passwordEncoder;\n\n    private final ExternalUrlSupplier externalUrl;\n\n    public TwoFactorAuthEndpoint(ReactiveExtensionClient client,\n        UserService userService,\n        TotpAuthService totpAuthService,\n        Validator validator,\n        PasswordEncoder passwordEncoder,\n        ExternalUrlSupplier externalUrl) {\n        this.client = client;\n        this.userService = userService;\n        this.totpAuthService = totpAuthService;\n        this.validator = validator;\n        this.passwordEncoder = passwordEncoder;\n        this.externalUrl = externalUrl;\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"TwoFactorAuthV1alpha1Uc\";\n        return route().nest(path(\"/authentications/two-factor\"),\n            () -> route()\n                .GET(\"/settings\", this::getTwoFactorSettings,\n                    builder -> builder.operationId(\"GetTwoFactorAuthenticationSettings\")\n                        .tag(tag)\n                        .description(\"Get Two-factor authentication settings.\")\n                        .response(responseBuilder().implementation(TwoFactorAuthSettings.class)))\n                .PUT(\"/settings/enabled\", this::enableTwoFactor,\n                    builder -> builder.operationId(\"EnableTwoFactor\")\n                        .tag(tag)\n                        .description(\"Enable Two-factor authentication\")\n                        .requestBody(requestBodyBuilder().implementation(PasswordRequest.class))\n                        .response(responseBuilder().implementation(TwoFactorAuthSettings.class)))\n                .PUT(\"/settings/disabled\", this::disableTwoFactor,\n                    builder -> builder.operationId(\"DisableTwoFactor\")\n                        .tag(tag)\n                        .description(\"Disable Two-factor authentication\")\n                        .requestBody(requestBodyBuilder().implementation(PasswordRequest.class))\n                        .response(responseBuilder().implementation(TwoFactorAuthSettings.class)))\n                .POST(\"/totp\", this::configureTotp,\n                    builder -> builder.operationId(\"ConfigurerTotp\")\n                        .tag(tag)\n                        .description(\"Configure a TOTP\")\n                        .requestBody(requestBodyBuilder().implementation(TotpRequest.class))\n                        .response(responseBuilder().implementation(TwoFactorAuthSettings.class)))\n                .DELETE(\"/totp/-\", this::deleteTotp,\n                    builder -> builder.operationId(\"DeleteTotp\")\n                        .tag(tag)\n                        .requestBody(requestBodyBuilder().implementation(PasswordRequest.class))\n                        .response(responseBuilder().implementation(TwoFactorAuthSettings.class)))\n                .GET(\"/totp/auth-link\", this::getTotpAuthLink,\n                    builder -> builder.operationId(\"GetTotpAuthLink\")\n                        .tag(tag)\n                        .description(\"Get TOTP auth link, including secret\")\n                        .response(responseBuilder().implementation(TotpAuthLinkResponse.class)))\n                .build()\n        ).build();\n    }\n\n    private Mono<ServerResponse> deleteTotp(ServerRequest request) {\n        var totpDeleteRequestMono = request.bodyToMono(PasswordRequest.class)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Request body required\")))\n            .doOnNext(passwordRequest -> this.validateRequest(passwordRequest, \"passwordRequest\",\n                request)\n            );\n\n        var twoFactorAuthSettings =\n            totpDeleteRequestMono.flatMap(passwordRequest -> getCurrentUser()\n                .filter(user -> {\n                    var rawPassword = passwordRequest.getPassword();\n                    var encodedPassword = user.getSpec().getPassword();\n                    return this.passwordEncoder.matches(rawPassword, encodedPassword);\n                })\n                .switchIfEmpty(\n                    Mono.error(() -> new ServerWebInputException(\"Invalid password\")))\n                .doOnNext(user -> {\n                    var spec = user.getSpec();\n                    spec.setTotpEncryptedSecret(null);\n                })\n                .flatMap(client::update)\n                .map(TwoFactorUtils::getTwoFactorAuthSettings));\n        return ServerResponse.ok().body(twoFactorAuthSettings, TwoFactorAuthSettings.class);\n    }\n\n    @Data\n    public static class PasswordRequest {\n\n        @NotBlank\n        private String password;\n\n    }\n\n    private Mono<ServerResponse> disableTwoFactor(ServerRequest request) {\n        return toggleTwoFactor(request, false);\n    }\n\n    private Mono<ServerResponse> enableTwoFactor(ServerRequest request) {\n        return toggleTwoFactor(request, true);\n    }\n\n    private Mono<ServerResponse> toggleTwoFactor(ServerRequest request, boolean enabled) {\n        return request.bodyToMono(PasswordRequest.class)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Request body required\")))\n            .doOnNext(passwordRequest -> this.validateRequest(passwordRequest,\n                \"passwordRequest\", request))\n            .flatMap(passwordRequest -> getCurrentUser()\n                .filter(user -> {\n                    var encodedPassword = user.getSpec().getPassword();\n                    var rawPassword = passwordRequest.getPassword();\n                    return passwordEncoder.matches(rawPassword, encodedPassword);\n                })\n                .switchIfEmpty(\n                    Mono.error(() -> new ServerWebInputException(\"Invalid password\")))\n                .doOnNext(user -> user.getSpec().setTwoFactorAuthEnabled(enabled))\n                .flatMap(client::update)\n                .map(TwoFactorUtils::getTwoFactorAuthSettings))\n            .flatMap(twoFactorAuthSettings -> ServerResponse.ok().bodyValue(twoFactorAuthSettings));\n    }\n\n    private Mono<ServerResponse> getTotpAuthLink(ServerRequest request) {\n        var authLinkResponse = getCurrentUser()\n            .map(user -> {\n                var username = user.getMetadata().getName();\n                var url = externalUrl.getURL(request.exchange().getRequest());\n                var authority = url.getAuthority();\n                var authKeyId = username + \":\" + authority;\n                var rawSecret = totpAuthService.generateTotpSecret();\n                var authLink = UriComponentsBuilder.fromUriString(\"otpauth://totp\")\n                    .path(authKeyId)\n                    .queryParam(\"secret\", rawSecret)\n                    .queryParam(\"digits\", 6)\n                    .build().toUri();\n                var authLinkResp = new TotpAuthLinkResponse();\n                authLinkResp.setAuthLink(authLink);\n                authLinkResp.setRawSecret(rawSecret);\n                return authLinkResp;\n            });\n\n        return ServerResponse.ok().body(authLinkResponse, TotpAuthLinkResponse.class);\n    }\n\n    @Data\n    public static class TotpAuthLinkResponse {\n\n        /**\n         * QR Code with base64 encoded.\n         */\n        private URI authLink;\n\n        private String rawSecret;\n    }\n\n    private Mono<ServerResponse> configureTotp(ServerRequest request) {\n        var totpRequestMono = request.bodyToMono(TotpRequest.class)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Request body required.\")))\n            .doOnNext(totpRequest -> this.validateRequest(totpRequest, \"totp\", request));\n\n        var configuredUser = totpRequestMono.flatMap(totpRequest -> {\n            // validate password\n            return getCurrentUser()\n                .filter(user -> {\n                    var encodedPassword = user.getSpec().getPassword();\n                    var rawPassword = totpRequest.getPassword();\n                    return passwordEncoder.matches(rawPassword, encodedPassword);\n                })\n                .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\"Invalid password\")))\n                .doOnNext(user -> {\n                    // TimeBasedOneTimePasswordUtil.\n                    var rawSecret = totpRequest.getSecret();\n                    int code;\n                    try {\n                        code = Integer.parseInt(totpRequest.getCode());\n                    } catch (NumberFormatException e) {\n                        throw new ServerWebInputException(\"Invalid code\");\n                    }\n                    var validated = totpAuthService.validateTotp(rawSecret, code);\n                    if (!validated) {\n                        throw new ServerWebInputException(\"Invalid secret or code\");\n                    }\n                    var encryptedSecret = totpAuthService.encryptSecret(rawSecret);\n                    user.getSpec().setTotpEncryptedSecret(encryptedSecret);\n                })\n                .flatMap(client::update);\n        });\n\n        var twoFactorAuthSettings =\n            configuredUser.map(TwoFactorUtils::getTwoFactorAuthSettings);\n\n        return ServerResponse.ok().body(twoFactorAuthSettings, TwoFactorAuthSettings.class);\n    }\n\n    private void validateRequest(Object target, String name, ServerRequest request) {\n        var bindingResult =\n            ValidationUtils.validate(target, name, validator, request.exchange());\n        if (bindingResult.hasErrors()) {\n            throw new RequestBodyValidationException(bindingResult);\n        }\n    }\n\n    @Data\n    public static class TotpRequest {\n\n        @NotBlank\n        private String secret;\n\n        @NotNull\n        private String code;\n\n        @NotBlank\n        private String password;\n\n    }\n\n    private Mono<ServerResponse> getTwoFactorSettings(ServerRequest request) {\n        return getCurrentUser()\n            .map(TwoFactorUtils::getTwoFactorAuthSettings)\n            .flatMap(settings -> ServerResponse.ok().bodyValue(settings));\n    }\n\n    private Mono<User> getCurrentUser() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .filter(TwoFactorAuthEndpoint::isAuthenticatedUser)\n            .switchIfEmpty(Mono.error(AccessDeniedException::new))\n            .map(Authentication::getName)\n            .flatMap(userService::getUser);\n    }\n\n    private static boolean isAuthenticatedUser(Authentication authentication) {\n        return authentication != null && !(authentication instanceof AnonymousAuthenticationToken);\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.security.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthRequiredException.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport java.net.URI;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.server.ResponseStatusException;\n\npublic class TwoFactorAuthRequiredException extends ResponseStatusException {\n\n    private static final URI type = URI.create(\"https://halo.run/probs/2fa-required\");\n\n    public TwoFactorAuthRequiredException(URI redirectURI) {\n        super(HttpStatus.UNAUTHORIZED, \"Two-factor authentication required\");\n        setType(type);\n        getBody().setProperty(\"redirectURI\", redirectURI);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthSecurityConfigurer.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;\n\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.config.web.server.SecurityWebFiltersOrder;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.web.server.authentication.AuthenticationWebFilter;\nimport org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.security.LoginHandlerEnhancer;\nimport run.halo.app.security.authentication.SecurityConfigurer;\nimport run.halo.app.security.authentication.twofactor.totp.TotpAuthService;\nimport run.halo.app.security.authentication.twofactor.totp.TotpAuthenticationManager;\nimport run.halo.app.security.authentication.twofactor.totp.TotpCodeAuthenticationConverter;\n\n@Component\n@Order(0)\npublic class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {\n\n    private final ServerSecurityContextRepository securityContextRepository;\n\n    private final TotpAuthService totpAuthService;\n\n    private final LoginHandlerEnhancer loginHandlerEnhancer;\n\n    private final ServerRequestCache serverRequestCache;\n\n    public TwoFactorAuthSecurityConfigurer(\n        ServerSecurityContextRepository securityContextRepository,\n        TotpAuthService totpAuthService, LoginHandlerEnhancer loginHandlerEnhancer,\n        ServerRequestCache serverRequestCache\n    ) {\n        this.securityContextRepository = securityContextRepository;\n        this.totpAuthService = totpAuthService;\n        this.loginHandlerEnhancer = loginHandlerEnhancer;\n        this.serverRequestCache = serverRequestCache;\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var authManager = new TotpAuthenticationManager(totpAuthService);\n        var filter = new AuthenticationWebFilter(authManager);\n        filter.setRequiresAuthenticationMatcher(\n            pathMatchers(HttpMethod.POST, \"/challenges/two-factor/totp\")\n        );\n        filter.setSecurityContextRepository(securityContextRepository);\n        filter.setServerAuthenticationConverter(new TotpCodeAuthenticationConverter());\n        filter.setAuthenticationSuccessHandler(\n            new TotpAuthenticationSuccessHandler(loginHandlerEnhancer, serverRequestCache)\n        );\n        filter.setAuthenticationFailureHandler(\n            new RedirectServerAuthenticationFailureHandler(\"/challenges/two-factor/totp?error\")\n        );\n        http.addFilterAt(filter, SecurityWebFiltersOrder.AUTHENTICATION);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthSettings.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport lombok.Data;\n\n@Data\npublic class TwoFactorAuthSettings {\n\n    private boolean enabled;\n\n    private boolean emailVerified;\n\n    private boolean totpConfigured;\n\n    /**\n     * Check if 2FA is available.\n     *\n     * @return true if 2FA is enabled and configured, false otherwise.\n     */\n    public boolean isAvailable() {\n        return enabled && totpConfigured;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthentication.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME;\n\nimport java.util.List;\nimport org.springframework.security.authentication.AbstractAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\n\n/**\n * Authentication token for two-factor authentication.\n *\n * @author johnniang\n */\npublic class TwoFactorAuthentication extends AbstractAuthenticationToken {\n\n    private final Authentication previous;\n\n    /**\n     * Creates a token with the supplied array of authorities.\n     *\n     * @param previous the previous authentication\n     */\n    public TwoFactorAuthentication(Authentication previous) {\n        super(List.of(new SimpleGrantedAuthority(ANONYMOUS_ROLE_NAME)));\n        this.previous = previous;\n    }\n\n    @Override\n    public Object getCredentials() {\n        return previous.getCredentials();\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return previous.getPrincipal();\n    }\n\n    @Override\n    public boolean isAuthenticated() {\n        // for further authentication\n        return false;\n    }\n\n    public Authentication getPrevious() {\n        return previous;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthenticationEntryPoint.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport java.net.URI;\nimport org.springframework.context.MessageSource;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.web.server.DefaultServerRedirectStrategy;\nimport org.springframework.security.web.server.ServerAuthenticationEntryPoint;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.exception.Exceptions;\n\npublic class TwoFactorAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {\n\n    public static ServerWebExchangeMatcher MATCHER = exchange -> exchange.getPrincipal()\n        .filter(TwoFactorAuthentication.class::isInstance)\n        .flatMap(a -> ServerWebExchangeMatcher.MatchResult.match())\n        .switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());\n\n    private static final URI REDIRECT_LOCATION = URI.create(\"/challenges/two-factor/totp\");\n\n    /**\n     * Because we don't want to cache the request before redirecting to the 2FA page,\n     * ServerRedirectStrategy is used to redirect the request.\n     */\n    private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();\n\n    private final MessageSource messageSource;\n\n    private final ServerResponse.Context context;\n\n    private static final ServerWebExchangeMatcher XHR_MATCHER = exchange -> {\n        if (exchange.getRequest().getHeaders().getOrEmpty(\"X-Requested-With\")\n            .contains(\"XMLHttpRequest\")) {\n            return ServerWebExchangeMatcher.MatchResult.match();\n        }\n        return ServerWebExchangeMatcher.MatchResult.notMatch();\n    };\n\n    public TwoFactorAuthenticationEntryPoint(MessageSource messageSource,\n        ServerResponse.Context context) {\n        this.messageSource = messageSource;\n        this.context = context;\n    }\n\n    @Override\n    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {\n        return XHR_MATCHER.matches(exchange)\n            .filter(ServerWebExchangeMatcher.MatchResult::isMatch)\n            .switchIfEmpty(\n                redirectStrategy.sendRedirect(exchange, REDIRECT_LOCATION).then(Mono.empty())\n            )\n            .flatMap(isXhr -> {\n                var errorResponse = Exceptions.createErrorResponse(\n                    new TwoFactorAuthRequiredException(REDIRECT_LOCATION),\n                    null, exchange, messageSource);\n                return ServerResponse.status(errorResponse.getStatusCode())\n                    .bodyValue(errorResponse.getBody())\n                    .flatMap(response -> response.writeTo(exchange, context));\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/TwoFactorUtils.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport org.apache.commons.lang3.StringUtils;\nimport run.halo.app.core.extension.User;\n\npublic enum TwoFactorUtils {\n    ;\n\n    public static TwoFactorAuthSettings getTwoFactorAuthSettings(User user) {\n        var spec = user.getSpec();\n        var tfaEnabled = defaultIfNull(spec.getTwoFactorAuthEnabled(), false);\n        var emailVerified = spec.isEmailVerified();\n        var totpEncryptedSecret = spec.getTotpEncryptedSecret();\n        var settings = new TwoFactorAuthSettings();\n        settings.setEnabled(tfaEnabled);\n        settings.setEmailVerified(emailVerified);\n        settings.setTotpConfigured(StringUtils.isNotBlank(totpEncryptedSecret));\n        return settings;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/totp/DefaultTotpAuthService.java",
    "content": "package run.halo.app.security.authentication.twofactor.totp;\n\nimport static com.j256.twofactorauth.TimeBasedOneTimePasswordUtil.generateBase32Secret;\nimport static com.j256.twofactorauth.TimeBasedOneTimePasswordUtil.validateCurrentNumber;\nimport static java.nio.file.StandardOpenOption.APPEND;\nimport static java.nio.file.StandardOpenOption.CREATE;\nimport static java.nio.file.StandardOpenOption.READ;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.GeneralSecurityException;\nimport java.security.KeyStore;\nimport java.security.KeyStoreException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.UnrecoverableEntryException;\nimport java.security.cert.CertificateException;\nimport javax.crypto.KeyGenerator;\nimport javax.crypto.SecretKey;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.crypto.codec.Hex;\nimport org.springframework.security.crypto.encrypt.AesBytesEncryptor;\nimport org.springframework.security.crypto.encrypt.BytesEncryptor;\nimport org.springframework.security.crypto.keygen.KeyGenerators;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@Slf4j\n@Component\npublic class DefaultTotpAuthService implements TotpAuthService {\n\n    private final BytesEncryptor encryptor;\n\n    public DefaultTotpAuthService(HaloProperties haloProperties) {\n        // init secret key\n        var keysRoot = haloProperties.getWorkDir().resolve(\"keys\");\n        this.encryptor = loadOrCreateEncryptor(keysRoot);\n    }\n\n    private BytesEncryptor loadOrCreateEncryptor(Path keysRoot) {\n        try {\n            if (Files.notExists(keysRoot)) {\n                Files.createDirectories(keysRoot);\n            }\n            var keyStorePath = keysRoot.resolve(\"halo.keystore\");\n            var password = \"changeit\".toCharArray();\n            var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());\n            if (Files.notExists(keyStorePath)) {\n                keyStore.load(null, password);\n            } else {\n                try (var is = Files.newInputStream(keyStorePath, READ)) {\n                    keyStore.load(is, password);\n                }\n            }\n\n            var alias = \"totp-secret-key\";\n            var entry = keyStore.getEntry(alias, new KeyStore.PasswordProtection(password));\n            SecretKey secretKey = null;\n            if (entry instanceof KeyStore.SecretKeyEntry secretKeyEntry) {\n                if (\"AES\".equalsIgnoreCase(secretKeyEntry.getSecretKey().getAlgorithm())) {\n                    secretKey = secretKeyEntry.getSecretKey();\n                }\n            }\n            if (secretKey == null) {\n                var generator = KeyGenerator.getInstance(\"AES\");\n                generator.init(128);\n                secretKey = generator.generateKey();\n                var secretKeyEntry = new KeyStore.SecretKeyEntry(secretKey);\n                keyStore.setEntry(alias, secretKeyEntry, new KeyStore.PasswordProtection(password));\n                try (var os = Files.newOutputStream(keyStorePath, CREATE, APPEND)) {\n                    keyStore.store(os, password);\n                }\n            }\n            return new AesBytesEncryptor(secretKey,\n                KeyGenerators.secureRandom(32),\n                AesBytesEncryptor.CipherAlgorithm.GCM);\n        } catch (IOException | KeyStoreException | CertificateException | NoSuchAlgorithmException\n                 | UnrecoverableEntryException e) {\n            throw new RuntimeException(\"Failed to initialize AesBytesEncryptor\", e);\n        }\n    }\n\n    @Override\n    public boolean validateTotp(String rawSecret, int code) {\n        try {\n            return validateCurrentNumber(rawSecret, code, 10 * 1000);\n        } catch (GeneralSecurityException e) {\n            log.warn(\"Error occurred when validate TOTP code\", e);\n            return false;\n        }\n    }\n\n    @Override\n    public String generateTotpSecret() {\n        return generateBase32Secret(32);\n    }\n\n    @Override\n    public String encryptSecret(String rawSecret) {\n        return new String(Hex.encode(encryptor.encrypt(rawSecret.getBytes())));\n    }\n\n    @Override\n    public String decryptSecret(String encryptedSecret) {\n        return new String(encryptor.decrypt(Hex.decode(encryptedSecret)));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthService.java",
    "content": "package run.halo.app.security.authentication.twofactor.totp;\n\npublic interface TotpAuthService {\n\n    boolean validateTotp(String rawSecret, int code);\n\n    String generateTotpSecret();\n\n    String encryptSecret(String rawSecret);\n\n    String decryptSecret(String encryptedSecret);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationManager.java",
    "content": "package run.halo.app.security.authentication.twofactor.totp;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.authentication.ReactiveAuthenticationManager;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.CredentialsContainer;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.HaloUserDetails;\nimport run.halo.app.security.authentication.exception.TwoFactorAuthException;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;\n\n/**\n * TOTP authentication manager.\n *\n * @author johnniang\n */\n@Slf4j\npublic class TotpAuthenticationManager implements ReactiveAuthenticationManager {\n\n    private final TotpAuthService totpAuthService;\n\n    public TotpAuthenticationManager(TotpAuthService totpAuthService) {\n        this.totpAuthService = totpAuthService;\n    }\n\n    @Override\n    public Mono<Authentication> authenticate(Authentication authentication) {\n        // it should be TotpAuthenticationToken\n        var code = (Integer) authentication.getCredentials();\n        log.debug(\"Got TOTP code {}\", code);\n\n        // get user details\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .cast(TwoFactorAuthentication.class)\n            .map(TwoFactorAuthentication::getPrevious)\n            .flatMap(previousAuth -> {\n                var principal = previousAuth.getPrincipal();\n                if (!(principal instanceof HaloUserDetails user)) {\n                    return Mono.error(\n                        new TwoFactorAuthException(\"Invalid authentication principal.\")\n                    );\n                }\n                var totpEncryptedSecret = user.getTotpEncryptedSecret();\n                if (StringUtils.isBlank(totpEncryptedSecret)) {\n                    return Mono.error(\n                        new TwoFactorAuthException(\"TOTP secret not configured.\")\n                    );\n                }\n                var rawSecret = totpAuthService.decryptSecret(totpEncryptedSecret);\n                var validated = totpAuthService.validateTotp(rawSecret, code);\n                if (!validated) {\n                    return Mono.error(new TwoFactorAuthException(\"Invalid TOTP code \" + code));\n                }\n                if (log.isDebugEnabled()) {\n                    log.debug(\n                        \"TOTP authentication for {} with code {} successfully.\",\n                        previousAuth.getName(), code);\n                }\n                if (previousAuth instanceof CredentialsContainer container) {\n                    container.eraseCredentials();\n                }\n                return Mono.just(previousAuth);\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationToken.java",
    "content": "package run.halo.app.security.authentication.twofactor.totp;\n\nimport java.util.List;\nimport org.springframework.security.authentication.AbstractAuthenticationToken;\n\npublic class TotpAuthenticationToken extends AbstractAuthenticationToken {\n\n    private final int code;\n\n    public TotpAuthenticationToken(int code) {\n        super(List.of());\n        this.code = code;\n    }\n\n    public int getCode() {\n        return code;\n    }\n\n    @Override\n    public Object getCredentials() {\n        return getCode();\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return getCode();\n    }\n\n    @Override\n    public boolean isAuthenticated() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpCodeAuthenticationConverter.java",
    "content": "package run.halo.app.security.authentication.twofactor.totp;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.web.server.authentication.ServerAuthenticationConverter;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.authentication.exception.TwoFactorAuthException;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;\n\n/**\n * TOTP code authentication converter.\n *\n * @author johnniang\n */\npublic class TotpCodeAuthenticationConverter implements ServerAuthenticationConverter {\n\n    private final String codeParameter = \"code\";\n\n    @Override\n    public Mono<Authentication> convert(ServerWebExchange exchange) {\n        // Check the request is authenticated before.\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .filter(TwoFactorAuthentication.class::isInstance)\n            .switchIfEmpty(Mono.error(\n                () -> new TwoFactorAuthException(\n                    \"MFA Authentication required.\"\n                ))\n            )\n            .flatMap(authentication -> exchange.getFormData())\n            .handle((formData, sink) -> {\n                var codeStr = formData.getFirst(codeParameter);\n                if (StringUtils.isBlank(codeStr)) {\n                    sink.error(new TwoFactorAuthException(\n                        \"Empty code parameter.\"\n                    ));\n                    return;\n                }\n                try {\n                    var code = Integer.parseInt(codeStr);\n                    sink.next(new TotpAuthenticationToken(code));\n                } catch (NumberFormatException e) {\n                    sink.error(new TwoFactorAuthException(\n                        \"Invalid code parameter \" + codeStr + '.')\n                    );\n                }\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/Attributes.java",
    "content": "package run.halo.app.security.authorization;\n\n/**\n * Attributes is used by an Authorizer to get information about a request\n * that is used to make an authorization decision.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface Attributes {\n\n    /**\n     * @return the verb associated with API requests(this includes get, list,\n     * watch, create, update, patch, delete, deletecollection, and proxy)\n     * or the lower-cased HTTP verb associated with non-API requests(this\n     * includes get, put, post, patch, and delete)\n     */\n    String getVerb();\n\n    /**\n     * @return when isReadOnly() == true, the request has no side effects, other than\n     * caching, logging, and other incidentals.\n     */\n    boolean isReadOnly();\n\n    /**\n     * @return The kind of object, if a request is for a REST object.\n     */\n    String getResource();\n\n    /**\n     * @return the subresource being requested, if present.\n     */\n    String getSubresource();\n\n    /**\n     * @return the name of the object as parsed off the request.  This will not be\n     * present for all request types, but will be present for: get, update, delete\n     */\n    String getName();\n\n    /**\n     * @return The group of the resource, if a request is for a REST object.\n     */\n    String getApiGroup();\n\n    /**\n     * @return the version of the group requested, if a request is for a REST object.\n     */\n    String getApiVersion();\n\n    /**\n     * @return true for requests to API resources, like /api/v1/nodes,\n     * and false for non-resource endpoints like /api, /healthz\n     */\n    boolean isResourceRequest();\n\n    /**\n     * @return returns the path of the request\n     */\n    String getPath();\n\n    String getSubName();\n\n    String getUserSpace();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/AttributesRecord.java",
    "content": "package run.halo.app.security.authorization;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class AttributesRecord implements Attributes {\n    private final RequestInfo requestInfo;\n\n    public AttributesRecord(RequestInfo requestInfo) {\n        this.requestInfo = requestInfo;\n    }\n\n    @Override\n    public String getVerb() {\n        return requestInfo.getVerb();\n    }\n\n    @Override\n    public boolean isReadOnly() {\n        String verb = requestInfo.getVerb();\n        return \"get\".equals(verb)\n            || \"list\".equals(verb)\n            || \"watch\".equals(verb);\n    }\n\n    @Override\n    public String getResource() {\n        return requestInfo.getResource();\n    }\n\n    @Override\n    public String getSubresource() {\n        return requestInfo.getSubresource();\n    }\n\n    @Override\n    public String getName() {\n        return requestInfo.getName();\n    }\n\n    @Override\n    public String getApiGroup() {\n        return requestInfo.getApiGroup();\n    }\n\n    @Override\n    public String getApiVersion() {\n        return requestInfo.getApiVersion();\n    }\n\n    @Override\n    public boolean isResourceRequest() {\n        return requestInfo.isResourceRequest();\n    }\n\n    @Override\n    public String getPath() {\n        return requestInfo.getPath();\n    }\n\n    @Override\n    public String getSubName() {\n        return requestInfo.getSubName();\n    }\n\n    @Override\n    public String getUserSpace() {\n        return requestInfo.getUserspace();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.Collection;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.core.GrantedAuthority;\n\n/**\n * Utility methods for manipulating GrantedAuthority collection.\n *\n * @author johnniang\n */\npublic enum AuthorityUtils {\n    ;\n\n    public static final String SCOPE_PREFIX = \"SCOPE_\";\n\n    public static final String ROLE_PREFIX = \"ROLE_\";\n\n    public static final String SUPER_ROLE_NAME = \"super-role\";\n\n    public static final String AUTHENTICATED_ROLE_NAME = \"authenticated\";\n\n    public static final String ANONYMOUS_ROLE_NAME = \"anonymous\";\n\n    public static final String COMMENT_MANAGEMENT_ROLE_NAME = \"role-template-manage-comments\";\n\n    public static final String POST_CONTRIBUTOR_ROLE_NAME = \"role-template-post-contributor\";\n\n    public static final String THEME_MANAGEMENT_ROLE_NAME = \"role-template-manage-themes\";\n\n    /**\n     * Converts an array of GrantedAuthority objects to a role set.\n     *\n     * @return a Set of the Strings obtained from each call to\n     * GrantedAuthority.getAuthority() and filtered by prefix \"ROLE_\".\n     */\n    public static Set<String> authoritiesToRoles(\n        Collection<? extends GrantedAuthority> authorities) {\n        return authorities.stream()\n            .map(GrantedAuthority::getAuthority)\n            .filter(authority -> StringUtils.startsWith(authority, ROLE_PREFIX))\n            .map(authority -> {\n                authority = StringUtils.removeStart(authority, ROLE_PREFIX);\n                return authority;\n            })\n            .collect(Collectors.toSet());\n    }\n\n    public static boolean containsSuperRole(Collection<String> roles) {\n        return roles.contains(SUPER_ROLE_NAME);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/AuthorizationExchangeConfigurers.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.Collections;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.authorization.AuthorizationDecision;\nimport org.springframework.security.authorization.AuthorizationResult;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.server.authentication.SwitchUserWebFilter;\nimport org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.security.authentication.SecurityConfigurer;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;\n\n/**\n * Authorization exchange configurers.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\nclass AuthorizationExchangeConfigurers {\n\n    private final AuthenticationTrustResolver authenticationTrustResolver =\n        new AuthenticationTrustResolverImpl();\n\n    @Bean\n    @Order(0)\n    SecurityConfigurer apiAuthorizationConfigurer(RoleService roleService) {\n        return http -> http.authorizeExchange(\n            spec -> spec.pathMatchers(\"/api/**\", \"/apis/**\", \"/actuator/**\")\n                .access(new RequestInfoAuthorizationManager(roleService)));\n    }\n\n    @Bean\n    @Order(100)\n    SecurityConfigurer unauthenticatedAuthorizationConfigurer() {\n        return http -> http.authorizeExchange(spec -> {\n            spec.pathMatchers(HttpMethod.GET, \"/login\", \"/signup\")\n                .access((authentication, context) -> authentication.map(\n                        a -> !authenticationTrustResolver.isAuthenticated(a)\n                    )\n                    .defaultIfEmpty(true)\n                    .map(AuthorizationDecision::new));\n        });\n    }\n\n    @Bean\n    @Order(200)\n    SecurityConfigurer preAuthenticationAuthorizationConfigurer() {\n        return http -> http.authorizeExchange(spec -> spec\n            .pathMatchers(\"/login/impersonate\")\n            .hasRole(AuthorityUtils.SUPER_ROLE_NAME)\n            .pathMatchers(\"/logout/impersonate\")\n            .hasAuthority(SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR)\n            .pathMatchers(\"/challenges/**\")\n            .access((authentication, context) ->\n                authentication.map(TwoFactorAuthentication.class::isInstance)\n                    .<AuthorizationResult>map(AuthorizationDecision::new)\n                    .switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))\n            )\n            .pathMatchers(\n                \"/login/**\",\n                \"/password-reset/**\",\n                \"/signup\"\n            )\n            .permitAll()\n            .pathMatchers(\"/logout\")\n            .access((authentication, context) ->\n                authentication.map(a -> !authenticationTrustResolver.isAnonymous(a))\n                    .<AuthorizationResult>map(AuthorizationDecision::new)\n                    .switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))\n            )\n        );\n    }\n\n    @Bean\n    @Order(300)\n    SecurityConfigurer authenticatedAuthorizationConfigurer() {\n        // Anonymous user is not allowed\n        return http -> http.authorizeExchange(\n            spec -> spec.pathMatchers(\n                    \"/console/**\",\n                    \"/uc/**\"\n                )\n                .authenticated()\n        );\n    }\n\n    @Bean\n    @Order(400)\n    SecurityConfigurer anonymousOrAuthenticatedAuthorizationConfigurer() {\n        return http -> http.authorizeExchange(\n            spec -> spec.matchers(createHtmlMatcher()).access((authentication, context) ->\n                // we only need to check the authentication is authenticated\n                // because we treat anonymous user as authenticated\n                authentication.map(Authentication::isAuthenticated)\n                    .<AuthorizationResult>map(AuthorizationDecision::new)\n                    .switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))\n            )\n        );\n    }\n\n    @Bean\n    @Order\n    SecurityConfigurer permitAllAuthorizationConfigurer() {\n        return http -> http.authorizeExchange(spec -> spec.anyExchange().permitAll());\n    }\n\n    private static ServerWebExchangeMatcher createHtmlMatcher() {\n        ServerWebExchangeMatcher get =\n            ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, \"/**\");\n        ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(\n            ServerWebExchangeMatchers.pathMatchers(\"/favicon.*\"));\n        MediaTypeServerWebExchangeMatcher html =\n            new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);\n        html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));\n        return new AndServerWebExchangeMatcher(get, notFavicon, html);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/AuthorizationRuleResolver.java",
    "content": "package run.halo.app.security.authorization;\n\nimport org.springframework.security.core.Authentication;\nimport reactor.core.publisher.Mono;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic interface AuthorizationRuleResolver {\n\n    Mono<AuthorizingVisitor> visitRules(Authentication authentication, RequestInfo requestInfo);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/AuthorizingVisitor.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport run.halo.app.core.extension.Role;\n\n/**\n * authorizing visitor short-circuits once allowed, and collects any resolution errors encountered.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class AuthorizingVisitor implements RuleAccumulator {\n    private final RbacRequestEvaluation requestEvaluation = new RbacRequestEvaluation();\n\n    private final Attributes requestAttributes;\n\n    private boolean allowed;\n\n    private String reason;\n\n    private final List<Throwable> errors = new ArrayList<>(4);\n\n    public AuthorizingVisitor(Attributes requestAttributes) {\n        this.requestAttributes = requestAttributes;\n    }\n\n    @Override\n    public boolean visit(String source, Role.PolicyRule rule, Throwable error) {\n        if (rule != null && requestEvaluation.ruleAllows(requestAttributes, rule)) {\n            this.allowed = true;\n            this.reason = String.format(\"RBAC: allowed by %s\", source);\n            return false;\n        }\n        if (error != null) {\n            this.errors.add(error);\n        }\n        return true;\n    }\n\n    public boolean isAllowed() {\n        return allowed;\n    }\n\n    public String getReason() {\n        return reason;\n    }\n\n    public List<Throwable> getErrors() {\n        return errors;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/DefaultRuleResolver.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport lombok.Data;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.RoleService;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Slf4j\npublic class DefaultRuleResolver implements AuthorizationRuleResolver {\n\n    private RoleService roleService;\n\n    public DefaultRuleResolver(RoleService roleService) {\n        this.roleService = roleService;\n    }\n\n    @Override\n    public Mono<AuthorizingVisitor> visitRules(Authentication authentication,\n        RequestInfo requestInfo) {\n        var roleNames = AuthorityUtils.authoritiesToRoles(authentication.getAuthorities());\n        var record = new AttributesRecord(requestInfo);\n        var visitor = new AuthorizingVisitor(record);\n\n        // If the request is an userspace scoped request,\n        // then we should check whether the user is the owner of the userspace.\n        if (StringUtils.isNotBlank(requestInfo.getUserspace())) {\n            if (!authentication.getName().equals(requestInfo.getUserspace())) {\n                return Mono.fromSupplier(() -> {\n                    visitor.visit(null, null, null);\n                    return visitor;\n                });\n            }\n        }\n\n        var stopVisiting = new AtomicBoolean(false);\n        return roleService.listDependenciesFlux(roleNames)\n            .filter(role -> !CollectionUtils.isEmpty(role.getRules()))\n            .doOnNext(role -> {\n                if (stopVisiting.get()) {\n                    return;\n                }\n                String roleName = role.getMetadata().getName();\n                var rules = role.getRules();\n                var source = roleBindingDescriber(roleName, authentication.getName());\n                for (var rule : rules) {\n                    if (!visitor.visit(source, rule, null)) {\n                        stopVisiting.set(true);\n                        return;\n                    }\n                }\n            })\n            .takeUntil(item -> stopVisiting.get())\n            .onErrorResume(t -> visitor.visit(null, null, t), t -> {\n                log.error(\"Error occurred when visiting rules\", t);\n                //Do nothing here\n                return Mono.empty();\n            })\n            .then(Mono.just(visitor));\n    }\n\n    String roleBindingDescriber(String roleName, String subject) {\n        return String.format(\"Binding role [%s] to [%s]\", roleName, subject);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/PolicyRuleList.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.ArrayList;\nimport java.util.LinkedList;\nimport java.util.List;\nimport run.halo.app.core.extension.Role;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class PolicyRuleList extends LinkedList<Role.PolicyRule> {\n    private final List<Throwable> errors = new ArrayList<>(4);\n\n    /**\n     * @return true if an error occurred when parsing PolicyRules\n     */\n    public boolean hasErrors() {\n        return !errors.isEmpty();\n    }\n\n    public List<Throwable> getErrors() {\n        return errors;\n    }\n\n    public PolicyRuleList addError(Throwable error) {\n        errors.add(error);\n        return this;\n    }\n\n    public PolicyRuleList addErrors(List<Throwable> errors) {\n        this.errors.addAll(errors);\n        return this;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/RbacRequestEvaluation.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.List;\nimport java.util.Objects;\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport run.halo.app.core.extension.Role;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class RbacRequestEvaluation {\n    interface WildCard {\n        String APIGroupAll = \"*\";\n        String ResourceAll = \"*\";\n        String VerbAll = \"*\";\n        String NonResourceAll = \"*\";\n    }\n\n    public boolean rulesAllow(Attributes requestAttributes, List<Role.PolicyRule> rules) {\n        for (Role.PolicyRule rule : rules) {\n            if (ruleAllows(requestAttributes, rule)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    protected boolean ruleAllows(Attributes requestAttributes, Role.PolicyRule rule) {\n        if (requestAttributes.isResourceRequest()) {\n            String combinedResource = requestAttributes.getResource();\n            if (StringUtils.isNotBlank(requestAttributes.getSubresource())) {\n                combinedResource =\n                    requestAttributes.getResource() + \"/\" + requestAttributes.getSubresource();\n            }\n            return verbMatches(rule, requestAttributes.getVerb())\n                && apiGroupMatches(rule, requestAttributes.getApiGroup())\n                && resourceMatches(rule, combinedResource, requestAttributes.getSubresource())\n                && resourceNameMatches(rule,\n                combineResourceName(requestAttributes.getName(), requestAttributes.getSubName()));\n        }\n        return verbMatches(rule, requestAttributes.getVerb())\n            && nonResourceURLMatches(rule, requestAttributes.getPath());\n    }\n\n    private String combineResourceName(String name, String subName) {\n        if (StringUtils.isBlank(name)) {\n            return subName;\n        }\n        if (StringUtils.isBlank(subName)) {\n            return name;\n        }\n        return name + \"/\" + subName;\n    }\n\n    protected boolean verbMatches(Role.PolicyRule rule, String requestedVerb) {\n        for (String ruleVerb : rule.getVerbs()) {\n            if (Objects.equals(ruleVerb, WildCard.VerbAll)) {\n                return true;\n            }\n            if (Objects.equals(ruleVerb, requestedVerb)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    protected boolean apiGroupMatches(Role.PolicyRule rule, String requestedGroup) {\n        for (String ruleGroup : rule.getApiGroups()) {\n            if (Objects.equals(ruleGroup, WildCard.APIGroupAll)) {\n                return true;\n            }\n            if (Objects.equals(ruleGroup, requestedGroup)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    protected boolean resourceMatches(Role.PolicyRule rule, String combinedRequestedResource,\n        String requestedSubresource) {\n        for (String ruleResource : rule.getResources()) {\n            // if everything is allowed, we match\n            if (Objects.equals(ruleResource, WildCard.ResourceAll)) {\n                return true;\n            }\n            // if we have an exact match, we match\n            if (Objects.equals(ruleResource, combinedRequestedResource)) {\n                return true;\n            }\n\n            // We can also match a */subresource.\n            // if there isn't a subresource, then continue\n            if (StringUtils.isBlank(requestedSubresource)) {\n                continue;\n            }\n            // if the rule isn't in the format */subresource, then we don't match, continue\n            if (StringUtils.length(ruleResource) == StringUtils.length(requestedSubresource) + 2\n                && StringUtils.startsWith(ruleResource, \"*/\")\n                && StringUtils.startsWith(ruleResource, requestedSubresource)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    protected boolean resourceNameMatches(Role.PolicyRule rule, String requestedName) {\n        if (ArrayUtils.isEmpty(rule.getResourceNames())) {\n            return true;\n        }\n        String[] requestedNameParts = ArrayUtils.nullToEmpty(StringUtils.split(requestedName, \"/\"));\n        for (String ruleName : rule.getResourceNames()) {\n            String[] patternParts = StringUtils.split(ruleName, \"/\");\n\n            for (int i = 0; i < patternParts.length; i++) {\n                String patternPart = patternParts[i];\n                String textPart = StringUtils.EMPTY;\n                if (requestedNameParts.length > i) {\n                    textPart = requestedNameParts[i];\n                }\n\n                if (!matchPart(patternPart, textPart)) {\n                    return false;\n                }\n            }\n\n            return true;\n        }\n        return false;\n    }\n\n    private static boolean matchPart(String patternPart, String textPart) {\n        if (patternPart.equals(\"*\")) {\n            return true;\n        } else if (patternPart.startsWith(\"*\")) {\n            return textPart.endsWith(patternPart.substring(1));\n        } else if (patternPart.endsWith(\"*\")) {\n            return textPart.startsWith(patternPart.substring(0, patternPart.length() - 1));\n        } else {\n            return patternPart.equals(textPart);\n        }\n    }\n\n    protected boolean nonResourceURLMatches(Role.PolicyRule rule, String requestedURL) {\n        for (String ruleURL : rule.getNonResourceURLs()) {\n            if (Objects.equals(ruleURL, WildCard.NonResourceAll)) {\n                return true;\n            }\n            if (Objects.equals(ruleURL, requestedURL)) {\n                return true;\n            }\n            if (StringUtils.endsWith(ruleURL, WildCard.NonResourceAll)\n                && StringUtils.startsWith(requestedURL,\n                StringUtils.stripEnd(ruleURL, WildCard.NonResourceAll))) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/RequestInfo.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.Objects;\nimport lombok.Getter;\nimport lombok.ToString;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\n\n/**\n * RequestInfo holds information parsed from the {@link ServerHttpRequest}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Getter\n@ToString\npublic class RequestInfo {\n    boolean isResourceRequest;\n    final String path;\n    String namespace;\n    String userspace;\n    String verb;\n    String apiPrefix;\n    String apiGroup;\n    String apiVersion;\n    String resource;\n\n    String name;\n\n    String subresource;\n\n    String subName;\n\n    String[] parts;\n\n    public RequestInfo(boolean isResourceRequest, String path, String verb) {\n        this(isResourceRequest, path, null, null, verb, null, null, null, null, null, null, null,\n            null);\n    }\n\n    public RequestInfo(boolean isResourceRequest, String path, String namespace, String userspace,\n        String verb,\n        String apiPrefix,\n        String apiGroup,\n        String apiVersion, String resource, String name, String subresource, String subName,\n        String[] parts) {\n        this.isResourceRequest = isResourceRequest;\n        this.path = StringUtils.defaultString(path);\n        this.namespace = StringUtils.defaultString(namespace);\n        this.userspace = StringUtils.defaultString(userspace);\n        this.verb = StringUtils.defaultString(verb);\n        this.apiPrefix = StringUtils.defaultString(apiPrefix);\n        this.apiGroup = StringUtils.defaultString(apiGroup);\n        this.apiVersion = StringUtils.defaultString(apiVersion);\n        this.resource = StringUtils.defaultString(resource);\n        this.subresource = StringUtils.defaultString(subresource);\n        this.subName = StringUtils.defaultString(subName);\n        this.name = StringUtils.defaultString(name);\n        this.parts = Objects.requireNonNullElseGet(parts, () -> new String[] {});\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/RequestInfoAuthorizationManager.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.List;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.security.authorization.AuthorizationDecision;\nimport org.springframework.security.authorization.AuthorizationResult;\nimport org.springframework.security.authorization.ReactiveAuthorizationManager;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.server.authorization.AuthorizationContext;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.RoleService;\n\n@Slf4j\npublic class RequestInfoAuthorizationManager\n    implements ReactiveAuthorizationManager<AuthorizationContext> {\n\n    private final AuthorizationRuleResolver ruleResolver;\n\n    public RequestInfoAuthorizationManager(RoleService roleService) {\n        this.ruleResolver = new DefaultRuleResolver(roleService);\n    }\n\n    @Override\n    public Mono<AuthorizationResult> authorize(Mono<Authentication> authentication,\n        AuthorizationContext context) {\n        var request = context.getExchange().getRequest();\n        var requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n\n        // We allow anonymous user to access some resources\n        // so we don't invoke AuthenticationTrustResolver.isAuthenticated\n        // to check if the user is authenticated\n        return authentication.filter(Authentication::isAuthenticated)\n            .flatMap(auth -> ruleResolver.visitRules(auth, requestInfo))\n            .doOnNext(visitor -> showErrorMessage(visitor.getErrors()))\n            .map(AuthorizingVisitor::isAllowed)\n            .defaultIfEmpty(false)\n            .map(AuthorizationDecision::new);\n    }\n\n    private void showErrorMessage(List<Throwable> errors) {\n        if (errors != null) {\n            errors.forEach(error -> log.error(\"Access decision error\", error));\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/RequestInfoFactory.java",
    "content": "package run.halo.app.security.authorization;\n\nimport java.util.Arrays;\nimport java.util.Objects;\nimport java.util.Set;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.server.PathContainer;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport run.halo.app.infra.ui.WebSocketUtils;\n\n/**\n * Creates {@link RequestInfo} from {@link ServerHttpRequest}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class RequestInfoFactory {\n    public static final RequestInfoFactory INSTANCE =\n        new RequestInfoFactory(Set.of(\"api\", \"apis\"), Set.of(\"api\"));\n\n    /**\n     * without leading and trailing slashes.\n     */\n    final Set<String> apiPrefixes;\n\n    /**\n     * without leading and trailing slashes.\n     */\n    final Set<String> grouplessApiPrefixes;\n\n    /**\n     * special verbs no subresources.\n     */\n    final Set<String> specialVerbs;\n\n    public RequestInfoFactory(Set<String> apiPrefixes, Set<String> grouplessApiPrefixes) {\n        this(apiPrefixes, grouplessApiPrefixes, Set.of(\"proxy\", \"watch\"));\n    }\n\n    public RequestInfoFactory(Set<String> apiPrefixes, Set<String> grouplessApiPrefixes,\n        Set<String> specialVerbs) {\n        this.apiPrefixes = apiPrefixes;\n        this.grouplessApiPrefixes = grouplessApiPrefixes;\n        this.specialVerbs = specialVerbs;\n    }\n\n    /**\n     * <p>newRequestInfo returns the information from the http request.  If error is not occurred,\n     * RequestInfo holds the information as best it is known before the failure\n     * It handles both resource and non-resource requests and fills in all the pertinent\n     * information.</p>\n     * <p>for each.</p>\n     * Valid Inputs:\n     * <p>Resource paths</p>\n     * <pre>\n     * /apis/{api-group}/{version}/namespaces\n     * /api/{version}/namespaces\n     * /api/{version}/namespaces/{namespace}\n     * /api/{version}/namespaces/{namespace}/{resource}\n     * /api/{version}/namespaces/{namespace}/{resource}/{resourceName}\n     * /api/{version}/userspaces/{userspace}/{resource}\n     * /api/{version}/userspaces/{userspace}/{resource}/{resourceName}\n     * /api/{version}/{resource}\n     * /api/{version}/{resource}/{resourceName}\n     * </pre>\n     * <p>Special verbs without subresources:</p>\n     * <pre>\n     * /api/{version}/proxy/{resource}/{resourceName}\n     * /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}\n     * </pre>\n     *\n     * <p>Special verbs with subresources:</p>\n     * <pre>\n     * /api/{version}/watch/{resource}\n     * /api/{version}/watch/namespaces/{namespace}/{resource}\n     * </pre>\n     *\n     * <p>NonResource paths:</p>\n     * <pre>\n     * /apis/{api-group}/{version}\n     * /apis/{api-group}\n     * /apis\n     * /api/{version}\n     * /api\n     * /healthz\n     * </pre>\n     *\n     * @param request http request\n     * @return request holds the information of both resource and non-resource requests\n     */\n    public RequestInfo newRequestInfo(ServerHttpRequest request) {\n        // non-resource request default\n        PathContainer path = request.getPath().pathWithinApplication();\n        RequestInfo requestInfo =\n            new RequestInfo(false, path.value(), request.getMethod().name().toLowerCase());\n\n        String[] currentParts = splitPath(path.value());\n\n        if (currentParts.length < 3) {\n            // return a non-resource request\n            return requestInfo;\n        }\n\n        if (!apiPrefixes.contains(currentParts[0])) {\n            // return a non-resource request\n            return requestInfo;\n        }\n        requestInfo.apiPrefix = currentParts[0];\n        currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);\n\n        if (!grouplessApiPrefixes.contains(requestInfo.apiPrefix)) {\n            // one part (APIPrefix) has already been consumed, so this is actually \"do we have\n            // four parts?\"\n            if (currentParts.length < 3) {\n                // return a non-resource request\n                return requestInfo;\n            }\n\n            requestInfo.apiGroup = StringUtils.defaultString(currentParts[0]);\n            currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);\n        }\n        requestInfo.isResourceRequest = true;\n        requestInfo.apiVersion = currentParts[0];\n        currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);\n        // handle input of form /{specialVerb}/*\n        Set<String> specialVerbs = Set.of(\"proxy\", \"watch\");\n        if (specialVerbs.contains(currentParts[0])) {\n            if (currentParts.length < 2) {\n                throw new IllegalArgumentException(\n                    String.format(\"unable to determine kind and namespace from url, %s\",\n                        request.getPath()));\n            }\n            requestInfo.verb = currentParts[0];\n            currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);\n        } else {\n            requestInfo.verb = switch (request.getMethod().name().toUpperCase()) {\n                case \"POST\" -> \"create\";\n                case \"GET\", \"HEAD\" -> \"get\";\n                case \"PUT\" -> \"update\";\n                case \"PATCH\" -> \"patch\";\n                case \"DELETE\" -> \"delete\";\n                default -> \"\";\n            };\n        }\n        // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative\n        // to kind\n        Set<String> namespaceSubresources = Set.of(\"status\", \"finalize\");\n        if (Objects.equals(currentParts[0], \"namespaces\")) {\n            if (currentParts.length > 1) {\n                requestInfo.namespace = currentParts[1];\n\n                // if there is another step after the namespace name and it is not a known\n                // namespace subresource\n                // move currentParts to include it as a resource in its own right\n                if (currentParts.length > 2 && !namespaceSubresources.contains(currentParts[2])) {\n                    currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length);\n                }\n            }\n        } else if (\"userspaces\".equals(currentParts[0])) {\n            if (currentParts.length > 1) {\n                requestInfo.userspace = currentParts[1];\n\n                // if there is another step after the userspace name\n                // move currentParts to include it as a resource in its own right\n                if (currentParts.length > 2) {\n                    currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length);\n                }\n            }\n        } else {\n            requestInfo.userspace = \"\";\n            requestInfo.namespace = \"\";\n        }\n\n        // parsing successful, so we now know the proper value for .Parts\n        requestInfo.parts = currentParts;\n        // special verbs no subresources\n        // parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret\n        if (requestInfo.parts.length >= 3 && !specialVerbs.contains(\n            requestInfo.verb)) {\n            requestInfo.subresource = requestInfo.parts[2];\n            // if there is another step after the subresource name and it is not a known\n            if (requestInfo.parts.length >= 4) {\n                requestInfo.subName = requestInfo.parts[3];\n            }\n        }\n\n        if (requestInfo.parts.length >= 2) {\n            requestInfo.name = requestInfo.parts[1];\n        }\n\n        if (requestInfo.parts.length >= 1) {\n            requestInfo.resource = requestInfo.parts[0];\n        }\n\n        // has name and no subresource but verb=create, then this is a non-resource request\n        if (StringUtils.isNotBlank(requestInfo.name) && StringUtils.isBlank(requestInfo.subresource)\n            && \"create\".equals(requestInfo.verb)) {\n            requestInfo.isResourceRequest = false;\n        }\n\n        // if there's no name on the request and we thought it was a get before, then the actual\n        // verb is a list or a watch\n        if (requestInfo.name.isEmpty() && \"get\".equals(requestInfo.verb)) {\n            var watch = request.getQueryParams().getFirst(\"watch\");\n            if (Boolean.parseBoolean(watch)) {\n                requestInfo.verb = \"watch\";\n            } else {\n                requestInfo.verb = \"list\";\n            }\n        }\n        // if there's no name on the request and we thought it was a deleted before, then the\n        // actual verb is deletecollection\n        if (Objects.equals(requestInfo.verb, \"delete\")) {\n            var deleteAll = request.getQueryParams().getFirst(\"all\");\n            if (Boolean.parseBoolean(deleteAll)) {\n                requestInfo.verb = \"deletecollection\";\n            }\n        }\n        if (\"list\".equals(requestInfo.verb)\n            && WebSocketUtils.isWebSocketUpgrade(request.getHeaders())) {\n            requestInfo.verb = \"watch\";\n        }\n        return requestInfo;\n    }\n\n    private String[] splitPath(String path) {\n        path = StringUtils.strip(path, \"/\");\n        if (StringUtils.isEmpty(path)) {\n            return new String[] {};\n        }\n        return StringUtils.split(path, \"/\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/authorization/RuleAccumulator.java",
    "content": "package run.halo.app.security.authorization;\n\nimport run.halo.app.core.extension.Role;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic interface RuleAccumulator {\n    boolean visit(String source, Role.PolicyRule rule, Throwable err);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/DeviceCookieResolver.java",
    "content": "package run.halo.app.security.device;\n\nimport java.time.Duration;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.lang.Nullable;\nimport org.springframework.web.server.ServerWebExchange;\n\npublic interface DeviceCookieResolver {\n    @Nullable\n    HttpCookie resolveCookie(ServerWebExchange exchange);\n\n    void setCookie(ServerWebExchange exchange, String value);\n\n    void expireCookie(ServerWebExchange exchange);\n\n    String getCookieName();\n\n    Duration getCookieMaxAge();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/DeviceCookieResolverImpl.java",
    "content": "package run.halo.app.security.device;\n\nimport java.time.Duration;\nimport lombok.Getter;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.http.ResponseCookie;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebExchange;\n\n@Getter\n@Component\npublic class DeviceCookieResolverImpl implements DeviceCookieResolver {\n    public static final String DEVICE_COOKIE_KEY = \"device_id\";\n\n    private final String cookieName = DEVICE_COOKIE_KEY;\n\n    private final Duration cookieMaxAge = Duration.ofDays(100);\n\n    @Override\n    public HttpCookie resolveCookie(ServerWebExchange exchange) {\n        return exchange.getRequest().getCookies().getFirst(getCookieName());\n    }\n\n    @Override\n    public void setCookie(ServerWebExchange exchange, String value) {\n        Assert.notNull(value, \"'value' is required\");\n        exchange.getResponse().getCookies()\n            .set(getCookieName(), initCookie(exchange, value).build());\n    }\n\n    @Override\n    public void expireCookie(ServerWebExchange exchange) {\n        ResponseCookie cookie = initCookie(exchange, \"\").maxAge(0).build();\n        exchange.getResponse().getCookies().set(this.cookieName, cookie);\n    }\n\n    private ResponseCookie.ResponseCookieBuilder initCookie(ServerWebExchange exchange,\n        String value) {\n        return ResponseCookie.from(this.cookieName, value)\n            .path(exchange.getRequest().getPath().contextPath().value() + \"/\")\n            .maxAge(getCookieMaxAge())\n            .httpOnly(true)\n            .secure(\"https\".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()))\n            .sameSite(\"Lax\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/DeviceEndpoint.java",
    "content": "package run.halo.app.security.device;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.security.Principal;\nimport java.util.Comparator;\nimport java.util.Map;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport lombok.experimental.Accessors;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.session.ReactiveFindByIndexNameSessionRepository;\nimport org.springframework.session.Session;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Device;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\n\n/**\n * Device endpoint for user profile,every user can only manage their own devices.\n *\n * @author guqing\n * @since 2.17.0\n */\n@Component\n@RequiredArgsConstructor\npublic class DeviceEndpoint implements CustomEndpoint {\n    private final ReactiveExtensionClient client;\n    private final ReactiveFindByIndexNameSessionRepository<?> sessionRepository;\n    private final DeviceService deviceService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        final var tag = \"DeviceV1alpha1Uc\";\n        return SpringdocRouteBuilder.route()\n            .GET(\"devices\", this::listDevices,\n                builder -> builder.operationId(\"ListDevices\")\n                    .description(\"List all user devices\")\n                    .tag(tag)\n                    .response(responseBuilder().implementationArray(DeviceDto.class))\n            )\n            .DELETE(\"devices/{deviceId}\", this::revokeDevice, builder -> builder\n                .operationId(\"RevokeDevice\")\n                .description(\"Revoke a own device\")\n                .tag(tag)\n                .parameter(parameterBuilder()\n                    .in(ParameterIn.PATH)\n                    .name(\"deviceId\")\n                    .description(\"Device ID\")\n                    .required(true)\n                )\n                .response(responseBuilder()\n                    .responseCode(String.valueOf(HttpStatus.NO_CONTENT))\n                )\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> revokeDevice(ServerRequest request) {\n        final var deviceId = request.pathVariable(\"deviceId\");\n        return principalName()\n            .flatMap(principalName -> deviceService.revoke(principalName, deviceId))\n            .then(ServerResponse.noContent().build());\n    }\n\n    private Mono<ServerResponse> listDevices(ServerRequest request) {\n        return getRequestContext(request)\n            .flatMapMany(context -> {\n                var listOptions = new ListOptions();\n                var query = equal(\"spec.principalName\", context.username());\n                listOptions.setFieldSelector(FieldSelector.of(query));\n                return client.listAll(Device.class, listOptions,\n                        Sort.by(\"metadata.creationTimestamp\"))\n                    .map(device -> {\n                        var sessionId = device.getSpec().getSessionId();\n                        var session = context.sessionMap().get(sessionId);\n                        if (session != null) {\n                            device.getSpec().setLastAccessedTime(session.getLastAccessedTime());\n                        }\n                        return new DeviceDto()\n                            .setDevice(device)\n                            .setCurrentDevice(context.sessionId().equals(sessionId))\n                            .setActive(session != null && !session.isExpired());\n                    })\n                    .sort(deviceDtoComparator());\n            })\n            .collectList()\n            .flatMap(deviceDto -> ServerResponse.ok().bodyValue(deviceDto));\n    }\n\n    Comparator<DeviceDto> deviceDtoComparator() {\n        return Comparator.comparing(DeviceDto::isCurrentDevice)\n            .thenComparing(DeviceDto::isActive)\n            .thenComparing(DeviceDto::getDevice, Comparator.comparing(device -> {\n                var accessedTime = device.getSpec().getLastAccessedTime();\n                return accessedTime == null ? device.getMetadata().getCreationTimestamp()\n                    : accessedTime;\n            }))\n            .reversed();\n    }\n\n    private Mono<RequestContext> getRequestContext(ServerRequest request) {\n        return principalName()\n            .flatMap(principalName -> {\n                var builder = RequestContext.builder()\n                    .sessionMap(Map.of())\n                    .username(principalName);\n                var sessionMapMono = sessionRepository.findByPrincipalName(principalName)\n                    .doOnNext(builder::sessionMap);\n                var sessionMono = request.exchange().getSession()\n                    .doOnNext(session -> builder.sessionId(session.getId()));\n                return Mono.when(sessionMapMono, sessionMono)\n                    .then(Mono.fromSupplier(builder::build));\n            });\n    }\n\n    @Builder\n    record RequestContext(String username, String sessionId,\n                          Map<String, ? extends Session> sessionMap) {\n    }\n\n    Mono<String> principalName() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Principal::getName);\n    }\n\n    @Data\n    @Accessors(chain = true)\n    @Schema(name = \"UserDevice\")\n    static class DeviceDto {\n        @Schema(requiredMode = REQUIRED)\n        private Device device;\n\n        @Schema(requiredMode = REQUIRED)\n        boolean currentDevice;\n\n        @Schema(requiredMode = REQUIRED)\n        boolean active;\n    }\n\n    @Override\n    public GroupVersion groupVersion() {\n        return GroupVersion.parseAPIVersion(\"uc.api.security.halo.run/v1alpha1\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/DeviceReconciler.java",
    "content": "package run.halo.app.security.device;\n\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\nimport static run.halo.app.extension.ExtensionUtil.isDeleted;\nimport static run.halo.app.extension.ExtensionUtil.removeFinalizers;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.time.Duration;\nimport java.util.Set;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.session.ReactiveSessionRepository;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.Device;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n@Component\n@RequiredArgsConstructor\npublic class DeviceReconciler implements Reconciler<Reconciler.Request> {\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n    private static final int MAX_DEVICES = 10;\n    static final String FINALIZER_NAME = \"device-protection\";\n    private final ReactiveSessionRepository<?> sessionRepository;\n    private final ExtensionClient client;\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(Device.class, request.name())\n            .ifPresent(device -> {\n                if (isDeleted(device)) {\n                    if (removeFinalizers(device.getMetadata(), Set.of(FINALIZER_NAME))) {\n                        sessionRepository.deleteById(device.getSpec().getSessionId())\n                            .block(BLOCKING_TIMEOUT);\n                        client.update(device);\n                    }\n                    return;\n                }\n                if (addFinalizers(device.getMetadata(), Set.of(FINALIZER_NAME))) {\n                    client.update(device);\n                }\n                revokeInactiveDevices(device.getSpec().getPrincipalName());\n            });\n        return Result.doNotRetry();\n    }\n\n    private void revokeInactiveDevices(String principalName) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            equal(\"spec.principalName\", principalName))\n        );\n        client.listAll(Device.class, listOptions,\n                Sort.by(\"metadata.creationTimestamp\").descending())\n            .stream()\n            .skip(MAX_DEVICES)\n            .filter(device -> sessionRepository.findById(device.getSpec().getSessionId())\n                .blockOptional(BLOCKING_TIMEOUT)\n                .isEmpty()\n            )\n            .forEach(client::delete);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new Device())\n            .syncAllOnStart(false)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/DeviceSecurityConfigurer.java",
    "content": "package run.halo.app.security.device;\n\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.security.config.web.server.SecurityWebFiltersOrder;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.security.authentication.SecurityConfigurer;\n\n@Component\n@RequiredArgsConstructor\nclass DeviceSecurityConfigurer implements SecurityConfigurer {\n\n    private final DeviceService deviceService;\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var filter = new DeviceSessionFilter(deviceService);\n        http.addFilterAfter(filter, SecurityWebFiltersOrder.REACTOR_CONTEXT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/DeviceServiceImpl.java",
    "content": "package run.halo.app.security.device;\n\nimport static run.halo.app.extension.ExtensionUtil.defaultSort;\nimport static run.halo.app.infra.utils.IpAddressUtils.getClientIp;\nimport static run.halo.app.security.authentication.rememberme.PersistentTokenBasedRememberMeServices.REMEMBER_ME_SERIES_REQUEST_NAME;\n\nimport java.security.Principal;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.UUID;\nimport java.util.function.Function;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.security.authentication.AuthenticationTrustResolver;\nimport org.springframework.security.authentication.AuthenticationTrustResolverImpl;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.session.ReactiveSessionRepository;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Device;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.security.authentication.rememberme.PersistentRememberMeTokenRepository;\n\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class DeviceServiceImpl implements DeviceService {\n    private final ReactiveExtensionClient client;\n    private final DeviceCookieResolver deviceCookieResolver;\n    private final ReactiveSessionRepository<?> sessionRepository;\n    private final ApplicationEventPublisher eventPublisher;\n    private final PersistentRememberMeTokenRepository rememberMeTokenRepository;\n    private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();\n\n    @Override\n    public Mono<Void> loginSuccess(ServerWebExchange exchange, Authentication authentication) {\n        return updateExistingDevice(exchange, authentication)\n            .switchIfEmpty(createDevice(exchange, authentication)\n                .flatMap(client::create)\n                .doOnNext(device -> {\n                    deviceCookieResolver.setCookie(exchange, device.getMetadata().getName());\n                    eventPublisher.publishEvent(new NewDeviceLoginEvent(this, device));\n                })\n            )\n            .then();\n    }\n\n    @Override\n    public Mono<Void> changeSessionId(ServerWebExchange exchange) {\n        var deviceIdCookie = deviceCookieResolver.resolveCookie(exchange);\n        if (deviceIdCookie == null) {\n            return Mono.empty();\n        }\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .filter(trustResolver::isAuthenticated)\n            .map(Principal::getName)\n            .flatMap(username -> {\n                var deviceId = deviceIdCookie.getValue();\n                return updateWithRetry(deviceId, username, device -> {\n                    var oldSessionId = device.getSpec().getSessionId();\n                    return exchange.getSession()\n                        .filter(session -> !session.getId().equals(oldSessionId))\n                        .flatMap(session -> {\n                            device.getSpec().setSessionId(session.getId());\n                            device.getSpec().setLastAccessedTime(session.getLastAccessTime());\n                            return sessionRepository.deleteById(oldSessionId);\n                        })\n                        .thenReturn(device);\n                }).then();\n            });\n    }\n\n    private Mono<Device> updateWithRetry(String deviceId, String username,\n        Function<Device, Mono<Device>> updateFunction) {\n        return Mono.defer(() -> client.fetch(Device.class, deviceId)\n                .filter(device -> device.getSpec().getPrincipalName().equals(username))\n                .flatMap(updateFunction)\n                .flatMap(client::update)\n            )\n            .retryWhen(Retry.backoff(8, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    private Mono<Device> updateExistingDevice(ServerWebExchange exchange,\n        Authentication authentication) {\n        var deviceIdCookie = deviceCookieResolver.resolveCookie(exchange);\n        if (deviceIdCookie == null) {\n            return Mono.empty();\n        }\n        var principalName = authentication.getName();\n        return updateWithRetry(deviceIdCookie.getValue(), principalName,\n            (Device existingDevice) -> {\n                var sessionId = existingDevice.getSpec().getSessionId();\n                return exchange.getSession()\n                    .flatMap(session -> {\n                        var userAgent =\n                            exchange.getRequest().getHeaders().getFirst(HttpHeaders.USER_AGENT);\n                        var deviceUa = existingDevice.getSpec().getUserAgent();\n                        if (!StringUtils.equals(deviceUa, userAgent)) {\n                            // User agent changed, create a new device\n                            return Mono.empty();\n                        }\n                        return Mono.just(session);\n                    })\n                    .flatMap(session -> {\n                        if (session.getId().equals(sessionId)) {\n                            return Mono.just(session);\n                        }\n                        return sessionRepository.deleteById(sessionId).thenReturn(session);\n                    })\n                    .map(session -> {\n                        existingDevice.getSpec().setSessionId(session.getId());\n                        existingDevice.getSpec().setLastAccessedTime(session.getLastAccessTime());\n                        existingDevice.getSpec().setLastAuthenticatedTime(Instant.now());\n                        return existingDevice;\n                    })\n                    .flatMap(this::removeRememberMeToken);\n            });\n    }\n\n    @Override\n    public Mono<Void> revoke(String principalName, String deviceId) {\n        return client.fetch(Device.class, deviceId)\n            .filter(device -> device.getSpec().getPrincipalName().equals(principalName))\n            .flatMap(this::removeRememberMeToken)\n            .flatMap(client::delete)\n            .flatMap(revoked -> sessionRepository.deleteById(revoked.getSpec().getSessionId()));\n    }\n\n    @Override\n    public Mono<Void> revoke(String username) {\n        var listOptions = ListOptions.builder()\n            .andQuery(Queries.equal(\"spec.principalName\", username))\n            .build();\n        return client.listAll(Device.class, listOptions, defaultSort())\n            .flatMap(this::removeRememberMeToken)\n            .flatMap(device -> sessionRepository.deleteById(device.getSpec().getSessionId())\n                .thenReturn(device)\n            )\n            .flatMap(client::delete)\n            .then();\n    }\n\n    private Mono<Device> removeRememberMeToken(Device device) {\n        var seriesId = device.getSpec().getRememberMeSeriesId();\n        if (StringUtils.isBlank(seriesId)) {\n            return Mono.just(device);\n        }\n        log.debug(\"Removing remember-me token for seriesId: {}\", seriesId);\n        return rememberMeTokenRepository.removeToken(seriesId)\n            .thenReturn(device);\n    }\n\n    Mono<Device> createDevice(ServerWebExchange exchange, Authentication authentication) {\n        Assert.notNull(authentication, \"Authentication must not be null.\");\n        return Mono.fromSupplier(\n                () -> {\n                    var device = new Device();\n                    device.setMetadata(new Metadata());\n                    device.getMetadata().setName(generateDeviceId());\n\n                    var userAgent =\n                        exchange.getRequest().getHeaders().getFirst(HttpHeaders.USER_AGENT);\n                    var deviceInfo = DeviceInfo.parse(userAgent);\n                    device.setSpec(new Device.Spec()\n                        .setUserAgent(userAgent)\n                        .setPrincipalName(authentication.getName())\n                        .setLastAuthenticatedTime(Instant.now())\n                        .setIpAddress(getClientIp(exchange.getRequest()))\n                        .setRememberMeSeriesId(\n                            exchange.getAttribute(REMEMBER_ME_SERIES_REQUEST_NAME))\n                    );\n                    device.getStatus()\n                        .setOs(deviceInfo.os())\n                        .setBrowser(deviceInfo.browser());\n                    return device;\n                })\n            .flatMap(device -> exchange.getSession()\n                .doOnNext(session -> {\n                    device.getSpec().setSessionId(session.getId());\n                    device.getSpec().setLastAccessedTime(session.getLastAccessTime());\n                })\n                .thenReturn(device)\n            );\n    }\n\n    String generateDeviceId() {\n        return UUID.randomUUID().toString()\n            .replace(\"-\", \"\").toLowerCase();\n    }\n\n    record DeviceInfo(String browser, String os) {\n        static final String UNKNOWN = \"Unknown\";\n        static final Pattern BROWSER_REGEX =\n            Pattern.compile(\"(MSIE|Trident|Edge|Edg|OPR|Opera|Chrome|Safari|Firefox\"\n                    + \"|FxiOS|SamsungBrowser|UCBrowser|UCWEB|CriOS|Silk|Raven\\\\|Raven\\\\|)\",\n                Pattern.CASE_INSENSITIVE);\n        static final Pattern BROWSER_VERSION_REGEX =\n            Pattern.compile(\"(?:version/|chrome/|firefox/|safari/|msie \"\n                    + \"|rv:|opr/|edg/|ucbrowser/|samsungbrowser/|crios/|silk/)(\\\\d+\\\\.\\\\d+)\",\n                Pattern.CASE_INSENSITIVE);\n\n        static final Pattern OS_REGEX =\n            Pattern.compile(\n                \"(Windows NT|Mac OS X|Android|Linux|iPhone|iPad|Windows Phone|OpenHarmony)\");\n        static final Pattern[] osRegexes = {\n            Pattern.compile(\"Windows NT (\\\\d+\\\\.\\\\d+)\"),\n            Pattern.compile(\"Mac OS X (\\\\d+[\\\\._]\\\\d+([\\\\._]\\\\d+)?)\"),\n            Pattern.compile(\"iPhone OS (\\\\d+_\\\\d+(_\\\\d+)?)\"),\n            Pattern.compile(\"Android (\\\\d+\\\\.\\\\d+(\\\\.\\\\d+)?)\"),\n            Pattern.compile(\"OpenHarmony (\\\\d+\\\\.\\\\d+(\\\\.\\\\d+)?)\")\n        };\n\n        public static DeviceInfo parse(String userAgent) {\n            return new DeviceInfo(concat(parseBrowser(userAgent).name(),\n                parseBrowser(userAgent).version()),\n                concat(parseOperatingSystem(userAgent).name(),\n                    parseOperatingSystem(userAgent).version())\n            );\n        }\n\n        private static Pair parseBrowser(String userAgent) {\n            Matcher matcher = BROWSER_REGEX.matcher(userAgent);\n            if (matcher.find()) {\n                String browserName = matcher.group(1);\n                matcher = BROWSER_VERSION_REGEX.matcher(userAgent);\n\n                if (matcher.find()) {\n                    String browserVersion = matcher.group(1);\n                    return new Pair(browserName, browserVersion);\n                } else {\n                    return new Pair(browserName, null);\n                }\n            } else {\n                return new Pair(UNKNOWN, null);\n            }\n        }\n\n        record Pair(String name, String version) {\n        }\n\n        private static Pair parseOperatingSystem(String userAgent) {\n            Matcher matcher = OS_REGEX.matcher(userAgent);\n            var osName = UNKNOWN;\n            if (matcher.find()) {\n                osName = matcher.group(1);\n            }\n            var osVersion = parseOsVersion(userAgent);\n            return new Pair(osName, osVersion);\n        }\n\n        private static String parseOsVersion(String userAgent) {\n            for (Pattern pattern : osRegexes) {\n                Matcher matcher = pattern.matcher(userAgent);\n                if (matcher.find()) {\n                    return matcher.group(1).replace(\"_\", \".\");\n                }\n            }\n            return \"\";\n        }\n\n        private static String concat(String name, String version) {\n            return StringUtils.isBlank(version) ? name : name + \" \" + version;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/DeviceSessionFilter.java",
    "content": "package run.halo.app.security.device;\n\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n@Slf4j\n@RequiredArgsConstructor\nclass DeviceSessionFilter implements WebFilter {\n\n    private final DeviceService deviceService;\n\n    @Override\n    @NonNull\n    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {\n        return exchange.getSession().flatMap(session -> {\n            var previousId = session.getId();\n            return chain.filter(exchange)\n                .then(Mono.defer(() -> {\n                    var currentId = session.getId();\n                    if (Objects.equals(previousId, currentId)) {\n                        return Mono.empty();\n                    }\n                    // only when session id changed\n                    log.debug(\"Session ID changed from {} to {}, updating device info.\",\n                        previousId, currentId);\n                    return deviceService.changeSessionId(exchange);\n                }));\n        });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/NewDeviceLoginEvent.java",
    "content": "package run.halo.app.security.device;\n\nimport lombok.Getter;\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.core.extension.Device;\n\n@Getter\npublic class NewDeviceLoginEvent extends ApplicationEvent {\n    private final Device device;\n\n    public NewDeviceLoginEvent(Object source, Device device) {\n        super(source);\n        this.device = device;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/device/NewDeviceLoginListener.java",
    "content": "package run.halo.app.security.device;\n\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeFormatter;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Device;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.notification.NotificationReasonEmitter;\nimport run.halo.app.notification.ReasonAttributes;\nimport run.halo.app.notification.UserIdentity;\n\n/**\n * <p>Sends a notification when a new device login,It listens for {@link NewDeviceLoginEvent}\n * asynchronously.</p>\n *\n * @author guqing\n * @since 2.17.0\n */\n@Component\n@RequiredArgsConstructor\npublic class NewDeviceLoginListener {\n    private static final String REASON_TYPE = \"new-device-login\";\n    private static final DateTimeFormatter DATE_TIME_FORMATTER =\n        DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss O\").withZone(ZoneOffset.systemDefault());\n    private final NotificationCenter notificationCenter;\n    private final NotificationReasonEmitter notificationReasonEmitter;\n\n    @EventListener\n    Mono<Void> onApplicationEvent(@NonNull NewDeviceLoginEvent event) {\n        return subscribeForNewDeviceLoginReason(event.getDevice())\n            .then(sendNewDeviceNotification(event.getDevice()));\n    }\n\n    Mono<Void> sendNewDeviceNotification(Device device) {\n        return notificationReasonEmitter.emit(REASON_TYPE, builder -> {\n            var attributes = new ReasonAttributes();\n            attributes.put(\"principalName\", device.getSpec().getPrincipalName());\n            attributes.put(\"os\", device.getStatus().getOs());\n            attributes.put(\"browser\", device.getStatus().getBrowser());\n            attributes.put(\"ipAddress\", device.getSpec().getIpAddress());\n            attributes.put(\"loginTime\",\n                DATE_TIME_FORMATTER.format(device.getSpec().getLastAuthenticatedTime()));\n            builder.attributes(attributes)\n                .author(UserIdentity.of(device.getSpec().getPrincipalName()))\n                .subject(Reason.Subject.builder()\n                    .apiVersion(Device.GROUP + \"/\" + Device.VERSION)\n                    .kind(Device.KIND)\n                    .name(device.getMetadata().getName())\n                    .title(\"在新设备上登录\")\n                    .build());\n        });\n    }\n\n    Mono<Void> subscribeForNewDeviceLoginReason(Device device) {\n        var principalName = device.getSpec().getPrincipalName();\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(principalName);\n\n        var reason = new Subscription.InterestReason();\n        reason.setReasonType(REASON_TYPE);\n        reason.setExpression(\"props.principalName == '%s'\".formatted(principalName));\n        return notificationCenter.subscribe(subscriber, reason)\n            .then();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/jackson2/HaloOAuth2AuthenticationTokenMixin.java",
    "content": "package run.halo.app.security.jackson2;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport run.halo.app.security.authentication.oauth2.HaloOAuth2AuthenticationToken;\n\n/**\n * Mixin for {@link HaloOAuth2AuthenticationToken}.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)\n@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,\n    getterVisibility = JsonAutoDetect.Visibility.NONE,\n    isGetterVisibility = JsonAutoDetect.Visibility.NONE)\n@JsonIgnoreProperties(ignoreUnknown = true)\nabstract class HaloOAuth2AuthenticationTokenMixin {\n\n    @JsonCreator\n    HaloOAuth2AuthenticationTokenMixin(\n        @JsonProperty(\"userDetails\") UserDetails userDetails,\n        @JsonProperty(\"original\") OAuth2AuthenticationToken original\n    ) {\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/jackson2/HaloSecurityJackson2Module.java",
    "content": "package run.halo.app.security.jackson2;\n\nimport com.fasterxml.jackson.core.Version;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport org.springframework.security.jackson2.SecurityJackson2Modules;\nimport org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;\nimport run.halo.app.security.authentication.login.HaloUser;\nimport run.halo.app.security.authentication.oauth2.HaloOAuth2AuthenticationToken;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;\n\n/**\n * Halo security Jackson2 module.\n *\n * @author johnniang\n */\npublic class HaloSecurityJackson2Module extends SimpleModule {\n\n    public HaloSecurityJackson2Module() {\n        super(HaloSecurityJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));\n    }\n\n    @Override\n    public void setupModule(SetupContext context) {\n        SecurityJackson2Modules.enableDefaultTyping(context.getOwner());\n        context.setMixInAnnotations(HaloUser.class, HaloUserMixin.class);\n        context.setMixInAnnotations(\n            TwoFactorAuthentication.class, TwoFactorAuthenticationMixin.class\n        );\n        context.setMixInAnnotations(\n            HaloOAuth2AuthenticationToken.class, HaloOAuth2AuthenticationTokenMixin.class\n        );\n        context.setMixInAnnotations(\n            SwitchUserGrantedAuthority.class, SwitchUserGrantedAuthorityMixIn.class\n        );\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/jackson2/HaloUserMixin.java",
    "content": "package run.halo.app.security.jackson2;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport org.springframework.security.core.userdetails.UserDetails;\n\n@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)\n@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility =\n    JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)\n@JsonIgnoreProperties(ignoreUnknown = true)\nabstract class HaloUserMixin {\n\n    HaloUserMixin(@JsonProperty(\"delegate\") UserDetails delegate,\n        @JsonProperty(\"twoFactorAuthEnabled\") boolean twoFactorAuthEnabled,\n        @JsonProperty(\"totpEncryptedSecret\") String totpEncryptedSecret) {\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/jackson2/SwitchUserGrantedAuthorityMixIn.java",
    "content": "/*\n * Copyright 2002-2024 the original author or authors.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage run.halo.app.security.jackson2;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;\nimport org.springframework.security.web.jackson2.WebServletJackson2Module;\n\n/**\n * Jackson mixin class to serialize/deserialize {@link SwitchUserGrantedAuthority}.\n * <b>This class is copied from repository spring-projects/spring-security.</b>\n *\n * @author Markus Heiden\n * @see WebServletJackson2Module\n * @see org.springframework.security.jackson2.SecurityJackson2Modules\n * @since 6.3\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)\n@JsonAutoDetect(\n    fieldVisibility = JsonAutoDetect.Visibility.ANY,\n    getterVisibility = JsonAutoDetect.Visibility.NONE,\n    isGetterVisibility = JsonAutoDetect.Visibility.NONE\n)\n@JsonIgnoreProperties(ignoreUnknown = true)\nabstract class SwitchUserGrantedAuthorityMixIn {\n\n    @JsonCreator\n    SwitchUserGrantedAuthorityMixIn(\n        @JsonProperty(\"role\") String role,\n        @JsonProperty(\"source\") Authentication source\n    ) {\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/jackson2/TwoFactorAuthenticationMixin.java",
    "content": "package run.halo.app.security.jackson2;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport org.springframework.security.core.Authentication;\n\n/**\n * This mixin class is used to serialize/deserialize TwoFactorAuthentication.\n *\n * @author johnniang\n */\n@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)\n@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,\n    getterVisibility = JsonAutoDetect.Visibility.NONE,\n    isGetterVisibility = JsonAutoDetect.Visibility.NONE)\n@JsonIgnoreProperties(ignoreUnknown = true)\nabstract class TwoFactorAuthenticationMixin {\n\n    @JsonCreator\n    TwoFactorAuthenticationMixin(\n        @JsonProperty(\"previous\") Authentication previous\n    ) {\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/DefaultPasswordResetAvailabilityProviders.java",
    "content": "package run.halo.app.security.preauth;\n\nimport java.util.List;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.properties.SecurityProperties;\nimport run.halo.app.infra.properties.SecurityProperties.PasswordResetMethod;\n\n/**\n * Default password reset availability providers.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\npublic class DefaultPasswordResetAvailabilityProviders\n    implements PasswordResetAvailabilityProviders {\n\n    private final SecurityProperties securityProperties;\n\n    private final List<PasswordResetAvailabilityProvider> providers;\n\n    public DefaultPasswordResetAvailabilityProviders(HaloProperties haloProperties,\n        ObjectProvider<PasswordResetAvailabilityProvider> providers) {\n        this.securityProperties = haloProperties.getSecurity();\n        this.providers = providers.orderedStream().toList();\n    }\n\n    @Override\n    public Flux<PasswordResetMethod> getAvailableMethods() {\n        return Flux.fromIterable(securityProperties.getPasswordResetMethods())\n            .filterWhen(method -> providers.stream()\n                .filter(provider -> provider.support(method.getName()))\n                .findFirst()\n                .map(provider -> provider.isAvailable(method))\n                .orElseGet(() -> Mono.just(false))\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/EmailPasswordResetAvailabilityProvider.java",
    "content": "package run.halo.app.security.preauth;\n\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.properties.SecurityProperties;\n\n/**\n * Email password reset availability provider.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\npublic class EmailPasswordResetAvailabilityProvider implements PasswordResetAvailabilityProvider {\n\n    @Override\n    public Mono<Boolean> isAvailable(SecurityProperties.PasswordResetMethod method) {\n        // TODO Check the email notifier is available in the future\n        return Mono.just(true);\n    }\n\n    @Override\n    public boolean support(String name) {\n        return \"email\".equals(name);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/PasswordResetAvailabilityProvider.java",
    "content": "package run.halo.app.security.preauth;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.properties.SecurityProperties;\n\n/**\n * Password reset availability provider.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface PasswordResetAvailabilityProvider {\n\n    /**\n     * Check if the password reset method is available.\n     *\n     * @param method password reset method\n     * @return true if available, false otherwise\n     */\n    Mono<Boolean> isAvailable(SecurityProperties.PasswordResetMethod method);\n\n    /**\n     * Check if the provider supports the name.\n     *\n     * @param name password reset method name\n     * @return true if supports, false otherwise\n     */\n    boolean support(String name);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/PasswordResetAvailabilityProviders.java",
    "content": "package run.halo.app.security.preauth;\n\nimport reactor.core.publisher.Flux;\nimport run.halo.app.infra.properties.SecurityProperties.PasswordResetMethod;\n\n/**\n * Password reset availability providers.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic interface PasswordResetAvailabilityProviders {\n\n    /**\n     * Get available password reset methods.\n     *\n     * @return available password reset methods\n     */\n    Flux<PasswordResetMethod> getAvailableMethods();\n\n    /**\n     * Get other available password reset methods.\n     *\n     * @param methodName method name\n     * @return other available password reset methods\n     */\n    default Flux<PasswordResetMethod> getOtherAvailableMethods(String methodName) {\n        return getAvailableMethods().filter(method -> !method.getName().equals(methodName));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/PreAuthEmailPasswordResetEndpoint.java",
    "content": "package run.halo.app.security.preauth;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\nimport static run.halo.app.infra.ValidationUtils.validate;\n\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport io.github.resilience4j.ratelimiter.RequestNotPermitted;\nimport io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;\nimport jakarta.validation.constraints.Email;\nimport jakarta.validation.constraints.NotBlank;\nimport jakarta.validation.constraints.Pattern;\nimport jakarta.validation.constraints.Size;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport lombok.Data;\nimport org.springframework.context.MessageSource;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.stereotype.Component;\nimport org.springframework.validation.Validator;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.EmailPasswordRecoveryService;\nimport run.halo.app.core.user.service.InvalidResetTokenException;\nimport run.halo.app.infra.ValidationUtils;\nimport run.halo.app.infra.actuator.GlobalInfoService;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.IpAddressUtils;\n\n/**\n * Pre-auth password reset endpoint.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\nclass PreAuthEmailPasswordResetEndpoint {\n\n    private static final String SEND_TEMPLATE = \"password-reset/email/send\";\n    private static final String RESET_TEMPLATE = \"password-reset/email/reset\";\n\n    private final RateLimiterRegistry rateLimiterRegistry;\n\n    public PreAuthEmailPasswordResetEndpoint(\n        RateLimiterRegistry rateLimiterRegistry\n    ) {\n        this.rateLimiterRegistry = rateLimiterRegistry;\n    }\n\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE + 100)\n    RouterFunction<ServerResponse> preAuthPasswordResetEndpoints(\n        GlobalInfoService globalInfoService,\n        PasswordResetAvailabilityProviders availabilityProviders,\n        MessageSource messageSource,\n        EmailPasswordRecoveryService emailService,\n        Validator validator\n    ) {\n        return RouterFunctions.nest(path(\"/password-reset/email\"), RouterFunctions.route()\n            .GET(\"\", request -> request.bind(SendForm.class)\n                .flatMap(sendForm -> ServerResponse.ok().render(SEND_TEMPLATE, Map.of(\n                    \"otherMethods\", availabilityProviders.getOtherAvailableMethods(\"email\"),\n                    \"globalInfo\", globalInfoService.getGlobalInfo(),\n                    \"form\", sendForm\n                )))\n            )\n            .GET(\"/{resetToken}\",\n                request -> {\n                    var token = request.pathVariable(\"resetToken\");\n                    return request.bind(ResetForm.class)\n                        .flatMap(resetForm -> {\n                            var model = new HashMap<String, Object>();\n                            model.put(\"form\", resetForm);\n                            model.put(\"globalInfo\", globalInfoService.getGlobalInfo());\n                            return emailService.getValidResetToken(token)\n                                .flatMap(resetToken -> {\n                                    // TODO Check the 2FA of the user\n                                    model.put(\"username\", resetToken.username());\n                                    return ServerResponse.ok().render(RESET_TEMPLATE, model);\n                                })\n                                .transformDeferred(rateLimiterForPasswordResetVerification(\n                                    request.exchange().getRequest()\n                                ))\n                                .onErrorResume(InvalidResetTokenException.class, e ->\n                                    ServerResponse.status(HttpStatus.FOUND)\n                                        .location(URI.create(\n                                            \"/password-reset/email?error=invalid_reset_token\")\n                                        )\n                                        .build()\n                                )\n                                .onErrorResume(RequestNotPermitted.class, e -> {\n                                    model.put(\"error\", \"rate_limit_exceeded\");\n                                    return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)\n                                        .render(RESET_TEMPLATE, model);\n                                });\n                        });\n                }\n            )\n            .POST(\"/{resetToken}\", request -> {\n                var token = request.pathVariable(\"resetToken\");\n                return request.bind(ResetForm.class)\n                    .flatMap(resetForm -> emailService.getValidResetToken(token)\n                        .flatMap(resetToken -> {\n                            var bindingResult = validate(resetForm, validator, request.exchange());\n                            var model = bindingResult.getModel();\n                            model.put(\"globalInfo\", globalInfoService.getGlobalInfo());\n                            model.put(\"username\", resetToken.username());\n                            if (!Objects.equals(\n                                resetForm.getPassword(), resetForm.getConfirmPassword()\n                            )) {\n                                bindingResult.rejectValue(\n                                    \"confirmPassword\",\n                                    \"validation.error.password.confirmPassword.mismatch\",\n                                    \"Password and confirm password mismatch\"\n                                );\n                            }\n                            if (bindingResult.hasErrors()) {\n                                return ServerResponse.badRequest().render(RESET_TEMPLATE, model);\n                            }\n                            return emailService.changePassword(resetForm.getPassword(), token)\n                                .then(ServerResponse.status(HttpStatus.FOUND)\n                                    .location(URI.create(\"/login?password_reset\"))\n                                    .build()\n                                )\n                                .transformDeferred(rateLimiterForPasswordResetVerification(\n                                    request.exchange().getRequest()\n                                ))\n                                .onErrorResume(RequestNotPermitted.class, e -> {\n                                    model.put(\"error\", \"rate_limit_exceeded\");\n                                    return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)\n                                        .render(RESET_TEMPLATE, model);\n                                });\n                        })\n                        .onErrorResume(InvalidResetTokenException.class,\n                            e -> ServerResponse.status(HttpStatus.FOUND)\n                                .location(URI.create(\n                                    \"/password-reset/email?error=invalid_reset_token\"\n                                ))\n                                .build()\n                        )\n                    );\n            })\n            .POST(\"\", contentType(MediaType.APPLICATION_FORM_URLENCODED),\n                request -> request.bind(SendForm.class)\n                    .flatMap(sendForm -> {\n                        // validate the send form\n                        var bindingResult = validate(sendForm, validator, request.exchange());\n                        var model = bindingResult.getModel();\n                        model.put(\"globalInfo\", globalInfoService.getGlobalInfo());\n                        if (bindingResult.hasErrors()) {\n                            return ServerResponse.badRequest().render(SEND_TEMPLATE, model);\n                        }\n                        var email = sendForm.getEmail().toLowerCase();\n                        return emailService.sendPasswordResetEmail(email)\n                            .then(Mono.defer(() -> {\n                                model.put(\"sent\", true);\n                                return ServerResponse.ok().render(SEND_TEMPLATE, model);\n                            }))\n                            .transformDeferred(rateLimiterForSendPasswordResetEmail(\n                                request.exchange().getRequest()\n                            ))\n                            .onErrorResume(RequestNotPermitted.class, e -> {\n                                model.put(\"error\", \"rate_limit_exceeded\");\n                                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)\n                                    .render(SEND_TEMPLATE, model);\n                            });\n                    })\n            )\n            .before(HaloUtils.noCache())\n            .build());\n    }\n\n    <T> RateLimiterOperator<T> rateLimiterForSendPasswordResetEmail(ServerHttpRequest request) {\n        var clientIp = IpAddressUtils.getClientIp(request);\n        var rateLimiterKey = \"send-password-reset-email-from-\" + clientIp;\n        var rateLimiter =\n            rateLimiterRegistry.rateLimiter(rateLimiterKey, \"send-password-reset-email\");\n        return RateLimiterOperator.of(rateLimiter);\n    }\n\n    <T> RateLimiterOperator<T> rateLimiterForPasswordResetVerification(ServerHttpRequest request) {\n        var clientIp = IpAddressUtils.getClientIp(request);\n        var rateLimiterKey = \"password-reset-email-verify-from-\" + clientIp;\n        var rateLimiter =\n            rateLimiterRegistry.rateLimiter(rateLimiterKey, \"password-reset-verification\");\n        return RateLimiterOperator.of(rateLimiter);\n    }\n\n    @Data\n    static class ResetForm {\n\n        @NotBlank\n        @Pattern(\n            regexp = ValidationUtils.PASSWORD_REGEX,\n            message = \"{validation.error.password.pattern}\"\n        )\n        @Size(min = 5, max = 257)\n        private String password;\n\n        @NotBlank\n        private String confirmPassword;\n\n    }\n\n    @Data\n    static class SendForm {\n\n        @NotBlank\n        @Email\n        private String email;\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/PreAuthLoginEndpoint.java",
    "content": "package run.halo.app.security.preauth;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\n\nimport java.net.URI;\nimport java.util.Base64;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.AuthProvider;\nimport run.halo.app.infra.actuator.GlobalInfoService;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.plugin.PluginConst;\nimport run.halo.app.security.AuthProviderService;\nimport run.halo.app.security.HaloServerRequestCache;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authentication.rememberme.RememberMeRequestCache;\nimport run.halo.app.security.authentication.rememberme.WebSessionRememberMeRequestCache;\n\n/**\n * Pre-auth login endpoints.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\nclass PreAuthLoginEndpoint {\n\n    private final CryptoService cryptoService;\n\n    private final GlobalInfoService globalInfoService;\n\n    private final AuthProviderService authProviderService;\n\n    private final ServerRequestCache serverRequestCache = new HaloServerRequestCache();\n\n    private final RememberMeRequestCache rememberMeRequestCache =\n        new WebSessionRememberMeRequestCache();\n\n    PreAuthLoginEndpoint(CryptoService cryptoService, GlobalInfoService globalInfoService,\n        AuthProviderService authProviderService) {\n        this.cryptoService = cryptoService;\n        this.globalInfoService = globalInfoService;\n        this.authProviderService = authProviderService;\n    }\n\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE + 100)\n    RouterFunction<ServerResponse> preAuthLoginEndpoints() {\n        return RouterFunctions.nest(path(\"/login\"), RouterFunctions.route()\n            .GET(\"\", request -> {\n                var exchange = request.exchange();\n                var contextPath = exchange.getRequest().getPath().contextPath().value();\n                var publicKey = cryptoService.readPublicKey()\n                    .map(key -> Base64.getEncoder().encodeToString(key));\n                var globalInfo = globalInfoService.getGlobalInfo().cache();\n                var authProviders = authProviderService.getEnabledProviders().cache();\n\n                var allFormProviders = authProviders\n                    .filter(ap -> AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))\n                    .cache();\n\n                var authProvider = Mono.justOrEmpty(request.queryParam(\"method\"))\n                    .flatMap(method -> allFormProviders\n                        .filter(ap -> Objects.equals(method, ap.getMetadata().getName()))\n                        .next()\n                        .switchIfEmpty(Mono.error(\n                            () -> new ServerWebInputException(\"Invalid login method \" + method))\n                        )\n                    )\n                    .switchIfEmpty(allFormProviders.next())\n                    .cache();\n\n                var fragmentTemplateName = authProvider.map(ap -> {\n                    var templateName = \"login_\" + ap.getMetadata().getName();\n                    return Optional.ofNullable(ap.getMetadata().getLabels())\n                        .map(labels -> labels.get(PluginConst.PLUGIN_NAME_LABEL_NAME))\n                        .filter(StringUtils::isNotBlank)\n                        .map(pluginName -> String.join(\":\", \"plugin\", pluginName, templateName))\n                        .orElse(templateName);\n                });\n\n                var socialAuthProviders = authProviders\n                    .filter(ap -> !AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))\n                    .cache();\n                var formAuthProviders = allFormProviders\n                    .filterWhen(ap -> authProvider\n                        .map(provider -> !Objects.equals(provider.getMetadata().getName(),\n                            ap.getMetadata().getName())\n                        )\n                    )\n                    .cache();\n\n                return serverRequestCache.saveRequest(exchange).then(Mono.defer(() ->\n                    ServerResponse.ok().render(\"login\", Map.of(\n                        \"action\", contextPath + \"/login\",\n                        \"publicKey\", publicKey,\n                        \"globalInfo\", globalInfo,\n                        \"authProvider\", authProvider,\n                        \"fragmentTemplateName\", fragmentTemplateName,\n                        \"socialAuthProviders\", socialAuthProviders,\n                        \"formAuthProviders\", formAuthProviders,\n                        \"rememberMe\", rememberMeRequestCache.isRememberMe(exchange)\n                        // TODO Add more models here\n                    ))\n                ));\n            })\n            .POST(\"/social/{authProviderName}\", request -> {\n                var authProviderName = request.pathVariable(\"authProviderName\");\n                return authProviderService.getEnabledProviders()\n                    .filter(ap -> Objects.equals(authProviderName, ap.getMetadata().getName()))\n                    .filter(ap -> !AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))\n                    .next()\n                    .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                        \"Auth provider \" + authProviderName + \" not found or not enabled.\"\n                    )))\n                    .flatMap(ap -> {\n                        var authenticationUrl = ap.getSpec().getAuthenticationUrl();\n                        return rememberMeRequestCache.saveRememberMe(request.exchange())\n                            .then(Mono.defer(() -> ServerResponse.status(HttpStatus.FOUND)\n                                .location(URI.create(authenticationUrl))\n                                .build()\n                            ));\n                    });\n            })\n            .before(HaloUtils.noCache())\n            .build());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/PreAuthSignUpEndpoint.java",
    "content": "package run.halo.app.security.preauth;\n\nimport static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;\nimport static org.springframework.http.MediaType.APPLICATION_JSON;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\nimport static run.halo.app.infra.ValidationUtils.validate;\n\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport io.github.resilience4j.ratelimiter.RequestNotPermitted;\nimport io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;\nimport jakarta.validation.constraints.Email;\nimport jakarta.validation.constraints.NotBlank;\nimport java.net.URI;\nimport lombok.Data;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.stereotype.Component;\nimport org.springframework.validation.BeanPropertyBindingResult;\nimport org.springframework.validation.FieldError;\nimport org.springframework.validation.Validator;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.EmailVerificationService;\nimport run.halo.app.core.user.service.SignUpData;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.infra.actuator.GlobalInfoService;\nimport run.halo.app.infra.exception.DuplicateNameException;\nimport run.halo.app.infra.exception.EmailAlreadyTakenException;\nimport run.halo.app.infra.exception.EmailVerificationFailed;\nimport run.halo.app.infra.exception.RateLimitExceededException;\nimport run.halo.app.infra.exception.RequestBodyValidationException;\nimport run.halo.app.infra.exception.RestrictedNameException;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.IpAddressUtils;\n\n/**\n * Pre-auth sign up endpoint.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\nclass PreAuthSignUpEndpoint {\n\n    private final GlobalInfoService globalInfoService;\n\n    private final Validator validator;\n\n    private final UserService userService;\n\n    private final EmailVerificationService emailVerificationService;\n\n    private final RateLimiterRegistry rateLimiterRegistry;\n\n    PreAuthSignUpEndpoint(GlobalInfoService globalInfoService,\n        Validator validator,\n        UserService userService,\n        EmailVerificationService emailVerificationService,\n        RateLimiterRegistry rateLimiterRegistry) {\n        this.globalInfoService = globalInfoService;\n        this.validator = validator;\n        this.userService = userService;\n        this.emailVerificationService = emailVerificationService;\n        this.rateLimiterRegistry = rateLimiterRegistry;\n    }\n\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE + 100)\n    RouterFunction<ServerResponse> preAuthSignUpEndpoints() {\n        return RouterFunctions.nest(path(\"/signup\"), RouterFunctions.route()\n            .GET(\"\", request -> {\n                var signUpData = new SignUpData();\n                var bindingResult = new BeanPropertyBindingResult(signUpData, \"form\");\n                var model = bindingResult.getModel();\n                model.put(\"globalInfo\", globalInfoService.getGlobalInfo());\n                return ServerResponse.ok().render(\"signup\", model);\n            })\n            .POST(\n                \"\",\n                contentType(APPLICATION_FORM_URLENCODED),\n                request -> request.bind(SignUpData.class)\n                    .flatMap(signUpData -> {\n                        // sign up\n                        var bindingResult = validate(signUpData, validator, request.exchange());\n                        var model = bindingResult.getModel();\n                        model.put(\"globalInfo\", globalInfoService.getGlobalInfo());\n                        if (bindingResult.hasErrors()) {\n                            return ServerResponse.ok().render(\"signup\", model);\n                        }\n                        return userService.signUp(signUpData)\n                            .flatMap(user -> ServerResponse.status(HttpStatus.FOUND)\n                                .location(URI.create(\"/login?signup\"))\n                                .build()\n                            )\n                            .doOnError(t -> {\n                                model.put(\"error\", \"unknown\");\n                                model.put(\"errorMessage\", t.getMessage());\n                            })\n                            .doOnError(EmailVerificationFailed.class,\n                                e -> {\n                                    bindingResult.addError(new FieldError(\"form\",\n                                        \"emailCode\",\n                                        signUpData.getEmailCode(),\n                                        true,\n                                        new String[] {\"signup.error.email-code.invalid\"},\n                                        null,\n                                        \"Invalid Email Code\"));\n                                }\n                            )\n                            .doOnError(EmailAlreadyTakenException.class, e -> {\n                                bindingResult.addError(new FieldError(\"form\",\n                                    \"email\",\n                                    signUpData.getEmail(),\n                                    true,\n                                    new String[] {\"signup.error.email.already-taken\"},\n                                    null,\n                                    \"Email Already Taken\"));\n                            })\n                            .doOnError(RateLimitExceededException.class,\n                                e -> model.put(\"error\", \"rate-limit-exceeded\")\n                            )\n                            .doOnError(DuplicateNameException.class,\n                                e -> model.put(\"error\", \"duplicate-username\")\n                            )\n                            .doOnError(RestrictedNameException.class,\n                                e -> model.put(\"error\", \"restricted-username\")\n                            )\n                            .onErrorResume(e -> ServerResponse.ok().render(\"signup\", model));\n                    })\n            )\n            .POST(\"/send-email-code\", contentType(APPLICATION_JSON),\n                request -> request.bodyToMono(SendEmailCodeBody.class)\n                    .flatMap(body -> {\n                        var bindingResult = validate(body, \"body\", validator, request.exchange());\n                        if (bindingResult.hasErrors()) {\n                            return Mono.error(new RequestBodyValidationException(bindingResult));\n                        }\n                        var email = body.getEmail();\n                        return emailVerificationService.sendRegisterVerificationCode(email)\n                            .transformDeferred(\n                                rateLimiterForSendingEmailCode(request.exchange().getRequest())\n                            )\n                            .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);\n                    })\n                    .then(ServerResponse.accepted().build())\n            )\n            .before(HaloUtils.noCache())\n            .build());\n    }\n\n    private <T> RateLimiterOperator<T> rateLimiterForSendingEmailCode(ServerHttpRequest request) {\n        var clientIp = IpAddressUtils.getClientIp(request);\n        var rateLimiterKey = \"send-email-code-for-signing-up-from-\" + clientIp;\n        var rateLimiter =\n            rateLimiterRegistry.rateLimiter(rateLimiterKey, \"send-email-verification-code\");\n        return RateLimiterOperator.of(rateLimiter);\n    }\n\n\n    @Data\n    public static class SendEmailCodeBody {\n\n        @Email\n        @NotBlank\n        String email;\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/PreAuthTwoFactorEndpoint.java",
    "content": "package run.halo.app.security.preauth;\n\nimport java.util.Map;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.infra.actuator.GlobalInfoService;\nimport run.halo.app.infra.utils.HaloUtils;\n\n/**\n * Pre-auth two-factor endpoints.\n *\n * @author johnniang\n * @since 2.20.0\n */\n@Component\nclass PreAuthTwoFactorEndpoint {\n\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE + 100)\n    RouterFunction<ServerResponse> preAuthTwoFactorEndpoints(GlobalInfoService globalInfoService) {\n        return RouterFunctions.route()\n            .GET(\"/challenges/two-factor/totp\",\n                request -> ServerResponse.ok().render(\"challenges/two-factor/totp\", Map.of(\n                    \"globalInfo\", globalInfoService.getGlobalInfo()\n                ))\n            )\n            .before(HaloUtils.noCache())\n            .build();\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/preauth/SystemSetupEndpoint.java",
    "content": "package run.halo.app.security.preauth;\n\nimport static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.path;\nimport static run.halo.app.infra.ValidationUtils.validate;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.Email;\nimport jakarta.validation.constraints.NotBlank;\nimport jakarta.validation.constraints.NotNull;\nimport jakarta.validation.constraints.Pattern;\nimport jakarta.validation.constraints.Size;\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Properties;\nimport lombok.Data;\nimport lombok.RequiredArgsConstructor;\nimport org.hibernate.validator.constraints.URL;\nimport org.springdoc.core.fn.builders.content.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.beans.factory.config.PlaceholderConfigurerSupport;\nimport org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.util.InMemoryResource;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.PropertyPlaceholderHelper;\nimport org.springframework.util.StreamUtils;\nimport org.springframework.validation.BeanPropertyBindingResult;\nimport org.springframework.validation.BindingResult;\nimport org.springframework.validation.Validator;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.ExternalUrlChangedEvent;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.InitializationStateGetter;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemState;\nimport run.halo.app.infra.ValidationUtils;\nimport run.halo.app.infra.exception.RequestBodyValidationException;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\nimport run.halo.app.plugin.PluginService;\nimport run.halo.app.security.SuperAdminInitializer;\nimport run.halo.app.theme.service.ThemeService;\n\n@Component\n@RequiredArgsConstructor\npublic class SystemSetupEndpoint {\n    static final String SETUP_TEMPLATE = \"setup\";\n    static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =\n        new PropertyPlaceholderHelper(\n            PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_PREFIX,\n            PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_SUFFIX\n        );\n\n    private final InitializationStateGetter initializationStateGetter;\n    private final SystemConfigFetcher systemConfigFetcher;\n    private final SuperAdminInitializer superAdminInitializer;\n    private final ReactiveExtensionClient client;\n    private final PluginService pluginService;\n    private final ThemeService themeService;\n    private final Validator validator;\n    private final ObjectProvider<R2dbcConnectionDetails> connectionDetails;\n    private final ExternalUrlSupplier externalUrl;\n    private final ApplicationEventPublisher eventPublisher;\n\n    @Bean\n    @Order(Ordered.HIGHEST_PRECEDENCE + 100)\n    RouterFunction<ServerResponse> setupPageRouter() {\n        final var tag = \"SystemV1alpha1Public\";\n        return SpringdocRouteBuilder.route()\n            .GET(path(\"/system/setup\").and(accept(MediaType.TEXT_HTML)), this::setupPage,\n                builder -> builder.operationId(\"JumpToSetupPage\")\n                    .description(\"Jump to setup page\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .content(Builder.contentBuilder()\n                            .mediaType(MediaType.TEXT_HTML_VALUE))\n                        .implementation(String.class)\n                    )\n            )\n            .POST(\"/system/setup\", contentType(MediaType.APPLICATION_FORM_URLENCODED), this::setup,\n                builder -> builder\n                    .operationId(\"SetupSystem\")\n                    .description(\"Setup system\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(SetupRequest.class))\n                        )\n                    )\n                    .response(responseBuilder()\n                        .responseCode(String.valueOf(HttpStatus.NO_CONTENT.value()))\n                        .implementation(Void.class)\n                    )\n            )\n            .before(HaloUtils.noCache())\n            .build();\n    }\n\n    private Mono<ServerResponse> setup(ServerRequest request) {\n        return initializationStateGetter.userInitialized()\n            .filter(initialized -> !initialized)\n            .flatMap(ignored -> request.bind(SetupRequest.class)\n                .flatMap(setupRequest -> {\n                    // validate it\n                    var bindingResult = validate(setupRequest, validator, request.exchange());\n                    if (bindingResult.hasErrors()) {\n                        return handleValidationErrors(bindingResult, request);\n                    }\n                    return doInitialization(setupRequest).then(handleSetupSuccessfully(request));\n                })\n            )\n            .switchIfEmpty(redirectToConsole());\n    }\n\n    private static Mono<ServerResponse> handleSetupSuccessfully(ServerRequest request) {\n        if (isHtmlRequest(request)) {\n            return redirectToConsole();\n        }\n        return ServerResponse.noContent().build();\n    }\n\n    private Mono<ServerResponse> handleValidationErrors(BindingResult bindingResult,\n        ServerRequest request) {\n        if (isHtmlRequest(request)) {\n            var model = bindingResult.getModel();\n            model.put(\"usingH2database\", usingH2database());\n            return ServerResponse.status(HttpStatus.BAD_REQUEST)\n                .render(SETUP_TEMPLATE, model);\n        }\n        return Mono.error(new RequestBodyValidationException(bindingResult));\n    }\n\n    private static boolean isHtmlRequest(ServerRequest request) {\n        return request.headers().accept().contains(MediaType.TEXT_HTML)\n            && !HaloUtils.isXhr(request.headers().asHttpHeaders());\n    }\n\n    private static Mono<ServerResponse> redirectToConsole() {\n        return ServerResponse.status(HttpStatus.FOUND).location(URI.create(\"/console\")).build();\n    }\n\n    private Mono<Void> doInitialization(SetupRequest body) {\n        var superUserMono = superAdminInitializer.initialize(\n                SuperAdminInitializer.InitializationParam.builder()\n                    .username(body.getUsername())\n                    .password(body.getPassword())\n                    .email(body.getEmail())\n                    .build()\n            )\n            .subscribeOn(Schedulers.boundedElastic());\n\n        var basicConfigMono = Mono.defer(() -> systemConfigFetcher.getConfigMap()\n                .flatMap(configMap -> {\n                    mergeToBasicConfig(body, configMap);\n                    return client.update(configMap);\n                })\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(t -> t instanceof OptimisticLockingFailureException)\n            )\n            .subscribeOn(Schedulers.boundedElastic())\n            .then(Mono.fromCallable(() -> {\n                eventPublisher.publishEvent(\n                    new ExternalUrlChangedEvent(this, URI.create(body.getExternalUrl()).toURL())\n                );\n                return null;\n            }));\n        return Mono.when(\n                basicConfigMono,\n                superUserMono,\n                initializeNecessaryData(body.getUsername()),\n                pluginService.installPresetPlugins(),\n                themeService.installPresetTheme()\n            )\n            .then(SystemState.upsetSystemState(client, state -> state.setIsSetup(true)));\n    }\n\n    private Mono<Void> initializeNecessaryData(String username) {\n        return loadPresetExtensions(username)\n            .concatMap(client::create)\n            .subscribeOn(Schedulers.boundedElastic())\n            .then();\n    }\n\n    private static void mergeToBasicConfig(SetupRequest body, ConfigMap configMap) {\n        Map<String, String> data = configMap.getData();\n        if (data == null) {\n            data = new LinkedHashMap<>();\n            configMap.setData(data);\n        }\n        String basic = data.getOrDefault(SystemSetting.Basic.GROUP, \"{}\");\n        var basicSetting = JsonUtils.jsonToObject(basic, SystemSetting.Basic.class);\n        basicSetting.setTitle(body.getSiteTitle());\n        basicSetting.setLanguage(body.getLanguage());\n        basicSetting.setExternalUrl(body.getExternalUrl());\n        data.put(SystemSetting.Basic.GROUP, JsonUtils.objectToJson(basicSetting));\n    }\n\n    private Mono<ServerResponse> setupPage(ServerRequest request) {\n        return initializationStateGetter.userInitialized()\n            .flatMap(initialized -> {\n                if (initialized) {\n                    return redirectToConsole();\n                }\n                var setupRequest = new SetupRequest();\n                setupRequest.setExternalUrl(\n                    externalUrl.getURL(request.exchange().getRequest()).toString()\n                );\n                var bindingResult = new BeanPropertyBindingResult(setupRequest, \"form\");\n                var model = bindingResult.getModel();\n                model.put(\"usingH2database\", usingH2database());\n                return ServerResponse.ok().render(SETUP_TEMPLATE, model);\n            });\n    }\n\n    private boolean usingH2database() {\n        var rcd = connectionDetails.getIfUnique();\n        if (rcd == null) {\n            // If no R2dbcConnectionDetails is available, we assume H2(mem) is used.\n            return true;\n        }\n        var options = rcd.getConnectionFactoryOptions();\n        return Optional.ofNullable(options.getValue(DRIVER))\n            .map(Object::toString)\n            .map(\"h2\"::equalsIgnoreCase)\n            .orElse(false);\n    }\n\n    @Data\n    static class SetupRequest {\n\n        @Schema(requiredMode = REQUIRED, minLength = 4, maxLength = 63)\n        @NotBlank\n        @Size(min = 4, max = 63)\n        @Pattern(regexp = ValidationUtils.NAME_REGEX,\n            message = \"{validation.error.username.pattern}\")\n        private String username;\n\n        @Schema(requiredMode = REQUIRED, minLength = 5, maxLength = 257)\n        @NotBlank\n        @Pattern(regexp = ValidationUtils.PASSWORD_REGEX,\n            message = \"{validation.error.password.pattern}\")\n        @Size(min = 5, max = 257)\n        private String password;\n\n        @Email\n        private String email;\n\n        @NotBlank\n        @Size(max = 80)\n        private String siteTitle;\n\n        @Pattern(regexp = \"^(zh-CN|zh-TW|en|es)$\")\n        private String language;\n\n        @NotNull\n        @URL(regexp = \"^(http|https).*\")\n        private String externalUrl;\n    }\n\n    Flux<Unstructured> loadPresetExtensions(String username) {\n        return Mono.fromCallable(\n                () -> {\n                    // read initial-data.yaml to string\n                    var classPathResource = new ClassPathResource(\"initial-data.yaml\");\n                    String rawContent = StreamUtils.copyToString(classPathResource.getInputStream(),\n                        StandardCharsets.UTF_8);\n                    // build properties\n                    var properties = new Properties();\n                    properties.setProperty(\"username\", username);\n                    properties.setProperty(\"timestamp\", Instant.now().toString());\n                    // replace placeholders\n                    var processedContent =\n                        PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(rawContent, properties);\n                    // load yaml to unstructured\n                    var stringResource =\n                        new InMemoryResource(processedContent.getBytes(StandardCharsets.UTF_8));\n                    var loader = new YamlUnstructuredLoader(stringResource);\n                    return loader.load();\n                })\n            .flatMapMany(Flux::fromIterable)\n            .subscribeOn(Schedulers.boundedElastic());\n    }\n}"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/session/InMemoryReactiveIndexedSessionRepository.java",
    "content": "package run.halo.app.security.session;\n\nimport com.google.common.cache.Cache;\nimport com.google.common.cache.CacheBuilder;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.session.DelegatingIndexResolver;\nimport org.springframework.session.IndexResolver;\nimport org.springframework.session.MapSession;\nimport org.springframework.session.PrincipalNameIndexResolver;\nimport org.springframework.session.ReactiveMapSessionRepository;\nimport org.springframework.session.Session;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\npublic class InMemoryReactiveIndexedSessionRepository extends ReactiveMapSessionRepository\n    implements ReactiveIndexedSessionRepository<MapSession>, DisposableBean {\n\n    final IndexResolver<MapSession> indexResolver =\n        new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>(PRINCIPAL_NAME_INDEX_NAME));\n\n    private final ConcurrentMap<String, Set<IndexKey>> sessionIdIndexMap =\n        new ConcurrentHashMap<>();\n    private final ConcurrentMap<IndexKey, Set<String>> indexSessionIdMap =\n        new ConcurrentHashMap<>();\n\n    /**\n     * Prevent other requests from being parsed and acquiring the session during its deletion,\n     * which could result in an unintended renewal. Currently, it acts as a buffer, and having a\n     * slightly prolonged expiration period is sufficient.\n     */\n    private final Cache<String, Boolean> invalidateSessionIds = CacheBuilder.newBuilder()\n        .expireAfterWrite(Duration.ofMinutes(10))\n        .maximumSize(10_000)\n        .build();\n\n    public InMemoryReactiveIndexedSessionRepository(Map<String, Session> sessions) {\n        super(sessions);\n    }\n\n    @Override\n    public Mono<Void> save(MapSession session) {\n        if (invalidateSessionIds.getIfPresent(session.getId()) != null) {\n            return this.deleteById(session.getId());\n        }\n        return super.save(session)\n            .then(updateIndex(session));\n    }\n\n    @Override\n    public Mono<Void> deleteById(String id) {\n        return removeIndex(id)\n            .then(Mono.defer(() -> {\n                invalidateSessionIds.put(id, true);\n                return super.deleteById(id);\n            }));\n    }\n\n    @Override\n    public Mono<Map<String, MapSession>> findByIndexNameAndIndexValue(String indexName,\n        String indexValue) {\n        var indexKey = new IndexKey(indexName, indexValue);\n        return Flux.fromStream((() -> indexSessionIdMap.getOrDefault(indexKey, Set.of()).stream()))\n            .flatMap(this::findById)\n            .collectMap(Session::getId);\n    }\n\n    @Override\n    public Mono<Map<String, MapSession>> findByPrincipalName(String principalName) {\n        return this.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);\n    }\n\n    @Override\n    public void destroy() {\n        sessionIdIndexMap.clear();\n        indexSessionIdMap.clear();\n        invalidateSessionIds.invalidateAll();\n    }\n\n    Mono<Void> removeIndex(String sessionId) {\n        return getIndexes(sessionId)\n            .doOnNext(indexKey -> indexSessionIdMap.computeIfPresent(indexKey,\n                (key, sessionIdSet) -> {\n                    sessionIdSet.remove(sessionId);\n                    return sessionIdSet.isEmpty() ? null : sessionIdSet;\n                })\n            )\n            .then(Mono.defer(() -> {\n                sessionIdIndexMap.remove(sessionId);\n                return Mono.empty();\n            }))\n            .then();\n    }\n\n    Mono<Void> updateIndex(MapSession session) {\n        return removeIndex(session.getId())\n            .then(Mono.defer(() -> {\n                if (!session.getId().equals(session.getOriginalId())) {\n                    return removeIndex(session.getOriginalId());\n                }\n                return Mono.empty();\n            }))\n            .then(Mono.defer(() -> {\n                indexResolver.resolveIndexesFor(session)\n                    .forEach((name, value) -> {\n                        IndexKey indexKey = new IndexKey(name, value);\n                        indexSessionIdMap.computeIfAbsent(indexKey,\n                                unusedSet -> ConcurrentHashMap.newKeySet())\n                            .add(session.getId());\n                        // Update sessionIdIndexMap\n                        sessionIdIndexMap.computeIfAbsent(session.getId(),\n                                unusedSet -> ConcurrentHashMap.newKeySet())\n                            .add(indexKey);\n                    });\n                return Mono.empty();\n            }))\n            .then();\n    }\n\n    Flux<IndexKey> getIndexes(String sessionId) {\n        return Flux.fromIterable(sessionIdIndexMap.getOrDefault(sessionId, Set.of()));\n    }\n\n    /**\n     * For testing purpose.\n     */\n    ConcurrentMap<String, Set<IndexKey>> getSessionIdIndexMap() {\n        return sessionIdIndexMap;\n    }\n\n    /**\n     * For testing purpose.\n     */\n    ConcurrentMap<IndexKey, Set<String>> getIndexSessionIdMap() {\n        return indexSessionIdMap;\n    }\n\n    record IndexKey(String attributeName, String attributeValue) {\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/session/ReactiveIndexedSessionRepository.java",
    "content": "package run.halo.app.security.session;\n\nimport org.springframework.session.ReactiveFindByIndexNameSessionRepository;\nimport org.springframework.session.ReactiveSessionRepository;\nimport org.springframework.session.Session;\n\npublic interface ReactiveIndexedSessionRepository<S extends Session>\n    extends ReactiveSessionRepository<S>, ReactiveFindByIndexNameSessionRepository<S> {\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/session/SessionInvalidationListener.java",
    "content": "package run.halo.app.security.session;\n\nimport java.time.Duration;\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.event.EventListener;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.session.ReactiveFindByIndexNameSessionRepository;\nimport org.springframework.session.ReactiveSessionRepository;\nimport org.springframework.session.Session;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.event.user.PasswordChangedEvent;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n@Component\n@RequiredArgsConstructor\npublic class SessionInvalidationListener {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private final ReactiveFindByIndexNameSessionRepository<? extends Session>\n        indexedSessionRepository;\n    private final ReactiveSessionRepository<? extends Session> sessionRepository;\n\n    @Async\n    @EventListener\n    public void onPasswordChanged(PasswordChangedEvent event) {\n        String username = event.getUsername();\n        // Invalidate session\n        invalidateUserSessions(username);\n    }\n\n    private void invalidateUserSessions(String username) {\n        indexedSessionRepository.findByPrincipalName(username)\n            .map(Map::keySet)\n            .flatMapMany(Flux::fromIterable)\n            .flatMap(sessionRepository::deleteById)\n            .then()\n            .block(BLOCKING_TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/security/switchuser/SwitchUserConfigurer.java",
    "content": "package run.halo.app.security.switchuser;\n\nimport org.springframework.security.config.web.server.SecurityWebFiltersOrder;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;\nimport org.springframework.security.web.server.authentication.SwitchUserWebFilter;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.security.HaloRedirectAuthenticationSuccessHandler;\nimport run.halo.app.security.authentication.SecurityConfigurer;\n\n/**\n * Switch user configurer.\n *\n * @author johnniang\n */\n@Component\nclass SwitchUserConfigurer implements SecurityConfigurer {\n\n    private final ReactiveUserDetailsService userDetailsService;\n\n    SwitchUserConfigurer(ReactiveUserDetailsService userDetailsService) {\n        this.userDetailsService = userDetailsService;\n    }\n\n    @Override\n    public void configure(ServerHttpSecurity http) {\n        var successHandler = new HaloRedirectAuthenticationSuccessHandler(\"/console\");\n        var failureHandler =\n            new RedirectServerAuthenticationFailureHandler(\"/login?error=impersonate\");\n        var filter = new SwitchUserWebFilter(userDetailsService, successHandler, failureHandler);\n        http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHORIZATION);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/CompositeTemplateResolver.java",
    "content": "package run.halo.app.theme;\n\nimport static java.util.Comparator.comparing;\nimport static java.util.Comparator.naturalOrder;\nimport static java.util.Comparator.nullsLast;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport org.thymeleaf.templateresolver.TemplateResolution;\nimport run.halo.app.infra.exception.NotFoundException;\n\n/**\n * Composite template resolver to control execution flow of multiple template resolvers.\n *\n * @author johnniang\n */\nclass CompositeTemplateResolver implements ITemplateResolver {\n\n    private final List<ITemplateResolver> resolvers;\n\n    public CompositeTemplateResolver(Collection<ITemplateResolver> resolvers) {\n        this.resolvers = Optional.ofNullable(resolvers).orElseGet(List::of)\n            .stream()\n            .distinct()\n            // we keep the same order comparison as\n            // org.thymeleaf.EngineConfiguration.TemplateResolverComparator\n            .sorted(comparing(ITemplateResolver::getOrder, nullsLast(naturalOrder())))\n            .toList();\n    }\n\n    @Override\n    public String getName() {\n        return resolvers.stream()\n            .map(ITemplateResolver::getName)\n            .collect(Collectors.joining(\", \"));\n    }\n\n    @Override\n    public Integer getOrder() {\n        // null order means to be the end of the resolvers.\n        return null;\n    }\n\n    @Override\n    public TemplateResolution resolveTemplate(IEngineConfiguration configuration,\n        String ownerTemplate, String template, Map<String, Object> templateResolutionAttributes) {\n        return resolvers.stream()\n            .map(resolver -> resolver.resolveTemplate(\n                configuration, ownerTemplate, template, templateResolutionAttributes)\n            )\n            .filter(Objects::nonNull)\n            .findFirst()\n            .orElseThrow(() -> new NotFoundException(\"Template \" + template + \" was not found.\"));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/DefaultTemplateEnum.java",
    "content": "package run.halo.app.theme;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic enum DefaultTemplateEnum {\n    INDEX(\"index\"),\n\n    CATEGORIES(\"categories\"),\n\n    CATEGORY(\"category\"),\n\n    ARCHIVES(\"archives\"),\n\n    POST(\"post\"),\n\n    TAG(\"tag\"),\n\n    TAGS(\"tags\"),\n\n    SINGLE_PAGE(\"page\"),\n\n    AUTHOR(\"author\");\n\n    private final String value;\n\n    DefaultTemplateEnum(String value) {\n        this.value = value;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/DefaultTemplateNameResolver.java",
    "content": "package run.halo.app.theme;\n\nimport static run.halo.app.plugin.PluginConst.SYSTEM_PLUGIN_NAME;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.PluginApplicationContext;\n\n/**\n * A default implementation of {@link TemplateNameResolver}, It will be provided for plugins to\n * resolve template name.\n *\n * @author guqing\n * @since 2.11.0\n */\npublic class DefaultTemplateNameResolver implements TemplateNameResolver {\n\n    private final ApplicationContext applicationContext;\n    private final ViewNameResolver viewNameResolver;\n\n    public DefaultTemplateNameResolver(ViewNameResolver viewNameResolver,\n        ApplicationContext applicationContext) {\n        this.applicationContext = applicationContext;\n        this.viewNameResolver = viewNameResolver;\n    }\n\n    @Override\n    public Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name) {\n        if (applicationContext instanceof PluginApplicationContext pluginApplicationContext) {\n            var pluginName = pluginApplicationContext.getPluginId();\n            return this.resolveTemplateNameOrDefault(exchange, name,\n                pluginClassPathTemplate(pluginName, name));\n        }\n        return resolveTemplateNameOrDefault(exchange, name,\n            pluginClassPathTemplate(SYSTEM_PLUGIN_NAME, name));\n    }\n\n    @Override\n    public Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name,\n        String defaultName) {\n        return viewNameResolver.resolveViewNameOrDefault(exchange, name, defaultName);\n    }\n\n    @Override\n    public Mono<Boolean> isTemplateAvailableInTheme(ServerWebExchange exchange, String name) {\n        return this.resolveTemplateNameOrDefault(exchange, name, \"\")\n            .filter(StringUtils::isNotBlank)\n            .hasElement();\n    }\n\n    String pluginClassPathTemplate(String pluginName, String templateName) {\n        return \"plugin:\" + pluginName + \":\" + templateName;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/DefaultViewNameResolver.java",
    "content": "package run.halo.app.theme;\n\nimport java.nio.file.Files;\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.boot.thymeleaf.autoconfigure.ThymeleafProperties;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * The {@link DefaultViewNameResolver} is used to resolve view name.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class DefaultViewNameResolver implements ViewNameResolver {\n    private static final String TEMPLATES = \"templates\";\n    private final ThemeResolver themeResolver;\n    private final ThymeleafProperties thymeleafProperties;\n\n    /**\n     * Resolves view name.\n     * If the {@param #name} cannot be resolved to the view, the {@param #defaultName} is returned.\n     */\n    @Override\n    public Mono<String> resolveViewNameOrDefault(ServerWebExchange exchange, String name,\n        String defaultName) {\n        if (StringUtils.isBlank(name)) {\n            return Mono.justOrEmpty(defaultName);\n        }\n        return themeResolver.getTheme(exchange)\n            .mapNotNull(themeContext -> {\n                String templateResourceName = computeResourceName(name);\n                var resourcePath = themeContext.getPath()\n                    .resolve(TEMPLATES)\n                    .resolve(templateResourceName);\n                return Files.exists(resourcePath) ? name : defaultName;\n            })\n            .switchIfEmpty(Mono.justOrEmpty(defaultName));\n    }\n\n    @Override\n    public Mono<String> resolveViewNameOrDefault(ServerRequest request, String name,\n        String defaultName) {\n        return resolveViewNameOrDefault(request.exchange(), name, defaultName);\n    }\n\n    String computeResourceName(String name) {\n        Assert.notNull(name, \"Name must not be null\");\n        return StringUtils.endsWith(name, thymeleafProperties.getSuffix())\n            ? name : name + thymeleafProperties.getSuffix();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/HaloViewResolver.java",
    "content": "package run.halo.app.theme;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.attoparser.ParseException;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.context.properties.PropertyMapper;\nimport org.springframework.boot.thymeleaf.autoconfigure.ThymeleafProperties;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.core.Ordered;\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.unit.DataSize;\nimport org.springframework.web.ErrorResponse;\nimport org.springframework.web.reactive.result.view.View;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.thymeleaf.exceptions.TemplateInputException;\nimport org.thymeleaf.exceptions.TemplateProcessingException;\nimport org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;\nimport org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.theme.finders.FinderRegistry;\nimport run.halo.app.theme.router.ModelConst;\n\n@Component(\"thymeleafReactiveViewResolver\")\npublic class HaloViewResolver extends ThymeleafReactiveViewResolver implements InitializingBean {\n\n    private final FinderRegistry finderRegistry;\n\n    private final ThymeleafProperties thymeleafProperties;\n\n    public HaloViewResolver(FinderRegistry finderRegistry,\n        ThymeleafProperties thymeleafProperties) {\n        this.finderRegistry = finderRegistry;\n        this.thymeleafProperties = thymeleafProperties;\n    }\n\n    @Override\n    protected Mono<View> loadView(String viewName, Locale locale) {\n        return super.loadView(viewName, locale)\n            .cast(HaloView.class)\n            .map(view -> {\n                // populate finders to view static variables\n                finderRegistry.getFinders().forEach(view::addStaticVariable);\n                return view;\n            });\n    }\n\n    @Override\n    public void afterPropertiesSet() throws Exception {\n        setViewClass(HaloView.class);\n        var map = PropertyMapper.get();\n        map.from(thymeleafProperties::getEncoding)\n            .to(this::setDefaultCharset);\n        map.from(thymeleafProperties::getExcludedViewNames)\n            .to(this::setExcludedViewNames);\n        map.from(thymeleafProperties::getViewNames)\n            .to(this::setViewNames);\n\n        var reactive = thymeleafProperties.getReactive();\n        map.from(reactive::getMediaTypes)\n            .to(this::setSupportedMediaTypes);\n        map.from(reactive::getFullModeViewNames)\n            .to(this::setFullModeViewNames);\n        map.from(reactive::getChunkedModeViewNames)\n            .to(this::setChunkedModeViewNames);\n        map.from(reactive::getMaxChunkSize)\n            .asInt(DataSize::toBytes)\n            .when(size -> size > 0)\n            .to(this::setResponseMaxChunkSizeBytes);\n        setOrder(Ordered.LOWEST_PRECEDENCE - 5);\n    }\n\n    public static class HaloView extends ThymeleafReactiveView {\n\n        public static final String CONTEXT_VIEW_KEY = \"reactorContextView\";\n\n        @Autowired\n        private TemplateEngineManager engineManager;\n\n        @Autowired\n        private ThemeResolver themeResolver;\n\n        @Override\n        public Mono<Void> render(Map<String, ?> model, MediaType contentType,\n            ServerWebExchange exchange) {\n            return themeResolver.getTheme(exchange).flatMap(theme -> {\n                // calculate the engine before rendering\n                setTemplateEngine(engineManager.getTemplateEngine(theme));\n                var noCache = (Boolean) exchange.getAttributes()\n                    .getOrDefault(ModelConst.NO_CACHE, false);\n                exchange.getAttributes()\n                    .put(ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE, !noCache);\n                return super.render(model, contentType, exchange)\n                    .onErrorMap(TemplateProcessingException.class::isInstance, tee -> {\n                        if (tee instanceof TemplateInputException) {\n                            // map the error response exception while fragment not found\n                            return Optional.of(tee)\n                                .map(Throwable::getCause)\n                                .filter(ParseException.class::isInstance)\n                                .map(Throwable::getCause)\n                                .filter(TemplateProcessingException.class::isInstance)\n                                .map(Throwable::getCause)\n                                .filter(ErrorResponse.class::isInstance)\n                                .orElse(tee);\n                        }\n                        // map the error response exception while template not found\n                        return Optional.of(tee)\n                            .map(Throwable::getCause)\n                            .filter(ErrorResponse.class::isInstance)\n                            .orElse(tee);\n                    });\n\n            });\n        }\n\n        @Override\n        @NonNull\n        protected Mono<Map<String, Object>> getModelAttributes(Map<String, ?> model,\n            @NonNull ServerWebExchange exchange) {\n            Mono<Map<String, Object>> contextBasedStaticVariables =\n                getContextBasedStaticVariables(exchange);\n            Mono<Map<String, Object>> modelAttributes = super.getModelAttributes(model, exchange);\n            return Mono.deferContextual(\n                contextView -> Flux.merge(modelAttributes, contextBasedStaticVariables)\n                    .collectList()\n                    .map(modelMapList -> {\n                        Map<String, Object> result = new HashMap<>();\n                        modelMapList.forEach(result::putAll);\n                        return result;\n                    })\n                    .doOnNext(attributes -> attributes.put(CONTEXT_VIEW_KEY, contextView))\n            );\n        }\n\n        @NonNull\n        private Mono<Map<String, Object>> getContextBasedStaticVariables(\n            ServerWebExchange exchange) {\n            ApplicationContext applicationContext = obtainApplicationContext();\n\n            return Mono.just(new HashMap<String, Object>())\n                .flatMap(staticVariables -> {\n                    List<Mono<Map<String, Object>>> monoList = applicationContext.getBeansOfType(\n                            ViewContextBasedVariablesAcquirer.class)\n                        .values()\n                        .stream()\n                        .map(acquirer -> acquirer.acquire(exchange))\n                        .toList();\n                    return Flux.merge(monoList)\n                        .collectList()\n                        .map(modelList -> {\n                            Map<String, Object> mergedModel = new HashMap<>();\n                            modelList.forEach(mergedModel::putAll);\n                            return mergedModel;\n                        })\n                        .map(mergedModel -> {\n                            staticVariables.putAll(mergedModel);\n                            return staticVariables;\n                        });\n                });\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ReactiveSpelVariableExpressionEvaluator.java",
    "content": "package run.halo.app.theme;\n\nimport static run.halo.app.theme.HaloViewResolver.HaloView.CONTEXT_VIEW_KEY;\n\nimport java.util.Optional;\nimport org.thymeleaf.context.IExpressionContext;\nimport org.thymeleaf.spring6.expression.SPELVariableExpressionEvaluator;\nimport org.thymeleaf.standard.expression.IStandardVariableExpression;\nimport org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;\nimport org.thymeleaf.standard.expression.StandardExpressionExecutionContext;\nimport reactor.util.context.ContextView;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * Reactive SPEL variable expression evaluator.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class ReactiveSpelVariableExpressionEvaluator\n    implements IStandardVariableExpressionEvaluator {\n\n    private final IStandardVariableExpressionEvaluator delegate;\n\n    public static final ReactiveSpelVariableExpressionEvaluator INSTANCE =\n        new ReactiveSpelVariableExpressionEvaluator();\n\n    public ReactiveSpelVariableExpressionEvaluator(IStandardVariableExpressionEvaluator delegate) {\n        this.delegate = delegate;\n    }\n\n    public ReactiveSpelVariableExpressionEvaluator() {\n        this(SPELVariableExpressionEvaluator.INSTANCE);\n    }\n\n    @Override\n    public Object evaluate(IExpressionContext context, IStandardVariableExpression expression,\n        StandardExpressionExecutionContext expContext) {\n        var returnValue = delegate.evaluate(context, expression, expContext);\n\n        var contextView = (ContextView) Optional.ofNullable(context.getVariable(CONTEXT_VIEW_KEY))\n            .filter(ContextView.class::isInstance)\n            .orElse(null);\n\n        return Optional.ofNullable(returnValue)\n            .map(value -> ReactiveUtils.blockReactiveValue(value, contextView))\n            .orElse(null);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/SiteSettingVariablesAcquirer.java",
    "content": "package run.halo.app.theme;\n\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.theme.finders.vo.SiteSettingVo;\n\n/**\n * Site setting variables acquirer.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class SiteSettingVariablesAcquirer implements ViewContextBasedVariablesAcquirer {\n\n    private final SystemConfigFetcher environmentFetcher;\n    private final ExternalUrlSupplier externalUrlSupplier;\n    private final SystemVersionSupplier systemVersionSupplier;\n\n    @Override\n    public Mono<Map<String, Object>> acquire(ServerWebExchange exchange) {\n        return environmentFetcher.getConfig()\n            .map(config -> {\n                var siteSettingVo = SiteSettingVo.from(config)\n                    .withUrl(externalUrlSupplier.getURL(exchange.getRequest()))\n                    .withVersion(systemVersionSupplier.get().toString());\n                return Map.of(\"site\", siteSettingVo);\n            });\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/TemplateEngineManager.java",
    "content": "package run.halo.app.theme;\n\nimport lombok.NonNull;\nimport org.pf4j.PluginManager;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.thymeleaf.autoconfigure.ThymeleafProperties;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.ConcurrentLruCache;\nimport org.thymeleaf.dialect.IDialect;\nimport org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;\nimport org.thymeleaf.spring6.dialect.SpringStandardDialect;\nimport org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;\nimport org.thymeleaf.templateresolver.FileTemplateResolver;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.theme.dialect.HaloProcessorDialect;\nimport run.halo.app.theme.engine.HaloTemplateEngine;\nimport run.halo.app.theme.engine.PluginClassloaderTemplateResolver;\nimport run.halo.app.theme.message.ThemeMessageResolver;\n\n/**\n * <p>The {@link TemplateEngineManager} uses an {@link ConcurrentLruCache LRU cache} to manage\n * theme's {@link ISpringWebFluxTemplateEngine}.</p>\n * <p>The default limit size of the {@link ConcurrentLruCache LRU cache} is\n * {@link TemplateEngineManager#CACHE_SIZE_LIMIT} to prevent unnecessary memory occupation.</p>\n * <p>If theme's {@link ISpringWebFluxTemplateEngine} already exists, it returns.</p>\n * <p>Otherwise, it checks whether the theme exists and creates the\n * {@link ISpringWebFluxTemplateEngine} into the LRU cache according to the {@link ThemeContext}\n * .</p>\n * <p>It is thread safe.</p>\n *\n * @author johnniang\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class TemplateEngineManager {\n    private static final int CACHE_SIZE_LIMIT = 5;\n    private final ConcurrentLruCache<CacheKey, ISpringWebFluxTemplateEngine> engineCache;\n\n    private final ThymeleafProperties thymeleafProperties;\n\n    private final ExternalUrlSupplier externalUrlSupplier;\n\n    private final PluginManager pluginManager;\n\n    private final ObjectProvider<ITemplateResolver> templateResolvers;\n\n    private final ObjectProvider<IDialect> dialects;\n\n    private final ThemeResolver themeResolver;\n\n    public TemplateEngineManager(ThymeleafProperties thymeleafProperties,\n        ExternalUrlSupplier externalUrlSupplier,\n        PluginManager pluginManager, ObjectProvider<ITemplateResolver> templateResolvers,\n        ObjectProvider<IDialect> dialects, ThemeResolver themeResolver) {\n        this.thymeleafProperties = thymeleafProperties;\n        this.externalUrlSupplier = externalUrlSupplier;\n        this.pluginManager = pluginManager;\n        this.templateResolvers = templateResolvers;\n        this.dialects = dialects;\n        this.themeResolver = themeResolver;\n        engineCache = new ConcurrentLruCache<>(CACHE_SIZE_LIMIT, this::templateEngineGenerator);\n    }\n\n    public ISpringWebFluxTemplateEngine getTemplateEngine(ThemeContext theme) {\n        CacheKey cacheKey = buildCacheKey(theme);\n        return engineCache.get(cacheKey);\n    }\n\n    public Mono<Void> clearCache(String themeName) {\n        return themeResolver.getThemeContext(themeName)\n            .doOnNext(themeContext -> engineCache.remove(buildCacheKey(themeContext)))\n            .then();\n    }\n\n    /**\n     * TemplateEngine LRU cache key.\n     *\n     * @param name from {@link #context}\n     * @param active from {@link #context}\n     * @param context must not be null\n     */\n    private record CacheKey(String name, boolean active, ThemeContext context) {\n    }\n\n    CacheKey buildCacheKey(ThemeContext context) {\n        return new CacheKey(context.getName(), context.isActive(), context);\n    }\n\n    private ISpringWebFluxTemplateEngine templateEngineGenerator(CacheKey cacheKey) {\n\n        var engine = new HaloTemplateEngine(new ThemeMessageResolver(cacheKey.context()));\n        engine.setEnableSpringELCompiler(thymeleafProperties.isEnableSpringElCompiler());\n        engine.setLinkBuilder(new ThemeLinkBuilder(cacheKey.context(), externalUrlSupplier));\n        engine.setRenderHiddenMarkersBeforeCheckboxes(\n            thymeleafProperties.isRenderHiddenMarkersBeforeCheckboxes());\n\n        var mainResolver = haloTemplateResolver();\n        mainResolver.setPrefix(cacheKey.context().getPath().resolve(\"templates\") + \"/\");\n        engine.addTemplateResolver(mainResolver);\n        var pluginTemplateResolver = createPluginClassloaderTemplateResolver();\n        engine.addTemplateResolver(pluginTemplateResolver);\n        // replace StandardDialect with SpringStandardDialect\n        engine.setDialect(new SpringStandardDialect() {\n            @Override\n            public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {\n                return ReactiveSpelVariableExpressionEvaluator.INSTANCE;\n            }\n        });\n        engine.addDialect(new HaloProcessorDialect());\n\n        templateResolvers.orderedStream().forEach(engine::addTemplateResolver);\n\n        // we collect all template resolvers and add them into composite template resolver\n        // to control the resolution flow\n        var compositeTemplateResolver =\n            new CompositeTemplateResolver(engine.getTemplateResolvers());\n        engine.setTemplateResolver(compositeTemplateResolver);\n\n        dialects.orderedStream().forEach(engine::addDialect);\n\n        return engine;\n    }\n\n    @NonNull\n    private PluginClassloaderTemplateResolver createPluginClassloaderTemplateResolver() {\n        var pluginTemplateResolver = new PluginClassloaderTemplateResolver(pluginManager);\n        pluginTemplateResolver.setPrefix(thymeleafProperties.getPrefix());\n        pluginTemplateResolver.setSuffix(thymeleafProperties.getSuffix());\n        pluginTemplateResolver.setTemplateMode(thymeleafProperties.getMode());\n        pluginTemplateResolver.setOrder(1);\n        if (thymeleafProperties.getEncoding() != null) {\n            pluginTemplateResolver.setCharacterEncoding(thymeleafProperties.getEncoding().name());\n        }\n        return pluginTemplateResolver;\n    }\n\n    FileTemplateResolver haloTemplateResolver() {\n        final var resolver = new FileTemplateResolver();\n        resolver.setTemplateMode(thymeleafProperties.getMode());\n        resolver.setPrefix(thymeleafProperties.getPrefix());\n        resolver.setSuffix(thymeleafProperties.getSuffix());\n        resolver.setCacheable(thymeleafProperties.isCache());\n        resolver.setCheckExistence(thymeleafProperties.isCheckTemplate());\n        if (thymeleafProperties.getEncoding() != null) {\n            resolver.setCharacterEncoding(thymeleafProperties.getEncoding().name());\n        }\n        return resolver;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ThemeContext.java",
    "content": "package run.halo.app.theme;\n\nimport java.nio.file.Path;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Builder\n@EqualsAndHashCode(of = \"name\")\npublic class ThemeContext {\n\n    public static final String THEME_PREVIEW_PARAM_NAME = \"preview-theme\";\n\n    private String name;\n\n    private Path path;\n\n    private boolean active;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ThemeContextBasedVariablesAcquirer.java",
    "content": "package run.halo.app.theme;\n\nimport java.util.Map;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.theme.finders.ThemeFinder;\n\n/**\n * Theme context based variables acquirer.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class ThemeContextBasedVariablesAcquirer implements ViewContextBasedVariablesAcquirer {\n    private final ThemeFinder themeFinder;\n    private final ThemeResolver themeResolver;\n\n    public ThemeContextBasedVariablesAcquirer(ThemeFinder themeFinder,\n        ThemeResolver themeResolver) {\n        this.themeFinder = themeFinder;\n        this.themeResolver = themeResolver;\n    }\n\n    @Override\n    public Mono<Map<String, Object>> acquire(ServerWebExchange exchange) {\n        return themeResolver.getTheme(exchange)\n            .flatMap(themeContext -> {\n                String name = themeContext.getName();\n                return themeFinder.getByName(name);\n            })\n            .map(themeVo -> Map.<String, Object>of(\"theme\", themeVo))\n            .defaultIfEmpty(Map.of());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ThemeLinkBuilder.java",
    "content": "package run.halo.app.theme;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.lang.NonNull;\nimport org.springframework.web.util.UriComponentsBuilder;\nimport org.thymeleaf.context.IExpressionContext;\nimport org.thymeleaf.linkbuilder.StandardLinkBuilder;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.utils.PathUtils;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class ThemeLinkBuilder extends StandardLinkBuilder {\n    public static final String THEME_ASSETS_PREFIX = \"/assets\";\n    public static final String THEME_PREVIEW_PREFIX = \"/themes\";\n\n    private final ThemeContext theme;\n    private final ExternalUrlSupplier externalUrlSupplier;\n\n    public ThemeLinkBuilder(ThemeContext theme, ExternalUrlSupplier externalUrlSupplier) {\n        this.theme = theme;\n        this.externalUrlSupplier = externalUrlSupplier;\n    }\n\n    @Override\n    protected String processLink(IExpressionContext context, String link) {\n        if (link == null || !linkInSite(externalUrlSupplier.get(), link)) {\n            return link;\n        }\n\n        if (StringUtils.isBlank(link)) {\n            link = \"/\";\n        }\n\n        if (isAssetsRequest(link)) {\n            return PathUtils.combinePath(THEME_PREVIEW_PREFIX, theme.getName(), link);\n        }\n\n        // not assets link\n        if (theme.isActive()) {\n            return link;\n        }\n\n        return UriComponentsBuilder.fromUriString(link)\n            .queryParam(ThemeContext.THEME_PREVIEW_PARAM_NAME, theme.getName())\n            .build().toString();\n    }\n\n    static boolean linkInSite(@NonNull URI externalUri, @NonNull String link) {\n        if (!PathUtils.isAbsoluteUri(link)) {\n            // relative uri is always in site\n            return true;\n        }\n        try {\n            URI requestUri = new URI(link);\n            return StringUtils.equals(externalUri.getAuthority(), requestUri.getAuthority());\n        } catch (URISyntaxException e) {\n            // ignore this link\n        }\n        return false;\n    }\n\n    private boolean isAssetsRequest(String link) {\n        String assetsPrefix = externalUrlSupplier.get().resolve(THEME_ASSETS_PREFIX).toString();\n        return link.startsWith(assetsPrefix) || link.startsWith(THEME_ASSETS_PREFIX);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ThemeLocaleContextResolver.java",
    "content": "package run.halo.app.theme;\n\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.TimeZone;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.LocaleUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.i18n.LocaleContext;\nimport org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.adapter.WebHttpHandlerBuilder;\nimport org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component(WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)\npublic class ThemeLocaleContextResolver extends AcceptHeaderLocaleContextResolver {\n\n    public static final String LANGUAGE_PARAMETER_NAME = \"language\";\n\n    public static final String LANGUAGE_COOKIE_NAME = LANGUAGE_PARAMETER_NAME;\n\n    public static final String TIME_ZONE_COOKIE_NAME = \"time_zone\";\n\n\n    @Override\n    @NonNull\n    public LocaleContext resolveLocaleContext(@NonNull ServerWebExchange exchange) {\n        var request = exchange.getRequest();\n        var locale = getLocaleFromQueryParameter(request)\n            .or(() -> getLocaleFromCookie(request))\n            .or(() -> UserLocaleRequestAttributeWriteFilter.getUserLocale(request))\n            .orElseGet(() -> super.resolveLocaleContext(exchange).getLocale());\n\n        if (LocaleUtils.isLanguageUndetermined(locale)) {\n            locale = null;\n        }\n\n        var timeZone = getTimeZoneFromCookie(request)\n            .orElseGet(TimeZone::getDefault);\n\n        return new SimpleTimeZoneAwareLocaleContext(locale, timeZone);\n    }\n\n    private Optional<Locale> getLocaleFromCookie(ServerHttpRequest request) {\n        return Optional.ofNullable(request.getCookies().getFirst(LANGUAGE_COOKIE_NAME))\n            .map(HttpCookie::getValue)\n            .filter(StringUtils::isNotBlank)\n            .map(Locale::forLanguageTag);\n    }\n\n    private Optional<Locale> getLocaleFromQueryParameter(ServerHttpRequest request) {\n        return Optional.ofNullable(request.getQueryParams().getFirst(LANGUAGE_PARAMETER_NAME))\n            .filter(StringUtils::isNotBlank)\n            .map(Locale::forLanguageTag);\n    }\n\n    private Optional<TimeZone> getTimeZoneFromCookie(ServerHttpRequest request) {\n        return Optional.ofNullable(request.getCookies().getFirst(TIME_ZONE_COOKIE_NAME))\n            .map(HttpCookie::getValue)\n            .filter(StringUtils::isNotBlank)\n            .map(TimeZone::getTimeZone);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ThemeResolver.java",
    "content": "package run.halo.app.theme;\n\nimport static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles;\n\nimport java.util.Collection;\nimport java.util.Set;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting.Theme;\nimport run.halo.app.infra.SystemSetting.ThemeRouteRules;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\n/**\n * @author johnniang\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class ThemeResolver {\n\n    private final SystemConfigFetcher environmentFetcher;\n\n    private final ThemeRootGetter themeRoot;\n    private final RoleService roleService;\n\n    public Mono<ThemeContext> getThemeContext(String themeName) {\n        Assert.hasText(themeName, \"Theme name cannot be empty\");\n        var path = themeRoot.get().resolve(themeName);\n        return Mono.just(ThemeContext.builder().name(themeName).path(path))\n            .flatMap(builder -> environmentFetcher.fetch(Theme.GROUP, Theme.class)\n                .mapNotNull(Theme::getActive)\n                .map(activatedTheme -> {\n                    boolean active = StringUtils.equals(activatedTheme, themeName);\n                    return builder.active(active);\n                })\n                .defaultIfEmpty(builder.active(false))\n            )\n            .map(ThemeContext.ThemeContextBuilder::build);\n    }\n\n    public Mono<ThemeContext> getTheme(ServerWebExchange exchange) {\n        return fetchThemeFromExchange(exchange)\n            .switchIfEmpty(Mono.defer(() -> fetchActivationState()\n                .map(themeState -> {\n                    var activatedTheme = themeState.activatedTheme();\n                    var builder = ThemeContext.builder();\n                    var themeName = exchange.getRequest().getQueryParams()\n                        .getFirst(ThemeContext.THEME_PREVIEW_PARAM_NAME);\n\n                    if (StringUtils.isBlank(themeName) || !themeState.supportsPreviewTheme()) {\n                        themeName = activatedTheme;\n                    }\n\n                    boolean active = StringUtils.equals(activatedTheme, themeName);\n                    var path = themeRoot.get().resolve(themeName);\n                    return builder.name(themeName)\n                        .path(path)\n                        .active(active)\n                        .build();\n                })\n                .doOnNext(themeContext ->\n                    exchange.getAttributes().put(ThemeContext.class.getName(), themeContext))\n            ));\n    }\n\n    public Mono<ThemeContext> fetchThemeFromExchange(ServerWebExchange exchange) {\n        return Mono.justOrEmpty(exchange)\n            .map(ServerWebExchange::getAttributes)\n            .filter(attrs -> attrs.containsKey(ThemeContext.class.getName()))\n            .map(attrs -> attrs.get(ThemeContext.class.getName()))\n            .cast(ThemeContext.class);\n    }\n\n    private Mono<ThemeActivationState> fetchActivationState() {\n        var builder = ThemeActivationState.builder();\n\n        var activatedMono = environmentFetcher.fetch(Theme.GROUP, Theme.class)\n            .map(Theme::getActive)\n            .switchIfEmpty(Mono.error(() -> new IllegalArgumentException(\"No theme activated\")))\n            .doOnNext(builder::activatedTheme);\n\n        var preivewDisabledMono = environmentFetcher.fetch(ThemeRouteRules.GROUP,\n                ThemeRouteRules.class)\n            .map(ThemeRouteRules::isDisableThemePreview)\n            .defaultIfEmpty(false)\n            .doOnNext(builder::previewDisabled);\n\n        var themeManageMono = ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .filter(au -> !AnonymousUserConst.isAnonymousUser(au.getName()))\n            .flatMap(au -> supportsPreviewTheme(authoritiesToRoles(au.getAuthorities())))\n            .doOnNext(builder::hasThemeManagementRole);\n\n        return Mono.when(activatedMono, preivewDisabledMono, themeManageMono)\n            .then(Mono.fromSupplier(builder::build));\n    }\n\n    private Mono<Boolean> supportsPreviewTheme(Collection<String> authorities) {\n        return roleService.contains(authorities, Set.of(AuthorityUtils.THEME_MANAGEMENT_ROLE_NAME))\n            .defaultIfEmpty(false);\n    }\n\n    @Builder\n    record ThemeActivationState(String activatedTheme, boolean previewDisabled,\n                                boolean hasThemeManagementRole) {\n\n        private boolean supportsPreviewTheme() {\n            if (hasThemeManagementRole) {\n                return true;\n            }\n            return !previewDisabled;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/UserLocaleRequestAttributeWriteFilter.java",
    "content": "package run.halo.app.theme;\n\nimport java.util.Locale;\nimport java.util.Optional;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n@Component\n@Order(Ordered.HIGHEST_PRECEDENCE)\n@RequiredArgsConstructor\npublic class UserLocaleRequestAttributeWriteFilter implements WebFilter {\n    public static final String USER_LOCALE_ATTRIBUTE =\n        UserLocaleRequestAttributeWriteFilter.class.getName() + \".USER_LOCALE_ATTRIBUTE\";\n\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    @NonNull\n    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {\n        return environmentFetcher.getBasic()\n            .map(SystemSetting.Basic::useSystemLocale)\n            .doOnNext(localeOpt -> localeOpt\n                .ifPresent(locale -> exchange.getAttributes().put(USER_LOCALE_ATTRIBUTE, locale))\n            )\n            .then(chain.filter(exchange));\n    }\n\n    public static Optional<Locale> getUserLocale(ServerHttpRequest request) {\n        return Optional.ofNullable((Locale) request.getAttributes()\n            .get(USER_LOCALE_ATTRIBUTE));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ViewContextBasedVariablesAcquirer.java",
    "content": "package run.halo.app.theme;\n\nimport java.util.Map;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n@FunctionalInterface\npublic interface ViewContextBasedVariablesAcquirer {\n\n    Mono<Map<String, Object>> acquire(ServerWebExchange exchange);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/ViewNameResolver.java",
    "content": "package run.halo.app.theme;\n\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * The {@link ViewNameResolver} is used to resolve view name if the view name cannot be resolved\n * to the view, the default view name is returned.\n *\n * @author guqing\n * @since 2.10.2\n */\npublic interface ViewNameResolver {\n    Mono<String> resolveViewNameOrDefault(ServerWebExchange exchange, String name,\n        String defaultName);\n\n    Mono<String> resolveViewNameOrDefault(ServerRequest request, String name,\n        String defaultName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/config/ThemeConfiguration.java",
    "content": "package run.halo.app.theme.config;\n\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.info.BuildProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;\nimport run.halo.app.theme.dialect.GeneratorMetaProcessor;\nimport run.halo.app.theme.dialect.HaloSpringSecurityDialect;\nimport run.halo.app.theme.dialect.LinkExpressionObjectDialect;\nimport run.halo.app.theme.dialect.TemplateHeadProcessor;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\n@Configuration\npublic class ThemeConfiguration {\n\n    @Bean\n    LinkExpressionObjectDialect linkExpressionObjectDialect() {\n        return new LinkExpressionObjectDialect();\n    }\n\n    @Bean\n    SpringSecurityDialect springSecurityDialect(\n        ServerSecurityContextRepository securityContextRepository,\n        ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {\n        return new HaloSpringSecurityDialect(securityContextRepository, expressionHandler);\n    }\n\n    @Bean\n    @ConditionalOnProperty(name = \"halo.theme.generator-meta-disabled\",\n        havingValue = \"false\",\n        matchIfMissing = true)\n    TemplateHeadProcessor generatorMetaProcessor(ObjectProvider<BuildProperties> buildProperties) {\n        return new GeneratorMetaProcessor(buildProperties);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/config/ThemeWebFluxConfigurer.java",
    "content": "package run.halo.app.theme.config;\n\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Map;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.http.CacheControl;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.HandlerMapping;\nimport org.springframework.web.reactive.config.ResourceHandlerRegistry;\nimport org.springframework.web.reactive.config.WebFluxConfigurer;\nimport org.springframework.web.reactive.resource.AbstractResourceResolver;\nimport org.springframework.web.reactive.resource.EncodedResourceResolver;\nimport org.springframework.web.reactive.resource.ResourceResolverChain;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.infra.utils.FileUtils;\n\n@Component\npublic class ThemeWebFluxConfigurer implements WebFluxConfigurer {\n\n    private final ThemeRootGetter themeRootGetter;\n\n    private final WebProperties.Resources resourcesProperties;\n\n    public ThemeWebFluxConfigurer(ThemeRootGetter themeRootGetter,\n        WebProperties webProperties) {\n        this.themeRootGetter = themeRootGetter;\n        this.resourcesProperties = webProperties.getResources();\n    }\n\n    @Override\n    public void addResourceHandlers(ResourceHandlerRegistry registry) {\n        var cacheControl = resourcesProperties.getCache().getCachecontrol().toHttpCacheControl();\n        if (cacheControl == null) {\n            cacheControl = CacheControl.empty();\n        }\n        var useLastModified = resourcesProperties.getCache().isUseLastModified();\n        registry.addResourceHandler(\"/themes/{themeName}/assets/{*resourcePaths}\")\n            .setCacheControl(cacheControl)\n            .setUseLastModified(useLastModified)\n            .resourceChain(true)\n            .addResolver(new EncodedResourceResolver())\n            .addResolver(new ThemePathResourceResolver(themeRootGetter.get()));\n    }\n\n    /**\n     * Theme path resource resolver. The resolver is used to resolve theme assets from the request\n     * path.\n     *\n     * @author johnniang\n     */\n    private static class ThemePathResourceResolver extends AbstractResourceResolver {\n\n        private final Path themeRoot;\n\n        private ThemePathResourceResolver(Path themeRoot) {\n            this.themeRoot = themeRoot;\n        }\n\n        @Override\n        protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,\n            String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) {\n            if (exchange == null) {\n                return Mono.empty();\n            }\n            Map<String, String> requiredAttribute =\n                exchange.getRequiredAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);\n            var themeName = requiredAttribute.get(\"themeName\");\n            var resourcePaths = requiredAttribute.get(\"resourcePaths\");\n\n            if (StringUtils.isAnyBlank(themeName, resourcePaths)) {\n                return Mono.empty();\n            }\n\n            var assetsPath = themeRoot.resolve(themeName + \"/templates/assets/\" + resourcePaths);\n            FileUtils.checkDirectoryTraversal(themeRoot, assetsPath);\n            var location = new FileSystemResource(assetsPath);\n            if (!location.isReadable()) {\n                return Mono.empty();\n            }\n            return Mono.just(location);\n        }\n\n        @Override\n        protected Mono<String> resolveUrlPathInternal(String resourceUrlPath,\n            List<? extends Resource> locations, ResourceResolverChain chain) {\n            throw new UnsupportedOperationException();\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractElementTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n * <p>Comment element tag processor.</p>\n * <p>Replace the comment tag <code>&#x3C;halo:comment /&#x3E;</code> with the given content.</p>\n *\n * @author guqing\n * @see CommentEnabledVariableProcessor\n * @since 2.0.0\n */\npublic class CommentElementTagProcessor extends AbstractElementTagProcessor {\n\n    private static final String TAG_NAME = \"comment\";\n\n    private static final int PRECEDENCE = 1000;\n\n    /**\n     * Constructor footer element tag processor with HTML mode, dialect prefix, comment tag name.\n     *\n     * @param dialectPrefix dialect prefix\n     */\n    public CommentElementTagProcessor(final String dialectPrefix) {\n        super(\n            TemplateMode.HTML, // This processor will apply only to HTML mode\n            dialectPrefix,     // Prefix to be applied to name for matching\n            TAG_NAME,          // Tag name: match specifically this tag\n            true,              // Apply dialect prefix to tag name\n            null,              // No attribute name: will match by tag name\n            false,             // No prefix to be applied to attribute name\n            PRECEDENCE);       // Precedence (inside dialect's own precedence)\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext context, IProcessableElementTag tag,\n        IElementTagStructureHandler structureHandler) {\n        var commentWidget = (CommentWidget) context.getVariable(\n            CommentEnabledVariableProcessor.COMMENT_WIDGET_OBJECT_VARIABLE);\n        if (commentWidget == null) {\n            structureHandler.replaceWith(\"\", false);\n            return;\n        }\n        commentWidget.render(SecureTemplateContextWrapper.wrap(context), tag, structureHandler);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.apache.commons.lang3.BooleanUtils.isFalse;\nimport static org.apache.commons.lang3.BooleanUtils.isTrue;\n\nimport java.time.Duration;\nimport java.util.Optional;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.core.convert.support.DefaultConversionService;\nimport org.thymeleaf.context.Contexts;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.context.IWebContext;\nimport org.thymeleaf.model.ITemplateEnd;\nimport org.thymeleaf.model.ITemplateStart;\nimport org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor;\nimport org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler;\nimport org.thymeleaf.spring6.context.SpringContextUtils;\nimport org.thymeleaf.standard.StandardDialect;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Comment enabled variable processor.\n * <p>Compute comment enabled state and set it to the model when the template is start rendering</p>\n * <p>It is not suitable for scenarios where there are multiple comment components on the same page\n * and some of them need to be controlled to be closed.</p>\n *\n * @author guqing\n * @since 2.9.0\n */\npublic class CommentEnabledVariableProcessor extends AbstractTemplateBoundariesProcessor {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    public static final String COMMENT_WIDGET_OBJECT_VARIABLE = CommentWidget.class.getName();\n    public static final String COMMENT_ENABLED_MODEL_ATTRIBUTE = \"haloCommentEnabled\";\n\n    public CommentEnabledVariableProcessor() {\n        super(TemplateMode.HTML, StandardDialect.PROCESSOR_PRECEDENCE);\n    }\n\n    @Override\n    public void doProcessTemplateStart(ITemplateContext context, ITemplateStart templateStart,\n        ITemplateBoundariesStructureHandler structureHandler) {\n        getCommentWidget(context).ifPresentOrElse(commentWidget -> {\n            populateAllowCommentAttribute(context, true);\n            structureHandler.setLocalVariable(COMMENT_WIDGET_OBJECT_VARIABLE, commentWidget);\n        }, () -> populateAllowCommentAttribute(context, false));\n    }\n\n    @Override\n    public void doProcessTemplateEnd(ITemplateContext context, ITemplateEnd templateEnd,\n        ITemplateBoundariesStructureHandler structureHandler) {\n        structureHandler.removeLocalVariable(COMMENT_WIDGET_OBJECT_VARIABLE);\n    }\n\n    static void populateAllowCommentAttribute(ITemplateContext context, boolean allowComment) {\n        if (Contexts.isWebContext(context)) {\n            IWebContext webContext = Contexts.asWebContext(context);\n            webContext.getExchange()\n                .setAttributeValue(COMMENT_ENABLED_MODEL_ATTRIBUTE, allowComment);\n        }\n    }\n\n    static Optional<CommentWidget> getCommentWidget(ITemplateContext context) {\n        final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);\n        SystemConfigFetcher environmentFetcher =\n            appCtx.getBean(SystemConfigFetcher.class);\n        var commentSetting = environmentFetcher.fetchComment()\n            .blockOptional(BLOCKING_TIMEOUT)\n            .orElseThrow();\n        var globalEnabled = isTrue(commentSetting.getEnable());\n        if (!globalEnabled) {\n            return Optional.empty();\n        }\n\n        if (Contexts.isWebContext(context)) {\n            IWebContext webContext = Contexts.asWebContext(context);\n            Object attributeValue = webContext.getExchange()\n                .getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE);\n            Boolean enabled = DefaultConversionService.getSharedInstance()\n                .convert(attributeValue, Boolean.class);\n            if (isFalse(enabled)) {\n                return Optional.empty();\n            }\n        }\n\n        ExtensionGetter extensionGetter = appCtx.getBean(ExtensionGetter.class);\n        return extensionGetter.getEnabledExtensions(CommentWidget.class)\n            .next()\n            .blockOptional(BLOCKING_TIMEOUT);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static org.apache.commons.lang3.StringUtils.defaultIfBlank;\nimport static org.thymeleaf.model.AttributeValueQuotes.DOUBLE;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.util.HtmlUtils;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.model.ITemplateEvent;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.SinglePageFinder;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * <p>The <code>head</code> html snippet injection processor for content template such as post\n * and page.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@Order(1)\n@AllArgsConstructor\npublic class ContentTemplateHeadProcessor implements TemplateHeadProcessor {\n    private static final String POST_NAME_VARIABLE = \"name\";\n    private final PostFinder postFinder;\n    private final SinglePageFinder singlePageFinder;\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        Mono<String> nameMono = Mono.justOrEmpty((String) context.getVariable(POST_NAME_VARIABLE));\n\n        Mono<List<Map<String, String>>> htmlMetasMono = Mono.empty();\n        if (isPostTemplate(context)) {\n            htmlMetasMono = nameMono.flatMap(postFinder::getByName)\n                .map(post -> {\n                    List<Map<String, String>> htmlMetas = post.getSpec().getHtmlMetas();\n                    String excerpt =\n                        post.getStatus() == null ? null : post.getStatus().getExcerpt();\n                    return excerptToMetaDescriptionIfAbsent(htmlMetas, excerpt);\n                });\n        } else if (isPageTemplate(context)) {\n            htmlMetasMono = nameMono.flatMap(singlePageFinder::getByName)\n                .map(page -> {\n                    List<Map<String, String>> htmlMetas = page.getSpec().getHtmlMetas();\n                    String excerpt =\n                        page.getStatus() == null ? null : page.getStatus().getExcerpt();\n                    return excerptToMetaDescriptionIfAbsent(htmlMetas, excerpt);\n                });\n        }\n\n        return htmlMetasMono\n            .doOnNext(\n                htmlMetas -> buildMetas(context.getModelFactory(), htmlMetas).forEach(model::add)\n            )\n            .then();\n    }\n\n    static List<Map<String, String>> excerptToMetaDescriptionIfAbsent(\n        List<Map<String, String>> htmlMetas,\n        String excerpt) {\n        String excerptNullSafe = StringUtils.defaultString(excerpt);\n        final String excerptSafe = HtmlUtils.htmlEscape(excerptNullSafe);\n        List<Map<String, String>> metas = new ArrayList<>(defaultIfNull(htmlMetas, List.of()));\n        metas.stream()\n            .filter(map -> Meta.DESCRIPTION.equals(map.get(Meta.NAME)))\n            .distinct()\n            .findFirst()\n            .ifPresentOrElse(map ->\n                    map.put(Meta.CONTENT, defaultIfBlank(map.get(Meta.CONTENT), excerptSafe)),\n                () -> {\n                    Map<String, String> map = new HashMap<>();\n                    map.put(Meta.NAME, Meta.DESCRIPTION);\n                    map.put(Meta.CONTENT, excerptSafe);\n                    metas.add(map);\n                });\n        return metas;\n    }\n\n    interface Meta {\n        String DESCRIPTION = \"description\";\n        String NAME = \"name\";\n        String CONTENT = \"content\";\n    }\n\n    private List<ITemplateEvent> buildMetas(IModelFactory modelFactory,\n        List<Map<String, String>> metas) {\n        return metas.stream()\n            .map(metaMap ->\n                modelFactory.createStandaloneElementTag(\"meta\", metaMap, DOUBLE, false, true)\n            ).collect(Collectors.toList());\n    }\n\n    private boolean isPostTemplate(ITemplateContext context) {\n        return DefaultTemplateEnum.POST.getValue()\n            .equals(context.getVariable(ModelConst.TEMPLATE_ID));\n    }\n\n    private boolean isPageTemplate(ITemplateContext context) {\n        return DefaultTemplateEnum.SINGLE_PAGE.getValue()\n            .equals(context.getVariable(ModelConst.TEMPLATE_ID));\n    }\n}"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/DefaultFaviconHeadProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n/**\n * Theme template <code>head</code> tag snippet injection processor for favicon.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class DefaultFaviconHeadProcessor implements TemplateHeadProcessor {\n\n    private final SystemConfigFetcher fetcher;\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        return fetchBasicSetting()\n            .filter(basic -> StringUtils.isNotBlank(basic.getFavicon()))\n            .map(basic -> {\n                IModelFactory modelFactory = context.getModelFactory();\n                model.add(modelFactory.createText(faviconSnippet(basic.getFavicon())));\n                return model;\n            })\n            .then();\n    }\n\n    private String faviconSnippet(String favicon) {\n        return String.format(\"<link rel=\\\"icon\\\" href=\\\"%s\\\" />\\n\", favicon);\n    }\n\n    private Mono<SystemSetting.Basic> fetchBasicSetting() {\n        return fetcher.fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/DefaultLinkExpressionFactory.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport java.util.Set;\nimport org.thymeleaf.context.IExpressionContext;\nimport org.thymeleaf.exceptions.TemplateProcessingException;\nimport org.thymeleaf.expression.IExpressionObjectFactory;\nimport org.thymeleaf.linkbuilder.ILinkBuilder;\nimport org.thymeleaf.util.Validate;\nimport run.halo.app.theme.ThemeLinkBuilder;\n\n/**\n * A default implementation of {@link IExpressionObjectFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class DefaultLinkExpressionFactory implements IExpressionObjectFactory {\n    private static final String THEME_EVALUATION_VARIABLE_NAME = \"theme\";\n\n    @Override\n    public Set<String> getAllExpressionObjectNames() {\n        return Set.of(THEME_EVALUATION_VARIABLE_NAME);\n    }\n\n    @Override\n    public Object buildObject(IExpressionContext context, String expressionObjectName) {\n        if (THEME_EVALUATION_VARIABLE_NAME.equals(expressionObjectName)) {\n            return new ThemeLinkExpressObject(context);\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isCacheable(String expressionObjectName) {\n        return THEME_EVALUATION_VARIABLE_NAME.equals(expressionObjectName);\n    }\n\n    public static class ThemeLinkExpressObject {\n        private final ILinkBuilder linkBuilder;\n        private final IExpressionContext context;\n\n        /**\n         * Construct an expression object that provides a set of methods to handle link in\n         * Javascript or HTML through {@link IExpressionContext}.\n         *\n         * @param context expression context\n         */\n        public ThemeLinkExpressObject(IExpressionContext context) {\n            Validate.notNull(context, \"Context cannot be null\");\n            this.context = context;\n            Set<ILinkBuilder> linkBuilders = context.getConfiguration().getLinkBuilders();\n            linkBuilder = linkBuilders.stream()\n                .findFirst()\n                .orElseThrow(() -> new TemplateProcessingException(\"Link builder not found\"));\n        }\n\n        public String assets(String path) {\n            String assetsPath = ThemeLinkBuilder.THEME_ASSETS_PREFIX + path;\n            return linkBuilder.buildLink(context, assetsPath, null);\n        }\n\n        public String route(String path) {\n            return linkBuilder.buildLink(context, path, null);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/DuplicateMetaTagProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport lombok.AllArgsConstructor;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.model.ITemplateEvent;\nimport org.thymeleaf.model.IText;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\n\n/**\n * <p>This processor will remove the duplicate meta tag with the same name in head tag and only\n * keep the last one.</p>\n * <p>This processor will be executed last.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Order\n@Component\n@AllArgsConstructor\npublic class DuplicateMetaTagProcessor implements TemplateHeadProcessor {\n    static final Pattern META_PATTERN = Pattern.compile(\"<meta[^>]+?name=\\\"([^\\\"]+)\\\"[^>]*>\\\\n*\");\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        IModel newModel = context.getModelFactory().createModel();\n\n        Map<String, IndexedModel> uniqueMetaTags = new LinkedHashMap<>();\n        List<IndexedModel> otherModel = new ArrayList<>();\n        for (int i = 0; i < model.size(); i++) {\n            ITemplateEvent templateEvent = model.get(i);\n            // If the current node is a text node, it is processed separately.\n            // Because the text node may contain multiple meta tags.\n            if (templateEvent instanceof IText textNode) {\n                String text = textNode.getText();\n                Matcher matcher = META_PATTERN.matcher(text);\n                while (matcher.find()) {\n                    String tagLine = matcher.group(0);\n                    String nameAttribute = matcher.group(1);\n                    // create a new text node to replace the original text node\n                    // replace multiple line breaks with one line break\n                    IText metaTagNode = context.getModelFactory()\n                        .createText(tagLine.replaceAll(\"\\\\n+\", \"\\n\"));\n                    uniqueMetaTags.put(nameAttribute, new IndexedModel(i, metaTagNode));\n                    text = text.replace(tagLine, \"\");\n                }\n                // put the rest of the text into the other model\n                IText otherText = context.getModelFactory()\n                    .createText(text);\n                otherModel.add(new IndexedModel(i, otherText));\n                continue;\n            }\n            if (templateEvent instanceof IProcessableElementTag tag) {\n                var indexedModel = new IndexedModel(i, tag);\n                if (\"meta\".equals(tag.getElementCompleteName())) {\n                    var attribute = tag.getAttribute(\"name\");\n                    if (attribute != null) {\n                        uniqueMetaTags.put(attribute.getValue(), indexedModel);\n                        continue;\n                    }\n                }\n            }\n            otherModel.add(new IndexedModel(i, templateEvent));\n        }\n\n        otherModel.addAll(uniqueMetaTags.values());\n        otherModel.stream().sorted(Comparator.comparing(IndexedModel::index))\n            .map(IndexedModel::templateEvent)\n            .forEach(newModel::add);\n\n        model.reset();\n        model.addModel(newModel);\n        return Mono.empty();\n    }\n\n    record IndexedModel(int index, ITemplateEvent templateEvent) {\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/EvaluationContextEnhancer.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static run.halo.app.theme.HaloViewResolver.HaloView.CONTEXT_VIEW_KEY;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport org.springframework.core.convert.TypeDescriptor;\nimport org.springframework.expression.AccessException;\nimport org.springframework.expression.EvaluationContext;\nimport org.springframework.expression.MethodExecutor;\nimport org.springframework.expression.MethodResolver;\nimport org.springframework.expression.PropertyAccessor;\nimport org.springframework.expression.TypedValue;\nimport org.springframework.expression.spel.CompilablePropertyAccessor;\nimport org.springframework.expression.spel.support.ReflectivePropertyAccessor;\nimport org.springframework.integration.json.JsonPropertyAccessor;\nimport org.springframework.lang.Nullable;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.ITemplateEnd;\nimport org.thymeleaf.model.ITemplateStart;\nimport org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor;\nimport org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport org.thymeleaf.standard.StandardDialect;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport reactor.util.context.ContextView;\nimport run.halo.app.infra.utils.ReactiveUtils;\n\n/**\n * Enhance the evaluation context to support reactive types.\n *\n * @author guqing\n * @author johnniang\n * @since 2.20.0\n */\npublic class EvaluationContextEnhancer extends AbstractTemplateBoundariesProcessor {\n\n    private static final int PRECEDENCE = StandardDialect.PROCESSOR_PRECEDENCE;\n\n    private static final JsonPropertyAccessor JSON_PROPERTY_ACCESSOR = new JsonPropertyAccessor();\n\n    public EvaluationContextEnhancer() {\n        super(TemplateMode.HTML, PRECEDENCE);\n    }\n\n    @Override\n    public void doProcessTemplateStart(ITemplateContext context, ITemplateStart templateStart,\n        ITemplateBoundariesStructureHandler structureHandler) {\n        var evluationContextObject = context.getVariable(\n            ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME\n        );\n        if (evluationContextObject instanceof ThymeleafEvaluationContext evaluationContext) {\n            evaluationContext.addPropertyAccessor(JSON_PROPERTY_ACCESSOR);\n            ReactiveReflectivePropertyAccessor.wrap(evaluationContext);\n            ReactiveMethodResolver.wrap(evaluationContext, context);\n        }\n    }\n\n    @Override\n    public void doProcessTemplateEnd(ITemplateContext context, ITemplateEnd templateEnd,\n        ITemplateBoundariesStructureHandler structureHandler) {\n        // nothing to do\n    }\n\n    /**\n     * A {@link PropertyAccessor} that wraps the original {@link ReflectivePropertyAccessor} and\n     * blocks the reactive value.\n     */\n    private static class ReactiveReflectivePropertyAccessor\n        extends ReflectivePropertyAccessor {\n        private final ReflectivePropertyAccessor delegate;\n\n        private ReactiveReflectivePropertyAccessor(ReflectivePropertyAccessor delegate) {\n            this.delegate = delegate;\n        }\n\n        @Override\n        public boolean canRead(EvaluationContext context, Object target, String name)\n            throws AccessException {\n            if (target == null) {\n                // For backward compatibility\n                return true;\n            }\n            return this.delegate.canRead(context, target, name);\n        }\n\n        @Override\n        public TypedValue read(EvaluationContext context, Object target, String name)\n            throws AccessException {\n            if (target == null) {\n                // For backward compatibility\n                return TypedValue.NULL;\n            }\n            var typedValue = delegate.read(context, target, name);\n            return Optional.of(typedValue)\n                .filter(tv ->\n                    Objects.nonNull(tv.getValue())\n                        && Objects.nonNull(tv.getTypeDescriptor())\n                        && ReactiveUtils.isReactiveType(tv.getTypeDescriptor().getType())\n                )\n                .map(tv -> new TypedValue(ReactiveUtils.blockReactiveValue(tv.getValue())))\n                .orElse(typedValue);\n        }\n\n        @Override\n        public boolean canWrite(EvaluationContext context, Object target, String name)\n            throws AccessException {\n            return delegate.canWrite(context, target, name);\n        }\n\n        @Override\n        public void write(EvaluationContext context, Object target, String name, Object newValue)\n            throws AccessException {\n            delegate.write(context, target, name, newValue);\n        }\n\n        @Override\n        public Class<?>[] getSpecificTargetClasses() {\n            return delegate.getSpecificTargetClasses();\n        }\n\n        @Override\n        public PropertyAccessor createOptimalAccessor(EvaluationContext context, Object target,\n            String name) {\n            var optimalAccessor = delegate.createOptimalAccessor(context, target, name);\n            if (optimalAccessor instanceof CompilablePropertyAccessor optimalPropertyAccessor) {\n                if (ReactiveUtils.isReactiveType(optimalPropertyAccessor.getPropertyType())) {\n                    return this;\n                }\n                return optimalPropertyAccessor;\n            }\n            return this;\n        }\n\n        static void wrap(ThymeleafEvaluationContext evaluationContext) {\n            var wrappedPropertyAccessors = evaluationContext.getPropertyAccessors()\n                .stream()\n                .map(propertyAccessor -> {\n                    if (propertyAccessor instanceof ReflectivePropertyAccessor reflectiveAccessor) {\n                        return new ReactiveReflectivePropertyAccessor(reflectiveAccessor);\n                    }\n                    return propertyAccessor;\n                })\n                // make the list mutable\n                .collect(Collectors.toCollection(ArrayList::new));\n            evaluationContext.setPropertyAccessors(wrappedPropertyAccessors);\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            return delegate.equals(obj);\n        }\n\n        @Override\n        public int hashCode() {\n            return delegate.hashCode();\n        }\n\n    }\n\n    /**\n     * A {@link MethodResolver} that wraps the original {@link MethodResolver} and blocks the\n     * reactive value.\n     *\n     * @param delegate the original {@link MethodResolver}\n     * @param templateContext the template context\n     */\n    private record ReactiveMethodResolver(MethodResolver delegate, ITemplateContext templateContext)\n        implements MethodResolver {\n\n        @Override\n        @Nullable\n        public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,\n            List<TypeDescriptor> argumentTypes) throws AccessException {\n            var executor = delegate.resolve(context, targetObject, name, argumentTypes);\n            return Optional.ofNullable(executor)\n                .map(methodExecutor -> new ReactiveMethodExecutor(methodExecutor, templateContext))\n                .orElse(null);\n        }\n\n        static void wrap(ThymeleafEvaluationContext evaluationContext, ITemplateContext context) {\n            var wrappedMethodResolvers = evaluationContext.getMethodResolvers()\n                .stream()\n                .<MethodResolver>map(\n                    methodResolver -> new ReactiveMethodResolver(methodResolver, context)\n                )\n                // make the list mutable\n                .collect(Collectors.toCollection(ArrayList::new));\n            evaluationContext.setMethodResolvers(wrappedMethodResolvers);\n        }\n\n    }\n\n    /**\n     * A {@link MethodExecutor} that wraps the original {@link MethodExecutor} and blocks the\n     * reactive value.\n     *\n     * @param delegate the original {@link MethodExecutor}\n     * @param templateContext the template context\n     */\n    private record ReactiveMethodExecutor(MethodExecutor delegate, ITemplateContext templateContext)\n        implements MethodExecutor {\n\n        @Override\n        public TypedValue execute(EvaluationContext context, Object target, Object... arguments)\n            throws AccessException {\n            var typedValue = delegate.execute(context, target, arguments);\n            return Optional.of(typedValue)\n                .filter(tv ->\n                    Objects.nonNull(tv.getValue())\n                        && Objects.nonNull(tv.getTypeDescriptor())\n                        && ReactiveUtils.isReactiveType(tv.getTypeDescriptor().getType())\n                )\n                .map(tv -> {\n                    var contextView = (ContextView) Optional.ofNullable(\n                            templateContext.getVariable(CONTEXT_VIEW_KEY)\n                        )\n                        .filter(ContextView.class::isInstance)\n                        .orElse(null);\n                    return new TypedValue(\n                        ReactiveUtils.blockReactiveValue(tv.getValue(), contextView)\n                    );\n                })\n                .orElse(typedValue);\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.thymeleaf.model.AttributeValueQuotes.DOUBLE;\n\nimport java.util.Map;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.info.BuildProperties;\nimport org.springframework.core.annotation.Order;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\n\n/**\n * Processor for generating generator meta.\n * Set the order to 0 for removing the meta in later TemplateHeadProcessor.\n *\n * @author johnniang\n */\n@Order(0)\npublic class GeneratorMetaProcessor implements TemplateHeadProcessor {\n\n    private final String generatorValue;\n\n    public GeneratorMetaProcessor(ObjectProvider<BuildProperties> buildProperties) {\n        this.generatorValue = \"Halo \" + buildProperties.stream().findFirst()\n            .map(BuildProperties::getVersion)\n            .orElse(\"Unknown\");\n    }\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        return Mono.fromRunnable(() -> {\n            var modelFactory = context.getModelFactory();\n            var generatorMeta = modelFactory.createStandaloneElementTag(\"meta\",\n                Map.of(\"name\", \"generator\", \"content\", generatorValue),\n                DOUBLE, false, true);\n            model.add(generatorMeta);\n        });\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.thymeleaf.spring6.context.SpringContextUtils.getApplicationContext;\n\nimport java.time.Duration;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.ITemplateEvent;\nimport org.thymeleaf.processor.element.AbstractElementModelProcessor;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Global head injection processor.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class GlobalHeadInjectionProcessor extends AbstractElementModelProcessor {\n\n    private static final Duration BLOCKING_TIMEOUT = Duration.ofSeconds(10);\n    /**\n     * Inserting tag will re-trigger this processor, in order to avoid the loop out trigger,\n     * this flag is required to prevent the loop problem.\n     */\n    private static final String PROCESS_FLAG =\n        GlobalHeadInjectionProcessor.class.getName() + \".PROCESSED\";\n\n    private static final String TAG_NAME = \"head\";\n    private static final int PRECEDENCE = 1000;\n\n    public GlobalHeadInjectionProcessor(final String dialectPrefix) {\n        super(\n            TemplateMode.HTML, // This processor will apply only to HTML mode\n            dialectPrefix,     // Prefix to be applied to name for matching\n            TAG_NAME,          // Tag name: match specifically this tag\n            false,              // Apply dialect prefix to tag name\n            null,              // No attribute name: will match by tag name\n            false,             // No prefix to be applied to attribute name\n            PRECEDENCE);       // Precedence (inside dialect's own precedence)\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        if (context.containsVariable(InjectionExcluderProcessor.EXCLUDE_INJECTION_VARIABLE)) {\n            return;\n        }\n\n        // note that this is important!!\n        Object processedAlready = context.getVariable(PROCESS_FLAG);\n        if (processedAlready != null) {\n            return;\n        }\n        structureHandler.setLocalVariable(PROCESS_FLAG, true);\n\n        // handle <head> tag\n        if (model.size() < 2) {\n            return;\n        }\n\n        /*\n         * Create the DOM structure that will be substituting our custom tag.\n         * The headline will be shown inside a '<div>' tag, and so this must\n         * be created first and then a Text node must be added to it.\n         */\n        IModel modelToInsert = model.cloneModel();\n        // close </head> tag\n        final ITemplateEvent closeHeadTag = modelToInsert.get(modelToInsert.size() - 1);\n        modelToInsert.remove(modelToInsert.size() - 1);\n\n        // open <head> tag\n        final ITemplateEvent openHeadTag = modelToInsert.get(0);\n        modelToInsert.remove(0);\n\n        // apply processors to modelToInsert\n        getTemplateHeadProcessors(context)\n            .concatMap(processor -> processor.process(\n                SecureTemplateContextWrapper.wrap(context), modelToInsert, structureHandler)\n            )\n            .then()\n            .block(BLOCKING_TIMEOUT);\n\n        // reset model to insert\n        model.reset();\n        model.add(openHeadTag);\n        model.addModel(modelToInsert);\n        model.add(closeHeadTag);\n    }\n\n    private Flux<TemplateHeadProcessor> getTemplateHeadProcessors(ITemplateContext context) {\n        var extensionGetter = getApplicationContext(context).getBeanProvider(ExtensionGetter.class)\n            .getIfUnique();\n        if (extensionGetter == null) {\n            return Flux.empty();\n        }\n        return extensionGetter.getExtensions(TemplateHeadProcessor.class);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/GlobalSeoProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static run.halo.app.theme.Constant.META_DESCRIPTION_VARIABLE_NAME;\n\nimport java.util.LinkedHashMap;\nimport lombok.AllArgsConstructor;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.AttributeValueQuotes;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n/**\n * Inject code to the template head tag according to the global seo settings.\n *\n * @author guqing\n * @see SystemSetting.Seo\n * @since 2.0.0\n */\n@Order(Ordered.HIGHEST_PRECEDENCE + 1)\n@Component\n@AllArgsConstructor\nclass GlobalSeoProcessor implements TemplateHeadProcessor {\n\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        return environmentFetcher.fetch(SystemSetting.Seo.GROUP, SystemSetting.Seo.class)\n            .switchIfEmpty(Mono.fromSupplier(SystemSetting.Seo::new))\n            .doOnNext(seo -> {\n                IModelFactory modelFactory = context.getModelFactory();\n                if (Boolean.TRUE.equals(seo.getBlockSpiders())) {\n                    var attributes = LinkedHashMap.<String, String>newLinkedHashMap(2);\n                    attributes.put(\"name\", \"robots\");\n                    attributes.put(\"content\", \"noindex\");\n                    var metaTag = modelFactory.createStandaloneElementTag(\n                        \"meta\",\n                        attributes,\n                        AttributeValueQuotes.DOUBLE,\n                        false,\n                        true\n                    );\n                    model.add(metaTag);\n                    return;\n                }\n                var seoMetaDescription = context.getVariable(META_DESCRIPTION_VARIABLE_NAME);\n                if (seoMetaDescription instanceof String description && !description.isBlank()) {\n                    var attributes = LinkedHashMap.<String, String>newLinkedHashMap(2);\n                    attributes.put(\"name\", \"description\");\n                    attributes.put(\"content\", description);\n                    var metaTag = modelFactory.createStandaloneElementTag(\n                        \"meta\",\n                        attributes,\n                        AttributeValueQuotes.DOUBLE,\n                        false,\n                        true\n                    );\n                    model.add(metaTag);\n                }\n            })\n            .then();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/HaloExpressionObjectFactory.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport java.util.Set;\nimport org.thymeleaf.context.IExpressionContext;\nimport org.thymeleaf.expression.IExpressionObjectFactory;\nimport run.halo.app.theme.dialect.expression.Annotations;\n\n/**\n * Builds the expression objects to be used by Halo dialects.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class HaloExpressionObjectFactory implements IExpressionObjectFactory {\n\n    public static final String ANNOTATIONS_EXPRESSION_OBJECT_NAME = \"annotations\";\n\n    protected static final Set<String> ALL_EXPRESSION_OBJECT_NAMES = Set.of(\n        ANNOTATIONS_EXPRESSION_OBJECT_NAME);\n\n    private static final Annotations ANNOTATIONS = new Annotations();\n\n    @Override\n    public Set<String> getAllExpressionObjectNames() {\n        return ALL_EXPRESSION_OBJECT_NAMES;\n    }\n\n    @Override\n    public Object buildObject(IExpressionContext context, String expressionObjectName) {\n        if (ANNOTATIONS_EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) {\n            return ANNOTATIONS;\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isCacheable(String expressionObjectName) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/HaloPostTemplateHandler.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.thymeleaf.spring6.context.SpringContextUtils.getApplicationContext;\n\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport org.springframework.lang.NonNull;\nimport org.springframework.util.CollectionUtils;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AbstractTemplateHandler;\nimport org.thymeleaf.model.IOpenElementTag;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.model.IStandaloneElementTag;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Template post-handler.\n *\n * @author johnniang\n * @since 2.20.0\n */\npublic class HaloPostTemplateHandler extends AbstractTemplateHandler {\n\n    private List<ElementTagPostProcessor> postProcessors = List.of();\n\n    @Override\n    public void setContext(ITemplateContext context) {\n        super.setContext(context);\n        this.postProcessors = Optional.ofNullable(getApplicationContext(context))\n            .map(appContext -> appContext.getBeanProvider(ExtensionGetter.class).getIfUnique())\n            .map(extensionGetter -> extensionGetter.getExtensionList(ElementTagPostProcessor.class))\n            .orElseGet(List::of);\n    }\n\n    @Override\n    public void handleStandaloneElement(IStandaloneElementTag standaloneElementTag) {\n        var processedTag = handleElementTag(standaloneElementTag);\n        super.handleStandaloneElement((IStandaloneElementTag) processedTag);\n    }\n\n    @Override\n    public void handleOpenElement(IOpenElementTag openElementTag) {\n        var processedTag = handleElementTag(openElementTag);\n        super.handleOpenElement((IOpenElementTag) processedTag);\n    }\n\n    @NonNull\n    private IProcessableElementTag handleElementTag(\n        @NonNull IProcessableElementTag processableElementTag\n    ) {\n        IProcessableElementTag processedTag = processableElementTag;\n        if (!CollectionUtils.isEmpty(postProcessors)) {\n            var tagProcessorChain = Mono.just(processableElementTag);\n            var context = getContext();\n            for (ElementTagPostProcessor elementTagPostProcessor : postProcessors) {\n                tagProcessorChain = tagProcessorChain.flatMap(\n                    tag -> elementTagPostProcessor.process(\n                            SecureTemplateContextWrapper.wrap(context), tag)\n                        .defaultIfEmpty(tag)\n                );\n            }\n            processedTag =\n                Objects.requireNonNull(tagProcessorChain.defaultIfEmpty(processableElementTag)\n                    .block(Duration.ofMinutes(1)));\n        }\n        return processedTag;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.thymeleaf.templatemode.TemplateMode.HTML;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.thymeleaf.dialect.AbstractProcessorDialect;\nimport org.thymeleaf.dialect.IExpressionObjectDialect;\nimport org.thymeleaf.dialect.IPostProcessorDialect;\nimport org.thymeleaf.expression.IExpressionObjectFactory;\nimport org.thymeleaf.postprocessor.IPostProcessor;\nimport org.thymeleaf.postprocessor.PostProcessor;\nimport org.thymeleaf.processor.IProcessor;\nimport org.thymeleaf.standard.StandardDialect;\n\n/**\n * Thymeleaf processor dialect for Halo.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class HaloProcessorDialect extends AbstractProcessorDialect\n    implements IExpressionObjectDialect, IPostProcessorDialect {\n    private static final String DIALECT_NAME = \"haloThemeProcessorDialect\";\n\n    private static final IExpressionObjectFactory HALO_EXPRESSION_OBJECTS_FACTORY =\n        new HaloExpressionObjectFactory();\n\n    public HaloProcessorDialect() {\n        // We will set this dialect the same \"dialect processor\" precedence as\n        // the Standard Dialect, so that processor executions can interleave.\n        super(DIALECT_NAME, \"halo\", StandardDialect.PROCESSOR_PRECEDENCE);\n    }\n\n    @Override\n    public Set<IProcessor> getProcessors(String dialectPrefix) {\n        final Set<IProcessor> processors = new HashSet<IProcessor>();\n        // add more processors\n        processors.add(new GlobalHeadInjectionProcessor(dialectPrefix));\n        processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));\n        processors.add(new EvaluationContextEnhancer());\n        processors.add(new CommentElementTagProcessor(dialectPrefix));\n        processors.add(new CommentEnabledVariableProcessor());\n        processors.add(new InjectionExcluderProcessor());\n        return processors;\n    }\n\n    @Override\n    public IExpressionObjectFactory getExpressionObjectFactory() {\n        return HALO_EXPRESSION_OBJECTS_FACTORY;\n    }\n\n    @Override\n    public int getDialectPostProcessorPrecedence() {\n        return Integer.MAX_VALUE;\n    }\n\n    @Override\n    public Set<IPostProcessor> getPostProcessors() {\n        return Set.of(new PostProcessor(HTML, HaloPostTemplateHandler.class, Integer.MAX_VALUE));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;\nimport static org.thymeleaf.extras.springsecurity6.dialect.processor.AuthorizeAttrProcessor.ATTR_NAME;\nimport static org.thymeleaf.extras.springsecurity6.dialect.processor.AuthorizeAttrProcessor.ATTR_PRECEDENCE;\nimport static run.halo.app.infra.AnonymousUserConst.PRINCIPAL;\nimport static run.halo.app.infra.AnonymousUserConst.Role;\n\nimport java.util.LinkedHashSet;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.security.access.expression.ExpressionUtils;\nimport org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;\nimport org.springframework.security.authentication.AnonymousAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContextImpl;\nimport org.springframework.security.util.MethodInvocationUtils;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.extras.springsecurity6.auth.AuthUtils;\nimport org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;\nimport org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;\nimport org.thymeleaf.extras.springsecurity6.util.SpringVersionSpecificUtils;\nimport org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.IProcessor;\nimport org.thymeleaf.standard.processor.AbstractStandardConditionalVisibilityTagProcessor;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\n/**\n * HaloSpringSecurityDialect overwrites value of thymeleafSpringSecurityContext.\n *\n * @author johnniang\n */\npublic class HaloSpringSecurityDialect extends SpringSecurityDialect implements InitializingBean {\n\n    private static final String SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME =\n        \"ThymeleafReactiveModelAdditions:\"\n            + SpringSecurityContextUtils.SECURITY_CONTEXT_MODEL_ATTRIBUTE_NAME;\n\n    private final ServerSecurityContextRepository securityContextRepository;\n\n    private final ObjectProvider<MethodSecurityExpressionHandler> expressionHandler;\n\n    public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository,\n        ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {\n        this.securityContextRepository = securityContextRepository;\n        this.expressionHandler = expressionHandler;\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        if (!SpringVersionUtils.isSpringWebFluxPresent()) {\n            return;\n        }\n\n        // We have to build an anonymous authentication token here because the token won't be saved\n        // into repository during anonymous authentication.\n        var anonymousAuthentication =\n            new AnonymousAuthenticationToken(\n                \"fallback\", PRINCIPAL, createAuthorityList(AuthorityUtils.ROLE_PREFIX + Role)\n            );\n        var anonymousSecurityContext = new SecurityContextImpl(anonymousAuthentication);\n\n        final Function<ServerWebExchange, Object> secCtxInitializer =\n            exchange -> securityContextRepository.load(exchange)\n                .defaultIfEmpty(anonymousSecurityContext);\n\n        // Just overwrite the value of the attribute\n        getExecutionAttributes().put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer);\n    }\n\n    @Override\n    public Set<IProcessor> getProcessors(String dialectPrefix) {\n        LinkedHashSet<IProcessor> processors = new LinkedHashSet<>();\n        processors.add(\n            new HaloAuthorizeAttrProcessor(TemplateMode.HTML, dialectPrefix, ATTR_NAME)\n        );\n        processors.addAll(super.getProcessors(dialectPrefix));\n        return processors;\n    }\n\n    public class HaloAuthorizeAttrProcessor\n        extends AbstractStandardConditionalVisibilityTagProcessor {\n\n        protected HaloAuthorizeAttrProcessor(TemplateMode templateMode, String dialectPrefix,\n            String attrName) {\n            super(templateMode, dialectPrefix, attrName, ATTR_PRECEDENCE - 10);\n        }\n\n        @Override\n        protected boolean isVisible(ITemplateContext context, IProcessableElementTag tag,\n            AttributeName attributeName, String attributeValue) {\n\n            final String attrValue = (attributeValue == null ? null : attributeValue.trim());\n\n            if (attrValue == null || attrValue.isEmpty()) {\n                return false;\n            }\n\n            final Authentication authentication = AuthUtils.getAuthenticationObject(context);\n\n            if (authentication == null) {\n                return false;\n            }\n\n            // resolve expr\n            var expr = Optional.of(attributeValue)\n                .filter(v -> v.startsWith(\"${\") && v.endsWith(\"}\"))\n                .map(v -> v.substring(2, v.length() - 1))\n                .orElse(attributeValue);\n\n            var expressionHandler = HaloSpringSecurityDialect.this.expressionHandler.getIfUnique();\n            if (expressionHandler == null) {\n                // no expression handler found\n                return false;\n            }\n\n            var expression = expressionHandler.getExpressionParser().parseExpression(expr);\n\n            var methodInvocation = MethodInvocationUtils.createFromClass(this,\n                HaloAuthorizeAttrProcessor.class,\n                \"dummyAuthorize\",\n                new Class[] {Authentication.class},\n                new Object[] {authentication}\n            );\n            var evaluationContext =\n                expressionHandler.createEvaluationContext(authentication, methodInvocation);\n\n            var expressionObjects = context.getExpressionObjects();\n            var wrappedEvolutionContext = SpringVersionSpecificUtils.wrapEvaluationContext(\n                evaluationContext, expressionObjects\n            );\n\n            return ExpressionUtils.evaluateAsBoolean(expression, wrappedEvolutionContext);\n        }\n\n        /**\n         * This method is only used to create a method invocation for the expression parser.\n         *\n         * @param authentication authentication object\n         * @return result of authorization expression evaluation\n         */\n        public Boolean dummyAuthorize(Authentication authentication) {\n            throw new UnsupportedOperationException(\"Should not be called\");\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/HaloTrackerProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.utils.PathUtils;\n\n/**\n * Get {@link GroupVersionKind} and {@code plural} from the view model to construct tracker\n * script tag and insert it into the head tag.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class HaloTrackerProcessor implements TemplateHeadProcessor {\n\n    public static final String SKIP_TRACKER =\n        HaloTrackerProcessor.class.getName() + \".SKIP_TRACKER\";\n\n    private final ExternalUrlSupplier externalUrlGetter;\n\n    public HaloTrackerProcessor(ExternalUrlSupplier externalUrlGetter) {\n        this.externalUrlGetter = externalUrlGetter;\n    }\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        // Check if tracker should be skipped\n        var isSkip = (Boolean) context.getVariable(SKIP_TRACKER);\n        if (BooleanUtils.isTrue(isSkip)) {\n            return Mono.empty();\n        }\n\n        final IModelFactory modelFactory = context.getModelFactory();\n        return Mono.just(getTrackerScript(context))\n            .filter(StringUtils::isNotBlank)\n            .map(trackerScript -> {\n                model.add(modelFactory.createText(trackerScript));\n                return trackerScript;\n            })\n            .then();\n    }\n\n    private String getTrackerScript(ITemplateContext context) {\n        String resourceName = (String) context.getVariable(\"name\");\n        String externalUrl = externalUrlGetter.get().getPath();\n        Object groupVersionKind = context.getVariable(\"groupVersionKind\");\n        Object plural = context.getVariable(\"plural\");\n        if (groupVersionKind == null || plural == null) {\n            return StringUtils.EMPTY;\n        }\n        if (!(groupVersionKind instanceof GroupVersionKind gvk)) {\n            return StringUtils.EMPTY;\n        }\n        return trackerScript(externalUrl, gvk.group(), (String) plural, resourceName);\n    }\n\n    private String trackerScript(String externalUrl, String group, String plural, String name) {\n        String jsSrc = PathUtils.combinePath(externalUrl, \"/halo-tracker.js\");\n        return \"\"\"\n            <script async defer src=\"%s\" data-group=\"%s\" data-plural=\"%s\" data-name=\"%s\"></script>\n            \"\"\".formatted(jsSrc, group, plural, name);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/IndexSeoProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * Processor for index page SEO.\n *\n * @author ryanwang\n */\n@Component\n@AllArgsConstructor\nclass IndexSeoProcessor implements TemplateHeadProcessor {\n\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        if (!isIndexTemplate(context)) {\n            return Mono.empty();\n        }\n        return environmentFetcher.fetch(SystemSetting.Seo.GROUP, SystemSetting.Seo.class)\n            .map(seo -> {\n                IModelFactory modelFactory = context.getModelFactory();\n\n                String keywords = seo.getKeywords();\n                if (StringUtils.isNotBlank(keywords)) {\n                    String keywordsMeta =\n                        \"<meta name=\\\"keywords\\\" content=\\\"\" + keywords + \"\\\" />\\n\";\n                    model.add(modelFactory.createText(keywordsMeta));\n                }\n\n                if (StringUtils.isNotBlank(seo.getDescription())) {\n                    String descriptionMeta =\n                        \"<meta name=\\\"description\\\" content=\\\"\" + seo.getDescription() + \"\\\" />\\n\";\n                    model.add(modelFactory.createText(descriptionMeta));\n                }\n                return model;\n            })\n            .then();\n    }\n\n    private boolean isIndexTemplate(ITemplateContext context) {\n        return DefaultTemplateEnum.INDEX.getValue()\n            .equals(context.getVariable(ModelConst.TEMPLATE_ID));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/InjectionExcluderProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport java.util.Set;\nimport java.util.regex.Pattern;\nimport org.springframework.util.Assert;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.ITemplateEnd;\nimport org.thymeleaf.model.ITemplateStart;\nimport org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor;\nimport org.thymeleaf.processor.templateboundaries.ITemplateBoundariesProcessor;\nimport org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler;\nimport org.thymeleaf.standard.StandardDialect;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n * <p>Determine whether the current template being rendered needs to exclude the processor of\n * code injection. If it needs to be excluded, set a local variable.</p>\n * <p>Why do you need to set a local variable here instead of directly judging in the processor?</p>\n * <p>Because the processor will process the fragment, and if you need to exclude the <code>login\n * .html</code> template and the login.html is only a fragment, then the exclusion logic will\n * fail, so here use {@link ITemplateBoundariesProcessor} events are only fired for the\n * first-level template to solve this problem.</p>\n *\n * @author guqing\n * @since 2.20.0\n */\npublic class InjectionExcluderProcessor extends AbstractTemplateBoundariesProcessor {\n\n    public static final String EXCLUDE_INJECTION_VARIABLE =\n        InjectionExcluderProcessor.class.getName() + \".EXCLUDE_INJECTION\";\n\n    private final PageInjectionExcluder injectionExcluder = new PageInjectionExcluder();\n\n    public InjectionExcluderProcessor() {\n        super(TemplateMode.HTML, StandardDialect.PROCESSOR_PRECEDENCE);\n    }\n\n    @Override\n    public void doProcessTemplateStart(ITemplateContext context, ITemplateStart templateStart,\n        ITemplateBoundariesStructureHandler structureHandler) {\n        if (isExcluded(context)) {\n            structureHandler.setLocalVariable(EXCLUDE_INJECTION_VARIABLE, true);\n        }\n    }\n\n    @Override\n    public void doProcessTemplateEnd(ITemplateContext context, ITemplateEnd templateEnd,\n        ITemplateBoundariesStructureHandler structureHandler) {\n        structureHandler.removeLocalVariable(EXCLUDE_INJECTION_VARIABLE);\n    }\n\n    /**\n     * Check if the template will be rendered is excluded injection.\n     *\n     * @param context template context\n     * @return true if the template is excluded, otherwise false\n     */\n    boolean isExcluded(ITemplateContext context) {\n        return injectionExcluder.isExcluded(context.getTemplateData().getTemplate());\n    }\n\n    static class PageInjectionExcluder {\n\n        private final Set<String> exactMatches = Set.of(\n            \"login\",\n            \"signup\",\n            \"logout\"\n        );\n\n        private final Set<Pattern> regexPatterns = Set.of(\n            Pattern.compile(\"error/.*\"),\n            Pattern.compile(\"challenges/.*\"),\n            Pattern.compile(\"password-reset/.*\")\n        );\n\n        public boolean isExcluded(String templateName) {\n            Assert.notNull(templateName, \"Template name must not be null\");\n            if (exactMatches.contains(templateName)) {\n                return true;\n            }\n\n            for (Pattern pattern : regexPatterns) {\n                if (pattern.matcher(templateName).matches()) {\n                    return true;\n                }\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/LinkExpressionObjectDialect.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.thymeleaf.dialect.AbstractDialect;\nimport org.thymeleaf.dialect.IExpressionObjectDialect;\nimport org.thymeleaf.expression.IExpressionObjectFactory;\n\n/**\n * An expression object dialect for theme link.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class LinkExpressionObjectDialect extends AbstractDialect implements\n    IExpressionObjectDialect {\n\n    private static final IExpressionObjectFactory LINK_EXPRESSION_OBJECTS_FACTORY =\n        new DefaultLinkExpressionFactory();\n\n    public LinkExpressionObjectDialect() {\n        super(\"themeLink\");\n    }\n\n    @Override\n    public IExpressionObjectFactory getExpressionObjectFactory() {\n        return LINK_EXPRESSION_OBJECTS_FACTORY;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/SecureTemplateContext.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.thymeleaf.spring6.expression.ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME;\n\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.context.IdentifierSequences;\nimport org.thymeleaf.engine.TemplateData;\nimport org.thymeleaf.expression.IExpressionObjects;\nimport org.thymeleaf.inline.IInliner;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n * Secure template context.\n *\n * @author johnniang\n * @since 2.20.0\n */\nclass SecureTemplateContext implements ITemplateContext {\n\n    private static final Set<String> DANGEROUS_VARIABLES =\n        Set.of(THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);\n\n    private final ITemplateContext delegate;\n\n    public SecureTemplateContext(ITemplateContext delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public TemplateData getTemplateData() {\n        return delegate.getTemplateData();\n    }\n\n    @Override\n    public TemplateMode getTemplateMode() {\n        return delegate.getTemplateMode();\n    }\n\n    @Override\n    public List<TemplateData> getTemplateStack() {\n        return delegate.getTemplateStack();\n    }\n\n    @Override\n    public List<IProcessableElementTag> getElementStack() {\n        return delegate.getElementStack();\n    }\n\n    @Override\n    public Map<String, Object> getTemplateResolutionAttributes() {\n        return delegate.getTemplateResolutionAttributes();\n    }\n\n    @Override\n    public IModelFactory getModelFactory() {\n        return delegate.getModelFactory();\n    }\n\n    @Override\n    public boolean hasSelectionTarget() {\n        return delegate.hasSelectionTarget();\n    }\n\n    @Override\n    public Object getSelectionTarget() {\n        return delegate.getSelectionTarget();\n    }\n\n    @Override\n    public IInliner getInliner() {\n        return delegate.getInliner();\n    }\n\n    @Override\n    public String getMessage(\n        Class<?> origin,\n        String key,\n        Object[] messageParameters,\n        boolean useAbsentMessageRepresentation\n    ) {\n        return delegate.getMessage(origin, key, messageParameters, useAbsentMessageRepresentation);\n    }\n\n    @Override\n    public String buildLink(String base, Map<String, Object> parameters) {\n        return delegate.buildLink(base, parameters);\n    }\n\n    @Override\n    public IdentifierSequences getIdentifierSequences() {\n        return delegate.getIdentifierSequences();\n    }\n\n    @Override\n    public IEngineConfiguration getConfiguration() {\n        return delegate.getConfiguration();\n    }\n\n    @Override\n    public IExpressionObjects getExpressionObjects() {\n        return delegate.getExpressionObjects();\n    }\n\n    @Override\n    public Locale getLocale() {\n        return delegate.getLocale();\n    }\n\n    @Override\n    public boolean containsVariable(String name) {\n        if (DANGEROUS_VARIABLES.contains(name)) {\n            return false;\n        }\n        return delegate.containsVariable(name);\n    }\n\n    @Override\n    public Set<String> getVariableNames() {\n        return delegate.getVariableNames()\n            .stream()\n            .filter(name -> !DANGEROUS_VARIABLES.contains(name))\n            .collect(Collectors.toSet());\n    }\n\n    @Override\n    public Object getVariable(String name) {\n        if (DANGEROUS_VARIABLES.contains(name)) {\n            return null;\n        }\n        return delegate.getVariable(name);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        SecureTemplateContext that = (SecureTemplateContext) o;\n        return Objects.equals(delegate, that.delegate);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(delegate);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/SecureTemplateContextWrapper.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.thymeleaf.context.Contexts;\nimport org.thymeleaf.context.ITemplateContext;\n\n/**\n * Wrap the delegate template context to a secure template context according to whether it is a\n * WebContext.\n *\n * @author guqing\n * @since 2.20.4\n */\npublic class SecureTemplateContextWrapper {\n\n    /**\n     * Wrap the delegate template context to a secure template context.\n     */\n    static SecureTemplateContext wrap(ITemplateContext delegate) {\n        if (Contexts.isWebContext(delegate)) {\n            return new SecureTemplateWebContext(delegate);\n        }\n        return new SecureTemplateContext(delegate);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/SecureTemplateWebContext.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.springframework.context.ApplicationContext;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.context.IWebContext;\nimport org.thymeleaf.web.IWebExchange;\n\n/**\n * Secure template web context.\n * <p>It's used to prevent some dangerous variables such as {@link ApplicationContext} from being\n * accessed.\n *\n * @author guqing\n * @see SecureTemplateContext\n * @since 2.20.4\n */\nclass SecureTemplateWebContext extends SecureTemplateContext implements IWebContext {\n    private final IWebContext delegate;\n\n    /**\n     * The delegate must be an instance of IWebContext to create a SecureTemplateWebContext.\n     */\n    public SecureTemplateWebContext(ITemplateContext delegate) {\n        super(delegate);\n        if (delegate instanceof IWebContext webContext) {\n            this.delegate = webContext;\n        } else {\n            throw new IllegalArgumentException(\"The delegate must be an instance of IWebContext\");\n        }\n    }\n\n    @Override\n    public IWebExchange getExchange() {\n        return delegate.getExchange();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.thymeleaf.spring6.context.SpringContextUtils.getApplicationContext;\n\nimport java.time.Duration;\nimport org.springframework.context.ApplicationContext;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractElementTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.spring6.context.SpringContextUtils;\nimport org.thymeleaf.templatemode.TemplateMode;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * <p>Footer element tag processor.</p>\n * <p>Replace the footer tag <code>&#x3C;halo:footer /&#x3E;</code> with the contents of the footer\n * field of the global configuration item.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class TemplateFooterElementTagProcessor extends AbstractElementTagProcessor {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private static final String TAG_NAME = \"footer\";\n    private static final int PRECEDENCE = 1000;\n\n    /**\n     * Constructor footer element tag processor with HTML mode, dialect prefix, footer tag name.\n     *\n     * @param dialectPrefix dialect prefix\n     */\n    public TemplateFooterElementTagProcessor(final String dialectPrefix) {\n        super(\n            TemplateMode.HTML, // This processor will apply only to HTML mode\n            dialectPrefix,     // Prefix to be applied to name for matching\n            TAG_NAME,          // Tag name: match specifically this tag\n            true,              // Apply dialect prefix to tag name\n            null,              // No attribute name: will match by tag name\n            false,             // No prefix to be applied to attribute name\n            PRECEDENCE);       // Precedence (inside dialect's own precedence)\n    }\n\n    @Override\n    protected void doProcess(ITemplateContext context, IProcessableElementTag tag,\n        IElementTagStructureHandler structureHandler) {\n\n        if (context.containsVariable(InjectionExcluderProcessor.EXCLUDE_INJECTION_VARIABLE)) {\n            return;\n        }\n\n        IModel modelToInsert = context.getModelFactory().createModel();\n        /*\n         * Obtain the Spring application context.\n         */\n        final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);\n\n        String globalFooterText = getGlobalFooterText(appCtx);\n        modelToInsert.add(context.getModelFactory().createText(globalFooterText));\n\n        getTemplateFooterProcessors(context)\n            .concatMap(processor -> processor.process(\n                SecureTemplateContextWrapper.wrap(context), tag, structureHandler, modelToInsert)\n            )\n            .then()\n            .block(BLOCKING_TIMEOUT);\n        structureHandler.replaceWith(modelToInsert, false);\n    }\n\n    private String getGlobalFooterText(ApplicationContext appCtx) {\n        SystemConfigFetcher fetcher =\n            appCtx.getBean(SystemConfigFetcher.class);\n        return fetcher.fetch(SystemSetting.CodeInjection.GROUP, SystemSetting.CodeInjection.class)\n            .map(SystemSetting.CodeInjection::getFooter)\n            .block(BLOCKING_TIMEOUT);\n    }\n\n    private Flux<TemplateFooterProcessor> getTemplateFooterProcessors(ITemplateContext context) {\n        var extensionGetter = getApplicationContext(context).getBeanProvider(ExtensionGetter.class)\n            .getIfUnique();\n        if (extensionGetter == null) {\n            return Flux.empty();\n        }\n        return extensionGetter.getExtensions(TemplateFooterProcessor.class);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/TemplateGlobalHeadProcessor.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.core.Ordered;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.processor.element.IElementModelStructureHandler;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * <p>Global custom head snippet injection for theme global setting.</p>\n * <p>Globally injected head snippet can be overridden by content template.</p>\n *\n * @author guqing\n * @since 2.0.0\n */\n@Order(Ordered.HIGHEST_PRECEDENCE + 2)\n@Component\npublic class TemplateGlobalHeadProcessor implements TemplateHeadProcessor {\n\n    private final SystemConfigFetcher fetcher;\n\n    public TemplateGlobalHeadProcessor(SystemConfigFetcher fetcher) {\n        this.fetcher = fetcher;\n    }\n\n    @Override\n    public Mono<Void> process(ITemplateContext context, IModel model,\n        IElementModelStructureHandler structureHandler) {\n        final IModelFactory modelFactory = context.getModelFactory();\n        return fetchCodeInjection()\n            .doOnNext(codeInjection -> {\n                String globalHeader = codeInjection.getGlobalHead();\n                if (StringUtils.isNotBlank(globalHeader)) {\n                    model.add(modelFactory.createText(globalHeader + \"\\n\"));\n                }\n\n                // add content head to model\n                String contentHeader = codeInjection.getContentHead();\n                if (StringUtils.isNotBlank(contentHeader) && isContentTemplate(context)) {\n                    model.add(modelFactory.createText(contentHeader + \"\\n\"));\n                }\n            })\n            .then();\n    }\n\n    private Mono<SystemSetting.CodeInjection> fetchCodeInjection() {\n        return fetcher.fetch(SystemSetting.CodeInjection.GROUP, SystemSetting.CodeInjection.class);\n    }\n\n    private boolean isContentTemplate(ITemplateContext context) {\n        String templateId = (String) context.getVariable(ModelConst.TEMPLATE_ID);\n        return DefaultTemplateEnum.POST.getValue().equals(templateId)\n            || DefaultTemplateEnum.SINGLE_PAGE.getValue().equals(templateId);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/dialect/expression/Annotations.java",
    "content": "package run.halo.app.theme.dialect.expression;\n\nimport java.util.Map;\nimport org.springframework.lang.NonNull;\nimport org.springframework.lang.Nullable;\nimport run.halo.app.theme.finders.vo.ExtensionVoOperator;\n\n/**\n * <p>Expression Object for performing annotations operations inside Halo Extra Expressions.</p>\n * An object of this class is usually available in variable evaluation expressions with the name\n * <code>#annotations</code>.\n *\n * @author guqing\n * @since 2.0.2\n */\npublic class Annotations {\n\n    /**\n     * Get annotation value from extension vo.\n     *\n     * @param extension extension vo\n     * @param key the key of annotation\n     * @return annotation value if exists, otherwise null\n     */\n    @Nullable\n    public String get(ExtensionVoOperator extension, String key) {\n        Map<String, String> annotations = extension.getMetadata().getAnnotations();\n        if (annotations == null) {\n            return null;\n        }\n        return annotations.get(key);\n    }\n\n    /**\n     * Returns the value to which the specified key is mapped, or defaultValue if\n     * <code>extension</code> contains no mapping for the key.\n     *\n     * @param extension extension vo\n     * @param key the key of annotation\n     * @return annotation value if exists, otherwise defaultValue\n     */\n    @NonNull\n    public String getOrDefault(ExtensionVoOperator extension, String key, String defaultValue) {\n        Map<String, String> annotations = extension.getMetadata().getAnnotations();\n        if (annotations == null) {\n            return defaultValue;\n        }\n        return annotations.getOrDefault(key, defaultValue);\n    }\n\n    /**\n     * Check if the extension has the specified annotation.\n     *\n     * @param extension extension vo\n     * @param key the key of annotation\n     * @return true if the extension has the specified annotation, otherwise false\n     */\n    public boolean contains(ExtensionVoOperator extension, String key) {\n        Map<String, String> annotations = extension.getMetadata().getAnnotations();\n        if (annotations == null) {\n            return false;\n        }\n        return annotations.containsKey(key);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/endpoint/ThemeEndpoint.java",
    "content": "package run.halo.app.theme.endpoint;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\nimport static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;\nimport static org.springdoc.core.fn.builders.content.Builder.contentBuilder;\nimport static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;\nimport static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;\nimport static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;\nimport static org.springframework.http.HttpStatus.NO_CONTENT;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\n\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport java.net.URI;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.util.List;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springdoc.core.fn.builders.operation.Builder;\nimport org.springdoc.webflux.core.fn.SpringdocRouteBuilder;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.multipart.FilePart;\nimport org.springframework.http.codec.multipart.Part;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.core.extension.endpoint.CustomEndpoint;\nimport run.halo.app.core.user.service.SettingConfigService;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.IListRequest;\nimport run.halo.app.infra.ReactiveUrlDataBufferFetcher;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.theme.TemplateEngineManager;\nimport run.halo.app.theme.service.ThemeService;\nimport run.halo.app.theme.service.ThemeUtils;\nimport tools.jackson.databind.node.ObjectNode;\n\n/**\n * Endpoint for managing themes.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Component\n@AllArgsConstructor\npublic class ThemeEndpoint implements CustomEndpoint {\n\n    private final ReactiveExtensionClient client;\n\n    private final ThemeRootGetter themeRoot;\n\n    private final ThemeService themeService;\n\n    private final TemplateEngineManager templateEngineManager;\n\n    private final SystemConfigFetcher systemEnvironmentFetcher;\n\n    private final ReactiveUrlDataBufferFetcher urlDataBufferFetcher;\n\n    private final SettingConfigService settingConfigService;\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        var tag = \"ThemeV1alpha1Console\";\n        return SpringdocRouteBuilder.route()\n            .POST(\"themes/install\", contentType(MediaType.MULTIPART_FORM_DATA),\n                this::install, builder -> builder.operationId(\"InstallTheme\")\n                    .description(\"Install a theme by uploading a zip file.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(InstallRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Theme.class))\n            )\n            .POST(\"themes/-/install-from-uri\", this::installFromUri,\n                builder -> builder.operationId(\"InstallThemeFromUri\")\n                    .description(\"Install a theme from uri.\")\n                    .tag(tag)\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(InstallFromUriRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Theme.class))\n            )\n            .POST(\"themes/{name}/upgrade-from-uri\", this::upgradeFromUri,\n                builder -> builder.operationId(\"UpgradeThemeFromUri\")\n                    .description(\"Upgrade a theme from uri.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .in(ParameterIn.PATH)\n                        .name(\"name\")\n                        .required(true)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder()\n                            .mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder()\n                                .implementation(UpgradeFromUriRequest.class))\n                        ))\n                    .response(responseBuilder()\n                        .implementation(Theme.class))\n            )\n            .POST(\"themes/{name}/upgrade\", this::upgrade,\n                builder -> builder.operationId(\"UpgradeTheme\")\n                    .description(\"Upgrade theme\")\n                    .tag(tag)\n                    .parameter(parameterBuilder().in(ParameterIn.PATH).name(\"name\").required(true))\n                    .requestBody(requestBodyBuilder().required(true)\n                        .content(contentBuilder().mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)\n                            .schema(schemaBuilder().implementation(UpgradeRequest.class))))\n                    .build())\n            .PUT(\"themes/{name}/reload\", this::reloadTheme,\n                builder -> builder.operationId(\"Reload\")\n                    .description(\"Reload theme setting.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(Theme.class))\n            )\n            .PUT(\"themes/{name}/reset-config\", this::resetSettingConfig,\n                builder -> builder.operationId(\"ResetThemeConfig\")\n                    .description(\"Reset the configMap of theme setting.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(ConfigMap.class))\n            )\n            .PUT(\"themes/{name}/json-config\", this::updateThemeJsonConfig,\n                builder -> builder.operationId(\"updateThemeJsonConfig\")\n                    .description(\"Update the configMap of theme setting.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .requestBody(requestBodyBuilder()\n                        .required(true)\n                        .content(contentBuilder().mediaType(MediaType.APPLICATION_JSON_VALUE)\n                            .schema(schemaBuilder().implementation(Object.class))))\n                    .response(responseBuilder()\n                        .responseCode(String.valueOf(NO_CONTENT.value()))\n                        .implementation(Void.class))\n            )\n            .PUT(\"themes/{name}/activation\", this::activateTheme,\n                builder -> builder.operationId(\"activateTheme\")\n                    .description(\"Activate a theme by name.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(Theme.class))\n            )\n            .PUT(\"/themes/{name}/invalidate-cache\", this::invalidateCache,\n                builder -> builder.operationId(\"InvalidateCache\")\n                    .description(\"Invalidate theme template cache.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .responseCode(String.valueOf(NO_CONTENT.value()))\n                    )\n            )\n            .GET(\"themes\", this::listThemes,\n                builder -> {\n                    builder.operationId(\"ListThemes\")\n                        .description(\"List themes.\")\n                        .tag(tag)\n                        .response(responseBuilder()\n                            .implementation(ListResult.generateGenericClass(Theme.class)));\n                    ThemeQuery.buildParameters(builder);\n                }\n            )\n            .GET(\"themes/-/activation\", this::fetchActivatedTheme,\n                builder -> builder.operationId(\"fetchActivatedTheme\")\n                    .description(\"Fetch the activated theme.\")\n                    .tag(tag)\n                    .response(responseBuilder()\n                        .implementation(Theme.class))\n            )\n            .GET(\"themes/{name}/setting\", this::fetchThemeSetting,\n                builder -> builder.operationId(\"fetchThemeSetting\")\n                    .description(\"Fetch setting of theme.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder()\n                        .implementation(Setting.class))\n            )\n            .GET(\"themes/{name}/json-config\", this::fetchThemeJsonConfig,\n                builder -> builder.operationId(\"fetchThemeJsonConfig\")\n                    .description(\n                        \"Fetch converted json config of theme by configured configMapName.\")\n                    .tag(tag)\n                    .parameter(parameterBuilder()\n                        .name(\"name\")\n                        .in(ParameterIn.PATH)\n                        .required(true)\n                        .implementation(String.class)\n                    )\n                    .response(responseBuilder().implementation(Object.class))\n            )\n            .build();\n    }\n\n    private Mono<ServerResponse> fetchThemeJsonConfig(ServerRequest request) {\n        return themeNameInPathVariableOrActivated(request)\n            .flatMap(themeName -> client.fetch(Theme.class, themeName))\n            .mapNotNull(theme -> theme.getSpec().getConfigMapName())\n            .flatMap(settingConfigService::fetchConfig)\n            .flatMap(json -> ServerResponse.ok().bodyValue(json));\n    }\n\n    private Mono<ServerResponse> updateThemeJsonConfig(ServerRequest request) {\n        final var themeName = request.pathVariable(\"name\");\n        return client.fetch(Theme.class, themeName)\n            .doOnNext(theme -> {\n                String configMapName = theme.getSpec().getConfigMapName();\n                if (StringUtils.isBlank(configMapName)) {\n                    throw new ServerWebInputException(\n                        \"Unable to complete the request because the theme configMapName is blank.\");\n                }\n            })\n            .flatMap(theme -> {\n                final var configMapName = theme.getSpec().getConfigMapName();\n                return request.bodyToMono(ObjectNode.class)\n                    .switchIfEmpty(\n                        Mono.error(new ServerWebInputException(\"Required request body is missing\")))\n                    .flatMap(configJsonData ->\n                        settingConfigService.upsertConfig(configMapName, configJsonData));\n            })\n            .then(ServerResponse.noContent().build());\n    }\n\n    private Mono<ServerResponse> invalidateCache(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return client.get(Theme.class, name)\n            .flatMap(theme -> templateEngineManager.clearCache(name))\n            .then(ServerResponse.noContent().build());\n    }\n\n    private Mono<ServerResponse> upgradeFromUri(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        var content = request.bodyToMono(UpgradeFromUriRequest.class)\n            .map(UpgradeFromUriRequest::uri)\n            .flatMapMany(urlDataBufferFetcher::fetch);\n\n        return themeService.upgrade(name, content)\n            .flatMap((updatedTheme) ->\n                templateEngineManager.clearCache(updatedTheme.getMetadata().getName())\n                    .thenReturn(updatedTheme)\n            )\n            .flatMap(theme -> ServerResponse.ok().bodyValue(theme));\n    }\n\n    private Mono<ServerResponse> installFromUri(ServerRequest request) {\n        var content = request.bodyToMono(InstallFromUriRequest.class)\n            .map(InstallFromUriRequest::uri)\n            .flatMapMany(urlDataBufferFetcher::fetch);\n\n        return themeService.install(content)\n            .flatMap(theme -> ServerResponse.ok().bodyValue(theme));\n    }\n\n    private Mono<ServerResponse> activateTheme(ServerRequest request) {\n        final var activatedThemeName = request.pathVariable(\"name\");\n        return client.fetch(Theme.class, activatedThemeName)\n            .switchIfEmpty(Mono.error(new NotFoundException(\"Theme not found.\")))\n            .flatMap(theme -> themeService.fetchSystemSetting()\n                .flatMap(themeSetting -> {\n                    // update active theme config\n                    themeSetting.setActive(activatedThemeName);\n                    return systemEnvironmentFetcher.getConfigMap()\n                        .filter(configMap -> configMap.getData() != null)\n                        .map(configMap -> {\n                            var themeConfigJson = JsonUtils.objectToJson(themeSetting);\n                            configMap.getData()\n                                .put(SystemSetting.Theme.GROUP, themeConfigJson);\n                            return configMap;\n                        });\n                })\n                .flatMap(client::update)\n                .retryWhen(Retry.backoff(5, Duration.ofMillis(300))\n                    .filter(OptimisticLockingFailureException.class::isInstance)\n                )\n                .thenReturn(theme)\n            )\n            .flatMap(activatedTheme -> ServerResponse.ok().bodyValue(activatedTheme));\n    }\n\n    private Mono<ServerResponse> fetchActivatedTheme(ServerRequest request) {\n        var activatedTheme = themeService.fetchActivatedTheme()\n            .switchIfEmpty(\n                Mono.error(() -> new NotFoundException(\"Activated theme not found or not set\"))\n            );\n        return ServerResponse.ok().body(activatedTheme, Theme.class);\n    }\n\n    private Mono<ServerResponse> fetchThemeSetting(ServerRequest request) {\n        return themeNameInPathVariableOrActivated(request)\n            .flatMap(name -> client.fetch(Theme.class, name))\n            .mapNotNull(theme -> theme.getSpec().getSettingName())\n            .flatMap(settingName -> client.fetch(Setting.class, settingName))\n            .flatMap(setting -> ServerResponse.ok().bodyValue(setting));\n    }\n\n    private Mono<String> themeNameInPathVariableOrActivated(ServerRequest request) {\n        Assert.notNull(request, \"request must not be null.\");\n        var themeName = request.pathVariable(\"name\");\n        if (\"-\".equals(themeName)) {\n            return themeService.fetchActivatedThemeName()\n                .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                    \"No activated theme found, unable to proceed the request.\"\n                )));\n        }\n        return Mono.just(themeName);\n    }\n\n    public static class ThemeQuery extends IListRequest.QueryListRequest {\n\n        public ThemeQuery(MultiValueMap<String, String> queryParams) {\n            super(queryParams);\n        }\n\n        @NonNull\n        public Boolean getUninstalled() {\n            return Boolean.parseBoolean(queryParams.getFirst(\"uninstalled\"));\n        }\n\n        public static void buildParameters(Builder builder) {\n            IListRequest.buildParameters(builder);\n            builder.parameter(parameterBuilder()\n                .name(\"uninstalled\")\n                .description(\"Whether to list uninstalled themes.\")\n                .in(ParameterIn.QUERY)\n                .implementation(Boolean.class)\n                .required(false));\n        }\n    }\n\n    // TODO Extract the method into ThemeService\n    Mono<ServerResponse> listThemes(ServerRequest request) {\n        MultiValueMap<String, String> queryParams = request.queryParams();\n        ThemeQuery query = new ThemeQuery(queryParams);\n        return Mono.defer(() -> {\n            if (query.getUninstalled()) {\n                return listUninstalled(query);\n            }\n            return client.list(Theme.class, null, null, query.getPage(), query.getSize());\n        }).flatMap(extensions -> ServerResponse.ok().bodyValue(extensions));\n    }\n\n    public interface IUpgradeRequest {\n\n        @Schema(requiredMode = REQUIRED, description = \"Theme zip file.\")\n        FilePart getFile();\n\n    }\n\n    public record UpgradeFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {\n    }\n\n    public static class UpgradeRequest implements IUpgradeRequest {\n\n        private final MultiValueMap<String, Part> multipartData;\n\n        public UpgradeRequest(MultiValueMap<String, Part> multipartData) {\n            this.multipartData = multipartData;\n        }\n\n        @Override\n        public FilePart getFile() {\n            var part = multipartData.getFirst(\"file\");\n            if (!(part instanceof FilePart filePart)) {\n                throw new ServerWebInputException(\"Invalid multipart type of file\");\n            }\n            if (!filePart.filename().endsWith(\".zip\")) {\n                throw new ServerWebInputException(\"Only zip extension supported\");\n            }\n            return filePart;\n        }\n\n    }\n\n    private Mono<ServerResponse> upgrade(ServerRequest request) {\n        // validate the theme first\n        var name = request.pathVariable(\"name\");\n        return request.multipartData()\n            .map(UpgradeRequest::new)\n            .map(UpgradeRequest::getFile)\n            .flatMap(filePart -> themeService.upgrade(name, filePart.content()))\n            .flatMap((updatedTheme) ->\n                templateEngineManager.clearCache(updatedTheme.getMetadata().getName())\n                    .thenReturn(updatedTheme))\n            .flatMap(updatedTheme -> ServerResponse.ok().bodyValue(updatedTheme));\n    }\n\n    Mono<ListResult<Theme>> listUninstalled(ThemeQuery query) {\n        Path path = themeRoot.get();\n        return ThemeUtils.listAllThemesFromThemeDir(path)\n            .collectList()\n            .flatMap(this::filterUnInstalledThemes)\n            .map(themes -> {\n                Integer page = query.getPage();\n                Integer size = query.getSize();\n                List<Theme> subList = ListResult.subList(themes, page, size);\n                return new ListResult<>(page, size, themes.size(), subList);\n            });\n    }\n\n    private Mono<List<Theme>> filterUnInstalledThemes(@NonNull List<Theme> allThemes) {\n        return client.list(Theme.class, null, null)\n            .map(theme -> theme.getMetadata().getName())\n            .collectList()\n            .map(installed -> allThemes.stream()\n                .filter(theme -> !installed.contains(theme.getMetadata().getName()))\n                .toList()\n            );\n    }\n\n    Mono<ServerResponse> reloadTheme(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return themeService.reloadTheme(name)\n            .flatMap(theme -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(theme));\n    }\n\n    Mono<ServerResponse> resetSettingConfig(ServerRequest request) {\n        String name = request.pathVariable(\"name\");\n        return themeService.resetSettingConfig(name)\n            .flatMap(theme -> ServerResponse.ok()\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(theme));\n    }\n\n    @Schema(name = \"ThemeInstallRequest\", types = \"object\")\n    public static class InstallRequest {\n\n        @Schema(hidden = true)\n        private final MultiValueMap<String, Part> multipartData;\n\n        public InstallRequest(MultiValueMap<String, Part> multipartData) {\n            this.multipartData = multipartData;\n        }\n\n        @Schema(requiredMode = REQUIRED, description = \"Theme zip file.\")\n        FilePart getFile() {\n            Part part = multipartData.getFirst(\"file\");\n            if (!(part instanceof FilePart file)) {\n                throw new ServerWebInputException(\n                    \"Invalid parameter of file, binary data is required\");\n            }\n            if (!Paths.get(file.filename()).toString().endsWith(\".zip\")) {\n                throw new ServerWebInputException(\n                    \"Invalid file type, only zip format is supported\");\n            }\n            return file;\n        }\n    }\n\n    public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {\n    }\n\n    Mono<ServerResponse> install(ServerRequest request) {\n        return request.multipartData()\n            .map(InstallRequest::new)\n            .map(InstallRequest::getFile)\n            .flatMap(filePart -> themeService.install(filePart.content()))\n            .flatMap(theme -> ServerResponse.ok().bodyValue(theme));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/engine/DefaultThemeTemplateAvailabilityProvider.java",
    "content": "package run.halo.app.theme.engine;\n\nimport java.nio.file.Files;\nimport org.springframework.boot.thymeleaf.autoconfigure.ThymeleafProperties;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.theme.ThemeContext;\n\n@Component\npublic class DefaultThemeTemplateAvailabilityProvider implements ThemeTemplateAvailabilityProvider {\n\n    private final ThymeleafProperties thymeleafProperties;\n\n    public DefaultThemeTemplateAvailabilityProvider(ThymeleafProperties thymeleafProperties) {\n        this.thymeleafProperties = thymeleafProperties;\n    }\n\n    @Override\n    public boolean isTemplateAvailable(ThemeContext themeContext, String viewName) {\n        var suffix = thymeleafProperties.getSuffix();\n        // Currently, we only support Path here.\n        var path = themeContext.getPath().resolve(\"templates\").resolve(viewName + suffix);\n        return Files.exists(path);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/engine/HaloTemplateEngine.java",
    "content": "package run.halo.app.theme.engine;\n\nimport java.nio.channels.ClosedByInterruptException;\nimport java.nio.charset.Charset;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferFactory;\nimport org.springframework.http.MediaType;\nimport org.thymeleaf.context.IContext;\nimport org.thymeleaf.messageresolver.IMessageResolver;\nimport org.thymeleaf.spring6.SpringWebFluxTemplateEngine;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Schedulers;\n\n/**\n * Default template engine implementation to be used in Halo.\n *\n * @author johnniang\n */\n@Slf4j\npublic class HaloTemplateEngine extends SpringWebFluxTemplateEngine {\n\n    private final IMessageResolver messageResolver;\n\n    public HaloTemplateEngine(IMessageResolver messageResolver) {\n        this.messageResolver = messageResolver;\n    }\n\n    @Override\n    protected void initializeSpringSpecific() {\n        // Before initialization, thymeleaf will overwrite message resolvers.\n        // So we need to add own message resolver at here.\n        addMessageResolver(messageResolver);\n    }\n\n    @Override\n    public Publisher<DataBuffer> processStream(String template, Set<String> markupSelectors,\n        IContext context, DataBufferFactory bufferFactory,\n        MediaType mediaType, Charset charset, int responseMaxChunkSizeBytes) {\n        var publisher = super.processStream(template, markupSelectors, context, bufferFactory,\n            mediaType, charset, responseMaxChunkSizeBytes);\n        // We have to subscribe on blocking thread, because some blocking operations will be present\n        // while processing.\n        if (publisher instanceof Mono<DataBuffer> mono) {\n            return mono.subscribeOn(Schedulers.boundedElastic())\n                .doOnError(Exception.class, e -> this.logTemplateError(e, template));\n        }\n        if (publisher instanceof Flux<DataBuffer> flux) {\n            return flux.subscribeOn(Schedulers.boundedElastic())\n                .doOnError(Exception.class, e -> this.logTemplateError(e, template));\n        }\n        return publisher;\n    }\n\n    private void logTemplateError(Exception e, String template) {\n        if (Exceptions.unwrap(e.getCause()) instanceof InterruptedException) {\n            log.warn(\"Interrupted while processing template: {}\", template);\n        }\n        if (e.getCause() instanceof ClosedByInterruptException) {\n            log.warn(\"Interrupted while outputting template: {}\", template);\n        }\n        // other exceptions will be caught by error handler\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/engine/PluginClassloaderTemplateResolver.java",
    "content": "package run.halo.app.theme.engine;\n\nimport static run.halo.app.plugin.PluginConst.SYSTEM_PLUGIN_NAME;\n\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.apache.commons.lang3.StringUtils;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginState;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport org.springframework.lang.Nullable;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.spring6.templateresource.SpringResourceTemplateResource;\nimport org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\n\n/**\n * Plugin classloader template resolver to resolve template by plugin classloader.\n *\n * @author guqing\n * @since 2.11.0\n */\npublic class PluginClassloaderTemplateResolver extends AbstractConfigurableTemplateResolver {\n\n    private final PluginManager haloPluginManager;\n    static final Pattern PLUGIN_TEMPLATE_PATTERN =\n        Pattern.compile(\"plugin:([A-Za-z0-9\\\\-.]+):(.+)\");\n\n    /**\n     * Create a new plugin classloader template resolver, not cacheable.\n     *\n     * @param pluginManager plugin manager must not be null\n     */\n    public PluginClassloaderTemplateResolver(PluginManager pluginManager) {\n        super();\n        this.haloPluginManager = pluginManager;\n        setCacheable(false);\n    }\n\n    @Override\n    protected ITemplateResource computeTemplateResource(\n        final IEngineConfiguration configuration, final String ownerTemplate, final String template,\n        final String resourceName, final String characterEncoding,\n        final Map<String, Object> templateResolutionAttributes) {\n        var matchResult = matchPluginTemplate(ownerTemplate, template);\n        if (!matchResult.matches()) {\n            return null;\n        }\n        String pluginName = matchResult.pluginName();\n        var classloader = getClassloaderByPlugin(pluginName);\n        if (classloader == null) {\n            return null;\n        }\n\n        var templateName = matchResult.templateName();\n        var ownerTemplateName = matchResult.ownerTemplateName();\n\n        String handledResourceName = computeResourceName(configuration, ownerTemplateName,\n            templateName, getPrefix(), getSuffix(), getForceSuffix(), getTemplateAliases(),\n            templateResolutionAttributes);\n\n        var resource = new DefaultResourceLoader(classloader)\n            .getResource(handledResourceName);\n        return new SpringResourceTemplateResource(resource, characterEncoding);\n    }\n\n    MatchResult matchPluginTemplate(String ownerTemplate, String template) {\n        boolean matches = false;\n        String pluginName = null;\n        String templateName = template;\n        String ownerTemplateName = ownerTemplate;\n        if (StringUtils.isNotBlank(ownerTemplate)) {\n            Matcher ownerTemplateMatcher = PLUGIN_TEMPLATE_PATTERN.matcher(ownerTemplate);\n            if (ownerTemplateMatcher.matches()) {\n                matches = true;\n                pluginName = ownerTemplateMatcher.group(1);\n                ownerTemplateName = ownerTemplateMatcher.group(2);\n            }\n        }\n        Matcher templateMatcher = PLUGIN_TEMPLATE_PATTERN.matcher(template);\n        if (templateMatcher.matches()) {\n            matches = true;\n            pluginName = templateMatcher.group(1);\n            templateName = templateMatcher.group(2);\n        }\n        return new MatchResult(pluginName, ownerTemplateName, templateName, matches);\n    }\n\n    record MatchResult(String pluginName, String ownerTemplateName, String templateName,\n                       boolean matches) {\n    }\n\n    @Nullable\n    private ClassLoader getClassloaderByPlugin(String pluginName) {\n        if (SYSTEM_PLUGIN_NAME.equals(pluginName)) {\n            return this.getClass().getClassLoader();\n        }\n        var pluginWrapper = haloPluginManager.getPlugin(pluginName);\n        if (pluginWrapper == null || !PluginState.STARTED.equals(pluginWrapper.getPluginState())) {\n            return null;\n        }\n        return pluginWrapper.getPluginClassLoader();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/engine/ThemeTemplateAvailabilityProvider.java",
    "content": "package run.halo.app.theme.engine;\n\nimport run.halo.app.theme.ThemeContext;\n\npublic interface ThemeTemplateAvailabilityProvider {\n\n    boolean isTemplateAvailable(ThemeContext themeContext, String viewName);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/CategoryFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.Collection;\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.theme.finders.vo.CategoryTreeVo;\nimport run.halo.app.theme.finders.vo.CategoryVo;\n\n/**\n * A finder for {@link Category}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface CategoryFinder {\n\n    Mono<CategoryVo> getByName(String name);\n\n    Flux<CategoryVo> getByNames(Collection<String> names);\n\n    Mono<ListResult<CategoryVo>> list(@Nullable Integer page, @Nullable Integer size);\n\n    Flux<CategoryVo> listAll();\n\n    Flux<CategoryTreeVo> listAsTree();\n\n    Flux<CategoryTreeVo> listAsTree(String name);\n\n    Mono<CategoryVo> getParentByName(String name);\n\n    Flux<CategoryVo> getBreadcrumbs(String name);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/CommentFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.Map;\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.theme.finders.vo.CommentVo;\nimport run.halo.app.theme.finders.vo.ReplyVo;\n\n/**\n * A finder for finding {@link Comment comments} in template.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface CommentFinder {\n\n    Mono<CommentVo> getByName(String name);\n\n    Mono<ListResult<CommentVo>> list(@Nullable Map<String, String> ref, @Nullable Integer page,\n        @Nullable Integer size);\n\n    Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,\n        @Nullable Integer size);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/CommentPublicQueryService.java",
    "content": "package run.halo.app.theme.finders;\n\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.theme.finders.vo.CommentVo;\nimport run.halo.app.theme.finders.vo.CommentWithReplyVo;\nimport run.halo.app.theme.finders.vo.ReplyVo;\n\n/**\n * comment finder.\n *\n * @author LIlGG\n */\npublic interface CommentPublicQueryService {\n    Mono<CommentVo> getByName(String name);\n\n    Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,\n        @Nullable Integer size);\n\n    Mono<ListResult<CommentVo>> list(Ref ref, @Nullable PageRequest pageRequest);\n\n    Mono<ListResult<CommentWithReplyVo>> convertToWithReplyVo(ListResult<CommentVo> comments,\n        int replySize);\n\n    Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,\n        @Nullable Integer size);\n\n    Mono<ListResult<ReplyVo>> listReply(String commentName, PageRequest pageRequest);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/ContributorFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.Collection;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.theme.finders.vo.ContributorVo;\n\n/**\n * A finder for {@link User}.\n */\npublic interface ContributorFinder {\n\n    Mono<ContributorVo> getContributor(String name);\n\n    Flux<ContributorVo> getContributors(Collection<String> names);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/DefaultFinderRegistry.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.stereotype.Component;\n\n/**\n * Finder registry for class annotated with {@link Finder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\npublic class DefaultFinderRegistry implements FinderRegistry, InitializingBean {\n    private final Map<String, List<String>> pluginFindersLookup = new ConcurrentHashMap<>();\n    private final Map<String, Object> finders = new ConcurrentHashMap<>(64);\n\n    private final ApplicationContext applicationContext;\n\n    public DefaultFinderRegistry(ApplicationContext applicationContext) {\n        this.applicationContext = applicationContext;\n    }\n\n    Object get(String name) {\n        return finders.get(name);\n    }\n\n    /**\n     * Given a name, register a Finder for it.\n     *\n     * @param name the canonical name\n     * @param finder the finder to be registered\n     * @throws IllegalStateException if the name is already existing\n     */\n    void putFinder(String name, Object finder) {\n        if (finders.containsKey(name)) {\n            throw new IllegalStateException(\n                \"Finder with name '\" + name + \"' is already registered\");\n        }\n        finders.put(name, finder);\n    }\n\n    /**\n     * Register a finder.\n     *\n     * @param finder register a finder that annotated with {@link Finder}\n     * @return the name of the finder\n     */\n    String putFinder(Object finder) {\n        var name = getFinderName(finder);\n        this.putFinder(name, finder);\n        return name;\n    }\n\n    private String getFinderName(Object finder) {\n        var annotation = finder.getClass().getAnnotation(Finder.class);\n        if (annotation == null) {\n            // should never happen\n            throw new IllegalStateException(\"Finder must be annotated with @Finder\");\n        }\n        String name = annotation.value();\n        if (name == null) {\n            name = finder.getClass().getSimpleName();\n        }\n        return name;\n    }\n\n    public void removeFinder(String name) {\n        finders.remove(name);\n    }\n\n    public Map<String, Object> getFinders() {\n        return Map.copyOf(finders);\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        // initialize finders from application context\n        applicationContext.getBeansWithAnnotation(Finder.class)\n            .forEach((beanName, finder) -> {\n                var finderName = getFinderName(finder);\n                this.putFinder(finderName, finder);\n            });\n    }\n\n    @Override\n    public void register(String pluginId, ApplicationContext pluginContext) {\n        pluginContext.getBeansWithAnnotation(Finder.class)\n            .forEach((beanName, finder) -> {\n                var finderName = getFinderName(finder);\n                this.putFinder(finderName, finder);\n                pluginFindersLookup\n                    .computeIfAbsent(pluginId, ignored -> new ArrayList<>())\n                    .add(finderName);\n            });\n    }\n\n    @Override\n    public void unregister(String pluginId) {\n        var finderNames = pluginFindersLookup.remove(pluginId);\n        if (finderNames != null) {\n            finderNames.forEach(finders::remove);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.Map;\nimport org.springframework.context.ApplicationContext;\n\n/**\n * Finder registry for class annotated with {@link Finder}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface FinderRegistry {\n\n    Map<String, Object> getFinders();\n\n    void register(String pluginId, ApplicationContext pluginContext);\n\n    void unregister(String pluginId);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/MenuFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.theme.finders.vo.MenuVo;\n\n/**\n * A finder for {@link run.halo.app.core.extension.Menu}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface MenuFinder {\n\n    Mono<MenuVo> getByName(String name);\n\n    Mono<MenuVo> getPrimary();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/PluginFinder.java",
    "content": "package run.halo.app.theme.finders;\n\n/**\n * A finder for {@link run.halo.app.core.extension.Plugin}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface PluginFinder {\n\n    boolean available(String pluginName);\n\n    boolean available(String pluginName, String requiresVersion);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/PostFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.Map;\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.theme.finders.impl.PostFinderImpl.PostQuery;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.NavigationPostVo;\nimport run.halo.app.theme.finders.vo.PostArchiveVo;\nimport run.halo.app.theme.finders.vo.PostVo;\n\n/**\n * A finder for {@link Post}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface PostFinder {\n\n    /**\n     * <p>Gets post detail by name.</p>\n     * We ensure the post is public, non-deleted and published.\n     *\n     * @param postName is post name\n     * @return post detail\n     */\n    Mono<PostVo> getByName(String postName);\n\n    Mono<ContentVo> content(String postName);\n\n    Mono<NavigationPostVo> cursor(String current);\n\n    Flux<ListedPostVo> listAll();\n\n    /**\n     * Lists posts by query params.\n     *\n     * @param params query params see {@link PostQuery}\n     */\n    Mono<ListResult<ListedPostVo>> list(Map<String, Object> params);\n\n    Mono<ListResult<ListedPostVo>> list(@Nullable Integer page, @Nullable Integer size);\n\n    Mono<ListResult<ListedPostVo>> listByCategory(@Nullable Integer page, @Nullable Integer size,\n        String categoryName);\n\n    Mono<ListResult<ListedPostVo>> listByTag(@Nullable Integer page, @Nullable Integer size,\n        String tag);\n\n    Mono<ListResult<ListedPostVo>> listByOwner(@Nullable Integer page, @Nullable Integer size,\n        String owner);\n\n    Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size);\n\n    Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year);\n\n    Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year, String month);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.List;\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.theme.ReactivePostContentHandler;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.PostVo;\n\npublic interface PostPublicQueryService {\n\n    /**\n     * Lists public posts by the given list options and page request.\n     *\n     * @param listOptions additional list options\n     * @param page page request must not be null\n     * @return a list of listed post vo\n     */\n    Mono<ListResult<ListedPostVo>> list(ListOptions listOptions, PageRequest page);\n\n    /**\n     * Converts post to listed post vo.\n     *\n     * @param post post must not be null\n     * @return listed post vo\n     */\n    Mono<ListedPostVo> convertToListedVo(@NonNull Post post);\n\n    Mono<List<ListedPostVo>> convertToListedVos(List<Post> posts);\n\n    /**\n     * Converts {@link Post} to post vo and populate post content by the given snapshot name.\n     * <p> This method will get post content by {@code snapshotName} and try to find\n     * {@link ReactivePostContentHandler}s to extend the content</p>\n     *\n     * @param post post must not be null\n     * @param snapshotName snapshot name must not be blank\n     * @return converted post vo\n     */\n    Mono<PostVo> convertToVo(Post post, String snapshotName);\n\n    /**\n     * Gets post content by post name.\n     * <p> This method will get post released content by post name and try to find\n     * {@link ReactivePostContentHandler}s to extend the content</p>\n     *\n     * @param postName post name must not be blank\n     * @return post content for theme-side\n     * @see ReactivePostContentHandler\n     */\n    Mono<ContentVo> getContent(String postName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/SinglePageConversionService.java",
    "content": "package run.halo.app.theme.finders;\n\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.theme.ReactiveSinglePageContentHandler;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ListedSinglePageVo;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * A service that converts {@link SinglePage} to {@link SinglePageVo}.\n *\n * @author guqing\n * @since 2.6.0\n */\npublic interface SinglePageConversionService {\n\n    /**\n     * Converts the given {@link SinglePage} to {@link SinglePageVo} and populate content by\n     * given snapshot name.\n     *\n     * @param singlePage the single page must not be null\n     * @param snapshotName the snapshot name to get content must not be blank\n     * @return the converted single page vo\n     * @see #convertToVo(SinglePage)\n     */\n    Mono<SinglePageVo> convertToVo(SinglePage singlePage, String snapshotName);\n\n    /**\n     * Converts the given {@link SinglePage} to {@link SinglePageVo}.\n     * <p>This method will query the additional information of the {@link SinglePageVo} needed to\n     * populate.</p>\n     * <p>This method will try to find {@link ReactiveSinglePageContentHandler}s to extend the\n     * content.</p>\n     *\n     * @param singlePage the single page must not be null\n     * @return the converted single page vo\n     * @see #getContent(String)\n     */\n    Mono<SinglePageVo> convertToVo(@NonNull SinglePage singlePage);\n\n    /**\n     * Gets content by given page name.\n     * <p>This method will get released content by page name and try to find\n     * {@link ReactiveSinglePageContentHandler}s to extend the content.</p>\n     *\n     * @param pageName page name must not be blank\n     * @return content of the specified page\n     * @since 2.7.0\n     */\n    Mono<ContentVo> getContent(String pageName);\n\n    Mono<ListedSinglePageVo> convertToListedVo(SinglePage singlePage);\n\n    Mono<ListResult<ListedSinglePageVo>> listBy(ListOptions listOptions, PageRequest pageRequest);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/SinglePageFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ListedSinglePageVo;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * A finder for {@link SinglePage}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface SinglePageFinder {\n\n    Mono<SinglePageVo> getByName(String pageName);\n\n    Mono<ContentVo> content(String pageName);\n\n    Mono<ListResult<ListedSinglePageVo>> list(@Nullable Integer page, @Nullable Integer size);\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/SiteStatsFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.theme.finders.vo.SiteStatsVo;\n\n/**\n * Site statistics finder.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface SiteStatsFinder {\n\n    Mono<SiteStatsVo> getStats();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/TagFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport java.util.Collection;\nimport java.util.List;\nimport org.springframework.lang.Nullable;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.theme.finders.vo.TagVo;\n\n/**\n * A finder for {@link Tag}.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface TagFinder {\n\n    Mono<TagVo> getByName(String name);\n\n    Flux<TagVo> getByNames(Collection<String> names);\n\n    Mono<ListResult<TagVo>> list(@Nullable Integer page, @Nullable Integer size);\n\n    List<TagVo> convertToVo(List<Tag> tags);\n\n    Flux<TagVo> listAll();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/ThemeFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport reactor.core.publisher.Mono;\nimport run.halo.app.theme.finders.vo.ThemeVo;\n\n/**\n * A finder for theme.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface ThemeFinder {\n\n    Mono<ThemeVo> activation();\n\n    Mono<ThemeVo> getByName(String themeName);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/ThumbnailFinder.java",
    "content": "package run.halo.app.theme.finders;\n\nimport reactor.core.publisher.Mono;\n\n/**\n * A dialect expression for image thumbnail.\n *\n * @author guqing\n * @since 2.19.0\n */\npublic interface ThumbnailFinder {\n\n    /**\n     * Generate thumbnail uri from given image uri and size.\n     *\n     * @param uri URI of the original image, must be encoded\n     * @param size the size of thumbnail to generate\n     * @return the generated thumbnail url\n     */\n    Mono<String> gen(String uri, String size);\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static run.halo.app.extension.index.query.Queries.notEqual;\n\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.Nullable;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.CategoryService;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.theme.finders.CategoryFinder;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.vo.CategoryTreeVo;\nimport run.halo.app.theme.finders.vo.CategoryVo;\n\n/**\n * A default implementation of {@link CategoryFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Slf4j\n@Finder(\"categoryFinder\")\n@RequiredArgsConstructor\npublic class CategoryFinderImpl implements CategoryFinder {\n    private final ReactiveExtensionClient client;\n    private final CategoryService categoryService;\n\n    @Override\n    public Mono<CategoryVo> getByName(String name) {\n        return client.fetch(Category.class, name)\n            .map(CategoryVo::from);\n    }\n\n    @Override\n    public Flux<CategoryVo> getByNames(Collection<String> names) {\n        if (CollectionUtils.isEmpty(names)) {\n            return Flux.empty();\n        }\n        var options = ListOptions.builder()\n            .andQuery(Queries.in(\"metadata.name\", names))\n            .build();\n        return client.listAll(Category.class, options, ExtensionUtil.defaultSort())\n            .map(CategoryVo::from);\n    }\n\n    static Sort defaultSort() {\n        return Sort.by(Sort.Order.desc(\"spec.priority\"),\n            Sort.Order.desc(\"metadata.creationTimestamp\"),\n            Sort.Order.desc(\"metadata.name\"));\n    }\n\n    @Override\n    public Mono<ListResult<CategoryVo>> list(Integer page, Integer size) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            notEqual(\"spec.hideFromList\", BooleanUtils.TRUE)\n        ));\n        return client.listBy(Category.class, listOptions,\n                PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort())\n            )\n            .map(list -> {\n                List<CategoryVo> categoryVos = list.get()\n                    .map(CategoryVo::from)\n                    .collect(Collectors.toList());\n                return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),\n                    categoryVos);\n            })\n            .defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));\n    }\n\n    @Override\n    public Flux<CategoryTreeVo> listAsTree() {\n        return listAll()\n            .collectList()\n            .flatMapMany(list -> Flux.fromIterable(listToTree(list, null)));\n    }\n\n    @Override\n    public Flux<CategoryTreeVo> listAsTree(String name) {\n        return listAllFor(name)\n            .collectList()\n            .flatMapMany(list -> Flux.fromIterable(listToTree(list, name)));\n    }\n\n    @Override\n    public Flux<CategoryVo> listAll() {\n        return client.listAll(Category.class, new ListOptions(), defaultSort())\n            .filter(category -> !category.getSpec().isHideFromList())\n            .map(CategoryVo::from);\n    }\n\n    private Flux<CategoryVo> listAllFor(String parentName) {\n        return Mono.defer(\n                () -> {\n                    if (StringUtils.isBlank(parentName)) {\n                        return Mono.just(false);\n                    }\n                    return categoryService.isCategoryHidden(parentName);\n                })\n            .flatMapMany(\n                isHidden -> client.listAll(Category.class, new ListOptions(), defaultSort())\n                    .filter(category -> {\n                        if (isHidden) {\n                            return true;\n                        }\n                        return !category.getSpec().isHideFromList();\n                    })\n                    .map(CategoryVo::from)\n            );\n    }\n\n    private List<CategoryTreeVo> listToTree(List<CategoryVo> categoryVos, @Nullable String name) {\n        Map<String, CategoryTreeVo> nameIdentityMap = categoryVos.stream()\n            .map(CategoryTreeVo::from)\n            .collect(Collectors.toMap(categoryVo -> categoryVo.getMetadata().getName(),\n                Function.identity()));\n\n        nameIdentityMap.forEach((nameKey, value) -> {\n            List<String> children = value.getSpec().getChildren();\n            if (children == null) {\n                return;\n            }\n            for (String child : children) {\n                CategoryTreeVo childNode = nameIdentityMap.get(child);\n                if (childNode != null) {\n                    childNode.setParentName(nameKey);\n                }\n            }\n        });\n        var tree = listToTree(nameIdentityMap.values(), name);\n        recomputePostCount(tree);\n        return tree;\n    }\n\n    private static List<CategoryTreeVo> listToTree(Collection<CategoryTreeVo> list, String name) {\n        Map<String, List<CategoryTreeVo>> parentNameIdentityMap = list.stream()\n            .filter(categoryTreeVo -> categoryTreeVo.getParentName() != null)\n            .collect(Collectors.groupingBy(CategoryTreeVo::getParentName));\n\n        list.forEach(node -> {\n            // sort children\n            List<CategoryTreeVo> children =\n                parentNameIdentityMap.getOrDefault(node.getMetadata().getName(), List.of())\n                    .stream()\n                    .sorted(defaultTreeNodeComparator())\n                    .toList();\n            node.setChildren(children);\n        });\n        return list.stream()\n            .filter(v -> StringUtils.isEmpty(name) ? v.getParentName() == null\n                : StringUtils.equals(v.getMetadata().getName(), name))\n            .sorted(defaultTreeNodeComparator())\n            .collect(Collectors.toList());\n    }\n\n    private CategoryTreeVo dummyVirtualRoot(List<CategoryTreeVo> treeNodes) {\n        Category.CategorySpec categorySpec = new Category.CategorySpec();\n        categorySpec.setSlug(\"/\");\n        return CategoryTreeVo.builder()\n            .metadata(new Metadata())\n            .spec(categorySpec)\n            .postCount(0)\n            .children(treeNodes)\n            .metadata(new Metadata())\n            .build();\n    }\n\n    void recomputePostCount(List<CategoryTreeVo> treeNodes) {\n        var rootNode = dummyVirtualRoot(treeNodes);\n        recomputePostCount(rootNode);\n    }\n\n    private int recomputePostCount(CategoryTreeVo rootNode) {\n        if (rootNode == null) {\n            return 0;\n        }\n\n        int originalPostCount = rootNode.getPostCount();\n\n        for (var child : rootNode.getChildren()) {\n            int childSum = recomputePostCount(child);\n            if (!child.getSpec().isPreventParentPostCascadeQuery()) {\n                rootNode.setPostCount(rootNode.getPostCount() + childSum);\n            }\n        }\n\n        return rootNode.getSpec().isPreventParentPostCascadeQuery() ? originalPostCount\n            : rootNode.getPostCount();\n    }\n\n    static Comparator<CategoryTreeVo> defaultTreeNodeComparator() {\n        Function<CategoryTreeVo, Integer> priority =\n            category -> Objects.requireNonNullElse(category.getSpec().getPriority(), 0);\n        Function<CategoryTreeVo, Instant> creationTimestamp =\n            category -> category.getMetadata().getCreationTimestamp();\n        Function<CategoryTreeVo, String> name =\n            category -> category.getMetadata().getName();\n        return Comparator.comparing(priority)\n            .thenComparing(creationTimestamp)\n            .thenComparing(name);\n    }\n\n    static Comparator<Category> defaultComparator() {\n        Function<Category, Integer> priority =\n            category -> Objects.requireNonNullElse(category.getSpec().getPriority(), 0);\n        Function<Category, Instant> creationTimestamp =\n            category -> category.getMetadata().getCreationTimestamp();\n        Function<Category, String> name =\n            category -> category.getMetadata().getName();\n        return Comparator.comparing(priority)\n            .thenComparing(creationTimestamp)\n            .thenComparing(name)\n            .reversed();\n    }\n\n    @Override\n    public Mono<CategoryVo> getParentByName(String name) {\n        return categoryService.getParentByName(name)\n            .map(CategoryVo::from);\n    }\n\n    @Override\n    public Flux<CategoryVo> getBreadcrumbs(String name) {\n        return listAllFor(name)\n            .collectList()\n            .map(list -> listToTree(list, null))\n            .flatMapMany(treeNodes -> {\n                var rootNode = dummyVirtualRoot(treeNodes);\n                var paths = new ArrayList<CategoryVo>();\n                findPathHelper(rootNode, name, paths);\n                return Flux.fromIterable(paths);\n            });\n    }\n\n    private static boolean findPathHelper(CategoryTreeVo node, String targetName,\n        List<CategoryVo> path) {\n        Assert.notNull(targetName, \"Target name must not be null\");\n        if (node == null) {\n            return false;\n        }\n\n        // null name is just a virtual root\n        if (node.getMetadata().getName() != null) {\n            path.add(CategoryTreeVo.toCategoryVo(node));\n        }\n\n        // node maybe a virtual root node so it may have null name\n        if (targetName.equals(node.getMetadata().getName())) {\n            return true;\n        }\n\n        for (CategoryTreeVo child : node.getChildren()) {\n            if (findPathHelper(child, targetName, path)) {\n                return true;\n            }\n        }\n\n        // if the target node is not in the current subtree, remove the current node to roll back\n        if (!path.isEmpty()) {\n            path.remove(path.size() - 1);\n        }\n        return false;\n    }\n\n    int pageNullSafe(Integer page) {\n        return ObjectUtils.defaultIfNull(page, 1);\n    }\n\n    int sizeNullSafe(Integer page) {\n        return ObjectUtils.defaultIfNull(page, 10);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/CommentFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport java.util.Map;\nimport lombok.RequiredArgsConstructor;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.theme.finders.CommentFinder;\nimport run.halo.app.theme.finders.CommentPublicQueryService;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.vo.CommentVo;\nimport run.halo.app.theme.finders.vo.ReplyVo;\n\n/**\n * A default implementation of {@link CommentFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"commentFinder\")\n@RequiredArgsConstructor\npublic class CommentFinderImpl implements CommentFinder {\n\n    private final CommentPublicQueryService commentPublicQueryService;\n\n    @Override\n    public Mono<CommentVo> getByName(String name) {\n        return commentPublicQueryService.getByName(name);\n    }\n\n    @Override\n    public Mono<ListResult<CommentVo>> list(Map<String, String> map, Integer page, Integer size) {\n        if (map == null) {\n            return commentPublicQueryService.list(null, page, size);\n        }\n        Ref ref = new Ref();\n        ref.setGroup(map.get(\"group\"));\n        ref.setVersion(map.get(\"version\"));\n        ref.setKind(map.get(\"kind\"));\n        ref.setName(map.get(\"name\"));\n        return commentPublicQueryService.list(ref, page, size);\n    }\n\n    @Override\n    public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size) {\n        return commentPublicQueryService.listReply(commentName, page, size);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static run.halo.app.core.extension.content.Comment.CommentOwner.ownerIdentity;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.extension.index.query.Queries.or;\n\nimport com.google.common.hash.Hashing;\nimport java.util.HashMap;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Function;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.Nullable;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.comment.OwnerInfo;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.theme.finders.CommentPublicQueryService;\nimport run.halo.app.theme.finders.vo.CommentStatsVo;\nimport run.halo.app.theme.finders.vo.CommentVo;\nimport run.halo.app.theme.finders.vo.CommentWithReplyVo;\nimport run.halo.app.theme.finders.vo.ExtensionVoOperator;\nimport run.halo.app.theme.finders.vo.ReplyVo;\n\n/**\n * comment public query service implementation.\n *\n * @author LIlGG\n * @author guqing\n */\n@Component\n@RequiredArgsConstructor\npublic class CommentPublicQueryServiceImpl implements CommentPublicQueryService {\n    private static final int DEFAULT_SIZE = 10;\n\n    private static final String COMMENT_VIEW_PERMISSION = \"role-template-view-comments\";\n\n    private final ReactiveExtensionClient client;\n    private final UserService userService;\n    private final CounterService counterService;\n\n    @Override\n    public Mono<CommentVo> getByName(String name) {\n        return client.fetch(Comment.class, name)\n            .flatMap(this::toCommentVo);\n    }\n\n    @Override\n    public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size) {\n        return list(ref,\n            PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultCommentSort()));\n    }\n\n    @Override\n    public Mono<ListResult<CommentVo>> list(Ref ref, PageRequest pageParam) {\n        var pageRequest = Optional.ofNullable(pageParam)\n            .map(page -> page.withSort(page.getSort().and(defaultCommentSort())))\n            .orElseGet(() -> PageRequestImpl.ofSize(10));\n        return populateCommentListOptions(ref)\n            .flatMap(listOptions -> client.listBy(Comment.class, listOptions, pageRequest))\n            .flatMap(listResult -> Flux.fromStream(listResult.get())\n                .map(this::toCommentVo)\n                .flatMapSequential(Function.identity())\n                .collectList()\n                .map(commentVos -> new ListResult<>(listResult.getPage(),\n                    listResult.getSize(),\n                    listResult.getTotal(),\n                    commentVos)\n                )\n            )\n            .defaultIfEmpty(ListResult.emptyResult());\n    }\n\n    @Override\n    public Mono<ListResult<CommentWithReplyVo>> convertToWithReplyVo(ListResult<CommentVo> comments,\n        int replySize) {\n        return Flux.fromIterable(comments.getItems())\n            .flatMapSequential(commentVo -> {\n                var commentName = commentVo.getMetadata().getName();\n                return listReply(commentName, 1, replySize)\n                    .map(replyList -> CommentWithReplyVo.from(commentVo)\n                        .setReplies(replyList)\n                    );\n            })\n            .collectList()\n            .map(result -> new ListResult<>(\n                comments.getPage(),\n                comments.getSize(),\n                comments.getTotal(),\n                result)\n            );\n    }\n\n    @Override\n    public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size) {\n        return listReply(commentName, PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size),\n            defaultReplySort()));\n    }\n\n    @Override\n    public Mono<ListResult<ReplyVo>> listReply(String commentName, PageRequest pageParam) {\n        // check comment\n        return client.get(Comment.class, commentName)\n            .flatMap(this::populateReplyListOptions)\n            .flatMap(listOptions -> {\n                var pageRequest = Optional.ofNullable(pageParam)\n                    .map(page -> page.withSort(page.getSort().and(defaultReplySort())))\n                    .orElse(PageRequestImpl.ofSize(0));\n                return client.listBy(Reply.class, listOptions, pageRequest)\n                    .flatMap(list -> Flux.fromStream(list.get().map(this::toReplyVo))\n                        .flatMapSequential(Function.identity())\n                        .collectList()\n                        .map(replyVos -> new ListResult<>(list.getPage(), list.getSize(),\n                            list.getTotal(),\n                            replyVos))\n                    );\n            })\n            .defaultIfEmpty(ListResult.emptyResult());\n    }\n\n    Mono<CommentVo> toCommentVo(Comment comment) {\n        Comment.CommentOwner owner = comment.getSpec().getOwner();\n        return Mono.just(CommentVo.from(comment))\n            .flatMap(commentVo -> populateStats(Comment.class, commentVo)\n                .doOnNext(commentVo::setStats)\n                .thenReturn(commentVo))\n            .flatMap(commentVo -> getOwnerInfo(owner)\n                .doOnNext(commentVo::setOwner)\n                .thenReturn(commentVo)\n            )\n            .flatMap(this::filterCommentSensitiveData);\n    }\n\n    private Mono<? extends CommentVo> filterCommentSensitiveData(CommentVo commentVo) {\n        var owner = commentVo.getOwner();\n        commentVo.setOwner(OwnerInfo\n            .builder()\n            .displayName(owner.getDisplayName())\n            .avatar(owner.getAvatar())\n            .kind(owner.getKind())\n            .build());\n\n        commentVo.getSpec().setIpAddress(\"\");\n        var specOwner = commentVo.getSpec().getOwner();\n        specOwner.setName(\"\");\n        var email = owner.getEmail();\n        if (StringUtils.isNotBlank(email)) {\n            var emailHash = Hashing.sha256()\n                .hashString(email.toLowerCase(), UTF_8)\n                .toString();\n            if (specOwner.getAnnotations() == null) {\n                specOwner.setAnnotations(new HashMap<>(2));\n            }\n            specOwner.getAnnotations()\n                .put(Comment.CommentOwner.EMAIL_HASH_ANNO, emailHash);\n        }\n        if (specOwner.getAnnotations() != null) {\n            specOwner.getAnnotations().remove(\"Email\");\n        }\n        return Mono.just(commentVo);\n    }\n\n    // @formatter:off\n    private <E extends AbstractExtension, T extends ExtensionVoOperator>\n        Mono<CommentStatsVo> populateStats(Class<E> clazz, T vo) {\n        return counterService.getByName(MeterUtils.nameOf(clazz, vo.getMetadata()\n                .getName()))\n            .map(counter -> CommentStatsVo.builder()\n                .upvote(counter.getUpvote())\n                .build()\n            )\n            .defaultIfEmpty(CommentStatsVo.empty());\n    }\n    // @formatter:on\n\n    Mono<ReplyVo> toReplyVo(Reply reply) {\n        return Mono.just(ReplyVo.from(reply))\n            .flatMap(replyVo -> populateStats(Reply.class, replyVo)\n                .doOnNext(replyVo::setStats)\n                .thenReturn(replyVo))\n            .flatMap(replyVo -> getOwnerInfo(reply.getSpec().getOwner())\n                .doOnNext(replyVo::setOwner)\n                .thenReturn(replyVo)\n            )\n            .flatMap(this::filterReplySensitiveData);\n    }\n\n    private Mono<? extends ReplyVo> filterReplySensitiveData(ReplyVo replyVo) {\n        var owner = replyVo.getOwner();\n        replyVo.setOwner(OwnerInfo\n            .builder()\n            .displayName(owner.getDisplayName())\n            .avatar(owner.getAvatar())\n            .kind(owner.getKind())\n            .build());\n\n        replyVo.getSpec().setIpAddress(\"\");\n        var specOwner = replyVo.getSpec().getOwner();\n        specOwner.setName(\"\");\n        var email = owner.getEmail();\n        if (StringUtils.isNotBlank(email)) {\n            var emailHash = Hashing.sha256()\n                .hashString(email.toLowerCase(), UTF_8)\n                .toString();\n            if (specOwner.getAnnotations() == null) {\n                specOwner.setAnnotations(new HashMap<>(2));\n            }\n            specOwner.getAnnotations()\n                .put(Comment.CommentOwner.EMAIL_HASH_ANNO, emailHash);\n        }\n        if (specOwner.getAnnotations() != null) {\n            specOwner.getAnnotations().remove(\"Email\");\n        }\n        return Mono.just(replyVo);\n    }\n\n    private Mono<OwnerInfo> getOwnerInfo(Comment.CommentOwner owner) {\n        if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {\n            return Mono.just(OwnerInfo.from(owner));\n        }\n        return userService.getUserOrGhost(owner.getName())\n            .map(OwnerInfo::from);\n    }\n\n    private Mono<ListOptions> populateCommentListOptions(@Nullable Ref ref) {\n        return populateVisibleListOptions(null)\n            .doOnNext(builder -> {\n                if (ref != null) {\n                    builder.andQuery(\n                        equal(\"spec.subjectRef\", Comment.toSubjectRefKey(ref)));\n                }\n            })\n            .map(ListOptions.ListOptionsBuilder::build);\n    }\n\n    private Mono<ListOptions.ListOptionsBuilder> populateVisibleListOptions(\n        @Nullable Comment comment) {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Authentication::getName)\n            .defaultIfEmpty(AnonymousUserConst.PRINCIPAL)\n            .zipWith(userService.hasSufficientRoles(Set.of(COMMENT_VIEW_PERMISSION))\n                .defaultIfEmpty(false))\n            .flatMap(tuple2 -> {\n                var username = tuple2.getT1();\n                var hasViewPermission = tuple2.getT2();\n                var commentHidden = false;\n                var isCommentOwner = false;\n                if (comment != null) {\n                    commentHidden = Boolean.TRUE.equals(comment.getSpec().getHidden());\n                    var owner = comment.getSpec().getOwner();\n                    isCommentOwner = owner != null && Objects.equals(\n                        ownerIdentity(owner.getKind(), owner.getName()),\n                        ownerIdentity(User.KIND, username)\n                    );\n                    boolean hasPermission =\n                        (!commentHidden) || (hasViewPermission || isCommentOwner);\n                    if (ExtensionUtil.isDeleted(comment) || !hasPermission) {\n                        return Mono.error(new ServerWebInputException(\n                            \"The comment was not found, hidden or deleted.\"\n                        ));\n                    }\n                }\n\n                var builder = ListOptions.builder();\n                builder.andQuery(isNull(\"metadata.deletionTimestamp\"));\n                var visibleQuery = and(\n                    equal(\"spec.hidden\", BooleanUtils.FALSE),\n                    equal(\"spec.approved\", BooleanUtils.TRUE)\n                );\n\n                var isAnonymous = AnonymousUserConst.isAnonymousUser(username);\n                if (isAnonymous) {\n                    builder.andQuery(visibleQuery);\n                } else if (!(hasViewPermission || (commentHidden && isCommentOwner))) {\n                    builder.andQuery(or(\n                        equal(\"spec.owner\", ownerIdentity(User.KIND, username)),\n                        visibleQuery\n                    ));\n                }\n                // View all replies if the user is not an anonymous user, has view permission\n                // or is the comment owner.\n                return Mono.just(builder);\n            });\n    }\n\n    private Mono<ListOptions> populateReplyListOptions(Comment comment) {\n        // The comment name must be equal to the comment name of the reply\n        // is approved and not hidden\n        return populateVisibleListOptions(comment)\n            .doOnNext(builder ->\n                builder.andQuery(equal(\"spec.commentName\", comment.getMetadata().getName()))\n            )\n            .map(ListOptions.ListOptionsBuilder::build);\n    }\n\n    static Sort defaultCommentSort() {\n        return Sort.by(Sort.Order.desc(\"spec.top\"),\n            Sort.Order.asc(\"spec.priority\"),\n            Sort.Order.desc(\"spec.creationTime\"),\n            Sort.Order.asc(\"metadata.name\")\n        );\n    }\n\n    static Sort defaultReplySort() {\n        return Sort.by(Sort.Order.asc(\"spec.creationTime\"),\n            Sort.Order.asc(\"metadata.name\")\n        );\n    }\n\n    int pageNullSafe(Integer page) {\n        return defaultIfNull(page, 1);\n    }\n\n    int sizeNullSafe(Integer size) {\n        return defaultIfNull(size, DEFAULT_SIZE);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/ContributorFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport java.util.Collection;\nimport lombok.RequiredArgsConstructor;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.theme.finders.ContributorFinder;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.vo.ContributorVo;\n\n/**\n * A default implementation of {@link ContributorFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"contributorFinder\")\n@RequiredArgsConstructor\npublic class ContributorFinderImpl implements ContributorFinder {\n\n    private final UserService userService;\n\n    @Override\n    public Mono<ContributorVo> getContributor(String name) {\n        return userService.getUserOrGhost(name).map(ContributorVo::from);\n    }\n\n    @Override\n    public Flux<ContributorVo> getContributors(Collection<String> names) {\n        return userService.getUsersOrGhosts(names).map(ContributorVo::from);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/MenuFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport java.time.Instant;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport lombok.AllArgsConstructor;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.util.comparator.Comparators;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Menu;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.MenuFinder;\nimport run.halo.app.theme.finders.vo.MenuItemVo;\nimport run.halo.app.theme.finders.vo.MenuVo;\n\n/**\n * A default implementation for {@link MenuFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"menuFinder\")\n@AllArgsConstructor\npublic class MenuFinderImpl implements MenuFinder {\n\n    private final ReactiveExtensionClient client;\n    private final SystemConfigFetcher environmentFetcher;\n\n    @Override\n    public Mono<MenuVo> getByName(String name) {\n        return listAsTree()\n            .filter(menu -> menu.getMetadata().getName().equals(name))\n            .next()\n            .switchIfEmpty(Mono.error(\n                () -> new NotFoundException(\"Menu with name \" + name + \" not found\")));\n    }\n\n    @Override\n    public Mono<MenuVo> getPrimary() {\n        return listAsTree().collectList()\n            .flatMap(menuVos -> {\n                if (CollectionUtils.isEmpty(menuVos)) {\n                    return Mono.empty();\n                }\n                return environmentFetcher.fetch(SystemSetting.Menu.GROUP, SystemSetting.Menu.class)\n                    .map(SystemSetting.Menu::getPrimary)\n                    .map(primaryConfig -> menuVos.stream()\n                        .filter(menuVo -> menuVo.getMetadata().getName().equals(primaryConfig))\n                        .findAny()\n                        .orElse(menuVos.get(0))\n                    )\n                    .defaultIfEmpty(menuVos.get(0));\n            })\n            .switchIfEmpty(\n                Mono.error(() -> new NotFoundException(\"No primary menu found\"))\n            );\n    }\n\n    Flux<MenuVo> listAll() {\n        return client.list(Menu.class, null, null)\n            .map(MenuVo::from);\n    }\n\n    Flux<MenuVo> listAsTree() {\n        return listAllMenuItem()\n            .collectList()\n            .map(MenuFinderImpl::populateParentName)\n            .flatMapMany(menuItemVos -> {\n                List<MenuItemVo> treeList = listToTree(menuItemVos);\n                Map<String, MenuItemVo> nameItemRootNodeMap = treeList.stream()\n                    .collect(Collectors.toMap(item -> item.getMetadata().getName(),\n                        Function.identity()));\n                return listAll()\n                    .map(menuVo -> {\n                        LinkedHashSet<String> menuItemNames = menuVo.getSpec().getMenuItems();\n                        if (menuItemNames == null) {\n                            return menuVo.withMenuItems(List.of());\n                        }\n                        List<MenuItemVo> menuItems = menuItemNames.stream()\n                            .map(nameItemRootNodeMap::get)\n                            .filter(Objects::nonNull)\n                            .sorted(defaultTreeNodeComparator())\n                            .toList();\n                        return menuVo.withMenuItems(menuItems);\n                    });\n            });\n    }\n\n    static List<MenuItemVo> listToTree(Collection<MenuItemVo> list) {\n        Map<String, List<MenuItemVo>> parentNameIdentityMap = list.stream()\n            .filter(menuItemVo -> menuItemVo.getParentName() != null)\n            .collect(Collectors.groupingBy(MenuItemVo::getParentName));\n\n        list.forEach(node -> {\n            // sort children\n            List<MenuItemVo> children =\n                parentNameIdentityMap.getOrDefault(node.getMetadata().getName(), List.of())\n                    .stream()\n                    .sorted(defaultTreeNodeComparator())\n                    .toList();\n            node.setChildren(children);\n        });\n\n        return list.stream()\n            .filter(v -> v.getParentName() == null)\n            .collect(Collectors.toList());\n    }\n\n    Flux<MenuItemVo> listAllMenuItem() {\n        return client.list(MenuItem.class, null, null)\n            .map(MenuItemVo::from);\n    }\n\n    static Comparator<MenuItemVo> defaultTreeNodeComparator() {\n        Function<MenuItemVo, Integer> priority = menuItem -> menuItem.getSpec().getPriority();\n        Function<MenuItemVo, Instant> createTime = menuItem -> menuItem.getMetadata()\n            .getCreationTimestamp();\n        Function<MenuItemVo, String> name = menuItem -> menuItem.getMetadata().getName();\n\n        return Comparator.comparing(priority)\n            .thenComparing(createTime, Comparators.nullsLow())\n            .thenComparing(name);\n    }\n\n    static Collection<MenuItemVo> populateParentName(List<MenuItemVo> menuItemVos) {\n        Map<String, MenuItemVo> nameIdentityMap = menuItemVos.stream()\n            .collect(Collectors.toMap(menuItem -> menuItem.getMetadata().getName(),\n                Function.identity()));\n\n        nameIdentityMap.forEach((name, value) -> {\n            LinkedHashSet<String> children = value.getSpec().getChildren();\n            if (children == null) {\n                return;\n            }\n            for (String child : children) {\n                MenuItemVo childNode = nameIdentityMap.get(child);\n                if (childNode != null) {\n                    childNode.setParentName(name);\n                }\n            }\n        });\n        return nameIdentityMap.values();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/PluginFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.util.Assert;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.PluginFinder;\n\n/**\n * Plugin finder implementation.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"pluginFinder\")\n@AllArgsConstructor\npublic class PluginFinderImpl implements PluginFinder {\n    private final PluginManager pluginManager;\n\n    @Override\n    public boolean available(String pluginName) {\n        if (StringUtils.isBlank(pluginName)) {\n            return false;\n        }\n        PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginName);\n        if (pluginWrapper == null) {\n            return false;\n        }\n        return PluginState.STARTED.equals(pluginWrapper.getPluginState());\n    }\n\n    @Override\n    public boolean available(String pluginName, String requiresVersion) {\n        Assert.notNull(requiresVersion, \"Requires version must not be null.\");\n        if (!this.available(pluginName)) {\n            return false;\n        }\n        var pluginWrapper = pluginManager.getPlugin(pluginName);\n        var pluginVersion = pluginWrapper.getDescriptor().getVersion();\n        return pluginManager.getVersionManager()\n            .checkVersionConstraint(pluginVersion, requiresVersion);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static run.halo.app.extension.PageRequestImpl.ofSize;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.in;\nimport static run.halo.app.extension.index.query.Queries.notEqual;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.CategoryService;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Condition;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.SortUtils;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.NavigationPostVo;\nimport run.halo.app.theme.finders.vo.PostArchiveVo;\nimport run.halo.app.theme.finders.vo.PostArchiveYearMonthVo;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.router.ReactiveQueryPostPredicateResolver;\n\n/**\n * A finder for {@link Post}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"postFinder\")\n@AllArgsConstructor\npublic class PostFinderImpl implements PostFinder {\n\n    private final ReactiveExtensionClient client;\n\n    private final PostPublicQueryService postPublicQueryService;\n\n    private final ReactiveQueryPostPredicateResolver postPredicateResolver;\n\n    private final CategoryService categoryService;\n\n    @Override\n    public Mono<PostVo> getByName(String postName) {\n        return postPredicateResolver.getPredicate()\n            .flatMap(predicate -> client.get(Post.class, postName)\n                .filter(predicate)\n                .flatMap(post -> postPublicQueryService.convertToVo(post,\n                    post.getSpec().getReleaseSnapshot())\n                )\n            );\n    }\n\n    @Override\n    public Mono<ContentVo> content(String postName) {\n        return postPublicQueryService.getContent(postName);\n    }\n\n    static Sort defaultSort() {\n        return Sort.by(Sort.Order.desc(\"spec.pinned\"),\n            Sort.Order.desc(\"spec.priority\"),\n            Sort.Order.desc(\"spec.publishTime\"),\n            Sort.Order.asc(\"metadata.name\")\n        );\n    }\n\n    static Sort archiveSort() {\n        return Sort.by(Sort.Order.desc(\"spec.publishTime\"),\n            Sort.Order.desc(\"metadata.name\")\n        );\n    }\n\n    @Override\n    public Mono<NavigationPostVo> cursor(String currentName) {\n        return client.fetch(Post.class, currentName)\n            // make sure the current post is published and has publishing time\n            .filter(p -> Post.isPublished(p.getMetadata()))\n            .filter(p -> p.getSpec() != null && p.getSpec().getPublishTime() != null)\n            .flatMap(currentPost -> {\n                var findPreviousPost = findPreviousPost(currentPost).map(Optional::of)\n                    .defaultIfEmpty(Optional.empty());\n                var findNextPost = findNextPost(currentPost).map(Optional::of)\n                    .defaultIfEmpty(Optional.empty());\n                return Mono.zip(findPreviousPost, findNextPost, (previous, next) ->\n                    NavigationPostVo.builder()\n                        .previous(previous.map(ListedPostVo::from).orElse(null))\n                        .next(next.map(ListedPostVo::from).orElse(null))\n                        .build()\n                );\n            })\n            .switchIfEmpty(Mono.fromSupplier(NavigationPostVo::empty));\n    }\n\n    private Mono<Post> findPreviousPost(Post currentPost) {\n        var publishTime = currentPost.getSpec().getPublishTime();\n        return postPredicateResolver.getListOptions()\n            .map(listOptions -> ListOptions.builder(listOptions)\n                .andQuery(notHiddenPostQuery())\n                .andQuery(Queries.lessThan(\"spec.publishTime\", publishTime))\n                .build()\n            )\n            .flatMap(listOptions -> {\n                var sort = Sort.by(\n                    Sort.Order.desc(\"spec.publishTime\"),\n                    Sort.Order.desc(\"metadata.name\")\n                );\n                return client.listBy(\n                    Post.class, listOptions, ofSize(1).withSort(sort)\n                );\n            })\n            .flatMap(listResult -> Mono.justOrEmpty(listResult.getItems().stream().findFirst()));\n    }\n\n    private Mono<Post> findNextPost(Post currentPost) {\n        var publishTime = currentPost.getSpec().getPublishTime();\n        return postPredicateResolver.getListOptions()\n            .map(listOptions -> ListOptions.builder(listOptions)\n                .andQuery(notHiddenPostQuery())\n                .andQuery(Queries.greaterThan(\"spec.publishTime\", publishTime))\n                .build()\n            )\n            .flatMap(listOptions -> {\n                var sort = Sort.by(\n                    Sort.Order.asc(\"spec.publishTime\"),\n                    Sort.Order.asc(\"metadata.name\")\n                );\n                return client.listBy(\n                    Post.class, listOptions, ofSize(1).withSort(sort)\n                );\n            })\n            .flatMap(listResult -> Mono.justOrEmpty(listResult.getItems().stream().findFirst()));\n    }\n\n    private static Condition notHiddenPostQuery() {\n        return notEqual(\"status.hideFromList\", BooleanUtils.TRUE);\n    }\n\n    @Override\n    public Mono<ListResult<ListedPostVo>> list(Map<String, Object> params) {\n        var query = Optional.ofNullable(params)\n            .map(map -> JsonUtils.mapToObject(map, PostQuery.class))\n            .orElseGet(PostQuery::new);\n        if (StringUtils.isNotBlank(query.getCategoryName())) {\n            return listChildrenCategories(query.getCategoryName())\n                .map(category -> category.getMetadata().getName())\n                .collectList()\n                .map(categoryNames -> ListOptions.builder(query.toListOptions())\n                    .andQuery(in(\"spec.categories\", categoryNames))\n                    .build()\n                )\n                .flatMap(\n                    listOptions -> postPublicQueryService.list(listOptions, query.toPageRequest()));\n        }\n        return postPublicQueryService.list(query.toListOptions(), query.toPageRequest());\n    }\n\n    @Override\n    public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size) {\n        var listOptions = ListOptions.builder()\n            .fieldQuery(notHiddenPostQuery())\n            .build();\n        return postPublicQueryService.list(listOptions, getPageRequest(page, size));\n    }\n\n    private PageRequestImpl getPageRequest(Integer page, Integer size) {\n        return PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort());\n    }\n\n    @Override\n    public Mono<ListResult<ListedPostVo>> listByCategory(Integer page, Integer size,\n        String categoryName) {\n        return listChildrenCategories(categoryName)\n            .map(category -> category.getMetadata().getName())\n            .collectList()\n            .flatMap(categoryNames -> {\n                var listOptions = new ListOptions();\n                var fieldQuery = in(\"spec.categories\", categoryNames);\n                listOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n                return postPublicQueryService.list(listOptions, getPageRequest(page, size));\n            });\n    }\n\n    private Flux<Category> listChildrenCategories(String categoryName) {\n        if (StringUtils.isBlank(categoryName)) {\n            return client.listAll(Category.class, new ListOptions(),\n                Sort.by(Sort.Order.asc(\"metadata.creationTimestamp\"),\n                    Sort.Order.desc(\"metadata.name\")));\n        }\n        return categoryService.listChildren(categoryName);\n    }\n\n    @Override\n    public Mono<ListResult<ListedPostVo>> listByTag(Integer page, Integer size, String tag) {\n        var fieldQuery = Queries.empty();\n        if (StringUtils.isNotBlank(tag)) {\n            fieldQuery = fieldQuery.and(equal(\"spec.tags\", tag));\n        }\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n        return postPublicQueryService.list(listOptions, getPageRequest(page, size));\n    }\n\n    @Override\n    public Mono<ListResult<ListedPostVo>> listByOwner(Integer page, Integer size, String owner) {\n        var fieldQuery = Queries.empty();\n        if (StringUtils.isNotBlank(owner)) {\n            fieldQuery = fieldQuery.and(equal(\"spec.owner\", owner));\n        }\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n        return postPublicQueryService.list(listOptions, getPageRequest(page, size));\n    }\n\n    @Override\n    public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size) {\n        return archives(page, size, null, null);\n    }\n\n    @Override\n    public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year) {\n        return archives(page, size, year, null);\n    }\n\n    @Override\n    public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year,\n        String month) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(notHiddenPostQuery()));\n        var labelSelectorBuilder = LabelSelector.builder();\n        if (StringUtils.isNotBlank(year)) {\n            labelSelectorBuilder.eq(Post.ARCHIVE_YEAR_LABEL, year);\n        }\n        if (StringUtils.isNotBlank(month)) {\n            labelSelectorBuilder.eq(Post.ARCHIVE_MONTH_LABEL, month);\n        }\n        listOptions.setLabelSelector(labelSelectorBuilder.build());\n        var pageRequest = PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), archiveSort());\n        return postPublicQueryService.list(listOptions, pageRequest)\n            .map(list -> {\n                Map<String, List<ListedPostVo>> yearPosts = list.get()\n                    .collect(Collectors.groupingBy(\n                        post -> HaloUtils.getYearText(post.getSpec().getPublishTime())));\n                List<PostArchiveVo> postArchives = yearPosts.entrySet().stream()\n                    .map(entry -> {\n                        String key = entry.getKey();\n                        // archives by month\n                        Map<String, List<ListedPostVo>> monthPosts = entry.getValue().stream()\n                            .collect(Collectors.groupingBy(\n                                post -> HaloUtils.getMonthText(post.getSpec().getPublishTime())));\n                        // convert to archive year month value objects\n                        List<PostArchiveYearMonthVo> monthArchives = monthPosts.entrySet()\n                            .stream()\n                            .map(monthEntry -> PostArchiveYearMonthVo.builder()\n                                .posts(monthEntry.getValue())\n                                .month(monthEntry.getKey())\n                                .build()\n                            )\n                            .sorted(\n                                Comparator.comparing(PostArchiveYearMonthVo::getMonth).reversed())\n                            .toList();\n                        return PostArchiveVo.builder()\n                            .year(String.valueOf(key))\n                            .months(monthArchives)\n                            .build();\n                    })\n                    .sorted(Comparator.comparing(PostArchiveVo::getYear).reversed())\n                    .toList();\n                return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),\n                    postArchives);\n            })\n            .defaultIfEmpty(ListResult.emptyResult());\n    }\n\n    @Override\n    public Flux<ListedPostVo> listAll() {\n        return postPredicateResolver.getListOptions()\n            .flatMapMany(listOptions -> client.listAll(Post.class, listOptions, defaultSort()))\n            .collectList()\n            .flatMap(postPublicQueryService::convertToListedVos)\n            .flatMapMany(Flux::fromIterable);\n    }\n\n    static int pageNullSafe(Integer page) {\n        return ObjectUtils.defaultIfNull(page, 1);\n    }\n\n    static int sizeNullSafe(Integer size) {\n        return ObjectUtils.defaultIfNull(size, 10);\n    }\n\n    @Data\n    public static class PostQuery {\n        private Integer page;\n        private Integer size;\n        private String categoryName;\n        private String tagName;\n        private String owner;\n        private List<String> sort;\n\n        public ListOptions toListOptions() {\n            var builder = ListOptions.builder();\n            var hasQuery = false;\n            if (StringUtils.isNotBlank(owner)) {\n                builder.andQuery(equal(\"spec.owner\", owner));\n                hasQuery = true;\n            }\n            if (StringUtils.isNotBlank(tagName)) {\n                builder.andQuery(equal(\"spec.tags\", tagName));\n                hasQuery = true;\n            }\n            // Exclude hidden posts when no query\n            if (!hasQuery) {\n                builder.fieldQuery(notHiddenPostQuery());\n            }\n            return builder.build();\n        }\n\n        public PageRequest toPageRequest() {\n            return PageRequestImpl.of(pageNullSafe(getPage()),\n                sizeNullSafe(getSize()), SortUtils.resolve(sort).and(defaultSort()));\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static java.util.Objects.requireNonNullElse;\nimport static java.util.Objects.requireNonNullElseGet;\nimport static run.halo.app.core.counter.MeterUtils.nameOf;\nimport static run.halo.app.core.user.service.UserService.GHOST_USER_NAME;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.ReactivePostContentHandler;\nimport run.halo.app.theme.finders.CategoryFinder;\nimport run.halo.app.theme.finders.ContributorFinder;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.TagFinder;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ContributorVo;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.finders.vo.StatsVo;\nimport run.halo.app.theme.router.ReactiveQueryPostPredicateResolver;\n\n@Component\n@RequiredArgsConstructor\npublic class PostPublicQueryServiceImpl implements PostPublicQueryService {\n\n    private final ReactiveExtensionClient client;\n\n    private final TagFinder tagFinder;\n\n    private final CategoryFinder categoryFinder;\n\n    private final ContributorFinder contributorFinder;\n\n    private final CounterService counterService;\n\n    private final PostService postService;\n\n    private final ExtensionGetter extensionGetter;\n\n    private final ReactiveQueryPostPredicateResolver postPredicateResolver;\n\n    @Override\n    public Mono<ListResult<ListedPostVo>> list(ListOptions queryOptions, PageRequest page) {\n        return postPredicateResolver.getListOptions()\n            .map(option -> {\n                var fieldSelector = queryOptions.getFieldSelector();\n                if (fieldSelector != null) {\n                    option.setFieldSelector(option.getFieldSelector()\n                        .andQuery(fieldSelector.query()));\n                }\n                var labelSelector = queryOptions.getLabelSelector();\n                if (labelSelector != null) {\n                    option.setLabelSelector(option.getLabelSelector().and(labelSelector));\n                }\n                return option;\n            })\n            .flatMap(listOptions -> client.listBy(Post.class, listOptions, page))\n            .flatMap(list -> convertToListedVos(list.getItems())\n                .map(\n                    postVos -> new ListResult<>(\n                        list.getPage(), list.getSize(), list.getTotal(), postVos\n                    )\n                )\n            )\n            .defaultIfEmpty(ListResult.emptyResult());\n    }\n\n\n    @Override\n    public Mono<ListedPostVo> convertToListedVo(@NonNull Post post) {\n        Assert.notNull(post, \"Post must not be null\");\n        ListedPostVo postVo = ListedPostVo.from(post);\n        postVo.setCategories(List.of());\n        postVo.setTags(List.of());\n        postVo.setContributors(List.of());\n\n        return Mono.just(postVo)\n            .flatMap(lp -> populateStats(postVo)\n                .doOnNext(lp::setStats)\n                .thenReturn(lp)\n            )\n            .flatMap(p -> {\n                String owner = p.getSpec().getOwner();\n                return contributorFinder.getContributor(owner)\n                    .doOnNext(p::setOwner)\n                    .thenReturn(p);\n            })\n            .flatMap(p -> {\n                List<String> tagNames = p.getSpec().getTags();\n                if (CollectionUtils.isEmpty(tagNames)) {\n                    return Mono.just(p);\n                }\n                return tagFinder.getByNames(tagNames)\n                    .collectList()\n                    .doOnNext(p::setTags)\n                    .thenReturn(p);\n            })\n            .flatMap(p -> {\n                List<String> categoryNames = p.getSpec().getCategories();\n                if (CollectionUtils.isEmpty(categoryNames)) {\n                    return Mono.just(p);\n                }\n                return categoryFinder.getByNames(categoryNames)\n                    .collectList()\n                    .doOnNext(p::setCategories)\n                    .thenReturn(p);\n            })\n            .flatMap(p -> contributorFinder.getContributors(p.getStatus().getContributors())\n                .collectList()\n                .doOnNext(p::setContributors)\n                .thenReturn(p)\n            )\n            .defaultIfEmpty(postVo);\n    }\n\n    @Override\n    public Mono<List<ListedPostVo>> convertToListedVos(List<Post> posts) {\n        var counterNames = new HashSet<String>(posts.size());\n        var userNames = new HashSet<String>();\n        var tagNames = new HashSet<String>();\n        var categoryNames = new HashSet<String>();\n        posts.forEach(post -> {\n            counterNames.add(nameOf(Post.class, post.getMetadata().getName()));\n            var spec = post.getSpec();\n            userNames.add(spec.getOwner());\n            var status = post.getStatus();\n            if (status != null && status.getContributors() != null) {\n                userNames.addAll(status.getContributors());\n            }\n            if (spec.getTags() != null) {\n                tagNames.addAll(spec.getTags());\n            }\n            if (spec.getCategories() != null) {\n                categoryNames.addAll(spec.getCategories());\n            }\n        });\n\n        var getCounters = counterService.getByNames(counterNames)\n            .collectMap(counter -> counter.getMetadata().getName());\n        var getContributors = contributorFinder.getContributors(userNames)\n            .collectMap(ContributorVo::getName);\n        var getTags = tagFinder.getByNames(tagNames)\n            .collectMap(tagVo -> tagVo.getMetadata().getName());\n        var getCategories = categoryFinder.getByNames(categoryNames)\n            .collectMap(categoryVo -> categoryVo.getMetadata().getName());\n\n        return Mono.zip(getCounters, getContributors, getTags, getCategories)\n            .map(tuple -> {\n                var counters = tuple.getT1();\n                var contributors = tuple.getT2();\n                var tags = tuple.getT3();\n                var categories = tuple.getT4();\n                return posts.stream()\n                    .map(post -> {\n                        var vo = ListedPostVo.from(post);\n                        vo.setCategories(List.of());\n                        vo.setTags(List.of());\n                        vo.setContributors(List.of());\n\n                        var spec = post.getSpec();\n                        var status = post.getStatus();\n                        var ghost = requireNonNullElseGet(\n                            contributors.get(GHOST_USER_NAME), ContributorVo::ghost\n                        );\n                        vo.setOwner(requireNonNullElse(contributors.get(spec.getOwner()), ghost));\n                        if (status != null && !CollectionUtils.isEmpty(status.getContributors())) {\n                            vo.setContributors(status.getContributors()\n                                .stream()\n                                .map(name -> requireNonNullElse(contributors.get(name), ghost))\n                                .toList());\n                        }\n\n                        if (!CollectionUtils.isEmpty(spec.getTags())) {\n                            vo.setTags(spec.getTags()\n                                .stream()\n                                .map(tags::get)\n                                .filter(Objects::nonNull)\n                                .toList());\n                        }\n                        if (!CollectionUtils.isEmpty(spec.getCategories())) {\n                            vo.setCategories(spec.getCategories()\n                                .stream()\n                                .map(categories::get)\n                                .filter(Objects::nonNull)\n                                .toList());\n                        }\n\n                        var counterName = nameOf(Post.class, post.getMetadata().getName());\n                        var counter = counters.get(counterName);\n                        if (counter != null) {\n                            vo.setStats(StatsVo.builder()\n                                .visit(counter.getVisit())\n                                .upvote(counter.getUpvote())\n                                .comment(counter.getApprovedComment())\n                                .build()\n                            );\n                        } else {\n                            vo.setStats(StatsVo.empty());\n                        }\n\n                        return vo;\n                    })\n                    .toList();\n            });\n    }\n\n    @Override\n    public Mono<PostVo> convertToVo(Post post, String snapshotName) {\n        final String baseSnapshotName = post.getSpec().getBaseSnapshot();\n        return convertToListedVo(post)\n            .map(PostVo::from)\n            .flatMap(postVo -> postService.getContent(snapshotName, baseSnapshotName)\n                .flatMap(wrapper -> extendPostContent(post, wrapper))\n                .doOnNext(postVo::setContent)\n                .thenReturn(postVo)\n            );\n    }\n\n    @Override\n    public Mono<ContentVo> getContent(String postName) {\n        return postPredicateResolver.getPredicate()\n            .flatMap(predicate -> client.get(Post.class, postName)\n                .filter(predicate)\n            )\n            .flatMap(post -> {\n                String releaseSnapshot = post.getSpec().getReleaseSnapshot();\n                return postService.getContent(releaseSnapshot, post.getSpec().getBaseSnapshot())\n                    .flatMap(wrapper -> extendPostContent(post, wrapper));\n            });\n    }\n\n    @NonNull\n    protected Mono<ContentVo> extendPostContent(Post post,\n        ContentWrapper wrapper) {\n        Assert.notNull(post, \"Post name must not be null\");\n        Assert.notNull(wrapper, \"Post content must not be null\");\n        return extensionGetter.getEnabledExtensions(ReactivePostContentHandler.class)\n            .reduce(Mono.fromSupplier(() -> ReactivePostContentHandler.PostContentContext.builder()\n                    .post(post)\n                    .content(wrapper.getContent())\n                    .raw(wrapper.getRaw())\n                    .rawType(wrapper.getRawType())\n                    .build()\n                ),\n                (contentMono, handler) -> contentMono.flatMap(handler::handle)\n            )\n            .flatMap(Function.identity())\n            .map(postContent -> ContentVo.builder()\n                .content(postContent.getContent())\n                .raw(postContent.getRaw())\n                .build()\n            );\n    }\n\n    private <T extends ListedPostVo> Mono<StatsVo> populateStats(T postVo) {\n        return counterService.getByName(nameOf(Post.class, postVo.getMetadata()\n                .getName())\n            )\n            .map(counter -> StatsVo.builder()\n                .visit(counter.getVisit())\n                .upvote(counter.getUpvote())\n                .comment(counter.getApprovedComment())\n                .build()\n            )\n            .defaultIfEmpty(StatsVo.empty());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/SinglePageConversionServiceImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.springframework.data.domain.Sort.Order.asc;\nimport static org.springframework.data.domain.Sort.Order.desc;\nimport static run.halo.app.core.extension.content.Post.VisibleEnum.PUBLIC;\nimport static run.halo.app.core.extension.content.SinglePage.PUBLISHED_LABEL;\nimport static run.halo.app.extension.ExtensionUtil.notDeleting;\nimport static run.halo.app.extension.index.query.Queries.equal;\n\nimport java.util.List;\nimport java.util.function.Function;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.SinglePageService;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.ReactiveSinglePageContentHandler;\nimport run.halo.app.theme.ReactiveSinglePageContentHandler.SinglePageContentContext;\nimport run.halo.app.theme.finders.ContributorFinder;\nimport run.halo.app.theme.finders.SinglePageConversionService;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ListedSinglePageVo;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\nimport run.halo.app.theme.finders.vo.StatsVo;\n\n/**\n * Default implementation of {@link SinglePageConversionService}.\n *\n * @author guqing\n * @since 2.6.0\n */\n@Component\n@RequiredArgsConstructor\npublic class SinglePageConversionServiceImpl implements SinglePageConversionService {\n\n    private final ReactiveExtensionClient client;\n\n    private final SinglePageService singlePageService;\n\n    private final ContributorFinder contributorFinder;\n\n    private final CounterService counterService;\n\n    private final ExtensionGetter extensionGetter;\n\n    @Override\n    public Mono<SinglePageVo> convertToVo(SinglePage singlePage, String snapshotName) {\n        return convert(singlePage, snapshotName);\n    }\n\n    @Override\n    public Mono<SinglePageVo> convertToVo(@NonNull SinglePage singlePage) {\n        return convert(singlePage, singlePage.getSpec().getReleaseSnapshot());\n    }\n\n    protected Mono<ContentVo> extendPageContent(SinglePage singlePage,\n        ContentWrapper wrapper) {\n        Assert.notNull(singlePage, \"SinglePage must not be null\");\n        Assert.notNull(wrapper, \"SinglePage content must not be null\");\n        return extensionGetter.getEnabledExtensions(\n                ReactiveSinglePageContentHandler.class)\n            .reduce(Mono.fromSupplier(() -> SinglePageContentContext.builder()\n                    .singlePage(singlePage)\n                    .content(wrapper.getContent())\n                    .raw(wrapper.getRaw())\n                    .rawType(wrapper.getRawType())\n                    .build()\n                ),\n                (contentMono, handler) -> contentMono.flatMap(handler::handle)\n            )\n            .flatMap(Function.identity())\n            .map(pageContent -> ContentVo.builder()\n                .content(pageContent.getContent())\n                .raw(pageContent.getRaw())\n                .build()\n            );\n    }\n\n    @Override\n    public Mono<ContentVo> getContent(String pageName) {\n        return client.get(SinglePage.class, pageName)\n            .flatMap(singlePage -> {\n                String releaseSnapshot = singlePage.getSpec().getReleaseSnapshot();\n                String baseSnapshot = singlePage.getSpec().getBaseSnapshot();\n                return singlePageService.getContent(releaseSnapshot, baseSnapshot)\n                    .flatMap(wrapper -> extendPageContent(singlePage, wrapper));\n            })\n            .map(wrapper -> ContentVo.builder().content(wrapper.getContent())\n                .raw(wrapper.getRaw()).build());\n    }\n\n    @Override\n    public Mono<ListedSinglePageVo> convertToListedVo(SinglePage singlePage) {\n        return Mono.fromSupplier(\n                () -> {\n                    ListedSinglePageVo pageVo = ListedSinglePageVo.from(singlePage);\n                    pageVo.setContributors(List.of());\n                    return pageVo;\n                })\n            .flatMap(this::populateStats)\n            .flatMap(this::populateContributors);\n    }\n\n    @Override\n    public Mono<ListResult<ListedSinglePageVo>> listBy(ListOptions listOptions,\n        PageRequest pageRequest) {\n        // rewrite list options\n        var rewroteListOptions = ListOptions.builder(listOptions)\n            .andQuery(notDeleting())\n            .andQuery(equal(\"spec.deleted\", Boolean.FALSE.toString()))\n            .andQuery(equal(\"spec.visible\", PUBLIC.name()))\n            .labelSelector()\n            .eq(PUBLISHED_LABEL, Boolean.TRUE.toString())\n            .end()\n            .build();\n\n        // rewrite sort\n        var rewroteSort = pageRequest.getSort()\n            .and(Sort.by(\n                desc(\"spec.pinned\"),\n                asc(\"spec.priority\")\n            ));\n\n        var rewrotePageRequest =\n            PageRequestImpl.of(pageRequest.getPageNumber(), pageRequest.getPageSize(), rewroteSort);\n\n        return client.listBy(SinglePage.class, rewroteListOptions, rewrotePageRequest)\n            .flatMap(list -> Flux.fromStream(list.get())\n                .flatMapSequential(this::convertToListedVo)\n                .collectList()\n                .map(pageVos ->\n                    new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), pageVos)\n                )\n            );\n    }\n\n\n    Mono<SinglePageVo> convert(SinglePage singlePage, String snapshotName) {\n        Assert.notNull(singlePage, \"Single page must not be null\");\n        Assert.hasText(snapshotName, \"Snapshot name must not be empty\");\n        return Mono.just(singlePage)\n            .map(page -> {\n                SinglePageVo pageVo = SinglePageVo.from(page);\n                pageVo.setContributors(List.of());\n                pageVo.setContent(ContentVo.empty());\n                return pageVo;\n            })\n            .flatMap(this::populateStats)\n            .flatMap(this::populateContributors)\n            .flatMap(page -> {\n                String baseSnapshot = page.getSpec().getBaseSnapshot();\n                return singlePageService.getContent(snapshotName, baseSnapshot)\n                    .flatMap(wrapper -> extendPageContent(singlePage, wrapper))\n                    .doOnNext(page::setContent)\n                    .thenReturn(page);\n            })\n            .flatMap(page -> contributorFinder.getContributor(page.getSpec().getOwner())\n                .doOnNext(page::setOwner)\n                .thenReturn(page)\n            );\n    }\n\n    <T extends ListedSinglePageVo> Mono<T> populateStats(T pageVo) {\n        String name = pageVo.getMetadata().getName();\n        return counterService.getByName(MeterUtils.nameOf(SinglePage.class, name))\n            .map(counter -> StatsVo.builder()\n                .visit(counter.getVisit())\n                .upvote(counter.getUpvote())\n                .comment(counter.getApprovedComment())\n                .build()\n            )\n            .doOnNext(pageVo::setStats)\n            .thenReturn(pageVo);\n    }\n\n    <T extends ListedSinglePageVo> Mono<T> populateContributors(T pageVo) {\n        List<String> names = pageVo.getStatus().getContributors();\n        if (CollectionUtils.isEmpty(names)) {\n            return Mono.just(pageVo);\n        }\n        return contributorFinder.getContributors(names)\n            .collectList()\n            .doOnNext(pageVo::setContributors)\n            .thenReturn(pageVo);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport java.security.Principal;\nimport java.util.Objects;\nimport java.util.function.Predicate;\nimport lombok.AllArgsConstructor;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.SinglePageConversionService;\nimport run.halo.app.theme.finders.SinglePageFinder;\nimport run.halo.app.theme.finders.vo.ContentVo;\nimport run.halo.app.theme.finders.vo.ListedSinglePageVo;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * A default implementation of {@link SinglePage}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"singlePageFinder\")\n@AllArgsConstructor\npublic class SinglePageFinderImpl implements SinglePageFinder {\n\n    private final ReactiveExtensionClient client;\n\n    private final SinglePageConversionService singlePagePublicQueryService;\n\n    @Override\n    public Mono<SinglePageVo> getByName(String pageName) {\n        return client.get(SinglePage.class, pageName)\n            .filterWhen(page -> queryPredicate().map(predicate -> predicate.test(page)))\n            .flatMap(singlePagePublicQueryService::convertToVo);\n    }\n\n    @Override\n    public Mono<ContentVo> content(String pageName) {\n        return singlePagePublicQueryService.getContent(pageName);\n    }\n\n    @Override\n    public Mono<ListResult<ListedSinglePageVo>> list(Integer page, Integer size) {\n        return singlePagePublicQueryService.listBy(\n            new ListOptions(),\n            PageRequestImpl.of(page, size)\n        );\n    }\n\n    Mono<Predicate<SinglePage>> queryPredicate() {\n        Predicate<SinglePage> predicate = page -> page.isPublished()\n            && Objects.equals(false, page.getSpec().getDeleted());\n        Predicate<SinglePage> visiblePredicate =\n            page -> Post.VisibleEnum.PUBLIC.equals(page.getSpec().getVisible());\n        return currentUserName()\n            .map(username -> predicate.and(\n                visiblePredicate.or(page -> username.equals(page.getSpec().getOwner())))\n            )\n            .defaultIfEmpty(predicate.and(visiblePredicate));\n    }\n\n    Mono<String> currentUserName() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Principal::getName)\n            .filter(name -> !AnonymousUserConst.isAnonymousUser(name));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\n\nimport lombok.AllArgsConstructor;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.SiteStatsFinder;\nimport run.halo.app.theme.finders.vo.SiteStatsVo;\n\n/**\n * A default implementation of {@link SiteStatsFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@AllArgsConstructor\n@Finder(\"siteStatsFinder\")\npublic class SiteStatsFinderImpl implements SiteStatsFinder {\n    private final ReactiveExtensionClient client;\n\n    @Override\n    public Mono<SiteStatsVo> getStats() {\n        return client.list(Counter.class, null, null)\n            .reduce(SiteStatsVo.empty(), (stats, counter) -> {\n                stats.setVisit(stats.getVisit() + counter.getVisit());\n                stats.setComment(stats.getComment() + counter.getApprovedComment());\n                stats.setUpvote(stats.getUpvote() + counter.getUpvote());\n                return stats;\n            })\n            .flatMap(siteStatsVo -> postCount()\n                .doOnNext(siteStatsVo::setPost)\n                .thenReturn(siteStatsVo)\n            )\n            .flatMap(siteStatsVo -> categoryCount()\n                .doOnNext(siteStatsVo::setCategory)\n                .thenReturn(siteStatsVo));\n    }\n\n    Mono<Integer> postCount() {\n        var listOptions = new ListOptions();\n        listOptions.setLabelSelector(LabelSelector.builder()\n            .eq(Post.PUBLISHED_LABEL, \"true\")\n            .build());\n        var fieldQuery = and(\n            isNull(\"metadata.deletionTimestamp\"),\n            equal(\"spec.deleted\", \"false\")\n        );\n        listOptions.setFieldSelector(FieldSelector.of(fieldQuery));\n        return client.listBy(Post.class, listOptions, PageRequestImpl.ofSize(1))\n            .map(result -> (int) result.getTotal());\n    }\n\n    Mono<Integer> categoryCount() {\n        return client.listBy(Category.class, new ListOptions(), PageRequestImpl.ofSize(1))\n            .map(ListResult::getTotal)\n            .map(Long::intValue);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/TagFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.util.CollectionUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.TagFinder;\nimport run.halo.app.theme.finders.vo.TagVo;\n\n/**\n * A default implementation of {@link TagFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"tagFinder\")\npublic class TagFinderImpl implements TagFinder {\n\n    public static final Comparator<Tag> DEFAULT_COMPARATOR =\n        Comparator.comparing(tag -> tag.getMetadata().getCreationTimestamp());\n\n    private final ReactiveExtensionClient client;\n\n    public TagFinderImpl(ReactiveExtensionClient client) {\n        this.client = client;\n    }\n\n    @Override\n    public Mono<TagVo> getByName(String name) {\n        return client.fetch(Tag.class, name)\n            .map(TagVo::from);\n    }\n\n    @Override\n    public Flux<TagVo> getByNames(Collection<String> names) {\n        if (CollectionUtils.isEmpty(names)) {\n            return Flux.empty();\n        }\n        var options = ListOptions.builder()\n            .andQuery(Queries.in(\"metadata.name\", names))\n            .build();\n        return client.listAll(Tag.class, options, ExtensionUtil.defaultSort())\n            .map(TagVo::from);\n    }\n\n    @Override\n    public Mono<ListResult<TagVo>> list(Integer page, Integer size) {\n        return listBy(new ListOptions(),\n            PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size)));\n    }\n\n    @Override\n    public List<TagVo> convertToVo(List<Tag> tags) {\n        if (CollectionUtils.isEmpty(tags)) {\n            return List.of();\n        }\n        return tags.stream()\n            .map(TagVo::from)\n            .collect(Collectors.toList());\n    }\n\n    @Override\n    public Flux<TagVo> listAll() {\n        return client.listAll(Tag.class, new ListOptions(),\n                Sort.by(Sort.Order.desc(\"metadata.creationTimestamp\")))\n            .map(TagVo::from);\n    }\n\n    private Mono<ListResult<TagVo>> listBy(ListOptions listOptions, PageRequest pageRequest) {\n        return client.listBy(Tag.class, listOptions, pageRequest)\n            .map(result -> {\n                List<TagVo> tagVos = result.get()\n                    .map(TagVo::from)\n                    .collect(Collectors.toList());\n                return new ListResult<>(result.getPage(), result.getSize(), result.getTotal(),\n                    tagVos);\n            });\n    }\n\n    int pageNullSafe(Integer page) {\n        return ObjectUtils.defaultIfNull(page, 1);\n    }\n\n    int sizeNullSafe(Integer size) {\n        return ObjectUtils.defaultIfNull(size, 10);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/ThemeFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.apache.commons.lang3.StringUtils;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.ThemeFinder;\nimport run.halo.app.theme.finders.vo.ThemeVo;\n\n/**\n * A default implementation for {@link ThemeFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Finder(\"themeFinder\")\npublic class ThemeFinderImpl implements ThemeFinder {\n\n    private final ReactiveExtensionClient client;\n    private final SystemConfigFetcher environmentFetcher;\n\n    public ThemeFinderImpl(ReactiveExtensionClient client,\n        SystemConfigFetcher environmentFetcher) {\n        this.client = client;\n        this.environmentFetcher = environmentFetcher;\n    }\n\n    @Override\n    public Mono<ThemeVo> activation() {\n        return environmentFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class)\n            .map(SystemSetting.Theme::getActive)\n            .flatMap(themeName -> client.fetch(Theme.class, themeName))\n            .flatMap(theme -> themeWithConfig(ThemeVo.from(theme)));\n    }\n\n    @Override\n    public Mono<ThemeVo> getByName(String themeName) {\n        return client.fetch(Theme.class, themeName)\n            .flatMap(theme -> themeWithConfig(ThemeVo.from(theme)));\n    }\n\n    private Mono<ThemeVo> themeWithConfig(ThemeVo themeVo) {\n        if (StringUtils.isBlank(themeVo.getSpec().getConfigMapName())) {\n            return Mono.just(themeVo);\n        }\n        return client.fetch(ConfigMap.class, themeVo.getSpec().getConfigMapName())\n            .map(configMap -> {\n                Map<String, JsonNode> config = new HashMap<>();\n                configMap.getData().forEach((k, v) -> {\n                    JsonNode jsonNode = JsonUtils.jsonToObject(v, JsonNode.class);\n                    config.put(k, jsonNode);\n                });\n                JsonNode configJson = JsonUtils.mapToObject(config, JsonNode.class);\n                return themeVo.withConfig(configJson);\n            })\n            .defaultIfEmpty(themeVo);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/impl/ThumbnailFinderImpl.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport java.net.URI;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.attachment.thumbnail.ThumbnailService;\nimport run.halo.app.theme.finders.Finder;\nimport run.halo.app.theme.finders.ThumbnailFinder;\n\n@Slf4j\n@Finder(\"thumbnail\")\n@RequiredArgsConstructor\npublic class ThumbnailFinderImpl implements ThumbnailFinder {\n\n    private final ThumbnailService thumbnailService;\n\n    @Override\n    public Mono<String> gen(String uriStr, String size) {\n        return Mono.fromCallable(() -> URI.create(uriStr))\n            .flatMap(uri -> thumbnailService.get(uri, ThumbnailSize.fromName(size)))\n            .map(URI::toASCIIString)\n            .onErrorComplete(IllegalArgumentException.class)\n            .defaultIfEmpty(uriStr);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/CategoryTreeVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport java.util.Objects;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A tree vo for {@link Category}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Builder\n@ToString\n@EqualsAndHashCode\npublic class CategoryTreeVo implements VisualizableTreeNode<CategoryTreeVo>, ExtensionVoOperator {\n\n    private MetadataOperator metadata;\n\n    private Category.CategorySpec spec;\n\n    private Category.CategoryStatus status;\n\n    private List<CategoryTreeVo> children;\n\n    private String parentName;\n\n    private Integer postCount;\n\n    /**\n     * Convert {@link CategoryVo} to {@link CategoryTreeVo}.\n     *\n     * @param category category value object\n     * @return category tree value object\n     */\n    public static CategoryTreeVo from(CategoryVo category) {\n        Assert.notNull(category, \"The category must not be null\");\n        return CategoryTreeVo.builder()\n            .metadata(category.getMetadata())\n            .spec(category.getSpec())\n            .status(category.getStatus())\n            .children(List.of())\n            .postCount(Objects.requireNonNullElse(category.getPostCount(), 0))\n            .build();\n    }\n\n    /**\n     * Convert {@link CategoryTreeVo} to {@link CategoryVo}.\n     */\n    public static CategoryVo toCategoryVo(CategoryTreeVo categoryTreeVo) {\n        Assert.notNull(categoryTreeVo, \"The category tree vo must not be null\");\n        return CategoryVo.builder()\n            .metadata(categoryTreeVo.getMetadata())\n            .spec(categoryTreeVo.getSpec())\n            .status(categoryTreeVo.getStatus())\n            .postCount(categoryTreeVo.getPostCount())\n            .build();\n    }\n\n    @Override\n    public String nodeText() {\n        return String.format(\"%s (%s)%s\", getSpec().getDisplayName(), getPostCount(),\n            spec.isPreventParentPostCascadeQuery() ? \" (Independent)\" : \"\");\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/CategoryVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Builder;\nimport lombok.EqualsAndHashCode;\nimport lombok.Value;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link Category}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@Builder\n@EqualsAndHashCode\npublic class CategoryVo implements ExtensionVoOperator {\n\n    MetadataOperator metadata;\n\n    Category.CategorySpec spec;\n\n    Category.CategoryStatus status;\n\n    Integer postCount;\n\n    /**\n     * Convert {@link Category} to {@link CategoryVo}.\n     *\n     * @param category category extension\n     * @return category value object\n     */\n    public static CategoryVo from(Category category) {\n        return CategoryVo.builder()\n            .metadata(category.getMetadata())\n            .spec(category.getSpec())\n            .status(category.getStatus())\n            .postCount(category.getStatusOrDefault().getVisiblePostCount())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/CommentStatsVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Builder;\nimport lombok.Value;\n\n/**\n * comment stats value object.\n *\n * @author LIlGG\n * @since 2.0.0\n */\n@Value\n@Builder\npublic class CommentStatsVo {\n    Integer upvote;\n\n    public static CommentStatsVo empty() {\n        return CommentStatsVo.builder()\n            .upvote(0)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\nimport run.halo.app.content.comment.OwnerInfo;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link Comment}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Accessors(chain = true)\n@EqualsAndHashCode\npublic class CommentVo implements ExtensionVoOperator {\n\n    @Schema(requiredMode = REQUIRED)\n    private MetadataOperator metadata;\n\n    @Schema(requiredMode = REQUIRED)\n    private Comment.CommentSpec spec;\n\n    private Comment.CommentStatus status;\n\n    @Schema(requiredMode = REQUIRED)\n    private OwnerInfo owner;\n\n    @Schema(requiredMode = REQUIRED)\n    private CommentStatsVo stats;\n\n    /**\n     * Convert {@link Comment} to {@link CommentVo}.\n     *\n     * @param comment comment extension\n     * @return a value object for {@link Comment}\n     */\n    public static CommentVo from(Comment comment) {\n        return new CommentVo()\n            .setMetadata(comment.getMetadata())\n            .setSpec(comment.getSpec())\n            .setStatus(comment.getStatus());\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/CommentWithReplyVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.experimental.Accessors;\nimport org.springframework.beans.BeanUtils;\nimport run.halo.app.extension.ListResult;\n\n/**\n * <p>A value object for comment with reply.</p>\n *\n * @author guqing\n * @since 2.14.0\n */\n@Data\n@Accessors(chain = true)\n@EqualsAndHashCode(callSuper = true)\npublic class CommentWithReplyVo extends CommentVo {\n\n    private ListResult<ReplyVo> replies;\n\n    /**\n     * Convert {@link CommentVo} to {@link CommentWithReplyVo}.\n     */\n    public static CommentWithReplyVo from(CommentVo commentVo) {\n        var commentWithReply = new CommentWithReplyVo();\n        BeanUtils.copyProperties(commentVo, commentWithReply);\n        commentWithReply.setReplies(ListResult.emptyResult());\n        return commentWithReply;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/ContentVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Builder;\nimport lombok.ToString;\nimport lombok.Value;\nimport run.halo.app.core.extension.content.Snapshot;\n\n/**\n * A value object for Content from {@link Snapshot}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@ToString\n@Builder\npublic class ContentVo {\n\n    String raw;\n\n    String content;\n\n    /**\n     * Empty content object.\n     */\n    public static ContentVo empty() {\n        return ContentVo.builder()\n            .raw(\"\")\n            .content(\"\")\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/ContributorVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Builder;\nimport lombok.ToString;\nimport lombok.Value;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link run.halo.app.core.extension.User}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@ToString\n@Builder\npublic class ContributorVo implements ExtensionVoOperator {\n\n    String name;\n\n    String displayName;\n\n    String avatar;\n\n    String bio;\n\n    String permalink;\n\n    MetadataOperator metadata;\n\n    /**\n     * Convert {@link User} to {@link ContributorVo}.\n     *\n     * @param user user extension\n     * @return contributor value object\n     */\n    public static ContributorVo from(User user) {\n        User.UserStatus status = user.getStatus();\n        String permalink = (status == null ? \"\" : status.getPermalink());\n        return builder().name(user.getMetadata().getName())\n            .displayName(user.getSpec().getDisplayName())\n            .avatar(user.getSpec().getAvatar())\n            .bio(user.getSpec().getBio())\n            .permalink(permalink)\n            .metadata(user.getMetadata())\n            .build();\n    }\n\n    /**\n     * Create a ghost contributor.\n     *\n     * @return a ghost contributor value object\n     */\n    public static ContributorVo ghost() {\n        var metadata = new Metadata();\n        metadata.setName(UserService.GHOST_USER_NAME);\n        return builder()\n            .name(\"ghost\")\n            .displayName(\"Ghost\")\n            // .avatar(\"/images/ghost.png\")\n            .bio(\"A ghost user.\")\n            .permalink(\"/authors/ghost\")\n            .metadata(metadata)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/ListedPostVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link Post}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@SuperBuilder\n@ToString\n@EqualsAndHashCode\npublic class ListedPostVo implements ExtensionVoOperator {\n\n    private MetadataOperator metadata;\n\n    private Post.PostSpec spec;\n\n    private Post.PostStatus status;\n\n    private List<CategoryVo> categories;\n\n    private List<TagVo> tags;\n\n    private List<ContributorVo> contributors;\n\n    private ContributorVo owner;\n\n    private StatsVo stats;\n\n    /**\n     * Convert {@link Post} to {@link ListedPostVo}.\n     *\n     * @param post post extension\n     * @return post value object\n     */\n    public static ListedPostVo from(Post post) {\n        Assert.notNull(post, \"The post must not be null.\");\n        Post.PostSpec spec = post.getSpec();\n        Post.PostStatus postStatus = post.getStatusOrDefault();\n        return ListedPostVo.builder()\n            .metadata(post.getMetadata())\n            .spec(spec)\n            .status(postStatus)\n            .categories(List.of())\n            .tags(List.of())\n            .contributors(List.of())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/ListedSinglePageVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link SinglePage}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@SuperBuilder\n@ToString\n@EqualsAndHashCode\npublic class ListedSinglePageVo implements ExtensionVoOperator {\n\n    private MetadataOperator metadata;\n\n    private SinglePage.SinglePageSpec spec;\n\n    private SinglePage.SinglePageStatus status;\n\n    private StatsVo stats;\n\n    private List<ContributorVo> contributors;\n\n    private ContributorVo owner;\n\n    /**\n     * Convert {@link SinglePage} to {@link ListedSinglePageVo}.\n     *\n     * @param singlePage single page extension\n     * @return special page value object\n     */\n    public static ListedSinglePageVo from(SinglePage singlePage) {\n        Assert.notNull(singlePage, \"The singlePage must not be null.\");\n        SinglePage.SinglePageSpec spec = singlePage.getSpec();\n        SinglePage.SinglePageStatus pageStatus = singlePage.getStatus();\n        return ListedSinglePageVo.builder()\n            .metadata(singlePage.getMetadata())\n            .spec(spec)\n            .status(pageStatus)\n            .contributors(List.of())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.ToString;\nimport org.apache.commons.lang3.StringUtils;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link MenuItem}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@ToString\n@Builder\npublic class MenuItemVo implements VisualizableTreeNode<MenuItemVo>, ExtensionVoOperator {\n\n    MetadataOperator metadata;\n\n    MenuItem.MenuItemSpec spec;\n\n    MenuItem.MenuItemStatus status;\n\n    List<MenuItemVo> children;\n\n    String parentName;\n\n    /**\n     * Gets menu item's display name.\n     */\n    public String getDisplayName() {\n        if (status != null && StringUtils.isNotBlank(status.getDisplayName())) {\n            return status.getDisplayName();\n        }\n        return spec.getDisplayName();\n    }\n\n    /**\n     * Convert {@link MenuItem} to {@link MenuItemVo}.\n     *\n     * @param menuItem menu item extension\n     * @return menu item value object\n     */\n    public static MenuItemVo from(MenuItem menuItem) {\n        MenuItem.MenuItemStatus status = menuItem.getStatus();\n        return MenuItemVo.builder()\n            .metadata(menuItem.getMetadata())\n            .spec(menuItem.getSpec())\n            .status(status)\n            .children(List.of())\n            .build();\n    }\n\n    @Override\n    public String nodeText() {\n        return getDisplayName();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.Iterator;\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.With;\nimport run.halo.app.core.extension.Menu;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link Menu}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@ToString\n@Builder\npublic class MenuVo implements ExtensionVoOperator {\n\n    MetadataOperator metadata;\n\n    Menu.Spec spec;\n\n    @With\n    List<MenuItemVo> menuItems;\n\n    /**\n     * Convert {@link Menu} to {@link MenuVo}.\n     *\n     * @param menu menu extension\n     * @return menu value object\n     */\n    public static MenuVo from(Menu menu) {\n        return builder()\n            .metadata(menu.getMetadata())\n            .spec(menu.getSpec())\n            .menuItems(List.of())\n            .build();\n    }\n\n    public void print(StringBuilder buffer) {\n        buffer.append(getSpec().getDisplayName());\n        buffer.append('\\n');\n        if (menuItems == null) {\n            return;\n        }\n        for (Iterator<MenuItemVo> it = menuItems.iterator(); it.hasNext(); ) {\n            MenuItemVo next = it.next();\n            if (it.hasNext()) {\n                next.print(buffer, \"├── \", \"│   \");\n            } else {\n                next.print(buffer, \"└── \", \"    \");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/NavigationPostVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Builder;\nimport org.jspecify.annotations.Nullable;\n\n/**\n * Post navigation vo to hold previous and next item.\n *\n * @param previous Previous post. It's publishing time is earlier than current post.\n * @param next Next post. It's publishing time is later than current post.\n * @author guqing\n * @author johnniang\n * @since 2.0.0\n */\n@Builder\npublic record NavigationPostVo(\n\n    @Schema(requiredMode = NOT_REQUIRED)\n    @Nullable\n    ListedPostVo previous,\n\n    @Schema(requiredMode = NOT_REQUIRED)\n    @Nullable\n    ListedPostVo next\n\n) {\n    /**\n     * Indicates whether it has next post.\n     *\n     * @return true if it has next post, false otherwise\n     */\n    public boolean hasNext() {\n        return next != null;\n    }\n\n    /**\n     * Indicates whether it has previous post.\n     *\n     * @return true if it has previous post, false otherwise\n     */\n    public boolean hasPrevious() {\n        return previous != null;\n    }\n\n    public static NavigationPostVo empty() {\n        return NavigationPostVo.builder().build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/PostArchiveVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Value;\n\n/**\n * Post archives by year and month.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@Builder\npublic class PostArchiveVo {\n\n    String year;\n\n    List<PostArchiveYearMonthVo> months;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/PostArchiveYearMonthVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Value;\n\n/**\n * Post archives by month.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@Builder\npublic class PostArchiveYearMonthVo {\n\n    String month;\n\n    List<ListedPostVo> posts;\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/PostVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.Post;\n\n/**\n * A value object for {@link Post}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@SuperBuilder\n@ToString\n@EqualsAndHashCode(callSuper = true)\npublic class PostVo extends ListedPostVo {\n\n    private ContentVo content;\n\n    /**\n     * Convert {@link Post} to {@link PostVo}.\n     *\n     * @param post post extension\n     * @return post value object\n     */\n    public static PostVo from(Post post) {\n        Assert.notNull(post, \"The post must not be null.\");\n        Post.PostSpec spec = post.getSpec();\n        Post.PostStatus postStatus = post.getStatusOrDefault();\n        return PostVo.builder()\n            .metadata(post.getMetadata())\n            .spec(spec)\n            .status(postStatus)\n            .categories(List.of())\n            .tags(List.of())\n            .contributors(List.of())\n            .content(new ContentVo(null, null))\n            .build();\n    }\n\n    /**\n     * Convert {@link Post} to {@link PostVo}.\n     */\n    public static PostVo from(ListedPostVo postVo) {\n        return builder()\n            .metadata(postVo.getMetadata())\n            .spec(postVo.getSpec())\n            .status(postVo.getStatus())\n            .categories(postVo.getCategories())\n            .tags(postVo.getTags())\n            .contributors(postVo.getContributors())\n            .owner(postVo.getOwner())\n            .stats(postVo.getStats())\n            .content(new ContentVo(\"\", \"\"))\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport run.halo.app.content.comment.OwnerInfo;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link Reply}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Builder\n@ToString\n@EqualsAndHashCode\npublic class ReplyVo implements ExtensionVoOperator {\n\n    @Schema(requiredMode = REQUIRED)\n    private MetadataOperator metadata;\n\n    @Schema(requiredMode = REQUIRED)\n    private Reply.ReplySpec spec;\n\n    @Schema(requiredMode = REQUIRED)\n    private OwnerInfo owner;\n\n    @Schema(requiredMode = REQUIRED)\n    private CommentStatsVo stats;\n\n    /**\n     * Convert {@link Reply} to {@link ReplyVo}.\n     *\n     * @param reply reply extension\n     * @return a value object for {@link Reply}\n     */\n    public static ReplyVo from(Reply reply) {\n        Reply.ReplySpec spec = reply.getSpec();\n        return ReplyVo.builder()\n            .metadata(reply.getMetadata())\n            .spec(spec)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/SinglePageVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.List;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\nimport lombok.experimental.SuperBuilder;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.SinglePage;\n\n/**\n * A value object for {@link SinglePage}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@SuperBuilder\n@ToString\n@EqualsAndHashCode(callSuper = true)\npublic class SinglePageVo extends ListedSinglePageVo {\n\n    private ContentVo content;\n\n    /**\n     * Convert {@link SinglePage} to {@link SinglePageVo}.\n     *\n     * @param singlePage single page extension\n     * @return special page value object\n     */\n    public static SinglePageVo from(SinglePage singlePage) {\n        Assert.notNull(singlePage, \"The singlePage must not be null.\");\n        SinglePage.SinglePageSpec spec = singlePage.getSpec();\n        SinglePage.SinglePageStatus pageStatus = singlePage.getStatus();\n        return SinglePageVo.builder()\n            .metadata(singlePage.getMetadata())\n            .spec(spec)\n            .status(pageStatus)\n            .contributors(List.of())\n            .content(new ContentVo(null, null))\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/SiteSettingVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.net.URL;\nimport java.util.Locale;\nimport java.util.Map;\nimport lombok.Builder;\nimport lombok.With;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.util.Assert;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemSetting.ThemeRouteRules;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Site setting value object for theme.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Builder\npublic record SiteSettingVo(\n\n    String title,\n\n    @With URL url,\n\n    @With String version,\n\n    String subtitle,\n\n    String logo,\n\n    String favicon,\n\n    String language,\n\n    Boolean allowRegistration,\n\n    PostSetting post,\n\n    SeoSetting seo,\n\n    Routes routes,\n\n    CommentSetting comment\n\n) {\n\n    /**\n     * Convert to system {@link ConfigMap} to {@link SiteSettingVo}.\n     *\n     * @param data config map data\n     * @return site setting value object\n     */\n    public static SiteSettingVo from(Map<String, String> data) {\n        Assert.notNull(data, \"Config data must not be null\");\n        SystemSetting.Basic basicSetting =\n            toObject(data.get(SystemSetting.Basic.GROUP), SystemSetting.Basic.class);\n\n        SystemSetting.User userSetting =\n            toObject(data.get(SystemSetting.User.GROUP), SystemSetting.User.class);\n\n        SystemSetting.Post postSetting =\n            toObject(data.get(SystemSetting.Post.GROUP), SystemSetting.Post.class);\n\n        SystemSetting.Seo seoSetting =\n            toObject(data.get(SystemSetting.Seo.GROUP), SystemSetting.Seo.class);\n\n        var routeRules = toObject(data.get(ThemeRouteRules.GROUP), ThemeRouteRules.class);\n\n        SystemSetting.Comment commentSetting = toObject(data.get(SystemSetting.Comment.GROUP),\n            SystemSetting.Comment.class);\n        return builder()\n            .title(basicSetting.getTitle())\n            .subtitle(basicSetting.getSubtitle())\n            .logo(basicSetting.getLogo())\n            .favicon(basicSetting.getFavicon())\n            .allowRegistration(userSetting.isAllowRegistration())\n            .language(basicSetting.useSystemLocale().orElse(Locale.getDefault()).toLanguageTag())\n            .post(PostSetting.builder()\n                .postPageSize(postSetting.getPostPageSize())\n                .archivePageSize(postSetting.getArchivePageSize())\n                .categoryPageSize(postSetting.getCategoryPageSize())\n                .tagPageSize(postSetting.getTagPageSize())\n                .authorPageSize(postSetting.getAuthorPageSize())\n                .build())\n            .seo(SeoSetting.builder()\n                .blockSpiders(seoSetting.getBlockSpiders())\n                .keywords(seoSetting.getKeywords())\n                .description(seoSetting.getDescription())\n                .build())\n            .comment(CommentSetting.builder()\n                .enable(commentSetting.getEnable())\n                .requireReviewForNew(commentSetting.getRequireReviewForNew())\n                .systemUserOnly(commentSetting.getSystemUserOnly())\n                .build())\n            .routes(Routes.builder()\n                .categoriesUri(StringUtils.prependIfMissing(routeRules.getCategories(), \"/\"))\n                .tagsUri(StringUtils.prependIfMissing(routeRules.getTags(), \"/\"))\n                .archivesUri(StringUtils.prependIfMissing(routeRules.getArchives(), \"/\"))\n                .build()\n            )\n            .build();\n    }\n\n    private static <T> T toObject(String json, Class<T> type) {\n        if (json == null) {\n            // empty object\n            json = \"{}\";\n        }\n        return JsonUtils.jsonToObject(json, type);\n    }\n\n    @Builder\n    public record PostSetting(\n        Integer postPageSize,\n        Integer archivePageSize,\n        Integer categoryPageSize,\n        Integer tagPageSize,\n        Integer authorPageSize\n    ) {\n    }\n\n    @Builder\n    public record SeoSetting(\n        Boolean blockSpiders,\n        String keywords,\n        String description\n    ) {\n    }\n\n    @Builder\n    public record CommentSetting(\n        Boolean enable,\n        Boolean systemUserOnly,\n        Boolean requireReviewForNew\n    ) {\n    }\n\n    @Builder\n    public record Routes(\n        String categoriesUri,\n        String tagsUri,\n        String archivesUri\n    ) {\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/SiteStatsVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n/**\n * A value object for site stats.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Data\n@Builder\npublic class SiteStatsVo {\n\n    private Integer visit;\n\n    private Integer upvote;\n\n    private Integer comment;\n\n    private Integer post;\n\n    private Integer category;\n\n    public static SiteStatsVo empty() {\n        return SiteStatsVo.builder()\n            .visit(0)\n            .upvote(0)\n            .comment(0)\n            .post(0)\n            .category(0)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/StatsVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Builder;\nimport lombok.Value;\n\n/**\n * Stats value object.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@Builder\npublic class StatsVo {\n\n    Integer visit;\n\n    Integer upvote;\n\n    Integer comment;\n\n    public static StatsVo empty() {\n        return StatsVo.builder()\n            .visit(0)\n            .upvote(0)\n            .comment(0)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/TagVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link Tag}.\n */\n@Value\n@Builder\npublic class TagVo implements ExtensionVoOperator {\n\n    MetadataOperator metadata;\n\n    Tag.TagSpec spec;\n\n    Tag.TagStatus status;\n\n    Integer postCount;\n\n    /**\n     * Convert {@link Tag} to {@link TagVo}.\n     *\n     * @param tag tag extension\n     * @return tag value object\n     */\n    public static TagVo from(Tag tag) {\n        Tag.TagSpec spec = tag.getSpec();\n        Tag.TagStatus status = tag.getStatusOrDefault();\n        return TagVo.builder()\n            .metadata(tag.getMetadata())\n            .spec(spec)\n            .status(status)\n            .postCount(defaultIfNull(status.getVisiblePostCount(), 0))\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/ThemeVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport lombok.Builder;\nimport lombok.ToString;\nimport lombok.Value;\nimport lombok.With;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.extension.MetadataOperator;\n\n/**\n * A value object for {@link Theme}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Value\n@Builder\n@ToString\npublic class ThemeVo implements ExtensionVoOperator {\n\n    MetadataOperator metadata;\n\n    Theme.ThemeSpec spec;\n\n    @With\n    JsonNode config;\n\n    /**\n     * Convert {@link Theme} to {@link ThemeVo}.\n     *\n     * @param theme theme extension\n     * @return theme value object\n     */\n    public static ThemeVo from(Theme theme) {\n        return ThemeVo.builder()\n            .metadata(theme.getMetadata())\n            .spec(theme.getSpec())\n            .config(null)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/UserVo.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport lombok.Builder;\nimport lombok.Value;\nimport org.apache.commons.lang3.ObjectUtils;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.extension.MetadataOperator;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@Value\n@Builder\npublic class UserVo implements ExtensionVoOperator {\n    MetadataOperator metadata;\n\n    User.UserSpec spec;\n\n    User.UserStatus status;\n\n    /**\n     * Converts to {@link UserVo} from {@link User}.\n     *\n     * @param user user extension\n     * @return user value object.\n     */\n    public static UserVo from(User user) {\n        User.UserStatus statusCopy =\n            JsonUtils.deepCopy(ObjectUtils.defaultIfNull(user.getStatus(), new User.UserStatus()));\n\n        User.UserSpec userSpecCopy = JsonUtils.deepCopy(user.getSpec());\n        userSpecCopy.setPassword(\"[PROTECTED]\");\n        return UserVo.builder()\n            .metadata(user.getMetadata())\n            .spec(userSpecCopy)\n            .status(statusCopy)\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/finders/vo/VisualizableTreeNode.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * Show Tree Hierarchy.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic interface VisualizableTreeNode<T extends VisualizableTreeNode<T>> {\n\n    /**\n     * Visualize tree node.\n     */\n    default void print(StringBuilder buffer, String prefix, String childrenPrefix) {\n        buffer.append(prefix);\n        buffer.append(nodeText());\n        buffer.append('\\n');\n        if (getChildren() == null) {\n            return;\n        }\n        for (Iterator<T> it = getChildren().iterator(); it.hasNext(); ) {\n            T next = it.next();\n            if (it.hasNext()) {\n                next.print(buffer, childrenPrefix + \"├── \", childrenPrefix + \"│   \");\n            } else {\n                next.print(buffer, childrenPrefix + \"└── \", childrenPrefix + \"    \");\n            }\n        }\n    }\n\n    String nodeText();\n\n    List<T> getChildren();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/message/ThemeMessageResolutionUtils.java",
    "content": "package run.halo.app.theme.message;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Properties;\nimport org.springframework.lang.Nullable;\nimport org.thymeleaf.exceptions.TemplateInputException;\nimport org.thymeleaf.exceptions.TemplateProcessingException;\nimport org.thymeleaf.util.StringUtils;\nimport run.halo.app.theme.ThemeContext;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class ThemeMessageResolutionUtils {\n\n    private static final Map<String, String> EMPTY_MESSAGES = Collections.emptyMap();\n    private static final String PROPERTIES_FILE_EXTENSION = \".properties\";\n    private static final String LOCATION = \"i18n\";\n\n    @Nullable\n    private static Reader messageReader(String messageResourceName, ThemeContext theme)\n        throws FileNotFoundException {\n        var themePath = theme.getPath();\n        File messageFile = themePath.resolve(messageResourceName).toFile();\n        if (!messageFile.exists()) {\n            return null;\n        }\n        final InputStream inputStream = new FileInputStream(messageFile);\n        return new BufferedReader(new InputStreamReader(new BufferedInputStream(inputStream)));\n    }\n\n    public static Map<String, String> resolveMessagesForTemplate(final Locale locale,\n        ThemeContext theme) {\n\n        // Compute all the resource names we should use: *_gl_ES-gheada.properties, *_gl_ES\n        // .properties, _gl.properties...\n        // The order here is important: as we will let values from more specific files\n        // overwrite those in less specific,\n        // (e.g. a value for gl_ES will have more precedence than a value for gl). So we will\n        // iterate these resource\n        // names from less specific to more specific.\n        final List<String>\n            messageResourceNames = computeMessageResourceNamesFromBase(locale);\n\n        // Build the combined messages\n        Map<String, String> combinedMessages = null;\n        for (final String messageResourceName : messageResourceNames) {\n            try {\n                final Reader messageResourceReader = messageReader(messageResourceName, theme);\n                if (messageResourceReader != null) {\n\n                    final Properties messageProperties =\n                        readMessagesResource(messageResourceReader);\n                    if (messageProperties != null && !messageProperties.isEmpty()) {\n\n                        if (combinedMessages == null) {\n                            combinedMessages = new HashMap<>(20);\n                        }\n\n                        for (final Map.Entry<Object, Object> propertyEntry :\n                            messageProperties.entrySet()) {\n                            combinedMessages.put((String) propertyEntry.getKey(),\n                                (String) propertyEntry.getValue());\n                        }\n\n                    }\n\n                }\n\n            } catch (final IOException ignored) {\n                // File might not exist, simply try the next one\n            }\n        }\n\n        if (combinedMessages == null) {\n            return EMPTY_MESSAGES;\n        }\n\n        return Collections.unmodifiableMap(combinedMessages);\n    }\n\n    private static List<String> computeMessageResourceNamesFromBase(final Locale locale) {\n\n        final List<String> resourceNames = new ArrayList<>(5);\n\n        if (StringUtils.isEmptyOrWhitespace(locale.getLanguage())) {\n            throw new TemplateProcessingException(\n                \"Locale \\\"\" + locale + \"\\\" \"\n                    + \"cannot be used as it does not specify a language.\");\n        }\n\n        resourceNames.add(getResourceName(\"default\"));\n        resourceNames.add(getResourceName(locale.getLanguage()));\n\n        if (!StringUtils.isEmptyOrWhitespace(locale.getCountry())) {\n            resourceNames.add(\n                getResourceName(locale.getLanguage() + \"_\" + locale.getCountry()));\n        }\n\n        if (!StringUtils.isEmptyOrWhitespace(locale.getVariant())) {\n            resourceNames.add(getResourceName(\n                locale.getLanguage() + \"_\" + locale.getCountry() + \"-\" + locale.getVariant()));\n        }\n\n        return resourceNames;\n\n    }\n\n    private static String getResourceName(String name) {\n        return LOCATION + \"/\" + name + PROPERTIES_FILE_EXTENSION;\n    }\n\n\n    private static Properties readMessagesResource(final Reader propertiesReader) {\n        if (propertiesReader == null) {\n            return null;\n        }\n        final Properties properties = new Properties();\n        try (propertiesReader) {\n            // Note Properties#load(Reader) this is JavaSE 6 specific, but Thymeleaf 3.0 does\n            // not support Java 5 anymore...\n            properties.load(propertiesReader);\n        } catch (final Exception e) {\n            throw new TemplateInputException(\"Exception loading messages file\", e);\n        }\n        // ignore errors closing\n        return properties;\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/message/ThemeMessageResolver.java",
    "content": "package run.halo.app.theme.message;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.thymeleaf.messageresolver.StandardMessageResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\nimport run.halo.app.theme.ThemeContext;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class ThemeMessageResolver extends StandardMessageResolver {\n\n    private final ThemeContext theme;\n\n    public ThemeMessageResolver(ThemeContext theme) {\n        this.theme = theme;\n    }\n\n    @Override\n    protected Map<String, String> resolveMessagesForTemplate(String template,\n        ITemplateResource templateResource,\n        Locale locale) {\n        var properties = new HashMap<String, String>();\n        Optional.ofNullable(ThemeMessageResolutionUtils.resolveMessagesForTemplate(locale, theme))\n            .ifPresent(properties::putAll);\n        Optional.ofNullable(super.resolveMessagesForTemplate(template, templateResource, locale))\n            .ifPresent(properties::putAll);\n        return Collections.unmodifiableMap(properties);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java",
    "content": "package run.halo.app.theme.router;\n\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.extension.index.query.Queries.or;\n\nimport java.security.Principal;\nimport java.util.Objects;\nimport java.util.function.Predicate;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.extension.router.selector.LabelSelector;\nimport run.halo.app.infra.AnonymousUserConst;\n\n/**\n * The default implementation of {@link ReactiveQueryPostPredicateResolver}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@Component\npublic class DefaultQueryPostPredicateResolver implements ReactiveQueryPostPredicateResolver {\n\n    @Override\n    public Mono<Predicate<Post>> getPredicate() {\n        Predicate<Post> predicate = post -> post.isPublished()\n            && !ExtensionUtil.isDeleted(post)\n            && Objects.equals(false, post.getSpec().getDeleted());\n        Predicate<Post> visiblePredicate =\n            post -> Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible());\n        return currentUserName()\n            .map(username -> predicate.and(\n                visiblePredicate.or(post -> username.equals(post.getSpec().getOwner())))\n            )\n            .defaultIfEmpty(predicate.and(visiblePredicate));\n    }\n\n    @Override\n    public Mono<ListOptions> getListOptions() {\n        var listOptions = new ListOptions();\n        listOptions.setLabelSelector(LabelSelector.builder()\n            .eq(Post.PUBLISHED_LABEL, \"true\").build());\n\n        var fieldQuery = and(\n            isNull(\"metadata.deletionTimestamp\"),\n            equal(\"spec.deleted\", \"false\")\n        );\n        var visibleQuery = equal(\"spec.visible\", Post.VisibleEnum.PUBLIC.name());\n        return currentUserName()\n            .map(username -> and(fieldQuery,\n                or(visibleQuery, equal(\"spec.owner\", username)))\n            )\n            .defaultIfEmpty(and(fieldQuery, visibleQuery))\n            .map(query -> {\n                listOptions.setFieldSelector(FieldSelector.of(query));\n                return listOptions;\n            });\n    }\n\n    Mono<String> currentUserName() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Principal::getName)\n            .filter(name -> !AnonymousUserConst.isAnonymousUser(name));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java",
    "content": "package run.halo.app.theme.router;\n\nimport static run.halo.app.theme.utils.PatternUtils.normalizePattern;\nimport static run.halo.app.theme.utils.PatternUtils.normalizePostPattern;\n\nimport java.util.Map;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.lang.NonNull;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.stereotype.Component;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.MetadataOperator;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.infra.SystemConfigChangedEvent;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemSetting.ThemeRouteRules;\n\n/**\n * {@link ExtensionPermalinkPatternUpdater} to update the value of key\n * {@link Constant#PERMALINK_PATTERN_ANNO} in {@link MetadataOperator#getAnnotations()}\n * of {@link Extension} when the pattern changed.\n *\n * @author guqing\n * @see Post\n * @see Category\n * @see Tag\n * @since 2.0.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\nclass ExtensionPermalinkPatternUpdater implements ApplicationListener<SystemConfigChangedEvent> {\n\n    private final ExtensionClient client;\n\n    @Override\n    @Async\n    public void onApplicationEvent(@NonNull SystemConfigChangedEvent event) {\n        var oldData = event.getOldData();\n        var newData = event.getNewData();\n        var oldRules = SystemSetting.get(oldData, ThemeRouteRules.GROUP, ThemeRouteRules.class);\n        if (oldRules == null) {\n            oldRules = ThemeRouteRules.empty();\n        }\n        var newRules = SystemSetting.get(newData, ThemeRouteRules.GROUP, ThemeRouteRules.class);\n        if (newRules == null) {\n            newRules = ThemeRouteRules.empty();\n        }\n        var archivesRuleChanged = !Objects.equals(oldRules.getArchives(), newRules.getArchives());\n        var postRuleChanged = !Objects.equals(oldRules.getPost(), newRules.getPost());\n        var categoriesRuleChanged =\n            !Objects.equals(oldRules.getCategories(), newRules.getCategories());\n        if (categoriesRuleChanged) {\n            var categoriesPattern = normalizePattern(newRules.getCategories());\n            updateCategoryPermalink(categoriesPattern);\n        }\n        var tagsRuleChanged = !Objects.equals(oldRules.getTags(), newRules.getTags());\n        if (tagsRuleChanged) {\n            var tagsPattern = normalizePattern(newRules.getTags());\n            log.info(\"Update tag permalink pattern for tags change: {}\", tagsPattern);\n            updateTagPermalink(tagsPattern);\n        }\n        if (archivesRuleChanged || categoriesRuleChanged || postRuleChanged) {\n            var postPattern = normalizePostPattern(newRules);\n            updatePostPermalink(postPattern);\n        }\n    }\n\n    private void updatePostPermalink(String pattern) {\n        log.debug(\"Update post permalink by new policy [{}]\", pattern);\n        // TODO Optimize by batch update\n        client.listAll(Post.class, new ListOptions(), Sort.unsorted())\n            .forEach(post -> updateIfPermalinkPatternChanged(post, pattern));\n    }\n\n    private void updateIfPermalinkPatternChanged(AbstractExtension extension, String pattern) {\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(extension);\n        String oldPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO);\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, pattern);\n\n        if (StringUtils.equals(oldPattern, pattern) && StringUtils.isNotBlank(oldPattern)) {\n            return;\n        }\n        // update permalink pattern annotation\n        client.update(extension);\n    }\n\n    private void updateCategoryPermalink(String pattern) {\n        log.debug(\"Update category and categories permalink by new policy [{}]\", pattern);\n        client.listAll(Category.class, new ListOptions(), Sort.unsorted())\n            .forEach(category -> updateIfPermalinkPatternChanged(category, pattern));\n    }\n\n    private void updateTagPermalink(String pattern) {\n        log.debug(\"Update tag and tags permalink by new policy [{}]\", pattern);\n        client.listAll(Tag.class, new ListOptions(), Sort.unsorted())\n            .forEach(tag -> updateIfPermalinkPatternChanged(tag, pattern));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/ModelMapUtils.java",
    "content": "package run.halo.app.theme.router;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.dialect.CommentWidget;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * A util class for building model map.\n *\n * @author guqing\n * @since 2.6.0\n */\npublic abstract class ModelMapUtils {\n    private static final Scheme POST_SCHEME = Scheme.buildFromType(Post.class);\n    private static final Scheme SINGLE_PAGE_SCHEME = Scheme.buildFromType(SinglePage.class);\n\n    /**\n     * Build post view model.\n     *\n     * @param postVo post vo\n     * @return model map\n     */\n    public static Map<String, Object> postModel(PostVo postVo) {\n        Map<String, Object> model = new HashMap<>();\n        model.put(\"name\", postVo.getMetadata().getName());\n        model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());\n        model.put(\"groupVersionKind\", POST_SCHEME.groupVersionKind());\n        model.put(\"plural\", POST_SCHEME.plural());\n        model.put(\"post\", postVo);\n        model.put(CommentWidget.ENABLE_COMMENT_ATTRIBUTE, postVo.getSpec().getAllowComment());\n        return model;\n    }\n\n    /**\n     * Build single page view model.\n     *\n     * @param pageVo page vo\n     * @return model map\n     */\n    public static Map<String, Object> singlePageModel(SinglePageVo pageVo) {\n        Map<String, Object> model = new HashMap<>();\n        model.put(\"name\", pageVo.getMetadata().getName());\n        model.put(\"groupVersionKind\", SINGLE_PAGE_SCHEME.groupVersionKind());\n        model.put(\"plural\", SINGLE_PAGE_SCHEME.plural());\n        model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());\n        model.put(\"singlePage\", pageVo);\n        model.put(CommentWidget.ENABLE_COMMENT_ATTRIBUTE, pageVo.getSpec().getAllowComment());\n        return model;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/PermalinkRuleChangedEvent.java",
    "content": "package run.halo.app.theme.router;\n\nimport org.springframework.context.ApplicationEvent;\nimport run.halo.app.theme.DefaultTemplateEnum;\n\npublic class PermalinkRuleChangedEvent extends ApplicationEvent {\n    private final DefaultTemplateEnum template;\n    private final String oldRule;\n    private final String rule;\n\n    public PermalinkRuleChangedEvent(Object source, DefaultTemplateEnum template,\n        String oldRule, String rule) {\n        super(source);\n        this.template = template;\n        this.oldRule = oldRule;\n        this.rule = rule;\n    }\n\n    public DefaultTemplateEnum getTemplate() {\n        return template;\n    }\n\n    public String getOldRule() {\n        return oldRule;\n    }\n\n    public String getRule() {\n        return rule;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/PreviewRouterFunction.java",
    "content": "package run.halo.app.theme.router;\n\nimport java.security.Principal;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.security.core.context.ReactiveSecurityContextHolder;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.dialect.HaloTrackerProcessor;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.SinglePageConversionService;\nimport run.halo.app.theme.finders.vo.ContributorVo;\nimport run.halo.app.theme.finders.vo.PostVo;\n\n/**\n * <p>Preview router for previewing posts and single pages.</p>\n *\n * @author guqing\n * @since 2.6.0\n */\n@Component\n@RequiredArgsConstructor\npublic class PreviewRouterFunction {\n    static final String SNAPSHOT_NAME_PARAM = \"snapshotName\";\n\n    private final ReactiveExtensionClient client;\n\n    private final PostPublicQueryService postPublicQueryService;\n\n    private final ViewNameResolver viewNameResolver;\n\n    private final PostService postService;\n\n    private final SinglePageConversionService singlePageConversionService;\n\n    @Bean\n    RouterFunction<ServerResponse> previewRouter() {\n        return RouterFunctions.route()\n            .GET(\"/preview/posts/{name}\", this::previewPost)\n            .GET(\"/preview/singlepages/{name}\", this::previewSinglePage)\n            .build();\n    }\n\n    private Mono<ServerResponse> previewPost(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return currentAuthenticatedUserName()\n            .flatMap(principal -> client.fetch(Post.class, name))\n            .flatMap(post -> {\n                String snapshotName = request.queryParam(SNAPSHOT_NAME_PARAM)\n                    .orElse(post.getSpec().getHeadSnapshot());\n                return convertToPostVo(post, snapshotName);\n            })\n            .flatMap(post -> canPreview(post.getContributors())\n                .doOnNext(canPreview -> {\n                    if (!canPreview) {\n                        throw new NotFoundException(\"Post not found.\");\n                    }\n                })\n                .thenReturn(post)\n            )\n            // Check permissions before throwing this exception\n            .switchIfEmpty(Mono.error(() -> new NotFoundException(\"Post not found.\")))\n            .flatMap(postVo -> {\n                String template = postVo.getSpec().getTemplate();\n                Map<String, Object> model = ModelMapUtils.postModel(postVo);\n                // Mark as preview mode for downstream view processing\n                request.exchange().getAttributes()\n                    .put(HaloTrackerProcessor.SKIP_TRACKER, Boolean.TRUE);\n                return viewNameResolver.resolveViewNameOrDefault(request, template,\n                        DefaultTemplateEnum.POST.getValue())\n                    .flatMap(templateName -> ServerResponse.ok().render(templateName, model));\n            });\n    }\n\n    private Mono<PostVo> convertToPostVo(Post post, String snapshotName) {\n        return postPublicQueryService.convertToVo(post, snapshotName)\n            .doOnNext(postVo -> {\n                // fake some attributes only for preview when they are not published\n                Post.PostSpec spec = postVo.getSpec();\n                if (spec.getPublishTime() == null) {\n                    spec.setPublishTime(Instant.now());\n                }\n                if (spec.getPublish() == null) {\n                    spec.setPublish(false);\n                }\n                Post.PostStatus status = postVo.getStatus();\n                if (status == null) {\n                    status = new Post.PostStatus();\n                    postVo.setStatus(status);\n                }\n                if (status.getLastModifyTime() == null) {\n                    status.setLastModifyTime(Instant.now());\n                }\n            });\n    }\n\n    private Mono<ServerResponse> previewSinglePage(ServerRequest request) {\n        final var name = request.pathVariable(\"name\");\n        return currentAuthenticatedUserName()\n            .flatMap(principal -> client.fetch(SinglePage.class, name))\n            .flatMap(singlePage -> {\n                String snapshotName = request.queryParam(SNAPSHOT_NAME_PARAM)\n                    .orElse(singlePage.getSpec().getHeadSnapshot());\n                return singlePageConversionService.convertToVo(singlePage, snapshotName);\n            })\n            .doOnNext(pageVo -> {\n                // fake some attributes only for preview when they are not published\n                SinglePage.SinglePageSpec spec = pageVo.getSpec();\n                if (spec.getPublishTime() == null) {\n                    spec.setPublishTime(Instant.now());\n                }\n                if (spec.getPublish() == null) {\n                    spec.setPublish(false);\n                }\n                SinglePage.SinglePageStatus status = pageVo.getStatus();\n                if (status == null) {\n                    status = new SinglePage.SinglePageStatus();\n                    pageVo.setStatus(status);\n                }\n                if (status.getLastModifyTime() == null) {\n                    status.setLastModifyTime(Instant.now());\n                }\n            })\n            .flatMap(singlePageVo -> canPreview(singlePageVo.getContributors())\n                .doOnNext(canPreview -> {\n                    if (!canPreview) {\n                        throw new NotFoundException(\"Single page not found.\");\n                    }\n                })\n                .thenReturn(singlePageVo)\n            )\n            // Check permissions before throwing this exception\n            .switchIfEmpty(Mono.error(() -> new NotFoundException(\"Single page not found.\")))\n            .flatMap(singlePageVo -> {\n                Map<String, Object> model = ModelMapUtils.singlePageModel(singlePageVo);\n                // Mark as preview mode for downstream view processing\n                request.exchange().getAttributes()\n                    .put(HaloTrackerProcessor.SKIP_TRACKER, Boolean.TRUE);\n                String template = singlePageVo.getSpec().getTemplate();\n                return viewNameResolver.resolveViewNameOrDefault(request, template,\n                        DefaultTemplateEnum.SINGLE_PAGE.getValue())\n                    .flatMap(viewName -> ServerResponse.ok().render(viewName, model));\n            });\n    }\n\n    private Mono<Boolean> canPreview(List<ContributorVo> contributors) {\n        Assert.notNull(contributors, \"The contributors must not be null\");\n        Set<String> contributorNames = contributors.stream()\n            .map(ContributorVo::getName)\n            .collect(Collectors.toSet());\n        return currentAuthenticatedUserName()\n            .map(contributorNames::contains)\n            .defaultIfEmpty(false);\n    }\n\n    Mono<String> currentAuthenticatedUserName() {\n        return ReactiveSecurityContextHolder.getContext()\n            .map(SecurityContext::getAuthentication)\n            .map(Principal::getName)\n            .filter(name -> !AnonymousUserConst.isAnonymousUser(name));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java",
    "content": "package run.halo.app.theme.router;\n\nimport java.util.function.Predicate;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\n\n/**\n * The reactive query post predicate resolver.\n *\n * @author guqing\n * @since 2.9.0\n */\npublic interface ReactiveQueryPostPredicateResolver {\n\n    Mono<Predicate<Post>> getPredicate();\n\n    Mono<ListOptions> getListOptions();\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/SinglePageRoute.java",
    "content": "package run.halo.app.theme.router;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.methods;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport org.apache.commons.lang3.BooleanUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RequestPredicate;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport org.springframework.web.util.UriUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionOperator;\nimport run.halo.app.extension.controller.Controller;\nimport run.halo.app.extension.controller.ControllerBuilder;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.finders.SinglePageFinder;\n\n/**\n * The {@link SinglePageRoute} for route request to specific template <code>page.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@RequiredArgsConstructor\npublic class SinglePageRoute\n    implements RouterFunction<ServerResponse>, Reconciler<Reconciler.Request>, DisposableBean {\n    private Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap =\n        new ConcurrentHashMap<>();\n\n    private final ExtensionClient client;\n\n    private final SinglePageFinder singlePageFinder;\n\n    private final ViewNameResolver viewNameResolver;\n\n    private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    private final LocaleContextResolver localeContextResolver;\n\n    @Override\n    @NonNull\n    public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {\n        return Flux.fromIterable(routerFunctions())\n            .concatMap(routerFunction -> routerFunction.route(request))\n            .next();\n    }\n\n    /**\n     * Set quickRouteMap. This method is only for testing.\n     *\n     * @param quickRouteMap fresh quickRouteMap.\n     */\n    void setQuickRouteMap(Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap) {\n        this.quickRouteMap = quickRouteMap;\n    }\n\n    @Override\n    public void accept(@NonNull RouterFunctions.Visitor visitor) {\n        routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor));\n    }\n\n    private List<RouterFunction<ServerResponse>> routerFunctions() {\n        return quickRouteMap.keySet().stream()\n            .map(nameSlugPair -> {\n                var routePath = singlePageRoute(nameSlugPair.slug());\n                return RouterFunctions.route(methods(HttpMethod.GET)\n                        .and(exactPath(routePath))\n                        .and(RequestPredicates.accept(MediaType.TEXT_HTML)),\n                    handlerFunction(nameSlugPair.name()));\n            })\n            .collect(Collectors.toList());\n    }\n\n    private RequestPredicate exactPath(String path) {\n        return request -> {\n            var encodedRoutePath = UriUtils.encodePath(path, StandardCharsets.UTF_8);\n            var requestPath = request.requestPath().pathWithinApplication().value();\n            return Objects.equals(requestPath, encodedRoutePath);\n        };\n    }\n\n    @Override\n    public Result reconcile(Request request) {\n        client.fetch(SinglePage.class, request.name())\n            .ifPresent(page -> {\n                var nameSlugPair = NameSlugPair.from(page);\n                if (ExtensionOperator.isDeleted(page)) {\n                    quickRouteMap.remove(nameSlugPair);\n                    return;\n                }\n                if (BooleanUtils.isTrue(page.getSpec().getDeleted())) {\n                    quickRouteMap.remove(nameSlugPair);\n                } else {\n                    // put new one\n                    if (page.isPublished()) {\n                        quickRouteMap.put(nameSlugPair, handlerFunction(request.name()));\n                    } else {\n                        quickRouteMap.remove(nameSlugPair);\n                    }\n                }\n            });\n        return new Result(false, null);\n    }\n\n    @Override\n    public Controller setupWith(ControllerBuilder builder) {\n        return builder\n            .extension(new SinglePage())\n            .build();\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        quickRouteMap.clear();\n    }\n\n    record NameSlugPair(String name, String slug) {\n        public static NameSlugPair from(SinglePage page) {\n            return new NameSlugPair(page.getMetadata().getName(), page.getSpec().getSlug());\n        }\n    }\n\n    String singlePageRoute(String slug) {\n        return StringUtils.prependIfMissing(slug, \"/\");\n    }\n\n    HandlerFunction<ServerResponse> handlerFunction(String name) {\n        return request -> singlePageFinder.getByName(name)\n            .doOnNext(singlePageVo -> {\n                titleVisibilityIdentifyCalculator.calculateTitle(\n                    singlePageVo.getSpec().getTitle(),\n                    singlePageVo.getSpec().getVisible(),\n                    localeContextResolver.resolveLocaleContext(request.exchange())\n                        .getLocale()\n                );\n            })\n            .flatMap(singlePageVo -> {\n                Map<String, Object> model = ModelMapUtils.singlePageModel(singlePageVo);\n                String template = singlePageVo.getSpec().getTemplate();\n                return viewNameResolver.resolveViewNameOrDefault(request, template,\n                        DefaultTemplateEnum.SINGLE_PAGE.getValue())\n                    .flatMap(viewName -> ServerResponse.ok().render(viewName, model));\n            })\n            .switchIfEmpty(\n                Mono.error(new NotFoundException(\"Single page not found\"))\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/ThemeCompositeRouterFunction.java",
    "content": "package run.halo.app.theme.router;\n\nimport static run.halo.app.theme.utils.PatternUtils.normalizePattern;\nimport static run.halo.app.theme.utils.PatternUtils.normalizePostPattern;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.context.SmartLifecycle;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigChangedEvent;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemSetting.ThemeRouteRules;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.router.factories.ArchiveRouteFactory;\nimport run.halo.app.theme.router.factories.AuthorPostsRouteFactory;\nimport run.halo.app.theme.router.factories.CategoriesRouteFactory;\nimport run.halo.app.theme.router.factories.CategoryPostRouteFactory;\nimport run.halo.app.theme.router.factories.IndexRouteFactory;\nimport run.halo.app.theme.router.factories.PostRouteFactory;\nimport run.halo.app.theme.router.factories.TagPostRouteFactory;\nimport run.halo.app.theme.router.factories.TagsRouteFactory;\n\n/**\n * <p>The combination router of theme templates is used to render theme templates, but does not\n * include <code>page.html</code> templates which is processed separately.</p>\n *\n * @author guqing\n * @see SinglePageRoute\n * @since 2.0.0\n */\n@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class ThemeCompositeRouterFunction implements\n    RouterFunction<ServerResponse>,\n    SmartLifecycle,\n    ApplicationListener<SystemConfigChangedEvent> {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private final SystemConfigFetcher environmentFetcher;\n\n    private final ArchiveRouteFactory archiveRouteFactory;\n    private final PostRouteFactory postRouteFactory;\n    private final CategoriesRouteFactory categoriesRouteFactory;\n    private final CategoryPostRouteFactory categoryPostRouteFactory;\n    private final TagPostRouteFactory tagPostRouteFactory;\n    private final TagsRouteFactory tagsRouteFactory;\n    private final AuthorPostsRouteFactory authorPostsRouteFactory;\n    private final IndexRouteFactory indexRouteFactory;\n\n    private List<RouterFunction<ServerResponse>> cachedRouters = List.of();\n    private volatile boolean running;\n\n    @Override\n    public void onApplicationEvent(SystemConfigChangedEvent event) {\n        var oldData = event.getOldData();\n        var newData = event.getNewData();\n        var oldRules = SystemSetting.get(oldData, ThemeRouteRules.GROUP,\n            ThemeRouteRules.class);\n        if (oldRules == null) {\n            oldRules = ThemeRouteRules.empty();\n        }\n        var newRules = SystemSetting.get(newData, ThemeRouteRules.GROUP,\n            ThemeRouteRules.class);\n        if (newRules == null) {\n            newRules = ThemeRouteRules.empty();\n        }\n        boolean rulesChanged = !Objects.equals(oldRules, newRules);\n        if (rulesChanged) {\n            log.info(\"Theme route rules changed, updating router functions...\");\n            if (log.isDebugEnabled()) {\n                log.debug(\"Old theme route rules: {}\", oldRules);\n                log.debug(\"New theme route rules: {}\", newRules);\n            }\n            this.cachedRouters = routerFunctions(newRules);\n            log.info(\"Theme route rules updated.\");\n        }\n    }\n\n    @Override\n    @NonNull\n    public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {\n        return Flux.fromIterable(cachedRouters)\n            .concatMap(routerFunction -> routerFunction.route(request))\n            .next();\n    }\n\n    @Override\n    public void accept(@NonNull RouterFunctions.Visitor visitor) {\n        cachedRouters.forEach(routerFunction -> routerFunction.accept(visitor));\n    }\n\n    private List<RouterFunction<ServerResponse>> routerFunctions(ThemeRouteRules rules) {\n        return transformedPatterns(rules).stream()\n            .map(this::createRouterFunction)\n            .toList();\n    }\n\n    private List<RouterFunction<ServerResponse>> routerFunctions() {\n        return transformedPatterns().stream()\n            .map(this::createRouterFunction)\n            .toList();\n    }\n\n    private RouterFunction<ServerResponse> createRouterFunction(RoutePattern routePattern) {\n        return switch (routePattern.identifier()) {\n            case POST -> postRouteFactory.create(routePattern.pattern());\n            case ARCHIVES -> archiveRouteFactory.create(routePattern.pattern());\n            case CATEGORIES -> categoriesRouteFactory.create(routePattern.pattern());\n            case CATEGORY -> categoryPostRouteFactory.create(routePattern.pattern());\n            case TAGS -> tagsRouteFactory.create(routePattern.pattern());\n            case TAG -> tagPostRouteFactory.create(routePattern.pattern());\n            case AUTHOR -> authorPostsRouteFactory.create(routePattern.pattern());\n            case INDEX -> indexRouteFactory.create(routePattern.pattern());\n            default ->\n                throw new IllegalStateException(\"Unexpected value: \" + routePattern.identifier());\n        };\n    }\n\n    @Override\n    public void start() {\n        if (running) {\n            return;\n        }\n        running = true;\n        this.cachedRouters = routerFunctions();\n    }\n\n    @Override\n    public void stop() {\n        if (!running) {\n            return;\n        }\n        running = false;\n        this.cachedRouters = List.of();\n    }\n\n    @Override\n    public boolean isRunning() {\n        return running;\n    }\n\n    record RoutePattern(DefaultTemplateEnum identifier, String pattern) {\n    }\n\n    private List<RoutePattern> transformedPatterns(ThemeRouteRules rules) {\n        List<RoutePattern> routePatterns = new ArrayList<>();\n        var archives = normalizePattern(rules.getArchives());\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.ARCHIVES, archives));\n\n        var categories = normalizePattern(rules.getCategories());\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.CATEGORIES, categories));\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.CATEGORY, categories));\n\n        var tags = normalizePattern(rules.getTags());\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.TAGS, tags));\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.TAG, tags));\n\n        var post = normalizePostPattern(rules);\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.POST, post));\n\n        // Add the index route to the end to prevent conflict with the queryParam rule of the post\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.AUTHOR, \"\"));\n        routePatterns.add(new RoutePattern(DefaultTemplateEnum.INDEX, \"/\"));\n        return routePatterns;\n\n    }\n\n    private List<RoutePattern> transformedPatterns() {\n        var rules = environmentFetcher.fetch(ThemeRouteRules.GROUP, ThemeRouteRules.class)\n            .blockOptional(BLOCKING_TIMEOUT)\n            .orElseGet(ThemeRouteRules::empty);\n        return transformedPatterns(rules);\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/TitleVisibilityIdentifyCalculator.java",
    "content": "package run.halo.app.theme.router;\n\nimport java.util.Locale;\nimport lombok.AllArgsConstructor;\nimport org.springframework.context.MessageSource;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport run.halo.app.core.extension.content.Post;\n\n@Component\n@AllArgsConstructor\npublic class TitleVisibilityIdentifyCalculator {\n\n    private final MessageSource messageSource;\n\n    /**\n     * Calculate title with visibility identification.\n     *\n     * @param title title must not be null\n     * @param visibleEnum visibility enum\n     */\n    public String calculateTitle(String title, Post.VisibleEnum visibleEnum, Locale locale) {\n        Assert.notNull(title, \"Title must not be null\");\n        if (Post.VisibleEnum.PRIVATE.equals(visibleEnum)) {\n            String identify = messageSource.getMessage(\n                \"title.visibility.identification.private\",\n                null,\n                \"\",\n                locale);\n            return title + identify;\n        }\n        return title;\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static run.halo.app.theme.router.PageUrlUtils.totalPage;\n\nimport java.util.List;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RequestPredicate;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.vo.PostArchiveVo;\nimport run.halo.app.theme.router.ModelConst;\nimport run.halo.app.theme.router.PageUrlUtils;\nimport run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;\nimport run.halo.app.theme.router.UrlContextListResult;\n\n/**\n * The {@link ArchiveRouteFactory} for generate {@link RouterFunction} specific to the template\n * <code>posts.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class ArchiveRouteFactory implements RouteFactory {\n\n    private final PostFinder postFinder;\n\n    private final SystemConfigFetcher environmentFetcher;\n\n    private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    private final LocaleContextResolver localeContextResolver;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String prefix) {\n        RequestPredicate requestPredicate = patterns(prefix).stream()\n            .map(RequestPredicates::GET)\n            .reduce(req -> false, RequestPredicate::or)\n            .and(accept(MediaType.TEXT_HTML));\n        return RouterFunctions.route(requestPredicate, handlerFunction());\n    }\n\n    HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> {\n            String templateName = DefaultTemplateEnum.ARCHIVES.getValue();\n            return ServerResponse.ok()\n                .render(templateName,\n                    Map.of(\"archives\", archivePosts(request),\n                        ModelConst.TEMPLATE_ID, templateName)\n                );\n        };\n    }\n\n    private List<String> patterns(String prefix) {\n        return List.of(\n            StringUtils.prependIfMissing(prefix, \"/\"),\n            PathUtils.combinePath(prefix, \"/page/{page:\\\\d+}\"),\n            PathUtils.combinePath(prefix, \"/{year:\\\\d{4}}\"),\n            PathUtils.combinePath(prefix, \"/{year:\\\\d{4}}/page/{page:\\\\d+}\"),\n            PathUtils.combinePath(prefix, \"/{year:\\\\d{4}}/{month:\\\\d{2}}\"),\n            PathUtils.combinePath(prefix,\n                \"/{year:\\\\d{4}}/{month:\\\\d{2}}/page/{page:\\\\d+}\")\n        );\n    }\n\n    private Mono<UrlContextListResult<PostArchiveVo>> archivePosts(ServerRequest request) {\n        ArchivePathVariables variables = ArchivePathVariables.from(request);\n        int pageNum = pageNumInPathVariable(request);\n        String requestPath = request.path();\n        return configuredPageSize(environmentFetcher, SystemSetting.Post::getArchivePageSize)\n            .flatMap(pageSize -> postFinder.archives(pageNum, pageSize, variables.getYear(),\n                variables.getMonth()))\n            .doOnNext(list -> list.get()\n                .map(PostArchiveVo::getMonths)\n                .flatMap(List::stream)\n                .flatMap(month -> month.getPosts().stream())\n                .forEach(postVo -> postVo.getSpec()\n                    .setTitle(titleVisibilityIdentifyCalculator.calculateTitle(\n                        postVo.getSpec().getTitle(),\n                        postVo.getSpec().getVisible(),\n                        localeContextResolver.resolveLocaleContext(request.exchange())\n                            .getLocale())\n                    )\n                )\n            )\n            .map(list -> new UrlContextListResult.Builder<PostArchiveVo>()\n                .listResult(list)\n                .nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list)))\n                .prevUrl(PageUrlUtils.prevPageUrl(requestPath))\n                .build());\n    }\n\n    @Data\n    static class ArchivePathVariables {\n        String year;\n        String month;\n        String page;\n\n        static ArchivePathVariables from(ServerRequest request) {\n            Map<String, String> variables = request.pathVariables();\n            return JsonUtils.mapToObject(variables, ArchivePathVariables.class);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static run.halo.app.theme.router.PageUrlUtils.totalPage;\n\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.AllArgsConstructor;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.security.authorization.AuthorityUtils;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.UserVo;\nimport run.halo.app.theme.router.ModelConst;\nimport run.halo.app.theme.router.PageUrlUtils;\nimport run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;\nimport run.halo.app.theme.router.UrlContextListResult;\n\n/**\n * The {@link AuthorPostsRouteFactory} for generate {@link RouterFunction} specific to the template\n * <code>index.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class AuthorPostsRouteFactory implements RouteFactory {\n\n    private final PostFinder postFinder;\n    private final ReactiveExtensionClient client;\n    private final RoleService roleService;\n    private SystemConfigFetcher environmentFetcher;\n\n    private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    private final LocaleContextResolver localeContextResolver;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String pattern) {\n        return RouterFunctions\n            .route(GET(\"/authors/{name}\").or(GET(\"/authors/{name}/page/{page:\\\\d+}\"))\n                .and(accept(MediaType.TEXT_HTML)), handlerFunction());\n    }\n\n    HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> {\n            String name = request.pathVariable(\"name\");\n            return hasPostManageRole(name)\n                .flatMap(hasPostManageRole -> {\n                    if (hasPostManageRole) {\n                        return ServerResponse.ok()\n                            .render(DefaultTemplateEnum.AUTHOR.getValue(),\n                                Map.of(\"author\", getByName(name),\n                                    \"posts\", postList(request, name),\n                                    ModelConst.TEMPLATE_ID, DefaultTemplateEnum.AUTHOR.getValue()\n                                )\n                            );\n                    }\n                    return Mono.error(new NotFoundException(\"Author page not found.\"));\n                });\n        };\n    }\n\n    protected Mono<Boolean> hasPostManageRole(String username) {\n        return roleService.getRolesByUsername(username)\n            .collectList()\n            .flatMap(roles -> roleService.contains(roles,\n                Set.of(AuthorityUtils.POST_CONTRIBUTOR_ROLE_NAME))\n            )\n            .defaultIfEmpty(false);\n    }\n\n    private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request, String name) {\n        String path = request.path();\n        int pageNum = pageNumInPathVariable(request);\n        return configuredPageSize(environmentFetcher, SystemSetting.Post::getAuthorPageSize)\n            .flatMap(pageSize -> postFinder.listByOwner(pageNum, pageSize, name))\n            .doOnNext(list -> {\n                list.getItems().forEach(listedPostVo -> {\n                    listedPostVo.getSpec().setTitle(\n                        titleVisibilityIdentifyCalculator.calculateTitle(\n                            listedPostVo.getSpec().getTitle(),\n                            listedPostVo.getSpec().getVisible(),\n                            localeContextResolver.resolveLocaleContext(request.exchange())\n                                .getLocale())\n                    );\n                });\n            })\n            .map(list -> new UrlContextListResult.Builder<ListedPostVo>()\n                .listResult(list)\n                .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))\n                .prevUrl(PageUrlUtils.prevPageUrl(path))\n                .build());\n    }\n\n    private Mono<UserVo> getByName(String name) {\n        return client.fetch(User.class, name)\n            .switchIfEmpty(Mono.error(() -> new NotFoundException(\"Author page not found.\")))\n            .map(UserVo::from);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/CategoriesRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\n\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.CategoryFinder;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * The {@link CategoriesRouteFactory} for generate {@link RouterFunction} specific to the\n * template\n * <code>categories.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class CategoriesRouteFactory implements RouteFactory {\n\n    private final CategoryFinder categoryFinder;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String prefix) {\n        return RouterFunctions.route(GET(StringUtils.prependIfMissing(prefix, \"/\")),\n            handlerFunction());\n    }\n\n    HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> ServerResponse.ok()\n            .render(DefaultTemplateEnum.CATEGORIES.getValue(),\n                Map.of(\"categories\", categoryFinder.listAsTree(),\n                    ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORIES.getValue()));\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.theme.router.PageUrlUtils.totalPage;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.theme.Constant;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.vo.CategoryVo;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.router.ModelConst;\nimport run.halo.app.theme.router.PageUrlUtils;\nimport run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;\nimport run.halo.app.theme.router.UrlContextListResult;\n\n/**\n * The {@link CategoryPostRouteFactory} for generate {@link RouterFunction} specific to the template\n * <code>category.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class CategoryPostRouteFactory implements RouteFactory {\n\n    private final PostFinder postFinder;\n\n    private final SystemConfigFetcher environmentFetcher;\n    private final ReactiveExtensionClient client;\n    private final ViewNameResolver viewNameResolver;\n\n    private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    private final LocaleContextResolver localeContextResolver;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String prefix) {\n        return RouterFunctions.route(GET(PathUtils.combinePath(prefix, \"/{slug}\"))\n            .or(GET(PathUtils.combinePath(prefix, \"/{slug}/page/{page:\\\\d+}\")))\n            .and(accept(MediaType.TEXT_HTML)), handlerFunction());\n    }\n\n    HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> {\n            String slug = request.pathVariable(\"slug\");\n            return fetchBySlug(slug)\n                .flatMap(categoryVo -> {\n                    Map<String, Object> model = new HashMap<>();\n                    model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue());\n                    model.put(\"posts\",\n                        postListByCategoryName(categoryVo.getMetadata().getName(), request));\n                    model.put(\"category\", categoryVo);\n                    model.put(\n                        Constant.META_DESCRIPTION_VARIABLE_NAME,\n                        categoryVo.getSpec().getDescription()\n                    );\n                    String template = categoryVo.getSpec().getTemplate();\n                    return viewNameResolver.resolveViewNameOrDefault(request, template,\n                            DefaultTemplateEnum.CATEGORY.getValue())\n                        .flatMap(viewName -> ServerResponse.ok().render(viewName, model));\n                })\n                .switchIfEmpty(\n                    Mono.error(new NotFoundException(\"Category not found with slug: \" + slug)));\n        };\n    }\n\n    Mono<CategoryVo> fetchBySlug(String slug) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            and(\n                equal(\"spec.slug\", slug),\n                isNull(\"metadata.deletionTimestamp\")\n            )\n        ));\n        return client.listBy(Category.class, listOptions, PageRequestImpl.ofSize(1))\n            .mapNotNull(result -> ListResult.first(result)\n                .map(CategoryVo::from)\n                .orElse(null)\n            );\n    }\n\n    private Mono<UrlContextListResult<ListedPostVo>> postListByCategoryName(String name,\n        ServerRequest request) {\n        String path = request.path();\n        int pageNum = pageNumInPathVariable(request);\n        return configuredPageSize(environmentFetcher, SystemSetting.Post::getCategoryPageSize)\n            .flatMap(pageSize -> postFinder.listByCategory(pageNum, pageSize, name))\n            .doOnNext(list -> list.forEach(postVo -> postVo.getSpec().setTitle(\n                    titleVisibilityIdentifyCalculator.calculateTitle(\n                        postVo.getSpec().getTitle(),\n                        postVo.getSpec().getVisible(),\n                        localeContextResolver.resolveLocaleContext(request.exchange())\n                            .getLocale()\n                    )\n                )\n            ))\n            .map(list -> new UrlContextListResult.Builder<ListedPostVo>()\n                .listResult(list)\n                .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))\n                .prevUrl(PageUrlUtils.prevPageUrl(path))\n                .build()\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static run.halo.app.theme.router.PageUrlUtils.totalPage;\n\nimport java.time.Duration;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport org.thymeleaf.context.LazyContextVariable;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.ReactiveUtils;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.router.ModelConst;\nimport run.halo.app.theme.router.PageUrlUtils;\nimport run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;\nimport run.halo.app.theme.router.UrlContextListResult;\n\n/**\n * The {@link IndexRouteFactory} for generate {@link RouterFunction} specific to the template\n * <code>index.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class IndexRouteFactory implements RouteFactory {\n\n    private static final Duration BLOCKING_TIMEOUT = ReactiveUtils.DEFAULT_TIMEOUT;\n\n    private final PostFinder postFinder;\n    private final SystemConfigFetcher environmentFetcher;\n    private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n    private final LocaleContextResolver localeContextResolver;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String pattern) {\n        return RouterFunctions\n            .route(GET(\"/\").or(GET(\"/page/{page:\\\\d+}\")\n                .or(GET(\"/index\")).or(GET(\"/index/page/{page:\\\\d+}\"))\n                .and(accept(MediaType.TEXT_HTML))), handlerFunction());\n    }\n\n    HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> Mono.deferContextual(contextView -> {\n            var posts = new LazyContextVariable<UrlContextListResult<ListedPostVo>>() {\n                @Override\n                protected UrlContextListResult<ListedPostVo> loadValue() {\n                    return postList(request).contextWrite(contextView).block(BLOCKING_TIMEOUT);\n                }\n            };\n            return ServerResponse.ok().render(DefaultTemplateEnum.INDEX.getValue(), Map.of(\n                \"posts\", posts,\n                ModelConst.TEMPLATE_ID, DefaultTemplateEnum.INDEX.getValue()\n            ));\n        });\n    }\n\n    private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request) {\n        String path = request.path();\n\n        return configuredPageSize(environmentFetcher, SystemSetting.Post::getPostPageSize)\n            .flatMap(pageSize -> postFinder.list(pageNumInPathVariable(request), pageSize))\n            .doOnNext(list -> list.getItems()\n                .forEach(listedPostVo -> listedPostVo.getSpec()\n                    .setTitle(titleVisibilityIdentifyCalculator.calculateTitle(\n                        listedPostVo.getSpec().getTitle(),\n                        listedPostVo.getSpec().getVisible(),\n                        localeContextResolver.resolveLocaleContext(request.exchange())\n                            .getLocale())\n                    )\n                )\n            )\n            .map(list -> new UrlContextListResult.Builder<ListedPostVo>()\n                .listResult(list)\n                .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))\n                .prevUrl(PageUrlUtils.prevPageUrl(path))\n                .build()\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\nimport static org.apache.commons.lang3.StringUtils.isNotBlank;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static run.halo.app.content.permalinks.PostPermalinkPolicy.DEFAULT_CATEGORY;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.Getter;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RequestPredicate;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.index.query.Queries;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.router.ModelMapUtils;\nimport run.halo.app.theme.router.ReactiveQueryPostPredicateResolver;\nimport run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;\n\n/**\n * The {@link PostRouteFactory} for generate {@link RouterFunction} specific to the template\n * <code>post.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class PostRouteFactory implements RouteFactory {\n\n    private final PostFinder postFinder;\n\n    private final ViewNameResolver viewNameResolver;\n\n    private final ReactiveExtensionClient client;\n\n    private final ReactiveQueryPostPredicateResolver queryPostPredicateResolver;\n\n    private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    private final LocaleContextResolver localeContextResolver;\n    private final PostService postService;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String pattern) {\n        var postParamPredicate = new PatternParser(pattern);\n        if (postParamPredicate.isQueryParamPattern()) {\n            RequestPredicate requestPredicate = postParamPredicate.toRequestPredicate();\n            return RouterFunctions.route(GET(\"/\")\n                .and(requestPredicate), queryParamHandlerFunction(postParamPredicate));\n        }\n        return RouterFunctions\n            .route(GET(pattern).and(accept(MediaType.TEXT_HTML)), handlerFunction());\n    }\n\n    HandlerFunction<ServerResponse> queryParamHandlerFunction(PatternParser paramPredicate) {\n        return request -> {\n            Map<String, String> variables = mergedVariables(request);\n            PostPatternVariable patternVariable = new PostPatternVariable();\n            Optional.ofNullable(variables.get(paramPredicate.getParamName()))\n                .ifPresent(value -> {\n                    switch (paramPredicate.getPlaceholderName()) {\n                        case \"name\" -> patternVariable.setName(value);\n                        case \"slug\" -> patternVariable.setSlug(value);\n                        default ->\n                            throw new IllegalArgumentException(\"Unsupported query param predicate\");\n                    }\n                });\n            return postResponse(request, patternVariable);\n        };\n    }\n\n    HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> {\n            PostPatternVariable patternVariable = PostPatternVariable.from(request);\n            return postResponse(request, patternVariable)\n                .switchIfEmpty(Mono.error(() -> new NotFoundException(\"Post not found.\")));\n        };\n    }\n\n    @NonNull\n    private Mono<ServerResponse> postResponse(ServerRequest request,\n        PostPatternVariable patternVariable) {\n        Mono<PostVo> postVoMono = bestMatchPost(patternVariable);\n        return postVoMono\n            .doOnNext(postVo -> {\n                postVo.getSpec().setTitle(\n                    titleVisibilityIdentifyCalculator.calculateTitle(\n                        postVo.getSpec().getTitle(),\n                        postVo.getSpec().getVisible(),\n                        localeContextResolver.resolveLocaleContext(request.exchange())\n                            .getLocale())\n                );\n            })\n            .flatMap(postVo -> {\n                Map<String, Object> model = ModelMapUtils.postModel(postVo);\n                return determineTemplate(request, postVo)\n                    .flatMap(templateName -> ServerResponse.ok().render(templateName, model));\n            });\n    }\n\n    Mono<String> determineTemplate(ServerRequest request, PostVo postVo) {\n        return Flux.fromIterable(defaultIfNull(postVo.getCategories(), List.of()))\n            .filter(category -> isNotBlank(category.getSpec().getPostTemplate()))\n            .concatMap(category -> viewNameResolver.resolveViewNameOrDefault(request,\n                category.getSpec().getPostTemplate(), null)\n            )\n            .next()\n            .switchIfEmpty(Mono.defer(() -> viewNameResolver.resolveViewNameOrDefault(request,\n                postVo.getSpec().getTemplate(),\n                DefaultTemplateEnum.POST.getValue())\n            ));\n    }\n\n    Mono<PostVo> bestMatchPost(PostPatternVariable variable) {\n        return postsByPredicates(variable)\n            .filter(post -> {\n                Map<String, String> labels = MetadataUtil.nullSafeLabels(post);\n                return matchIfPresent(variable.getName(), post.getMetadata().getName())\n                    && matchIfPresent(variable.getSlug(), post.getSpec().getSlug())\n                    && matchIfPresent(variable.getYear(), labels.get(Post.ARCHIVE_YEAR_LABEL))\n                    && matchIfPresent(variable.getMonth(), labels.get(Post.ARCHIVE_MONTH_LABEL))\n                    && matchIfPresent(variable.getDay(), labels.get(Post.ARCHIVE_DAY_LABEL));\n            })\n            .filterWhen(post -> {\n                if (isNotBlank(variable.getCategorySlug())) {\n                    var categoryNames = post.getSpec().getCategories();\n                    return postService.listCategories(categoryNames)\n                        .next()\n                        .filter(category -> category.getSpec().getSlug()\n                            .equals(variable.getCategorySlug())\n                        )\n                        .map(category -> category.getSpec().getSlug())\n                        .switchIfEmpty(Mono.defer(() -> {\n                            if (DEFAULT_CATEGORY.equals(variable.getCategorySlug())) {\n                                return Mono.just(DEFAULT_CATEGORY);\n                            }\n                            return Mono.empty();\n                        }))\n                        .hasElement();\n                }\n                return Mono.just(true);\n            })\n            .next()\n            .flatMap(post -> postFinder.getByName(post.getMetadata().getName()));\n    }\n\n    Flux<Post> postsByPredicates(PostPatternVariable patternVariable) {\n        if (isNotBlank(patternVariable.getName())) {\n            return fetchPostsByName(patternVariable.getName());\n        }\n        if (isNotBlank(patternVariable.getSlug())) {\n            return fetchPostsBySlug(patternVariable.getSlug());\n        }\n        return Flux.empty();\n    }\n\n    private Flux<Post> fetchPostsByName(String name) {\n        return queryPostPredicateResolver.getPredicate()\n            .flatMap(predicate -> client.fetch(Post.class, name)\n                .filter(predicate)\n            )\n            .flux();\n    }\n\n    private Flux<Post> fetchPostsBySlug(String slug) {\n        return queryPostPredicateResolver.getListOptions()\n            .flatMapMany(listOptions -> {\n                if (isNotBlank(slug)) {\n                    var other = Queries.equal(\"spec.slug\", slug);\n                    listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(other));\n                }\n                return client.listAll(Post.class, listOptions, Sort.unsorted());\n            });\n    }\n\n    private boolean matchIfPresent(String variable, String target) {\n        return StringUtils.isBlank(variable) || StringUtils.equals(target, variable);\n    }\n\n    @Data\n    static class PostPatternVariable {\n        String name;\n        String slug;\n        String year;\n        String month;\n        String day;\n        String categorySlug;\n\n        static PostPatternVariable from(ServerRequest request) {\n            Map<String, String> variables = mergedVariables(request);\n            return JsonUtils.mapToObject(variables, PostPatternVariable.class);\n        }\n    }\n\n    static Map<String, String> mergedVariables(ServerRequest request) {\n        Map<String, String> pathVariables = request.pathVariables();\n        MultiValueMap<String, String> queryParams = request.queryParams();\n        Map<String, String> mergedVariables = new LinkedHashMap<>();\n        for (String paramKey : queryParams.keySet()) {\n            mergedVariables.put(paramKey, queryParams.getFirst(paramKey));\n        }\n        // path variables higher priority will override query params\n        mergedVariables.putAll(pathVariables);\n        return mergedVariables;\n    }\n\n    @Getter\n    static class PatternParser {\n        private static final Pattern PATTERN_COMPILE = Pattern.compile(\"([^&?]*)=\\\\{(.*?)\\\\}(&|$)\");\n\n        private final String pattern;\n        private String paramName;\n        private String placeholderName;\n        private final boolean isQueryParamPattern;\n\n        PatternParser(String pattern) {\n            this.pattern = pattern;\n            var matcher = PATTERN_COMPILE.matcher(pattern);\n            if (matcher.find()) {\n                this.paramName = matcher.group(1);\n                this.placeholderName = matcher.group(2);\n                this.isQueryParamPattern = true;\n            } else {\n                this.isQueryParamPattern = false;\n            }\n        }\n\n        RequestPredicate toRequestPredicate() {\n            if (!this.isQueryParamPattern) {\n                throw new IllegalStateException(\"Not a query param pattern: \" + pattern);\n            }\n\n            return RequestPredicates.queryParam(paramName, value -> true);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/RouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.apache.commons.lang3.ObjectUtils.defaultIfNull;\n\nimport java.util.function.Function;\nimport org.apache.commons.lang3.math.NumberUtils;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic interface RouteFactory {\n    RouterFunction<ServerResponse> create(String pattern);\n\n    default Mono<Integer> configuredPageSize(\n        SystemConfigFetcher environmentFetcher,\n        Function<SystemSetting.Post, Integer> mapper) {\n        return environmentFetcher.fetchPost()\n            .map(p -> defaultIfNull(mapper.apply(p), ModelConst.DEFAULT_PAGE_SIZE));\n    }\n\n    default int pageNumInPathVariable(ServerRequest request) {\n        String page = request.pathVariables().get(\"page\");\n        return NumberUtils.toInt(page, 1);\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static run.halo.app.extension.index.query.Queries.and;\nimport static run.halo.app.extension.index.query.Queries.equal;\nimport static run.halo.app.extension.index.query.Queries.isNull;\nimport static run.halo.app.theme.router.PageUrlUtils.totalPage;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.router.selector.FieldSelector;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.utils.PathUtils;\nimport run.halo.app.theme.Constant;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.TagFinder;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.TagVo;\nimport run.halo.app.theme.router.PageUrlUtils;\nimport run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;\nimport run.halo.app.theme.router.UrlContextListResult;\n\n/**\n * The {@link TagPostRouteFactory} for generate {@link RouterFunction} specific to the template\n * <code>tag.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class TagPostRouteFactory implements RouteFactory {\n\n    private final ReactiveExtensionClient client;\n    private final SystemConfigFetcher environmentFetcher;\n    private final TagFinder tagFinder;\n    private final PostFinder postFinder;\n\n    private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    private final LocaleContextResolver localeContextResolver;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String prefix) {\n        return RouterFunctions\n            .route(GET(PathUtils.combinePath(prefix, \"/{slug}\"))\n                .or(GET(PathUtils.combinePath(prefix, \"/{slug}/page/{page:\\\\d+}\")))\n                .and(accept(MediaType.TEXT_HTML)), handlerFunction());\n    }\n\n    private HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> tagBySlug(request.pathVariable(\"slug\"))\n            .flatMap(tagVo -> {\n                int pageNum = pageNumInPathVariable(request);\n                String path = request.path();\n                var postList = postList(tagVo.getMetadata().getName(), pageNum, path)\n                    .doOnNext(list -> list.forEach(postVo ->\n                        postVo.getSpec().setTitle(\n                            titleVisibilityIdentifyCalculator.calculateTitle(\n                                postVo.getSpec().getTitle(),\n                                postVo.getSpec().getVisible(),\n                                localeContextResolver.resolveLocaleContext(request.exchange())\n                                    .getLocale()\n                            )\n                        )\n                    ));\n                Map<String, Object> model = new HashMap<>();\n                model.put(\"name\", tagVo.getMetadata().getName());\n                model.put(\"posts\", postList);\n                model.put(\"tag\", tagVo);\n                model.put(\n                    Constant.META_DESCRIPTION_VARIABLE_NAME,\n                    tagVo.getSpec().getDescription()\n                );\n                return ServerResponse.ok()\n                    .render(DefaultTemplateEnum.TAG.getValue(), model);\n            });\n    }\n\n    private Mono<UrlContextListResult<ListedPostVo>> postList(String name, Integer page,\n        String requestPath) {\n        return configuredPageSize(environmentFetcher, SystemSetting.Post::getTagPageSize)\n            .flatMap(pageSize -> postFinder.listByTag(page, pageSize, name))\n            .map(list -> new UrlContextListResult.Builder<ListedPostVo>()\n                .listResult(list)\n                .nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list)))\n                .prevUrl(PageUrlUtils.prevPageUrl(requestPath))\n                .build()\n            );\n    }\n\n    private Mono<TagVo> tagBySlug(String slug) {\n        var listOptions = new ListOptions();\n        listOptions.setFieldSelector(FieldSelector.of(\n            and(\n                equal(\"spec.slug\", slug),\n                isNull(\"metadata.deletionTimestamp\")\n            )\n        ));\n        return client.listBy(Tag.class, listOptions, PageRequestImpl.ofSize(1))\n            .mapNotNull(result -> ListResult.first(result).orElse(null))\n            .flatMap(tag -> tagFinder.getByName(tag.getMetadata().getName()))\n            .switchIfEmpty(\n                Mono.error(new NotFoundException(\"Tag not found with slug: \" + slug)));\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/router/factories/TagsRouteFactory.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\n\nimport java.util.Map;\nimport lombok.AllArgsConstructor;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.TagFinder;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * The {@link TagsRouteFactory} for generate {@link RouterFunction} specific to the template\n * <code>tags.html</code>.\n *\n * @author guqing\n * @since 2.0.0\n */\n@Component\n@AllArgsConstructor\npublic class TagsRouteFactory implements RouteFactory {\n\n    private final TagFinder tagFinder;\n\n    @Override\n    public RouterFunction<ServerResponse> create(String prefix) {\n        return RouterFunctions\n            .route(GET(StringUtils.prependIfMissing(prefix, \"/\"))\n                .and(accept(MediaType.TEXT_HTML)), handlerFunction());\n    }\n\n    private HandlerFunction<ServerResponse> handlerFunction() {\n        return request -> ServerResponse.ok()\n            .render(DefaultTemplateEnum.TAGS.getValue(),\n                Map.of(\"tags\", tagFinder.listAll(),\n                    ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAGS.getValue()\n                )\n            );\n    }\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/service/ThemeService.java",
    "content": "package run.halo.app.theme.service;\n\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.util.StringUtils;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.infra.SystemSetting;\n\npublic interface ThemeService {\n\n    Mono<Void> installPresetTheme();\n\n    Mono<Theme> install(Publisher<DataBuffer> content);\n\n    Mono<Theme> upgrade(String themeName, Publisher<DataBuffer> content);\n\n    Mono<Theme> reloadTheme(String name);\n\n    Mono<ConfigMap> resetSettingConfig(String name);\n\n    /**\n     * Fetch activated theme.\n     *\n     * @return the activated theme\n     */\n    Mono<Theme> fetchActivatedTheme();\n\n    /**\n     * Fetch system setting of theme.\n     *\n     * @return the system setting of theme\n     */\n    Mono<SystemSetting.Theme> fetchSystemSetting();\n\n    /**\n     * Fetch activated theme name.\n     *\n     * @return the activated theme name\n     */\n    default Mono<String> fetchActivatedThemeName() {\n        return fetchSystemSetting()\n            .mapNotNull(SystemSetting.Theme::getActive)\n            .filter(StringUtils::hasText);\n    }\n\n    // TODO Migrate other useful methods in ThemeEndpoint in the future.\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/service/ThemeServiceImpl.java",
    "content": "package run.halo.app.theme.service;\n\nimport static org.springframework.util.FileSystemUtils.copyRecursively;\nimport static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;\nimport static run.halo.app.infra.utils.FileUtils.unzip;\nimport static run.halo.app.theme.service.ThemeUtils.loadThemeManifest;\nimport static run.halo.app.theme.service.ThemeUtils.locateThemeManifest;\nimport static run.halo.app.theme.service.ThemeUtils.unzipThemeTo;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport lombok.AllArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jspecify.annotations.Nullable;\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.UrlResource;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.core.io.buffer.DefaultDataBufferFactory;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.Assert;\nimport org.springframework.util.ResourceUtils;\nimport org.springframework.util.StreamUtils;\nimport org.springframework.web.server.ServerErrorException;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.core.extension.notification.NotificationTemplate;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.infra.exception.ThemeAlreadyExistsException;\nimport run.halo.app.infra.exception.ThemeUpgradeException;\nimport run.halo.app.infra.exception.UnsatisfiedAttributeValueException;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.infra.utils.SettingUtils;\nimport run.halo.app.infra.utils.VersionUtils;\n\n@Slf4j\n@Service\n@AllArgsConstructor\npublic class ThemeServiceImpl implements ThemeService {\n\n    private final ReactiveExtensionClient client;\n\n    private final ThemeRootGetter themeRoot;\n\n    private final HaloProperties haloProperties;\n\n    private final SystemVersionSupplier systemVersionSupplier;\n\n    private final SystemConfigFetcher systemConfigFetcher;\n\n    private final Scheduler scheduler = Schedulers.boundedElastic();\n\n    @Override\n    public Mono<Void> installPresetTheme() {\n        var themeProps = haloProperties.getTheme();\n        var location = themeProps.getInitializer().getLocation();\n        return Mono.using(\n                () -> Files.createTempDirectory(\"halo-theme-preset\"),\n                tempDir -> Mono.fromCallable(() -> {\n                    var themeUrl = ResourceUtils.getURL(location);\n                    var resource = new UrlResource(themeUrl);\n                    var tempThemePath = tempDir.resolve(\"theme.zip\");\n                    FileUtils.copyResource(resource, tempThemePath);\n                    return tempThemePath;\n                }).flatMap(themePath -> {\n                    var content = DataBufferUtils.read(new FileSystemResource(themePath),\n                        DefaultDataBufferFactory.sharedInstance,\n                        StreamUtils.BUFFER_SIZE);\n                    // We don't want to run on the bounded elastic scheduler again, so pass null\n                    // here.\n                    return doInstall(content, null);\n                }),\n                FileUtils::deleteRecursivelyAndSilently\n            )\n            .subscribeOn(scheduler)\n            .onErrorResume(IOException.class, e -> {\n                log.warn(\"Failed to initialize theme from {}\", location, e);\n                return Mono.empty();\n            })\n            .onErrorResume(ThemeAlreadyExistsException.class, e -> {\n                log.warn(\"Failed to initialize theme from {}, because it already exists\", location);\n                return Mono.empty();\n            })\n            .then();\n    }\n\n    @Override\n    public Mono<Theme> install(Publisher<DataBuffer> content) {\n        return doInstall(content, this.scheduler);\n    }\n\n    @Override\n    public Mono<Theme> upgrade(String themeName, Publisher<DataBuffer> content) {\n        var checkTheme = client.fetch(Theme.class, themeName)\n            .switchIfEmpty(Mono.error(() -> new ServerWebInputException(\n                \"The given theme with name \" + themeName + \" did not exist\")\n            ));\n        var upgradeTheme = Mono.using(\n            () -> Files.createTempDirectory(\"halo-theme-\"),\n            tempDir -> {\n                var locateThemeManifest = Mono.fromCallable(\n                        () -> locateThemeManifest(tempDir).orElse(null)\n                    )\n                    .switchIfEmpty(Mono.error(() -> new ThemeUpgradeException(\n                        \"Missing theme manifest file: theme.yaml or theme.yml\",\n                        \"problemDetail.theme.upgrade.missingManifest\", null)\n                    ));\n                return unzip(content, tempDir)\n                    .then(locateThemeManifest)\n                    .flatMap(themeManifest -> {\n                        if (log.isDebugEnabled()) {\n                            log.debug(\"Found theme manifest file: {}\", themeManifest);\n                        }\n                        var newTheme = loadThemeManifest(themeManifest);\n                        if (!Objects.equals(themeName, newTheme.getMetadata().getName())) {\n                            if (log.isDebugEnabled()) {\n                                log.error(\"Want theme name: {}, but provided: {}\", themeName,\n                                    newTheme.getMetadata().getName());\n                            }\n                            return Mono.error(new ThemeUpgradeException(\n                                \"Please make sure the theme name is correct\",\n                                \"problemDetail.theme.upgrade.nameMismatch\",\n                                new Object[] {newTheme.getMetadata().getName(), themeName}));\n                        }\n                        var copyTheme = Mono.fromCallable(() -> {\n                            var themePath = themeRoot.get().resolve(themeName);\n                            // TODO Create backup before deleting\n                            deleteRecursivelyAndSilently(themePath);\n                            copyRecursively(themeManifest.getParent(), themePath);\n                            return themePath;\n                        });\n                        return copyTheme.then(this.persistent(newTheme, true));\n                    });\n            },\n            FileUtils::deleteRecursivelyAndSilently\n        ).subscribeOn(scheduler);\n        return checkTheme.then(upgradeTheme);\n    }\n\n    private Mono<Theme> doInstall(Publisher<DataBuffer> content, @Nullable Scheduler scheduler) {\n        var themeRoot = this.themeRoot.get();\n        return unzipThemeTo(content, themeRoot, scheduler)\n            .flatMap(theme -> persistent(theme, false));\n    }\n\n    /**\n     * Creates theme manifest and related unstructured resources.\n     * TODO: In case of failure in saving midway, the problem of data consistency needs to be\n     * solved.\n     *\n     * @param themeManifest the theme custom model\n     * @return a theme custom model\n     * @see Theme\n     */\n    private Mono<Theme> persistent(Unstructured themeManifest, boolean isUpgrade) {\n        Assert.state(StringUtils.equals(Theme.KIND, themeManifest.getKind()),\n            \"Theme manifest kind must be Theme.\");\n        var newTheme = Unstructured.OBJECT_MAPPER.convertValue(themeManifest, Theme.class);\n        final Mono<Theme> createOrUpdateTheme;\n        if (isUpgrade) {\n            createOrUpdateTheme = client.get(Theme.class, newTheme.getMetadata().getName())\n                .doOnNext(theme -> updateTheme(theme, newTheme))\n                .flatMap(client::update);\n        } else {\n            createOrUpdateTheme = client.create(newTheme);\n        }\n        return createOrUpdateTheme\n            .doOnNext(theme -> {\n                String systemVersion = systemVersionSupplier.get().toStableVersion().toString();\n                String requires = theme.getSpec().getRequires();\n                if (!VersionUtils.satisfiesRequires(systemVersion, requires)) {\n                    throw new UnsatisfiedAttributeValueException(\n                        String.format(\"The theme requires a minimum system version of %s, \"\n                                + \"but the current version is %s.\",\n                            requires, systemVersion),\n                        \"problemDetail.theme.version.unsatisfied.requires\",\n                        new String[] {requires, systemVersion});\n                }\n            })\n            .flatMap(theme -> {\n                var unstructureds = ThemeUtils.loadThemeResources(getThemePath(theme));\n                if (unstructureds.stream()\n                    .filter(hasSettingsYaml(theme))\n                    .count() > 1) {\n                    return Mono.error(new IllegalStateException(\n                        \"Theme must only have one settings.yaml or settings.yml.\"));\n                }\n                if (unstructureds.stream()\n                    .filter(hasConfigYaml(theme))\n                    .count() > 1) {\n                    return Mono.error(new IllegalStateException(\n                        \"Theme must only have one config.yaml or config.yml.\"));\n                }\n                return Flux.fromIterable(unstructureds)\n                    .filter(unstructured -> ExtensionWhitelist.of(theme).isAllowed(unstructured))\n                    .doOnNext(unstructured ->\n                        populateThemeNameLabel(unstructured, theme.getMetadata().getName()))\n                    .flatMap(unstructured -> {\n                        if (isUpgrade) {\n                            return createOrUpdate(unstructured);\n                        }\n                        return client.create(unstructured);\n                    })\n                    .then()\n                    .thenReturn(theme);\n            });\n    }\n\n    private Mono<Unstructured> createOrUpdate(Unstructured unstructured) {\n        return Mono.defer(() -> client.fetch(unstructured.groupVersionKind(),\n                    unstructured.getMetadata().getName())\n                .flatMap(existUnstructured -> {\n                    unstructured.getMetadata()\n                        .setVersion(existUnstructured.getMetadata().getVersion());\n                    return client.update(unstructured);\n                })\n                .switchIfEmpty(Mono.defer(() -> client.create(unstructured)))\n            )\n            .retryWhen(Retry.backoff(5, Duration.ofMillis(100))\n                .filter(OptimisticLockingFailureException.class::isInstance));\n    }\n\n    @Override\n    public Mono<Theme> reloadTheme(String name) {\n        return client.fetch(Theme.class, name)\n            .flatMap(oldTheme -> {\n                String settingName = oldTheme.getSpec().getSettingName();\n                return waitForSettingDeleted(settingName)\n                    .then(waitForAnnotationSettingsDeleted(name));\n            })\n            .then(Mono.defer(() -> {\n                Path themePath = themeRoot.get().resolve(name);\n                Path themeManifestPath = ThemeUtils.resolveThemeManifest(themePath);\n                if (themeManifestPath == null) {\n                    throw new IllegalArgumentException(\n                        \"The manifest file [theme.yaml] is required.\");\n                }\n                Unstructured unstructured = loadThemeManifest(themeManifestPath);\n                Theme newTheme = Unstructured.OBJECT_MAPPER.convertValue(unstructured,\n                    Theme.class);\n                return client.fetch(Theme.class, name)\n                    .map(oldTheme -> {\n                        newTheme.getMetadata().setVersion(oldTheme.getMetadata().getVersion());\n                        return newTheme;\n                    })\n                    .flatMap(client::update);\n            }))\n            .flatMap(theme -> Flux.fromIterable(ThemeUtils.loadThemeResources(getThemePath(theme)))\n                .filter(unstructured -> ExtensionWhitelist.of(theme).isAllowed(unstructured))\n                .doOnNext(unstructured -> populateThemeNameLabel(unstructured, name))\n                .flatMap(this::createOrUpdate)\n                .then(Mono.just(theme))\n            );\n    }\n\n    private static void populateThemeNameLabel(Unstructured unstructured, String themeName) {\n        Map<String, String> labels = unstructured.getMetadata().getLabels();\n        if (labels == null) {\n            labels = new HashMap<>();\n            unstructured.getMetadata().setLabels(labels);\n        }\n        labels.put(Theme.THEME_NAME_LABEL, themeName);\n    }\n\n    @Override\n    public Mono<ConfigMap> resetSettingConfig(String name) {\n        return client.fetch(Theme.class, name)\n            .filter(theme -> StringUtils.isNotBlank(theme.getSpec().getSettingName()))\n            .flatMap(theme -> {\n                String configMapName = theme.getSpec().getConfigMapName();\n                String settingName = theme.getSpec().getSettingName();\n                return client.fetch(Setting.class, settingName)\n                    .map(SettingUtils::settingDefinedDefaultValueMap)\n                    .flatMap(data -> updateConfigMapData(configMapName, data));\n            });\n    }\n\n    @Override\n    public Mono<Theme> fetchActivatedTheme() {\n        return fetchSystemSetting().mapNotNull(SystemSetting.Theme::getActive)\n            .flatMap(name -> client.fetch(Theme.class, name));\n    }\n\n    @Override\n    public Mono<SystemSetting.Theme> fetchSystemSetting() {\n        return systemConfigFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class);\n    }\n\n    private Mono<ConfigMap> updateConfigMapData(String configMapName, Map<String, String> data) {\n        return client.fetch(ConfigMap.class, configMapName)\n            .flatMap(configMap -> {\n                configMap.setData(data);\n                return client.update(configMap);\n            })\n            .retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))\n                .filter(t -> t instanceof OptimisticLockingFailureException));\n    }\n\n    private Mono<Void> waitForSettingDeleted(String settingName) {\n        return client.fetch(Setting.class, settingName)\n            .flatMap(setting -> client.delete(setting)\n                .flatMap(deleted -> client.fetch(Setting.class, settingName)\n                    .flatMap(s -> Mono.error(\n                        () -> new IllegalStateException(\"Re-check if the setting is deleted.\")\n                    ))\n                    .retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))\n                        .filter(IllegalStateException.class::isInstance)\n                    )\n                )\n            )\n            .then();\n    }\n\n    private Mono<Void> waitForAnnotationSettingsDeleted(String themeName) {\n        return client.list(AnnotationSetting.class,\n                annotationSetting -> {\n                    Map<String, String> labels = MetadataUtil.nullSafeLabels(annotationSetting);\n                    return StringUtils.equals(themeName, labels.get(Theme.THEME_NAME_LABEL));\n                }, null)\n            .flatMap(annotationSetting -> client.delete(annotationSetting)\n                .flatMap(deleted -> client.fetch(AnnotationSetting.class,\n                        annotationSetting.getMetadata().getName())\n                    .flatMap(latest -> Mono.error(\n                        new IllegalStateException(\"AnnotationSetting is not deleted yet.\")\n                    ))\n                    .retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))\n                        .filter(t -> t instanceof IllegalStateException)\n                    )\n                )\n            )\n            .then();\n    }\n\n    private Path getThemePath(Theme theme) {\n        return themeRoot.get().resolve(theme.getMetadata().getName());\n    }\n\n    private Predicate<Unstructured> hasSettingsYaml(Theme theme) {\n        return unstructured -> Setting.KIND.equals(unstructured.getKind())\n            && theme.getSpec().getSettingName().equals(unstructured.getMetadata().getName());\n    }\n\n    private Predicate<Unstructured> hasConfigYaml(Theme theme) {\n        return unstructured -> ConfigMap.KIND.equals(unstructured.getKind())\n            && theme.getSpec().getConfigMapName().equals(unstructured.getMetadata().getName());\n    }\n\n    Mono<Theme> deleteThemeAndWaitForComplete(String themeName) {\n        return client.fetch(Theme.class, themeName)\n            .flatMap(client::delete)\n            .flatMap(deletingTheme -> waitForThemeDeleted(themeName)\n                .thenReturn(deletingTheme));\n    }\n\n    Mono<Void> waitForThemeDeleted(String themeName) {\n        return client.fetch(Theme.class, themeName)\n            .flatMap(theme -> Mono.error(\n                new IllegalStateException(\"Re-check if the theme is deleted successfully\")\n            ))\n            .retryWhen(Retry.fixedDelay(20, Duration.ofMillis(100))\n                .filter(IllegalStateException.class::isInstance)\n                .onRetryExhaustedThrow((spec, signal) ->\n                    new ServerErrorException(\"Wait timeout for theme deleted\", null)))\n            .then();\n    }\n\n    static class ExtensionWhitelist {\n        private final Set<AllowedExtension> rules;\n\n        private ExtensionWhitelist(Theme theme) {\n            this.rules = getRules(theme);\n        }\n\n        public static ExtensionWhitelist of(Theme theme) {\n            return new ExtensionWhitelist(theme);\n        }\n\n        public boolean isAllowed(Unstructured unstructured) {\n            return this.rules.stream()\n                .anyMatch(rule -> rule.matches(unstructured));\n        }\n\n        private Set<AllowedExtension> getRules(Theme theme) {\n            var rules = new HashSet<AllowedExtension>();\n            rules.add(AllowedExtension.of(AnnotationSetting.class));\n            rules.add(AllowedExtension.of(NotificationTemplate.class));\n\n            var configMapName = theme.getSpec().getConfigMapName();\n            if (StringUtils.isNotBlank(configMapName)) {\n                rules.add(AllowedExtension.of(ConfigMap.class, configMapName));\n            }\n\n            var settingName = theme.getSpec().getSettingName();\n            if (StringUtils.isNotBlank(settingName)) {\n                rules.add(AllowedExtension.of(Setting.class, settingName));\n            }\n            return rules;\n        }\n    }\n\n    record AllowedExtension(String apiGroup, String kind, String name) {\n        AllowedExtension {\n            Assert.notNull(apiGroup, \"The apiGroup must not be null\");\n            Assert.notNull(kind, \"Kind must not be null\");\n        }\n\n        public static <E extends Extension> AllowedExtension of(Class<E> clazz) {\n            return of(clazz, null);\n        }\n\n        public static <E extends Extension> AllowedExtension of(Class<E> clazz, String name) {\n            var gvk = GroupVersionKind.fromExtension(clazz);\n            return new AllowedExtension(gvk.group(), gvk.kind(), name);\n        }\n\n        public boolean matches(Unstructured unstructured) {\n            var groupVersionKind = unstructured.groupVersionKind();\n            return this.apiGroup.equals(groupVersionKind.group())\n                && this.kind.equals(groupVersionKind.kind())\n                && (this.name == null || this.name.equals(unstructured.getMetadata().getName()));\n        }\n    }\n\n    private static void updateTheme(Theme existing, Theme updating) {\n        var existingSpec = existing.getSpec();\n        var updatingSpec = updating.getSpec();\n        // merge spec\n        existingSpec.setAuthor(updatingSpec.getAuthor());\n        existingSpec.setCustomTemplates(updatingSpec.getCustomTemplates());\n        existingSpec.setDescription(updatingSpec.getDescription());\n        existingSpec.setDisplayName(updatingSpec.getDisplayName());\n        existingSpec.setHomepage(updatingSpec.getHomepage());\n        existingSpec.setIssues(updatingSpec.getIssues());\n        existingSpec.setLicense(updatingSpec.getLicense());\n        existingSpec.setLogo(updatingSpec.getLogo());\n        existingSpec.setRepo(updatingSpec.getRepo());\n        existingSpec.setSettingName(updatingSpec.getSettingName());\n        existingSpec.setVersion(updatingSpec.getVersion());\n        existingSpec.setRequires(updatingSpec.getRequires());\n        // Do not overwrite configMapName to avoid data loss\n\n        var existingMeta = existing.getMetadata();\n        var updatingMeta = updating.getMetadata();\n        // merge labels\n        if (updatingMeta.getLabels() != null) {\n            if (existingMeta.getLabels() == null) {\n                existingMeta.setLabels(new HashMap<>());\n            }\n            existingMeta.getLabels().putAll(updatingMeta.getLabels());\n        }\n        // merge annotations\n        if (updatingMeta.getAnnotations() != null) {\n            if (existingMeta.getAnnotations() == null) {\n                existingMeta.setAnnotations(new HashMap<>());\n            }\n            existingMeta.getAnnotations().putAll(updatingMeta.getAnnotations());\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/service/ThemeUtils.java",
    "content": "package run.halo.app.theme.service;\n\nimport static org.springframework.util.FileSystemUtils.copyRecursively;\nimport static run.halo.app.infra.utils.FileUtils.createTempDir;\nimport static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;\nimport static run.halo.app.infra.utils.FileUtils.unzip;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.stream.BaseStream;\nimport java.util.stream.Stream;\nimport lombok.experimental.UtilityClass;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jspecify.annotations.Nullable;\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.util.CollectionUtils;\nimport org.springframework.web.server.ResponseStatusException;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.core.scheduler.Scheduler;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.exception.ThemeAlreadyExistsException;\nimport run.halo.app.infra.exception.ThemeInstallationException;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\n\n@Slf4j\n@UtilityClass\npublic class ThemeUtils {\n    private static final String THEME_TMP_PREFIX = \"halo-theme-\";\n    private static final String[] THEME_MANIFESTS = {\"theme.yaml\", \"theme.yml\"};\n\n    public static Flux<Theme> listAllThemesFromThemeDir(Path themesDir) {\n        return walkThemesFromPath(themesDir)\n            .filter(Files::isDirectory)\n            .map(ThemeUtils::findThemeManifest)\n            .flatMap(Flux::fromIterable)\n            .filter(unstructured -> unstructured.getKind().equals(Theme.KIND))\n            .map(unstructured -> Unstructured.OBJECT_MAPPER.convertValue(unstructured,\n                Theme.class))\n            .sort(Comparator.comparing(theme -> theme.getMetadata().getName()));\n    }\n\n    private static Flux<Path> walkThemesFromPath(Path path) {\n        return Flux.using(() -> Files.walk(path, 2),\n                Flux::fromStream,\n                BaseStream::close\n            )\n            .subscribeOn(Schedulers.boundedElastic());\n    }\n\n    private static List<Unstructured> findThemeManifest(Path themePath) {\n        List<Resource> resources = new ArrayList<>(4);\n        for (String themeResource : THEME_MANIFESTS) {\n            Path resourcePath = themePath.resolve(themeResource);\n            if (Files.exists(resourcePath)) {\n                resources.add(new FileSystemResource(resourcePath));\n            }\n        }\n        if (CollectionUtils.isEmpty(resources)) {\n            return List.of();\n        }\n        return new YamlUnstructuredLoader(resources.toArray(new Resource[0]))\n            .load();\n    }\n\n    static List<Unstructured> loadThemeResources(Path themePath) {\n        try (Stream<Path> paths = Files.list(themePath)) {\n            List<FileSystemResource> resources = paths\n                .filter(path -> {\n                    String pathString = path.toString();\n                    return pathString.endsWith(\".yaml\") || pathString.endsWith(\".yml\");\n                })\n                .filter(path -> {\n                    String pathString = path.toString();\n                    for (String themeManifest : THEME_MANIFESTS) {\n                        if (pathString.endsWith(themeManifest)) {\n                            return false;\n                        }\n                    }\n                    return true;\n                })\n                .map(FileSystemResource::new)\n                .toList();\n            return new YamlUnstructuredLoader(resources.toArray(new Resource[0]))\n                .load();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    static Mono<Unstructured> unzipThemeTo(Publisher<DataBuffer> content, Path themeWorkDir,\n        @Nullable Scheduler scheduler) {\n        return unzipThemeTo(content, themeWorkDir, false, scheduler)\n            .onErrorMap(e -> !(e instanceof ResponseStatusException), e -> {\n                log.error(\"Failed to unzip theme\", e);\n                throw new ServerWebInputException(\"Failed to unzip theme\");\n            });\n    }\n\n    static Mono<Unstructured> unzipThemeTo(Publisher<DataBuffer> content, Path themeWorkDir,\n        boolean override, @Nullable Scheduler scheduler) {\n        var unzipThem = Mono.usingWhen(\n            createTempDir(THEME_TMP_PREFIX, null),\n            tempDir -> {\n                var locateThemeManifest = Mono.fromCallable(\n                        () -> locateThemeManifest(tempDir).orElse(null)\n                    )\n                    .switchIfEmpty(\n                        Mono.error(() -> new ThemeInstallationException(\n                            \"Missing theme manifest\",\n                            \"problemDetail.theme.install.missingManifest\", null\n                        ))\n                    );\n                return unzip(content, tempDir, null)\n                    .then(locateThemeManifest)\n                    .<Unstructured>handle((themeManifestPath, sink) -> {\n                        var theme = loadThemeManifest(themeManifestPath);\n                        var themeName = theme.getMetadata().getName();\n                        var themeTargetPath = themeWorkDir.resolve(themeName);\n                        try {\n                            if (!override && !FileUtils.isEmpty(themeTargetPath)) {\n                                sink.error(new ThemeAlreadyExistsException(themeName));\n                                return;\n                            }\n                            // install theme to theme work dir\n                            copyRecursively(themeManifestPath.getParent(), themeTargetPath);\n                            sink.next(theme);\n                        } catch (IOException e) {\n                            deleteRecursivelyAndSilently(themeTargetPath);\n                            sink.error(e);\n                        }\n                    });\n            },\n            tempDir -> FileUtils.deleteRecursivelyAndSilently(tempDir, null)\n        );\n        if (scheduler != null) {\n            return unzipThem.subscribeOn(scheduler);\n        }\n        return unzipThem;\n    }\n\n    static Unstructured loadThemeManifest(Path themeManifestPath) {\n        var unstructureds = new YamlUnstructuredLoader(new FileSystemResource(themeManifestPath))\n            .load();\n        if (CollectionUtils.isEmpty(unstructureds)) {\n            throw new ThemeInstallationException(\"Missing theme manifest\",\n                \"problemDetail.theme.install.missingManifest\", null);\n        }\n        return unstructureds.get(0);\n    }\n\n    @Nullable\n    static Path resolveThemeManifest(Path tempDirectory) {\n        for (String themeManifest : THEME_MANIFESTS) {\n            Path path = tempDirectory.resolve(themeManifest);\n            if (Files.exists(path)) {\n                return path;\n            }\n        }\n        return null;\n    }\n\n    static Optional<Path> locateThemeManifest(Path path) {\n        if (!Files.isDirectory(path)) {\n            return Optional.empty();\n        }\n        var queue = new LinkedList<Path>();\n        queue.add(path);\n        var manifest = Optional.<Path>empty();\n        while (!queue.isEmpty()) {\n            var current = queue.pop();\n            try (Stream<Path> subPaths = Files.list(current)) {\n                manifest = subPaths.filter(Files::isReadable)\n                    .filter(subPath -> {\n                        if (Files.isDirectory(subPath)) {\n                            queue.add(subPath);\n                            return false;\n                        }\n                        return true;\n                    })\n                    .filter(Files::isRegularFile)\n                    .filter(ThemeUtils::isManifest)\n                    .findFirst();\n            } catch (IOException e) {\n                throw Exceptions.propagate(e);\n            }\n            if (manifest.isPresent()) {\n                break;\n            }\n        }\n        return manifest;\n    }\n\n    static boolean isManifest(Path file) {\n        if (!Files.isRegularFile(file)) {\n            return false;\n        }\n        return Set.of(THEME_MANIFESTS).contains(file.getFileName().toString());\n    }\n\n}\n"
  },
  {
    "path": "application/src/main/java/run/halo/app/theme/utils/PatternUtils.java",
    "content": "package run.halo.app.theme.utils;\n\nimport static org.apache.commons.lang3.StringUtils.prependIfMissing;\nimport static org.apache.commons.lang3.StringUtils.removeEnd;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.util.Assert;\nimport run.halo.app.infra.SystemSetting.ThemeRouteRules;\n\n/**\n * Pattern utility methods.\n *\n * @author johnniang\n * @since 2.22.0\n */\npublic enum PatternUtils {\n    ;\n\n    /**\n     * Normalize the pattern by ensuring it starts with a \"/\" and does not end with a \"/\".\n     *\n     * @param pattern the pattern to normalize, must not be blank or just \"/\"\n     * @return the normalized pattern\n     */\n    public static String normalizePattern(String pattern) {\n        Assert.hasText(pattern, \"Pattern must not be blank\");\n        Assert.isTrue(!\"/\".equals(pattern.trim()), \"Pattern must not be just '/'\");\n        pattern = prependIfMissing(pattern.trim(), \"/\");\n        return removeEnd(pattern, \"/\");\n    }\n\n\n    /**\n     * Normalize the post pattern, if the post pattern starts with /archives/ or /categories/,\n     * replace it with the corresponding pattern from rules.\n     *\n     * @param rules the theme route rules\n     * @return the normalized post pattern\n     */\n    public static String normalizePostPattern(ThemeRouteRules rules) {\n        var postPattern = normalizePattern(rules.getPost());\n        if (StringUtils.startsWith(postPattern, \"/archives/\")) {\n            var archivesPattern = normalizePattern(rules.getArchives());\n            postPattern = archivesPattern + StringUtils.removeStart(postPattern, \"/archives\");\n        } else if (StringUtils.startsWith(postPattern, \"/categories/\")) {\n            var categoriesPattern = normalizePattern(rules.getCategories());\n            postPattern = categoriesPattern + StringUtils.removeStart(postPattern, \"/categories\");\n        }\n        return postPattern;\n    }\n}\n"
  },
  {
    "path": "application/src/main/resources/META-INF/spring-devtools.properties",
    "content": "# See https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.devtools.restart.customizing-the-classload for more\nrestart.include.apimodule=/api-[\\\\w\\\\d-\\\\.]+\\\\.jar\n"
  },
  {
    "path": "application/src/main/resources/application-dev.yaml",
    "content": "server:\n  port: 8090\n\nspring:\n  output:\n    ansi:\n      enabled: always\n  thymeleaf:\n    cache: false\n  web:\n    resources:\n      cache:\n        cachecontrol:\n          no-cache: true\n        use-last-modified: false\n\nhalo:\n  security:\n    basic-auth:\n      disabled: false\n  ui:\n    proxy:\n      endpoint: http://localhost:3000/\n      enabled: true\n  plugin:\n    runtime-mode: development # development, deployment\n  work-dir: ${user.home}/halo2-dev\nlogging:\n  level:\n    org.springframework.data.r2dbc: DEBUG\n    org.springframework.r2dbc: DEBUG\n    run.halo: DEBUG\n    web: DEBUG\n    org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler: DEBUG\nspringdoc:\n  cache:\n    disabled: true\n  api-docs:\n    enabled: true\n    version: OPENAPI_3_0\n  swagger-ui:\n    enabled: true\n  show-actuator: true\n\nmanagement:\n  endpoints:\n    web:\n      exposure:\n        include: \"*\"\n"
  },
  {
    "path": "application/src/main/resources/application-doc.yaml",
    "content": "springdoc:\n  cache:\n    disabled: true\n  api-docs:\n    enabled: true\n    version: OPENAPI_3_0\n\nspring:\n  main:\n    banner-mode: off\n  r2dbc:\n    url: r2dbc:h2:mem:///halo\n\nhalo:\n  extension:\n    controller:\n      disabled: true\n"
  },
  {
    "path": "application/src/main/resources/application-mariadb.yaml",
    "content": "spring:\n  r2dbc:\n    url: r2dbc:pool:mariadb://localhost:3306/halo\n    username: root\n    password: mariadb\n  sql:\n    init:\n      mode: always\n      platform: mariadb\n"
  },
  {
    "path": "application/src/main/resources/application-mysql.yaml",
    "content": "spring:\n  r2dbc:\n    url: r2dbc:pool:mysql://localhost:3306/halo\n    username: root\n    password: openmysql\n  sql:\n    init:\n      mode: always\n      platform: mysql\n"
  },
  {
    "path": "application/src/main/resources/application-postgresql.yaml",
    "content": "spring:\n  r2dbc:\n    url: r2dbc:pool:postgresql://localhost:5432/halo\n    username: postgres\n    password: openpostgresql\n  sql:\n    init:\n      mode: always\n      platform: postgresql\n"
  },
  {
    "path": "application/src/main/resources/application-win.yaml",
    "content": "spring:\n  r2dbc:\n    url: r2dbc:h2:file:///~/halo2-dev/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE\nhalo:\n  work-dir: ${user.home}/halo2-dev"
  },
  {
    "path": "application/src/main/resources/application.yaml",
    "content": "server:\n  port: 8090\n  forward-headers-strategy: native\n  compression:\n    enabled: true\nspring:\n  output:\n    ansi:\n      enabled: detect\n  r2dbc:\n    url: r2dbc:h2:file:///${halo.work-dir}/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE\n    username: admin\n    password: 123456\n  sql:\n    init:\n      mode: always\n      platform: h2\n  http:\n    codecs:\n      max-in-memory-size: 10MB\n  messages:\n    basename: config.i18n.messages\n  web:\n    error:\n      whitelabel:\n        enabled: false\n    resources:\n      cache:\n        cachecontrol:\n          max-age: 365d\n  thymeleaf:\n    reactive:\n      maxChunkSize: 8KB\n  cache:\n    type: caffeine\n    caffeine:\n      spec: expireAfterAccess=1h, maximumSize=10000\n  threads:\n    virtual:\n      enabled: true\n\nhalo:\n  work-dir: ${user.home}/.halo2\n  attachment:\n    resource-mappings:\n      - pathPattern: /upload/**\n        locations:\n          - migrate-from-1.x\n  security:\n    password-reset-methods:\n      - name: email\n        href: /password-reset/email\n        icon: /images/password-reset-methods/email.svg\n\nspringdoc:\n  api-docs:\n    enabled: false\n  writer-with-order-by-keys: true\n\nlogging:\n  level:\n    org.thymeleaf.TemplateEngine: OFF\n  file:\n    name: ${halo.work-dir}/logs/halo.log\n  logback:\n    rollingpolicy:\n      max-file-size: 10MB\n      total-size-cap: 1GB\n      max-history: 0\n\nmanagement:\n  endpoints:\n    web:\n      exposure:\n        include: \"*\"\n  endpoint:\n    shutdown:\n      access: unrestricted\n    heapdump:\n      access: unrestricted\n    health:\n      show-details: when-authorized\n      show-components: when-authorized\n      roles: super-role\n      probes:\n        enabled: true\n  info:\n    java:\n      enabled: true\n    os:\n      enabled: true\n\nresilience4j.ratelimiter:\n  configs:\n    authentication:\n      limitForPeriod: 3\n      limitRefreshPeriod: 1m\n      timeoutDuration: 0\n    comment-creation:\n      limitForPeriod: 10\n      limitRefreshPeriod: 1m\n      timeoutDuration: 0s\n    signup:\n      limitForPeriod: 3\n      limitRefreshPeriod: 1h\n      timeoutDuration: 0s\n    send-email-verification-code:\n      limitForPeriod: 1\n      limitRefreshPeriod: 1m\n      timeoutDuration: 0s\n    verify-email:\n      limitForPeriod: 3\n      limitRefreshPeriod: 1h\n      timeoutDuration: 0s\n    send-password-reset-email:\n      limitForPeriod: 10\n      limitRefreshPeriod: 1m\n      timeoutDuration: 0s\n    password-reset-verification:\n      limitForPeriod: 10\n      limitRefreshPeriod: 1m\n      timeoutDuration: 0s\nr2dbc:\n  migrate:\n    resources-path: classpath:/db/migration/${spring.sql.init.platform}/*.sql\n    dialect: ${spring.sql.init.platform}\n"
  },
  {
    "path": "application/src/main/resources/banner.txt",
    "content": "${AnsiColor.BLUE}\n    __  __      __\n   / / / /___ _/ /___\n  / /_/ / __ `/ / __ \\\n / __  / /_/ / / /_/ /\n/_/ /_/\\__,_/_/\\____/\n${AnsiColor.BRIGHT_YELLOW}\nVersion: ${application.version}${AnsiColor.DEFAULT}"
  },
  {
    "path": "application/src/main/resources/config/i18n/messages.properties",
    "content": "# Title definitions\nproblemDetail.title.org.springframework.web.server.ServerWebInputException=Bad Request\nproblemDetail.title.run.halo.app.infra.exception.UnsatisfiedAttributeValueException=Unsatisfied Request Attribute value\nproblemDetail.title.org.springframework.web.server.UnsupportedMediaTypeStatusException=Unsupported Media Type\nproblemDetail.title.org.springframework.web.server.MissingRequestValueException=Missing Request Value\nproblemDetail.title.org.springframework.web.server.UnsatisfiedRequestParameterException=Unsatisfied Request Parameter\nproblemDetail.title.org.springframework.web.bind.support.WebExchangeBindException=Data Binding or Validation Failure\nproblemDetail.title.org.springframework.web.server.NotAcceptableStatusException=Not Acceptable\nproblemDetail.title.org.springframework.web.server.ServerErrorException=Server Error\nproblemDetail.title.org.springframework.web.server.MethodNotAllowedException=Method Not Allowed\nproblemDetail.title.org.springframework.security.authentication.BadCredentialsException=Bad Credentials\nproblemDetail.title.run.halo.app.extension.exception.SchemaViolationException=Schema Violation\nproblemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=Attachment Already Exists\nproblemDetail.title.run.halo.app.infra.exception.FileTypeNotAllowedException=File Type Not Allowed\nproblemDetail.title.run.halo.app.infra.exception.FileSizeExceededException=File Size Exceeded\nproblemDetail.title.run.halo.app.infra.exception.AccessDeniedException=Access Denied\nproblemDetail.title.run.halo.app.infra.exception.RequestRestrictedException=Request Restricted\nproblemDetail.title.reactor.core.Exceptions.RetryExhaustedException=Retry Exhausted\nproblemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=Theme Install Error\nproblemDetail.title.run.halo.app.infra.exception.ThemeUpgradeException=Theme Upgrade Error\nproblemDetail.title.run.halo.app.infra.exception.ThemeAlreadyExistsException=Theme Already Exists Error\nproblemDetail.title.run.halo.app.infra.exception.PluginInstallationException=Plugin Install Error\nproblemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin Already Exists Error\nproblemDetail.title.run.halo.app.infra.exception.DuplicateNameException=Duplicate Name Error\nproblemDetail.title.run.halo.app.infra.exception.RateLimitExceededException=Request Not Permitted\nproblemDetail.title.run.halo.app.infra.exception.NotFoundException=Resource Not Found\nproblemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=Email Verification Failed\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=Cyclic Dependency Detected\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies Not Found\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Wrong Dependency Version\nproblemDetail.title.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Dependents Not Disabled\nproblemDetail.title.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Dependencies Not Enabled\nproblemDetail.title.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=User Already Bound Error\nproblemDetail.title.run.halo.app.infra.exception.PluginRuntimeIncompatibleException=Plugin Runtime Incompatible\nproblemDetail.title.run.halo.app.infra.exception.RestrictedNameException=Restricted Name\nproblemDetail.title.internalServerError=Internal Server Error\nproblemDetail.title.conflict=Conflict\n\n# Detail definitions\nproblemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException=Content type {0} is not supported. Supported media types: {1}.\nproblemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException.parseError=Could not parse Content-Type.\nproblemDetail.org.springframework.web.server.MissingRequestValueException=Required {0} '{1}' is not present.\nproblemDetail.org.springframework.web.server.UnsatisfiedRequestParameterException=Parameter conditions \"{0}\" not met for actual request parameters.\nproblemDetail.org.springframework.web.bind.support.WebExchangeBindException=Invalid request content. Global errors: {0}. Field errors: {1}.\nproblemDetail.org.springframework.web.server.NotAcceptableStatusException=Acceptable representations: {0}.\nproblemDetail.org.springframework.web.server.NotAcceptableStatusException.parseError=Could not parse Accept header.\nproblemDetail.org.springframework.web.server.ServerErrorException={0}.\nproblemDetail.org.springframework.security.authentication.BadCredentialsException=The username or password is incorrect.\nproblemDetail.org.springframework.web.server.MethodNotAllowedException=Request method {0} is not supported. Supported methods: {1}.\nproblemDetail.run.halo.app.extension.exception.SchemaViolationException={1} of schema {0}.\nproblemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=File {0} already exists, please rename it and try again.\nproblemDetail.run.halo.app.infra.exception.DuplicateNameException=Duplicate name detected, please rename it and retry.\nproblemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin {0} already exists.\nproblemDetail.run.halo.app.infra.exception.RateLimitExceededException=API rate limit exceeded, please try again later.\nproblemDetail.run.halo.app.infra.exception.EmailVerificationFailed=Invalid email verification code.\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=A cyclic dependency was detected.\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies \"{0}\" were not found.\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Dependencies have wrong version: {0}.\nproblemDetail.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Plugin dependents {0} are not fully disabled, please disable them first.\nproblemDetail.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Plugin dependencies {0} are not fully enabled, please enable them first.\nproblemDetail.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=The user {0} has already been bound to another OAuth2 user, cannot automatically bind the current OAuth2 user.\nproblemDetail.run.halo.app.infra.exception.PluginRuntimeIncompatibleException=The plugin is incompatible with the current Halo runtime, please upgrade the plugin or downgrade the Halo runtime.\n\nproblemDetail.index.duplicateKey=The value of {0} already exists for unique index {1}, please rename it and retry.\nproblemDetail.user.email.verify.maxAttempts=Too many verification attempts, please try again later.\nproblemDetail.user.email.verify.emailInUse=The email has been used, please change the email and retry.\nproblemDetail.user.password.unsatisfied=The password does not meet the specifications.\nproblemDetail.user.username.unsatisfied=The username does not meet the specifications.\nproblemDetail.user.oldPassword.notMatch=The old password does not match.\nproblemDetail.user.password.notMatch=The password does not match.\nproblemDetail.user.signUpFailed.disallowed=System does not allow new users to register.\nproblemDetail.user.duplicateName=The username {0} already exists, please rename it and retry.\nproblemDetail.user.displayName.restricted=The display name {0} is a reserved name, please change it and retry.\nproblemDetail.comment.turnedOff=The comment function has been turned off.\nproblemDetail.comment.systemUsersOnly=Allow only system users to comment\nproblemDetail.theme.upgrade.missingManifest=Missing theme manifest file \"theme.yaml\" or \"theme.yml\".\nproblemDetail.theme.upgrade.nameMismatch=The current theme name {0} did not match the installed theme name.\nproblemDetail.theme.install.missingManifest=Missing theme manifest file \"theme.yaml\" or manifest file does not conform to the theme specification.\nproblemDetail.theme.install.alreadyExists=Theme {0} already exists.\nproblemDetail.theme.version.unsatisfied.requires=The theme requires a minimum system version of {0}, but the current version is {1}.\nproblemDetail.directoryTraversal=Directory traversal detected. Base path is {0}, but real path is {1}.\nproblemDetail.plugin.version.unsatisfied.requires=Plugin requires a minimum system version of {0}, but the current version is {1}.\nproblemDetail.plugin.missingManifest=Missing plugin manifest file \"plugin.yaml\" or manifest file does not conform to the specification.\nproblemDetail.internalServerError=Something went wrong, please try again later.\nproblemDetail.conflict=Conflict detected, please check the data and retry.\nproblemDetail.migration.backup.notFound=The backup file does not exist or has been deleted.\nproblemDetail.attachment.upload.fileSizeExceeded=Make sure the file size is less than {0}.\nproblemDetail.attachment.upload.fileTypeNotSupported=Unsupported upload of {0} type files.\nproblemDetail.attachment.upload.fileTypeNotMatch=The file type {0} does not match the file extension, and the upload is rejected.\nproblemDetail.comment.waitingForApproval=Comment is awaiting approval.\n\ntitle.visibility.identification.private=(Private)\nsignup.error.confirm-password-not-match=The confirmation password does not match the password.\nsignup.error.email-code.invalid=Invalid email code.\nsignup.error.email.already-taken=Email address is already taken.\n\nvalidation.error.email.pattern=The email format is incorrect\nvalidation.error.username.pattern=The username can only be lowercase and can only contain letters, numbers, hyphens, and dots, starting and ending with characters.\nvalidation.error.password.pattern=The password can only use uppercase and lowercase letters (A-Z, a-z), numbers (0-9), and the following special characters: !@#$%^&*\nvalidation.error.password.size=The password length must be between {0} and {1}\n"
  },
  {
    "path": "application/src/main/resources/config/i18n/messages_es.properties",
    "content": "# Title definitions\nproblemDetail.title.org.springframework.web.server.ServerWebInputException=Solicitud incorrecta\nproblemDetail.title.run.halo.app.infra.exception.UnsatisfiedAttributeValueException=Valor de atributo de solicitud no satisfecho\nproblemDetail.title.org.springframework.web.server.UnsupportedMediaTypeStatusException=Tipo de medio no soportado\nproblemDetail.title.org.springframework.web.server.MissingRequestValueException=Valor de solicitud faltante\nproblemDetail.title.org.springframework.web.server.UnsatisfiedRequestParameterException=Parámetro de solicitud no satisfecho\nproblemDetail.title.org.springframework.web.bind.support.WebExchangeBindException=Fallo en la validación o vinculación de datos\nproblemDetail.title.org.springframework.web.server.NotAcceptableStatusException=No aceptable\nproblemDetail.title.org.springframework.web.server.ServerErrorException=Error del servidor\nproblemDetail.title.org.springframework.web.server.MethodNotAllowedException=Método no permitido\nproblemDetail.title.org.springframework.security.authentication.BadCredentialsException=Credenciales incorrectas\nproblemDetail.title.run.halo.app.extension.exception.SchemaViolationException=Violación de esquema\nproblemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=El archivo adjunto ya existe\nproblemDetail.title.run.halo.app.infra.exception.FileTypeNotAllowedException=Tipo de archivo no permitido\nproblemDetail.title.run.halo.app.infra.exception.FileSizeExceededException=Tamaño de archivo excedido\nproblemDetail.title.run.halo.app.infra.exception.AccessDeniedException=Acceso denegado\nproblemDetail.title.run.halo.app.infra.exception.RequestRestrictedException=Solicitud restringida\nproblemDetail.title.reactor.core.Exceptions.RetryExhaustedException=Reintentos agotados\nproblemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=Error de instalación del tema\nproblemDetail.title.run.halo.app.infra.exception.ThemeUpgradeException=Error de actualización del tema\nproblemDetail.title.run.halo.app.infra.exception.ThemeAlreadyExistsException=Error: El tema ya existe\nproblemDetail.title.run.halo.app.infra.exception.PluginInstallationException=Error de instalación del complemento\nproblemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=Error: El complemento ya existe\nproblemDetail.title.run.halo.app.infra.exception.DuplicateNameException=Error de nombre duplicado\nproblemDetail.title.run.halo.app.infra.exception.RateLimitExceededException=Solicitud no permitida (Límite de tasa excedido)\nproblemDetail.title.run.halo.app.infra.exception.NotFoundException=Recurso no encontrado\nproblemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=Fallo en la verificación del correo electrónico\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=Dependencia cíclica detectada\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencias no encontradas\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Versión de dependencia incorrecta\nproblemDetail.title.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Dependientes no desactivados\nproblemDetail.title.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Dependencias no activadas\nproblemDetail.title.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=Error: Usuario ya vinculado\nproblemDetail.title.run.halo.app.infra.exception.PluginRuntimeIncompatibleException=Entorno de ejecución del complemento incompatible\nproblemDetail.title.run.halo.app.infra.exception.RestrictedNameException=Nombre restringido\nproblemDetail.title.internalServerError=Error interno del servidor\nproblemDetail.title.conflict=Conflicto\n\n# Detail definitions\nproblemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException=El tipo de contenido {0} no está soportado. Tipos de medios soportados: {1}.\nproblemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException.parseError=No se pudo analizar el Content-Type.\nproblemDetail.org.springframework.web.server.MissingRequestValueException=El {0} requerido ''{1}'' no está presente.\nproblemDetail.org.springframework.web.server.UnsatisfiedRequestParameterException=No se cumplen las condiciones del parámetro \"{0}\" para los parámetros de la solicitud actual.\nproblemDetail.org.springframework.web.bind.support.WebExchangeBindException=Contenido de la solicitud inválido. Errores globales: {0}. Errores de campo: {1}.\nproblemDetail.org.springframework.web.server.NotAcceptableStatusException=Representaciones aceptables: {0}.\nproblemDetail.org.springframework.web.server.NotAcceptableStatusException.parseError=No se pudo analizar el encabezado Accept.\nproblemDetail.org.springframework.web.server.ServerErrorException={0}.\nproblemDetail.org.springframework.security.authentication.BadCredentialsException=El nombre de usuario o la contraseña son incorrectos.\nproblemDetail.org.springframework.web.server.MethodNotAllowedException=El método de solicitud {0} no está soportado. Métodos soportados: {1}.\nproblemDetail.run.halo.app.extension.exception.SchemaViolationException={1} del esquema {0}.\nproblemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=El archivo {0} ya existe, por favor cámbiale el nombre e inténtalo de nuevo.\nproblemDetail.run.halo.app.infra.exception.DuplicateNameException=Se ha detectado un nombre duplicado, por favor cámbiale el nombre e inténtalo de nuevo.\nproblemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=El complemento {0} ya existe.\nproblemDetail.run.halo.app.infra.exception.RateLimitExceededException=Se ha excedido el límite de tasa de la API, por favor inténtalo de nuevo más tarde.\nproblemDetail.run.halo.app.infra.exception.EmailVerificationFailed=Código de verificación de correo electrónico inválido.\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=Se ha detectado una dependencia cíclica.\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=No se encontraron las dependencias \"{0}\".\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Las dependencias tienen una versión incorrecta: {0}.\nproblemDetail.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Los dependientes del complemento {0} no están totalmente desactivados, por favor desactívalos primero.\nproblemDetail.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Las dependencias del complemento {0} no están totalmente activadas, por favor actívalas primero.\nproblemDetail.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=El usuario {0} ya ha sido vinculado a otro usuario de OAuth2, no se puede vincular automáticamente el usuario de OAuth2 actual.\nproblemDetail.run.halo.app.infra.exception.PluginRuntimeIncompatibleException=El complemento es incompatible con el entorno de ejecución de Halo actual, por favor actualiza el complemento o degrada el entorno de Halo.\n\nproblemDetail.index.duplicateKey=El valor de {0} ya existe para el índice único {1}, por favor cámbiale el nombre e inténtalo de nuevo.\nproblemDetail.user.email.verify.maxAttempts=Demasiados intentos de verificación, por favor inténtalo de nuevo más tarde.\nproblemDetail.user.email.verify.emailInUse=El correo electrónico ya está en uso, por favor cámbialo e inténtalo de nuevo.\nproblemDetail.user.password.unsatisfied=La contraseña no cumple con las especificaciones.\nproblemDetail.user.username.unsatisfied=El nombre de usuario no cumple con las especificaciones.\nproblemDetail.user.oldPassword.notMatch=La contraseña antigua no coincide.\nproblemDetail.user.password.notMatch=La contraseña no coincide.\nproblemDetail.user.signUpFailed.disallowed=El sistema no permite que nuevos usuarios se registren.\nproblemDetail.user.duplicateName=El nombre de usuario {0} ya existe, por favor cámbiale el nombre e inténtalo de nuevo.\nproblemDetail.user.displayName.restricted=El nombre para mostrar {0} es un nombre reservado, por favor cámbialo e inténtalo de nuevo.\nproblemDetail.comment.turnedOff=La función de comentarios ha sido desactivada.\nproblemDetail.comment.systemUsersOnly=Solo se permite comentar a los usuarios del sistema.\nproblemDetail.theme.upgrade.missingManifest=Falta el archivo de manifiesto del tema \"theme.yaml\" o \"theme.yml\".\nproblemDetail.theme.upgrade.nameMismatch=El nombre del tema actual {0} no coincide con el nombre del tema instalado.\nproblemDetail.theme.install.missingManifest=Falta el archivo de manifiesto del tema \"theme.yaml\" o el archivo no cumple con las especificaciones del tema.\nproblemDetail.theme.install.alreadyExists=El tema {0} ya existe.\nproblemDetail.theme.version.unsatisfied.requires=El tema requiere una versión mínima del sistema de {0}, pero la versión actual es {1}.\nproblemDetail.directoryTraversal=Travesía de directorios detectada. La ruta base es {0}, pero la ruta real es {1}.\nproblemDetail.plugin.version.unsatisfied.requires=El complemento requiere una versión mínima del sistema de {0}, pero la versión actual es {1}.\nproblemDetail.plugin.missingManifest=Falta el archivo de manifiesto del complemento \"plugin.yaml\" o el archivo no cumple con las especificaciones.\nproblemDetail.internalServerError=Algo salió mal, por favor inténtalo de nuevo más tarde.\nproblemDetail.conflict=Conflicto detectado, por favor revisa los datos e inténtalo de nuevo.\nproblemDetail.migration.backup.notFound=El archivo de respaldo no existe o ha sido eliminado.\nproblemDetail.attachment.upload.fileSizeExceeded=Asegúrate de que el tamaño del archivo sea inferior a {0}.\nproblemDetail.attachment.upload.fileTypeNotSupported=Carga no soportada para archivos de tipo {0}.\nproblemDetail.attachment.upload.fileTypeNotMatch=El tipo de archivo {0} no coincide con la extensión del archivo y la carga ha sido rechazada.\nproblemDetail.comment.waitingForApproval=El comentario está esperando aprobación.\n\ntitle.visibility.identification.private=(Privado)\nsignup.error.confirm-password-not-match=La contraseña de confirmación no coincide con la contraseña.\nsignup.error.email-code.invalid=Código de correo electrónico inválido.\nsignup.error.email.already-taken=La dirección de correo electrónico ya está en uso.\n\nvalidation.error.email.pattern=El formato del correo electrónico es incorrecto\nvalidation.error.username.pattern=El nombre de usuario solo puede estar en minúsculas y solo puede contener letras, números, guiones y puntos, comenzando y terminando con caracteres.\nvalidation.error.password.pattern=La contraseña solo puede usar letras mayúsculas y minúsculas (A-Z, a-z), números (0-9) y los siguientes caracteres especiales: !@#$%^&*\nvalidation.error.password.size=La longitud de la contraseña debe estar entre {0} y {1}\n"
  },
  {
    "path": "application/src/main/resources/config/i18n/messages_zh.properties",
    "content": "problemDetail.title.org.springframework.web.server.ServerWebInputException=请求参数有误\nproblemDetail.title.org.springframework.security.authentication.BadCredentialsException=无效凭据\nproblemDetail.title.run.halo.app.infra.exception.UnsatisfiedAttributeValueException=请求参数属性值不满足要求\nproblemDetail.title.run.halo.app.infra.exception.PluginInstallationException=插件安装失败\nproblemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=附件已存在\nproblemDetail.title.run.halo.app.infra.exception.FileTypeNotAllowedException=文件类型不允许\nproblemDetail.title.run.halo.app.infra.exception.FileSizeExceededException=文件大小超出限制\nproblemDetail.title.run.halo.app.infra.exception.RequestRestrictedException=请求受限\nproblemDetail.title.run.halo.app.infra.exception.DuplicateNameException=名称重复\nproblemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=插件已存在\nproblemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主题安装失败\nproblemDetail.title.run.halo.app.infra.exception.ThemeAlreadyExistsException=主题已存在\nproblemDetail.title.run.halo.app.infra.exception.RateLimitExceededException=请求限制\nproblemDetail.title.run.halo.app.infra.exception.NotFoundException=资源不存在\nproblemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=邮箱验证失败\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=循环依赖\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖未找到\nproblemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本错误\nproblemDetail.title.run.halo.app.infra.exception.PluginDependentsNotDisabledException=子插件未禁用\nproblemDetail.title.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=依赖未启用\nproblemDetail.title.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=用户已绑定错误\nproblemDetail.title.run.halo.app.infra.exception.PluginRuntimeIncompatibleException=插件运行时不兼容\nproblemDetail.title.run.halo.app.infra.exception.RestrictedNameException=名称受限\n\nproblemDetail.title.internalServerError=服务器内部错误\nproblemDetail.title.conflict=冲突\n\nproblemDetail.org.springframework.security.authentication.BadCredentialsException=用户名或密码错误。\nproblemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=文件 {0} 已存在，建议更名后重试。\nproblemDetail.run.halo.app.infra.exception.DuplicateNameException=检测到有重复的名称，请重命名后重试。\nproblemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=插件 {0} 已经存在。\nproblemDetail.run.halo.app.infra.exception.RateLimitExceededException=请求过于频繁，请稍候再试。\nproblemDetail.run.halo.app.infra.exception.EmailVerificationFailed=验证码错误或已失效。\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=检测到循环依赖。\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖“{0}”未找到。\nproblemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本有误：{0}。\nproblemDetail.run.halo.app.infra.exception.PluginDependentsNotDisabledException=子插件 {0} 未完全禁用，请先禁用它们。\nproblemDetail.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=插件依赖 {0} 未完全启用，请先启用它们。\nproblemDetail.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=用户 {0} 已经绑定到另一个 OAuth2 用户，无法自动绑定当前 OAuth2 用户。\nproblemDetail.run.halo.app.infra.exception.PluginRuntimeIncompatibleException=插件和当前 Halo 运行时不兼容，请升级插件或降级 Halo 运行时。\n\nproblemDetail.index.duplicateKey=唯一索引 {1} 中的值 {0} 已存在，请更名后重试。\nproblemDetail.user.email.verify.maxAttempts=尝试次数过多，请稍候再试。\nproblemDetail.user.email.verify.emailInUse=邮箱已被使用, 请更换邮箱后重试。\nproblemDetail.user.password.unsatisfied=密码不符合规范。\nproblemDetail.user.username.unsatisfied=用户名不符合规范。\nproblemDetail.user.oldPassword.notMatch=旧密码不匹配。\nproblemDetail.user.password.notMatch=密码不匹配。\nproblemDetail.user.signUpFailed.disallowed=系统不允许注册新用户。\nproblemDetail.user.duplicateName=用户名 {0} 已存在，请更换用户名后重试。\nproblemDetail.user.displayName.restricted=显示名称 {0} 为保留名称，请更换后重试。\nproblemDetail.plugin.version.unsatisfied.requires=插件要求一个最小的系统版本为 {0}, 但当前版本为 {1}。\nproblemDetail.plugin.missingManifest=缺少 plugin.yaml 配置文件或配置文件不符合规范。\nproblemDetail.theme.version.unsatisfied.requires=主题要求一个最小的系统版本为 {0}, 但当前版本为 {1}。\nproblemDetail.theme.install.missingManifest=缺少 theme.yaml 配置文件或配置文件不符合规范。\nproblemDetail.theme.install.alreadyExists=主题 {0} 已存在。\nproblemDetail.internalServerError=服务器内部发生错误，请稍候再试。\nproblemDetail.conflict=检测到冲突，请检查数据后重试。\nproblemDetail.migration.backup.notFound=备份文件不存在或已删除。\nproblemDetail.attachment.upload.fileSizeExceeded=最大支持上传 {0} 大小的文件。\nproblemDetail.attachment.upload.fileTypeNotSupported=不支持上传 {0} 类型的文件。\nproblemDetail.attachment.upload.fileTypeNotMatch=文件类型 {0} 与文件扩展名不匹配，上传被拒绝。\nproblemDetail.comment.waitingForApproval=评论审核中。\n\ntitle.visibility.identification.private=（私有）\nsignup.error.confirm-password-not-match=确认密码与密码不匹配。\nsignup.error.email-code.invalid=邮箱验证码无效。\nsignup.error.email.already-taken=邮箱地址已被注册。\n\nvalidation.error.email.pattern=邮箱格式不正确\nvalidation.error.username.pattern=用户名只能小写且只能包含字母、数字、中划线和点，以字符开头和结尾\nvalidation.error.password.pattern=密码只能使用大小写字母 (A-Z, a-z)、数字 (0-9)，以及以下特殊字符: !@#$%^&*.?\nvalidation.error.password.size=密码长度必须在 {0} 到 {1} 之间\n"
  },
  {
    "path": "application/src/main/resources/db/migration/h2/.gitkeep",
    "content": ""
  },
  {
    "path": "application/src/main/resources/db/migration/mariadb/.gitkeep",
    "content": ""
  },
  {
    "path": "application/src/main/resources/db/migration/mysql/.gitkeep",
    "content": ""
  },
  {
    "path": "application/src/main/resources/db/migration/postgresql/.gitkeep",
    "content": ""
  },
  {
    "path": "application/src/main/resources/extensions/attachment-local-policy.yaml",
    "content": "apiVersion: storage.halo.run/v1alpha1\nkind: PolicyTemplate\nmetadata:\n  name: local\nspec:\n  displayName: 本地存储\n  settingName: local-policy-template-setting\n---\napiVersion: storage.halo.run/v1alpha1\nkind: Policy\nmetadata:\n  name: default-policy\n  finalizers:\n    - system-protection\nspec:\n  displayName: 本地存储\n  templateName: local\n  configMapName: default-policy-config\n---\napiVersion: v1alpha1\nkind: ConfigMap\nmetadata:\n  name: default-policy-config\n  labels:\n    storage.halo.run/policy-owner: default-policy\n    halo.run/do-not-overwrite: \"true\"\ndata:\n  default: \"{\\\"location\\\":\\\"\\\"}\"\n---\napiVersion: v1alpha1\nkind: Setting\nmetadata:\n  name: local-policy-template-setting\nspec:\n  forms:\n    - group: default\n      label: Default\n      formSchema:\n        - $formkit: text\n          name: location\n          label: 存储位置\n          help: ~/.halo2/attachments/upload 下的子目录\n        - $formkit: text\n          name: maxFileSize\n          label: 最大单文件大小\n          validation: [ [ 'matches', '/^(0|[1-9]\\d*)(?:[KMG]B)?$/' ] ]\n          validation-visibility: \"live\"\n          validation-messages:\n            matches: \"输入格式错误，遵循：整数 + 大写的单位（KB, MB, GB）\"\n          help: \"0 表示不限制，示例：5KB、10MB、1GB\"\n        - $formkit: checkbox\n          name: allowedFileTypes\n          label: 文件类型限制\n          help: 限制允许上传的文件类型\n          options:\n            - label: 无限制\n              value: ALL\n            - label: 图片\n              value: IMAGE\n            - label: SVG\n              value: SVG\n            - label: 视频\n              value: VIDEO\n            - label: 音频\n              value: AUDIO\n            - label: 文档\n              value: DOCUMENT\n            - label: 压缩包\n              value: ARCHIVE\n        - $formkit: checkbox\n          name: alwaysRenameFilename\n          id: alwaysRenameFilename\n          label: 是否总是重命名文件名\n          help: 勾选后上传后的文件名将被重命名\n          value: false\n        - $formkit: group\n          if: $get(alwaysRenameFilename).value\n          id: renameStrategy\n          name: renameStrategy\n          label: 重命名策略\n          value:\n            method: RANDOM\n            randomLength: 32\n          children:\n            - $formkit: radio\n              name: method\n              label: 重命名方法\n              options:\n                - label: 随机字符串\n                  value: RANDOM\n                - label: UUID\n                  value: UUID\n                - label: 时间戳（毫秒级）\n                  value: TIMESTAMP\n            - $formkit: number\n              number: integer\n              if: \"$value.method === RANDOM\"\n              name: randomLength\n              id: randomLength\n              label: 随机文件名长度\n              help: 默认值为 32。因为文件名的长度限制，随机文件名的长度范围为 [8, 64]。\n              validation: \"between:8,64\"\n              validation-visibility: live\n              min: 8\n              max: 64\n            - $formkit: checkbox\n              name: excludeOriginalFilename\n              label: 是否排除原始文件名\n              help: 勾选后重命名后的文件名将不包含原始文件名\n---\napiVersion: storage.halo.run/v1alpha1\nkind: Group\nmetadata:\n  name: user-avatar-group\n  labels:\n    halo.run/hidden: \"true\"\n  finalizers:\n    - system-protection\nspec:\n  displayName: UserAvatar"
  },
  {
    "path": "application/src/main/resources/extensions/authproviders.yaml",
    "content": "apiVersion: auth.halo.run/v1alpha1\nkind: AuthProvider\nmetadata:\n  name: local\n  labels:\n    auth.halo.run/auth-binding: \"false\"\n    auth.halo.run/privileged: \"true\"\n  finalizers:\n    - system-protection\nspec:\n  displayName: Local\n  description: Built-in authentication for Halo.\n  logo: /images/login-methods/login-with-credentials.svg\n  website: https://www.halo.run\n  authenticationUrl: /login\n  method: post\n  rememberMeSupport: true\n  authType: form"
  },
  {
    "path": "application/src/main/resources/extensions/extension-definitions.yaml",
    "content": "## TODO: Currently, Halo does not support i18n for configuration file descriptions\n## So Simplified Chinese is temporarily used as the default description language.\n\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: username-password-logout-handler\n  labels:\n    auth.halo.run/extension-point-name: \"additional-webfilter\"\nspec:\n  className: run.halo.app.security.authentication.login.UsernamePasswordLogoutHandler\n  extensionPointName: additional-webfilter\n  displayName: \"用户名密码注销处理器\"\n  description: \"用于用户名和密码认证的注销处理器\"\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: delegating-logout-page-generating-webfilter\n  labels:\n    auth.halo.run/extension-point-name: \"additional-webfilter\"\nspec:\n  className: run.halo.app.security.authentication.login.DelegatingLogoutPageGeneratingWebFilter\n  extensionPointName: additional-webfilter\n  displayName: \"注销页面生成过滤器\"\n  description: \"用于生成默认的注销页面\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: halo-email-notifier\nspec:\n  className: run.halo.app.notification.EmailNotifier\n  extensionPointName: reactive-notifier\n  displayName: \"邮件通知器\"\n  description: \"支持通过电子邮件向用户发送通知\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: search-engine-lucene\nspec:\n  className: run.halo.app.search.lucene.LuceneSearchEngine\n  extensionPointName: search-engine\n  displayName: \"Lucene 搜索引擎\"\n  description: \"Halo 自带的本地搜索引擎\"\n  icon: /images/extension-points/lucene.png\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: post-content-thumbnail-handler\nspec:\n  className: run.halo.app.content.PostContentThumbnailHandler\n  extensionPointName: reactive-post-content-handler\n  displayName: \"文章内容缩略图处理\"\n  description: \"处理文章的 HTML 内容为 img 标签追加缩略图\"\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: page-content-thumbnail-handler\nspec:\n  className: run.halo.app.content.PageContentThumbnailHandler\n  extensionPointName: reactive-page-content-handler\n  displayName: \"自定义页面内容缩略图处理\"\n  description: \"处理页面的 HTML 内容为 img 标签追加缩略图\"\n"
  },
  {
    "path": "application/src/main/resources/extensions/extensionpoint-definitions.yaml",
    "content": "## TODO: Currently, Halo does not support i18n for configuration file descriptions\n## So Simplified Chinese is temporarily used as the default description language.\n\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: additional-webfilter\nspec:\n  className: run.halo.app.security.AdditionalWebFilter\n  displayName: \"附加 Web 过滤器\"\n  type: MULTI_INSTANCE\n  description: \"用于 Web 请求的链式处理，可以用来实现跨领域、与应用无关的需求，如安全性、超时等\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: reactive-post-content-handler\nspec:\n  className: run.halo.app.theme.ReactivePostContentHandler\n  displayName: \"文章内容处理器\"\n  type: MULTI_INSTANCE\n  description: \"扩展在主题侧显示的文章内容\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: reactive-singlepage-content-handler\nspec:\n  className: run.halo.app.theme.ReactiveSinglePageContentHandler\n  displayName: \"页面内容处理器\"\n  type: MULTI_INSTANCE\n  description: \"扩展在主题侧显示的页面内容\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: comment-widget\nspec:\n  className: run.halo.app.theme.dialect.CommentWidget\n  displayName: \"评论组件\"\n  type: SINGLETON\n  description: \"扩展在文章页面中显示的评论组件\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: username-password-authentication-manager\nspec:\n  className: run.halo.app.security.authentication.login.UsernamePasswordAuthenticationManager\n  displayName: \"用户名密码认证管理器\"\n  type: SINGLETON\n  description: \"扩展用户名密码认证\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: reactive-notifier\nspec:\n  className: run.halo.app.notification.ReactiveNotifier\n  displayName: \"消息通知器\"\n  type: MULTI_INSTANCE\n  description: \"扩展消息通知器，以向用户发送通知\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: search-engine\nspec:\n  className: run.halo.app.search.SearchEngine\n  displayName: \"搜索引擎\"\n  type: SINGLETON\n  description: \"扩展内容搜索引擎\"\n\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: template-footer-processor\nspec:\n  className: run.halo.app.theme.dialect.TemplateFooterProcessor\n  displayName: 页脚标签内容处理器\n  type: MULTI_INSTANCE\n  description: \"提供用于扩展 <halo:footer/> 标签内容的扩展方式。\"\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: excerpt-generator\nspec:\n  className: run.halo.app.content.ExcerptGenerator\n  displayName: 摘要生成器\n  type: SINGLETON\n  description: \"提供自动生成摘要的方式扩展，如使用算法提取或使用 AI 生成。\"\n---\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionPointDefinition\nmetadata:\n  name: thumbnail-provider\nspec:\n  className: run.halo.app.core.attachment.ThumbnailProvider\n  displayName: 图片缩略图生成\n  type: MULTI_INSTANCE\n  description: \"提供生成图片缩略图的扩展方式\"\n"
  },
  {
    "path": "application/src/main/resources/extensions/notification-templates.yaml",
    "content": "apiVersion: notification.halo.run/v1alpha1\nkind: NotificationTemplate\nmetadata:\n  name: template-new-comment-on-post\nspec:\n  reasonSelector:\n    reasonType: new-comment-on-post\n    language: default\n  template:\n    title: \"[(${commenter})] 评论了你的文章《[(${postTitle})]》\"\n    rawBody: |\n      [(${subscriber.displayName})] 你好：\n\n        [(${commenter})] 评论了你的文章 《[(${postTitle})]》，以下是评论的具体内容：\n\n        [(${content})]\n    htmlBody: |\n      <div class=\"notification-content\">\n        <div class=\"head\">\n          <p class=\"honorific\" th:text=\"|${subscriber.displayName} 你好：|\"></p>\n        </div>\n        <div class=\"body\">\n          <p>\n            <span th:text=\"${commenter}\"></span> 评论了你的文章\n            <a th:href=\"${postUrl}\" target=\"_blank\" th:text=\"|《${postTitle}》|\"></a>\n            <span>，以下是评论的具体内容：</span>\n          </p>\n          <div class=\"content\" th:utext=\"${content}\"></div>\n        </div>\n        <div></div>\n      </div>\n\n---\napiVersion: notification.halo.run/v1alpha1\nkind: NotificationTemplate\nmetadata:\n  name: template-new-comment-on-single-page\nspec:\n  reasonSelector:\n    reasonType: new-comment-on-single-page\n    language: default\n  template:\n    title: \"[(${commenter})] 评论了你的页面《[(${pageTitle})]》\"\n    rawBody: |\n      [(${subscriber.displayName})] 你好：\n\n        [(${commenter})] 评论了你的页面 《[(${pageTitle})]》，以下是评论的具体内容：\n\n        [(${content})]\n    htmlBody: |\n      <div class=\"notification-content\">\n        <div class=\"head\">\n          <p class=\"honorific\" th:text=\"|${subscriber.displayName} 你好：|\"></p>\n        </div>\n        <div class=\"body\">\n          <p>\n            <span th:text=\"${commenter}\"></span> 评论了你的页面\n            <a th:href=\"${pageUrl}\" target=\"_blank\" th:text=\"|《${pageTitle}》|\"></a>\n            <span>，以下是评论的具体内容：</span>\n          </p>\n          <div class=\"content\" th:utext=\"${content}\"></div>\n        </div>\n        <div></div>\n      </div>\n\n---\napiVersion: notification.halo.run/v1alpha1\nkind: NotificationTemplate\nmetadata:\n  name: template-someone-replied-to-you\nspec:\n  reasonSelector:\n    reasonType: someone-replied-to-you\n    language: default\n  template:\n    title: \"[(${replier})] 在评论中回复了你\"\n    rawBody: |\n      [(${subscriber.displayName})] 你好：\n\n        [(${replier})] 在评论“[(${isQuoteReply ? quoteContent : commentContent})]”中回复了你，以下是回复的具体内容：\n\n        [(${content})]\n    htmlBody: |\n      <div class=\"notification-content\">\n        <div class=\"head\">\n          <p class=\"honorific\" th:text=\"|${subscriber.displayName} 你好：|\"></p>\n        </div>\n        <div class=\"body\">\n          <p>\n            <span th:text=\"${replier}\"></span> 在\n            <a\n              th:href=\"${commentSubjectUrl}\"\n              target=\"_blank\"\n              th:text=\"|《${commentSubjectTitle}》|\"\n            ></a>\n            <span>中回复了你。</span>\n          </p>\n          <p>你的评论：</p>\n          <div class=\"content\" th:utext=\"${isQuoteReply ? quoteContent : commentContent}\"></div>\n          <p>回复的内容：</p>\n          <div class=\"content\" th:utext=\"${content}\"></div>\n        </div>\n        <div></div>\n      </div>\n---\napiVersion: notification.halo.run/v1alpha1\nkind: NotificationTemplate\nmetadata:\n  name: template-email-verification\nspec:\n  reasonSelector:\n    reasonType: email-verification\n    language: default\n  template:\n    title: \"邮箱验证-[(${site.title})]\"\n    rawBody: |\n      【[(${site.title})]】你的邮箱验证码是：[(${code})]，请在 [(${expirationAtMinutes})] 分钟内完成验证。\n    htmlBody: |\n      <div class=\"notification-content\">\n        <div class=\"head\">\n          <p class=\"honorific\" th:text=\"|${username} 你好：|\"></p>\n        </div>\n        <div class=\"body\">\n          <p>使用下面的动态验证码（OTP）验证您的电子邮件地址。</p>\n          <div class=\"verify-code\" style=\"font-size:24px;line-height:24px;color:#333;\">\n            <b th:text=\"${code}\"></b>\n          </div>\n          <p th:text=\"|动态验证码的有效期为 ${expirationAtMinutes} 分钟。|\"></p>\n          <p>如果您没有尝试验证您的电子邮件地址，请忽略此电子邮件。</p>\n        </div>\n      </div>\n---\napiVersion: notification.halo.run/v1alpha1\nkind: NotificationTemplate\nmetadata:\n  name: template-reset-password-by-email\nspec:\n  reasonSelector:\n    reasonType: reset-password-by-email\n    language: default\n  template:\n    title: \"重置密码-[(${site.title})]\"\n    rawBody: |\n      【[(${site.title})]】你已经请求了重置密码，可以链接来重置密码：[(${link})]，请在 [(${expirationAtMinutes})] 分钟内完成重置。\n    htmlBody: |\n      <div class=\"notification-content\">\n        <div class=\"head\">\n          <p class=\"honorific\" th:text=\"|${username} 你好：|\"></p>\n        </div>\n        <div class=\"body\">\n          <p>你已经请求了重置密码，可以点击下面的链接来重置密码：</p>\n          <div class=\"reset-link\" style=\"line-height:24px;\">\n            <span th:text=\"${link}\"></span>\n          </div>\n          <p th:text=\"|链接有效期为 ${expirationAtMinutes} 分钟，请尽快完成重置。|\"></p>\n          <p>如果您没有请求重置密码，请忽略此电子邮件。</p>\n        </div>\n      </div>\n---\napiVersion: notification.halo.run/v1alpha1\nkind: NotificationTemplate\nmetadata:\n  name: template-new-device-login\nspec:\n  reasonSelector:\n    reasonType: new-device-login\n    language: default\n  template:\n    title: \"你的 [(${site.title})] 账号被用于在 [(${os})] 上登录\"\n    rawBody: |\n      [(${subscriber.displayName})] 你好：\n\n        你的 [(${site.title})] 账号被用于在 [(${os})] 的 [(${browser})] 上登录。\n        时间：[(${loginTime})]\n        IP 地址：[(${ipAddress})] \n        如果你知悉上述信息，请忽略此电子邮件。\n        如果你最近没有使用你的 Halo 账号登录并相信有人可能访问了你的账户，请尽快重设你的密码。\n    htmlBody: |\n      <div class=\"notification-content\">\n        <div class=\"head\">\n          <p class=\"honorific\" th:text=\"|${subscriber.displayName} 你好：|\"></p>\n        </div>\n        <div class=\"body\">\n          <p th:text=\"|你的 ${site.title} 账号被用于在 ${os} 的 ${browser} 上登录：|\"></p>\n          <div class=\"device-info\">\n            <p th:text=\"|时间： ${loginTime}|\"></p>\n            <p th:text=\"|IP 地址： ${ipAddress}|\"></p>\n          </div>\n          <p>如果你知悉上述信息，请忽略此电子邮件。</p>\n          <p th:text=\"|如果你最近没有使用你的 ${site.title} 账号登录并相信有人可能访问了你的账户，请尽快重设你的密码。|\"></p>\n        </div>\n      </div>\n"
  },
  {
    "path": "application/src/main/resources/extensions/notification.yaml",
    "content": "apiVersion: notification.halo.run/v1alpha1\nkind: NotifierDescriptor\nmetadata:\n  name: default-email-notifier\nspec:\n  displayName: '邮件通知'\n  description: '通过邮件将通知发送给用户'\n  notifierExtName: 'halo-email-notifier'\n  senderSettingRef:\n    name: 'notifier-setting-for-email'\n    group: 'sender'\n---\napiVersion: v1alpha1\nkind: Setting\nmetadata:\n  name: notifier-setting-for-email\nspec:\n  forms:\n    - group: sender\n      label: 发件设置\n      formSchema:\n        - $formkit: checkbox\n          label: \"启用邮件通知器\"\n          value: false\n          name: enable\n        - $formkit: verificationForm\n          if: \"$enable\"\n          action: /apis/console.api.notification.halo.run/v1alpha1/notifiers/default-email-notifier/verify-connection\n          label: 测试邮箱\n          children:\n            - $formkit: text\n              label: \"用户名\"\n              name: username\n              validation: required\n            - $formkit: text\n              if: \"$enable\"\n              label: \"发信地址\"\n              name: \"sender\"\n              help: \"如果用户名为实际发信地址，可忽略\"\n            - $formkit: password\n              label: \"密码\"\n              name: password\n              validation: required\n            - $formkit: text\n              label: \"显示名称\"\n              name: displayName\n            - $formkit: text\n              label: \"SMTP 服务器地址\"\n              name: host\n              validation: required\n            - $formkit: text\n              label: \"端口号\"\n              name: port\n              validation: required\n            - $formkit: select\n              label: \"加密方式\"\n              name: encryption\n              value: \"SSL\"\n              options:\n                - label: \"SSL\"\n                  value: \"SSL\"\n                - label: \"TLS\"\n                  value: \"TLS\"\n                - label: \"不加密\"\n                  value: \"NONE\"\n---\napiVersion: notification.halo.run/v1alpha1\nkind: ReasonType\nmetadata:\n  name: new-comment-on-post\n  annotations:\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"uc:posts:publish\" ]\nspec:\n  displayName: \"我的文章收到新评论\"\n  description: \"如果有读者在你的文章下方留下了新的评论，你将会收到一条通知，告诉你有新的评论。\n  这个通知事件可以帮助你及时了解读者对你的文章的反馈，以便你更好地与读者互动，提高文章的质量和受欢迎程度。\"\n  properties:\n    - name: postName\n      type: string\n      description: \"The name of the post.\"\n    - name: postOwner\n      type: string\n      description: \"The user name of the post owner.\"\n    - name: postTitle\n      type: string\n    - name: postUrl\n      type: string\n    - name: commenter\n      type: string\n      description: \"The display name of the commenter.\"\n    - name: commentName\n      type: string\n      description: \"The name of the comment.\"\n    - name: content\n      type: string\n      description: \"The content of the comment.\"\n---\napiVersion: notification.halo.run/v1alpha1\nkind: ReasonType\nmetadata:\n  name: new-comment-on-single-page\n  annotations:\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"system:singlepages:manage\" ]\nspec:\n  displayName: \"我的自定义页面收到新评论\"\n  description: \"当你创建的自定义页面收到新评论时，你将会收到一条通知，告诉你有新的评论。\"\n  properties:\n    - name: pageName\n      type: string\n      description: \"The name of the single page.\"\n    - name: pageOwner\n      type: string\n      description: \"The user name of the page owner.\"\n    - name: pageTitle\n      type: string\n    - name: pageUrl\n      type: string\n    - name: commenter\n      type: string\n      description: \"The display name of the commenter.\"\n    - name: commentName\n      type: string\n      description: \"The name of the comment.\"\n    - name: content\n      type: string\n      description: \"The content of the comment.\"\n---\napiVersion: notification.halo.run/v1alpha1\nkind: ReasonType\nmetadata:\n  name: someone-replied-to-you\nspec:\n  displayName: \"有人回复了我\"\n  description: \"如果有其他用户回复了你的评论，你将会收到一条通知，告诉你有人回复了你。\"\n  properties:\n    - name: commentName\n      type: string\n      description: \"The name of the comment.\"\n    - name: commentSubjectTitle\n      type: string\n    - name: commentSubjectUrl\n      type: string\n    - name: quoteContent\n      type: string\n      optional: true\n      description: \"The content of quoted reply.\"\n    - name: isQuoteReply\n      type: boolean\n    - name: commentContent\n      type: string\n    - name: repliedOwner\n      type: string\n      description: \"The owner of the comment or reply that has been replied to.\"\n    - name: replyOwner\n      type: string\n      description: \"The user who created the current reply.\"\n    - name: replier\n      type: string\n      description: \"The display name of the replier.\"\n    - name: replyName\n      type: string\n      description: \"The name of the reply.\"\n    - name: content\n      type: string\n      description: \"The content of the reply.\"\n---\napiVersion: notification.halo.run/v1alpha1\nkind: ReasonType\nmetadata:\n  name: email-verification\n  labels:\n    halo.run/hidden: \"true\"\nspec:\n  displayName: \"邮箱验证\"\n  description: \"当你的邮箱被用于注册账户时，会收到一条带有验证码的邮件，你需要点击邮件中的链接来验证邮箱是否属于你。\"\n  properties:\n    - name: username\n      type: string\n      description: \"The username of the user.\"\n    - name: code\n      type: string\n      description: \"The verification code.\"\n    - name: expirationAtMinutes\n      type: string\n      description: \"The expiration minutes of the verification code, such as 5 minutes.\"\n---\napiVersion: notification.halo.run/v1alpha1\nkind: ReasonType\nmetadata:\n  name: reset-password-by-email\n  labels:\n    halo.run/hidden: \"true\"\nspec:\n  displayName: \"根据邮件地址重置密码\"\n  description: \"当你通过邮件地址找回密码时，会收到一条带密码重置链接的邮件，你需要点击邮件中的链接来重置密码。\"\n  properties:\n    - name: username\n      type: string\n      description: \"The username of the user.\"\n    - name: link\n      type: string\n      description: \"The reset link.\"\n    - name: expirationAtMinutes\n      type: string\n      description: \"The expiration minutes of the reset link, such as 30 minutes.\"\n---\napiVersion: notification.halo.run/v1alpha1\nkind: ReasonType\nmetadata:\n  name: new-device-login\nspec:\n  displayName: \"新设备登录\"\n  description: \"当你的账户在新设备上登录时，你会收到一条通知，告诉你有新设备登录了你的账户。\"\n  properties:\n    - name: os\n      type: string\n      description: \"The operating system of the device.\"\n    - name: browser\n      type: string\n      description: \"The browser of the device.\"\n    - name: ipAddress\n      type: string\n      description: \"The IP address of the device.\"\n    - name: loginTime\n      type: string\n      description: \"The login time of the device.\"\n    - name: principalName\n      type: string\n      description: \"The principal name of the device.\""
  },
  {
    "path": "application/src/main/resources/extensions/role-template-actuator.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-actuator\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Actuator Management\"\n    rbac.authorization.halo.run/display-name: \"Actuator Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:actuator:manage\"]\nrules:\n  - nonResourceURLs: [ \"actuator\", \"/actuator/*\" ]\n    verbs: [ \"get\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-anonymous.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: anonymous\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-own-permissions\", \"role-template-public-apis\" ]\nrules:\n  - apiGroups: [ \"api.halo.run\" ]\n    resources: [ \"comments\", \"comments/reply\" ]\n    verbs: [ \"create\", \"get\", \"list\" ]\n  - apiGroups: [ \"api.halo.run\" ]\n    resources: [ \"*\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users\" ]\n    resourceNames: [ \"-\" ]\n    verbs: [ \"get\" ]\n  - nonResourceURLs: [ \"/apis/api.halo.run/v1alpha1/trackers/*\" ]\n    verbs: [ \"create\" ]\n  - nonResourceURLs: [ \"/actuator/globalinfo\", \"/actuator/health\", \"/actuator/health/*\", \"/login/public-key\" ]\n    verbs: [ \"get\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-public-apis\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"api.halo.run\" ]\n    resources: [ \"*\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.content.halo.run\" ]\n    resources: [ \"*\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.plugin.halo.run\" ]\n    resources: [ \"*\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.notification.halo.run\" ]\n    resources: [ \"subscriptions/unsubscribe\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.storage.halo.run\" ]\n    resources: [ \"thumbnails/via-uri\" ]\n    verbs: [ \"get\", \"list\" ]"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-attachment.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-attachments\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-attachments\\\" ]\"\n    rbac.authorization.halo.run/module: \"Attachments Management\"\n    rbac.authorization.halo.run/display-name: \"Attachment Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:attachments:manage\"]\nrules:\n  - apiGroups: [ \"storage.halo.run\" ]\n    resources: [ \"attachments\", \"policies\", \"policytemplates\", \"groups\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"attachments\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"attachments/upload-from-url\" ]\n    verbs: [ \"create\" ]\n  - apiGroups: [ \"\" ]\n    resources: [ \"settings\" ]\n    verbs: [ \"get\" ]\n  - nonResourceURLs: [\n      \"/apis/api.console.halo.run/v1alpha1/attachments/upload\"\n    ]\n    verbs: [ \"create\" ]\n  - apiGroups: [ \"console.api.storage.halo.run\" ]\n    resources: [ \"attachments/upload\" ]\n    verbs: [ \"create\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-attachments\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Attachments Management\"\n    rbac.authorization.halo.run/display-name: \"Attachment View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:attachments:view\"]\nrules:\n  - apiGroups: [ \"storage.halo.run\" ]\n    resources: [ \"attachments\", \"policies\", \"policytemplates\", \"groups\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"attachments\" ]\n    verbs: [ \"get\", \"list\" ]"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-authenticated.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: authenticated\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: |\n      [ \n        \"role-template-own-user-info\",\n        \"role-template-own-permissions\",\n        \"role-template-change-own-password\",\n        \"role-template-stats\",\n        \"role-template-annotation-setting\",\n        \"role-template-manage-own-pat\",\n        \"role-template-manage-own-authentications\",\n        \"role-template-user-notification\"\n      ]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"configmaps\" ]\n    resourceNames: [ \"system-states\" ]\n    verbs: [ \"get\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"auth-providers\" ]\n    verbs: [ \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"plugins/bundle.js\", \"plugins/bundle.css\" ]\n    resourceNames: [ \"-\" ]\n    verbs: [ \"get\" ]\n  - apiGroups: [ \"uc.api.auth.halo.run\" ]\n    resources: [ \"user-connections/disconnect\" ]\n    verbs: [ \"update\" ]\n  - apiGroups: [ \"uc.api.halo.run\" ]\n    resources: [ \"user-preferences\", \"annotationsettings\" ]\n    verbs: [ \"get\", \"list\", \"update\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-own-user-info\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users\" ]\n    resourceNames: [ \"-\" ]\n    verbs: [ \"get\", \"update\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users/avatar\" ]\n    resourceNames: [ \"-\" ]\n    verbs: [ \"create\", \"delete\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users/send-email-verification-code\", \"users/verify-email\" ]\n    resourceNames: [ \"-\" ]\n    verbs: [ \"create\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-own-permissions\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users/permissions\" ]\n    resourceNames: [ \"-\" ]\n    verbs: [ \"list\", \"get\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-change-own-password\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users/password\" ]\n    resourceNames: [ \"-\" ]\n    verbs: [ \"update\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-stats\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"stats\" ]\n    verbs: [ \"get\", \"list\" ]\n\n---\napiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: role-template-annotation-setting\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"annotationsettings\" ]\n    verbs: [ \"get\", \"list\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-own-pat\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"uc.api.security.halo.run\" ]\n    resources: [ \"personalaccesstokens\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"uc.api.security.halo.run\" ]\n    resources: [ \"personalaccesstokens/actions\" ]\n    verbs: [ \"update\" ]\n\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-own-authentications\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"uc.api.security.halo.run\" ]\n    resources: [ \"authentications\", \"authentications/totp\", \"authentications/settings\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"uc.api.security.halo.run\" ]\n    resources: [ \"devices\" ]\n    verbs: [ \"get\", \"list\", \"delete\" ]\n---\napiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: role-template-user-notification\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"api.notification.halo.run\" ]\n    resources: [ \"notifications\" ]\n    verbs: [ \"get\", \"list\", \"delete\" ]\n  - apiGroups: [ \"api.notification.halo.run\" ]\n    resources: [ \"notifications/mark-as-read\", \"notifications/mark-specified-as-read\" ]\n    verbs: [ \"update\" ]\n  - apiGroups: [ \"api.notification.halo.run\" ]\n    resources: [ \"notifiers/receiver-config\" ]\n    verbs: [ \"get\", \"update\" ]\n  - apiGroups: [ \"api.notification.halo.run\" ]\n    resources: [ \"notification-preferences\" ]\n    verbs: [ \"create\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-cache.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-cache\n  deletionTimestamp: 2024-06-01T00:00:00Z\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Cache Management\"\n    rbac.authorization.halo.run/display-name: \"Cache Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:caches:manage\"]\nrules:\n  - apiGroups: [\"api.console.halo.run\"]\n    resources: [\"caches\"]\n    verbs: [\"delete\"]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-category.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-categories\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-categories\\\" ]\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"system:categories:manage\", \"uc:categories:manage\" ]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"categories\" ]\n    verbs: [ \"*\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-categories\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"system:categories:view\", \"uc:categories:view\" ]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"categories\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-comment.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-comments\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-comments\\\" ]\"\n    rbac.authorization.halo.run/module: \"Comments Management\"\n    rbac.authorization.halo.run/display-name: \"Comment Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:comments:manage\"]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"comments\", \"replies\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"comments\", \"comments/reply\", \"replies\" ]\n    verbs: [ \"*\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-comments\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Comments Management\"\n    rbac.authorization.halo.run/display-name: \"Comment View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:comments:view\"]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"comments\", \"replies\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"comments\", \"comments/reply\", \"replies\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-configmap.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-configmaps\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-configmaps\\\" ]\"\n    rbac.authorization.halo.run/module: \"ConfigMaps Management\"\n    rbac.authorization.halo.run/display-name: \"ConfigMap Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:configmaps:manage\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"configmaps\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-configmaps\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"ConfigMaps Management\"\n    rbac.authorization.halo.run/display-name: \"ConfigMap View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:configmaps:view\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"configmaps\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-menu.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-menus\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-menus\\\" ]\"\n    rbac.authorization.halo.run/module: \"Menus Management\"\n    rbac.authorization.halo.run/display-name: \"Menu Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:menus:manage\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"menus\", \"menuitems\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"console.api.halo.run\" ]\n    resources: [ \"systemconfigs\" ]\n    resourceNames: [ \"menu\" ]\n    verbs: [ \"update\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-menus\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Menus Management\"\n    rbac.authorization.halo.run/display-name: \"Menu View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:menus:view\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"menus\", \"menuitems\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"console.api.halo.run\" ]\n    resources: [ \"systemconfigs\" ]\n    resourceNames: [ \"menu\" ]\n    verbs: [ \"get\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-migration.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-migration\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Migration Management\"\n    rbac.authorization.halo.run/display-name: \"Migration Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:migrations:manage\"]\nrules:\n  - apiGroups: [ \"console.api.migration.halo.run\" ]\n    resources: [ \"restorations\" ]\n    verbs: [ \"create\" ]\n  - apiGroups: [ \"console.api.migration.halo.run\" ]\n    resources: [ \"backup-files\" ]\n    verbs: [ \"list\" ]\n  - apiGroups: [ \"console.api.migration.halo.run\" ]\n    resources: [ \"backups/files\" ]\n    verbs: [ \"get\" ]\n  - apiGroups: [ \"migration.halo.run\" ]\n    resources: [ \"backups\" ]\n    verbs: [ \"list\", \"get\", \"create\", \"update\", \"delete\", \"patch\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-notification.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-notifier-config\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Notification Configuration\"\n    rbac.authorization.halo.run/display-name: \"Configure Notifier\"\nrules:\n  - apiGroups: [ \"notification.halo.run\" ]\n    resources: [ \"notifierDescriptors\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"notifiers/sender-config\" ]\n    verbs: [ \"get\", \"update\" ]\n  - apiGroups: [ \"console.api.notification.halo.run\" ]\n    resources: [ \"notifiers/verify-connection\" ]\n    resourceNames: [ \"default-email-notifier\" ]\n    verbs: [ \"create\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-permissions.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-permissions\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-permissions\\\" ]\"\n    rbac.authorization.halo.run/module: \"Permissions Management\"\n    rbac.authorization.halo.run/display-name: \"Permissions Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:permissions:manage\"]\nrules:\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users/permissions\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-permissions\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Permissions Management\"\n    rbac.authorization.halo.run/display-name: \"Permissions View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:permissions:view\"]\nrules:\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users/permissions\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-plugin.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-plugins\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-view-plugins\" ]\n    rbac.authorization.halo.run/module: \"Plugins Management\"\n    rbac.authorization.halo.run/display-name: \"Plugin Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:plugins:manage\"]\nrules:\n  - apiGroups: [ \"plugin.halo.run\" ]\n    resources: [ \"plugins\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"plugins/upgrade\", \"plugins/resetconfig\", \"plugins/config\", \"plugins/json-config\",\n                 \"plugins/reload\", \"plugins/install-from-uri\", \"plugins/upgrade-from-uri\", \"plugins/plugin-state\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"plugin-presets\" ]\n    verbs: [ \"list\" ]\n  - nonResourceURLs: [ \"/apis/api.console.halo.run/v1alpha1/plugins/*\" ]\n    verbs: [ \"create\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-plugins\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Plugins Management\"\n    rbac.authorization.halo.run/display-name: \"Plugin View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:plugins:view\"]\nrules:\n  - apiGroups: [ \"plugin.halo.run\" ]\n    resources: [ \"plugins\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"plugins\", \"plugins/setting\", \"plugins/config\", \"plugins/json-config\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-post.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-posts\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-view-posts\", \"role-template-manage-snapshots\", \"role-template-manage-tags\", \"role-template-manage-categories\", \"role-template-post-author\" ]\n    rbac.authorization.halo.run/module: \"Posts Management\"\n    rbac.authorization.halo.run/display-name: \"Post Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:posts:manage\"]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"posts\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"posts\", \"posts/publish\", \"posts/unpublish\", \"posts/recycle\", \"posts/content\", \"indices/post\", \"posts/revert-content\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-posts\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-view-snapshots\", \"role-template-view-tags\", \"role-template-view-categories\" ]\n    rbac.authorization.halo.run/module: \"Posts Management\"\n    rbac.authorization.halo.run/display-name: \"Post View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:posts:view\"]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"posts\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"posts\", \"posts/head-content\", \"posts/release-content\", \"posts/snapshot\", \"posts/content\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-role.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-roles\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-view-roles\", \"role-template-manage-permissions\" ]\n    rbac.authorization.halo.run/module: \"Roles Management\"\n    rbac.authorization.halo.run/display-name: \"Role Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:roles:manage\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"roles\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-roles\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Roles Management\"\n    rbac.authorization.halo.run/display-name: \"Role View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:roles:view\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"roles\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-setting.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-settings\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-settings\\\", \\\"role-template-notifier-config\\\" ]\"\n    rbac.authorization.halo.run/module: \"Settings Management\"\n    rbac.authorization.halo.run/display-name: \"Setting Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:settings:manage\", \"system:notifier:configuration\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"settings\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"auth-providers/enable\", \"auth-providers/disable\" ]\n    verbs: [ \"update\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-settings\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Settings Management\"\n    rbac.authorization.halo.run/display-name: \"Setting View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:settings:view\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"settings\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"auth-providers\" ]\n    verbs: [ \"get\", \"list\" ]"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-singlepage.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-singlepages\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-singlepages\\\", \\\"role-template-manage-snapshots\\\" ]\"\n    rbac.authorization.halo.run/module: \"SinglePages Management\"\n    rbac.authorization.halo.run/display-name: \"SinglePage Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:singlepages:manage\"]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"singlepages\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"singlepages\", \"singlepages/publish\", \"singlepages/content\", \"singlepages/revert-content\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-singlepages\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-snapshots\\\" ]\"\n    rbac.authorization.halo.run/module: \"SinglePages Management\"\n    rbac.authorization.halo.run/display-name: \"SinglePage View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:singlepages:view\"]\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"singlepages\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"singlepages\", \"singlepages/head-content\", \"singlepages/release-content\", \"singlepages/snapshot\", \"singlepages/content\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-snapshot.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-snapshots\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-snapshots\\\" ]\"\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"snapshots\" ]\n    verbs: [ \"*\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-snapshots\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"snapshots\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-tag.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-tags\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-tags\\\" ]\"\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"tags\" ]\n    verbs: [ \"*\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-tags\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\nrules:\n  - apiGroups: [ \"content.halo.run\" ]\n    resources: [ \"tags\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"tags\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-theme.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-themes\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: \"[ \\\"role-template-view-themes\\\" ]\"\n    rbac.authorization.halo.run/module: \"Themes Management\"\n    rbac.authorization.halo.run/display-name: \"Theme Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:themes:manage\"]\nrules:\n  - apiGroups: [ \"theme.halo.run\" ]\n    resources: [ \"themes\" ]\n    verbs: [ \"*\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"themes\", \"themes/reload\", \"themes/resetconfig\", \"themes/config\", \"themes/json-config\",\n                 \"themes/activation\", \"themes/install-from-uri\", \"themes/upgrade-from-uri\", \"themes/invalidate-cache\" ]\n    verbs: [ \"*\" ]\n  - nonResourceURLs: [ \"/apis/api.console.halo.run/themes/install\" ]\n    verbs: [ \"create\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-themes\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Themes Management\"\n    rbac.authorization.halo.run/display-name: \"Theme View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:themes:view\"]\nrules:\n  - apiGroups: [ \"theme.halo.run\" ]\n    resources: [ \"themes\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"themes\", \"themes/activation\", \"themes/setting\", \"themes/config\", \"themes/json-config\" ]\n    verbs: [ \"get\", \"list\" ]\n\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-uc-attachment.yaml",
    "content": "apiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: role-template-uc-attachment-manager\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Attachments Management\"\n    rbac.authorization.halo.run/display-name: \"UC Attachment Manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"uc:attachments:manage\" ]\nrules:\n  - apiGroups: [ \"uc.api.storage.halo.run\" ]\n    resources: [ \"attachments\", \"attachments/upload\", \"attachments/upload-from-url\" ]\n    verbs: [ \"create\", \"list\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-uc-content.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: post-editor\n  labels:\n    rbac.authorization.halo.run/system-reserved: \"true\"\n  annotations:\n    # Currently, yaml definition does not support i18n, please see https://github.com/halo-dev/halo/issues/3573\n    rbac.authorization.halo.run/display-name: \"文章管理员\"\n    rbac.authorization.halo.run/dependencies: |\n      [\"role-template-manage-posts\"]\nrules: [ ]\n\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: post-author\n  labels:\n    rbac.authorization.halo.run/system-reserved: \"true\"\n  annotations:\n    # Currently, yaml definition does not support i18n, please see https://github.com/halo-dev/halo/issues/3573\n    rbac.authorization.halo.run/display-name: \"作者\"\n    rbac.authorization.halo.run/disallow-access-console: \"true\"\n    rbac.authorization.halo.run/redirect-on-login: \"/uc\"\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-post-author\" ]\nrules: [ ]\n\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-post-author\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Posts Management\"\n    # Currently, yaml definition does not support i18n, please see https://github.com/halo-dev/halo/issues/3573\n    rbac.authorization.halo.run/display-name: \"Post Author\"\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-post-contributor\", \"role-template-post-publisher\", \"role-template-recycle-my-post\",\n        \"role-template-uc-attachment-manager\" ]\nrules: [ ]\n\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: post-contributor\n  labels:\n    rbac.authorization.halo.run/system-reserved: \"true\"\n  annotations:\n    # Currently, yaml definition does not support i18n, please see https://github.com/halo-dev/halo/issues/3573\n    rbac.authorization.halo.run/display-name: \"投稿者\"\n    rbac.authorization.halo.run/disallow-access-console: \"true\"\n    rbac.authorization.halo.run/redirect-on-login: \"/uc\"\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-post-contributor\" ]\nrules: [ ]\n\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-post-contributor\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Posts Management\"\n    # Currently, yaml definition does not support i18n, please see https://github.com/halo-dev/halo/issues/3573\n    rbac.authorization.halo.run/display-name: \"Post Contributor\"\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-view-categories\", \"role-template-view-tags\" ]\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"uc:posts:manage\" ]\nrules:\n  - apiGroups: [ \"uc.api.content.halo.run\" ]\n    resources: [ \"posts\" ]\n    verbs: [ \"get\", \"list\", \"create\", \"update\", \"delete\" ]\n  - apiGroups: [ \"uc.api.content.halo.run\" ]\n    resources: [ \"posts/draft\" ]\n    verbs: [ \"update\", \"get\" ]\n\n---\napiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: role-template-post-publisher\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Posts Management\"\n    rbac.authorization.halo.run/display-name: \"Post Publisher\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"uc:posts:publish\" ]\nrules:\n  - apiGroups: [ \"uc.api.content.halo.run\" ]\n    resources: [ \"posts/publish\", \"posts/unpublish\" ]\n    verbs: [ \"update\" ]\n\n---\n# TODO remove this in next major version\napiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: role-template-recycle-my-post\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Posts Management\"\n    rbac.authorization.halo.run/display-name: \"Recycle My Post\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [ \"uc:posts:recycle\" ]\nrules:\n  - apiGroups: [ \"uc.api.content.halo.run\" ]\n    resources: [ \"posts/recycle\" ]\n    verbs: [ \"delete\" ]\n\n---\napiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: role-template-post-attachment-manager\n  deletionTimestamp: 2024-09-30T14:00:41.813954138Z\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Posts Management\"\n    rbac.authorization.halo.run/display-name: \"Post Attachment Manager\"\nrules: [ ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/role-template-user.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-manage-users\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/dependencies: |\n      [ \"role-template-view-users\", \"role-template-change-password\" ]\n    rbac.authorization.halo.run/module: \"Users Management\"\n    rbac.authorization.halo.run/display-name: \"User manage\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:users:manage\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"users\" ]\n    verbs: [ \"create\", \"patch\", \"update\", \"delete\", \"deletecollection\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users\", \"users/permissions\", \"users/password\", \"users/avatar\" ]\n    verbs: [ \"*\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-view-users\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Users Management\"\n    rbac.authorization.halo.run/display-name: \"User View\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"system:users:view\"]\nrules:\n  - apiGroups: [ \"\" ]\n    resources: [ \"users\" ]\n    verbs: [ \"get\", \"list\" ]\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users\" ]\n    verbs: [ \"get\", \"list\" ]\n---\napiVersion: v1alpha1\nkind: \"Role\"\nmetadata:\n  name: role-template-change-password\n  labels:\n    halo.run/role-template: \"true\"\n    halo.run/hidden: \"true\"\n  annotations:\n    rbac.authorization.halo.run/module: \"Users Management\"\n    rbac.authorization.halo.run/display-name: \"User Password Change\"\nrules:\n  - apiGroups: [ \"api.console.halo.run\" ]\n    resources: [ \"users/password\" ]\n    verbs: [ \"update\" ]\n"
  },
  {
    "path": "application/src/main/resources/extensions/system-configurable-configmap.yaml",
    "content": "apiVersion: v1alpha1\nkind: \"ConfigMap\"\nmetadata:\n  name: system-default\ndata:\n  user: |\n    {\n      \"allowRegistration\": false,\n      \"mustVerifyEmailOnRegistration\": false,\n      \"defaultRole\": \"guest\",\n      \"avatarPolicy\": \"default-policy\",\n      \"ucAttachmentPolicy\": \"default-policy\"\n    }\n  attachment: |\n    {\n      \"console\": {\n        \"policyName\": \"default-policy\"\n      },\n      \"uc\": {\n        \"policyName\": \"default-policy\"\n      },\n      \"avatar\": {\n        \"policyName\": \"default-policy\"\n      }\n    }\n  theme: |\n    {\n      \"active\": \"theme-earth\"\n    }\n  routeRules: |\n    {\n      \"categories\": \"categories\",\n      \"archives\": \"archives\",\n      \"post\": \"/archives/{slug}\",\n      \"tags\": \"tags\"\n    }\n  codeInjection: |\n    {\n     \"globalHead\": \"\",\n     \"footer\": \"\"\n    }\n  post: |\n    {\n      \"review\": false,\n      \"postPageSize\": 10,\n      \"archivePageSize\": 10,\n      \"categoryPageSize\": 10,\n      \"tagPageSize\": 10,\n      \"authorPageSize\": 10,\n      \"slugGenerationStrategy\": \"generateByTitle\",\n      \"attachmentPolicyName\": \"default-policy\"\n    }\n  comment: |\n    {\n      \"enable\": true,\n      \"requireReviewForNew\": true,\n      \"systemUserOnly\": true\n    }\n  menu: |\n    {\n      \"primary\": \"primary\"\n    }\n  extensionPointEnabled: |\n    {\n      \"search-engine\": [\"search-engine-lucene\"]\n    }\n  authProvider: |\n    {\n      \"states\": [{\n          \"name\": \"local\",\n          \"enabled\": true,\n          \"priority\": 0\n      }]\n    }\n"
  },
  {
    "path": "application/src/main/resources/extensions/system-default-role.yaml",
    "content": "apiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: guest\n  labels:\n    rbac.authorization.halo.run/system-reserved: \"true\"\n  annotations:\n    rbac.authorization.halo.run/display-name: \"访客\"\n    rbac.authorization.halo.run/disallow-access-console: \"true\"\n    rbac.authorization.halo.run/redirect-on-login: \"/uc\"\nrules: []\n\n---\napiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: super-role\n  labels:\n    rbac.authorization.halo.run/system-reserved: \"true\"\n  annotations:\n    rbac.authorization.halo.run/display-name: \"超级管理员\"\n    rbac.authorization.halo.run/ui-permissions: |\n      [\"*\"]\nrules:\n  - apiGroups: [\"*\"]\n    resources: [\"*\"]\n    nonResourceURLs: [\"*\"]\n    verbs: [\"*\"]\n"
  },
  {
    "path": "application/src/main/resources/extensions/system-setting.yaml",
    "content": "apiVersion: v1alpha1\nkind: Setting\nmetadata:\n  name: system\nspec:\n  forms:\n    - group: basic\n      label: 基本设置\n      formSchema:\n        - $formkit: text\n          label: \"站点标题\"\n          name: title\n          validation: required\n        - $formkit: text\n          label: \"站点副标题\"\n          name: subtitle\n        - $formkit: attachment\n          label: Logo\n          name: logo\n          width: \"4rem\"\n          accepts:\n            - 'image/*'\n        - $formkit: attachment\n          label: Favicon\n          name: favicon\n          width: \"4rem\"\n          accepts:\n            - 'image/*'\n        - $formkit: select\n          label: \"首选语言\"\n          name: language\n          value: 'zh-CN'\n          options:\n            - label: 'English'\n              value: 'en'\n            - label: 'Español'\n              value: 'es'\n            - label: '简体中文'\n              value: 'zh-CN'\n            - label: '繁体中文'\n              value: 'zh-TW'\n    - group: post\n      label: 文章设置\n      formSchema:\n        - $formkit: number\n          label: \"文章列表显示条数\"\n          name: postPageSize\n          value: 10\n          min: 1\n          max: 100\n          validation: required | max:100\n        - $formkit: number\n          label: \"归档页文章显示条数\"\n          name: archivePageSize\n          value: 10\n          min: 1\n          max: 100\n          validation: required | max:100\n        - $formkit: number\n          label: \"分类页文章显示条数\"\n          name: categoryPageSize\n          value: 10\n          min: 1\n          max: 100\n          validation: required | max:100\n        - $formkit: number\n          label: \"标签页文章显示条数\"\n          name: tagPageSize\n          value: 10\n          min: 1\n          max: 100\n          validation: required\n        - $formkit: number\n          label: \"作者页文章显示条数\"\n          name: authorPageSize\n          value: 10\n          min: 1\n          max: 100\n          validation: required\n        - $formkit: select\n          label: \"别名生成策略\"\n          name: slugGenerationStrategy\n          value: 'generateByTitle'\n          options:\n            - label: '根据标题'\n              value: 'generateByTitle'\n            - label: '时间戳'\n              value: 'timestamp'\n            - label: 'Short UUID'\n              value: 'shortUUID'\n            - label: 'UUID'\n              value: 'UUID'\n          help: 此选项仅在创建文章时生效，修改此选项不会影响已有文章\n        - $formkit: attachmentPolicySelect\n          name: attachmentPolicyName\n          label: \"附件存储策略\"\n          value: \"default-policy\"\n          help: 用于指定在文章编辑器中上传的默认附件存储策略（已过时，请使用附件配置中的管理端附件配置）\n        - $formkit: attachmentGroupSelect\n          name: attachmentGroupName\n          label: \"附件存储组\"\n          value: \"\"\n          help: 用于指定在文章编辑器中上传的默认附件存储分组（已过时，请使用附件配置中的管理端附件配置）\n    - group: seo\n      label: SEO 设置\n      formSchema:\n        - $formkit: checkbox\n          name: blockSpiders\n          label: \"屏蔽搜索引擎\"\n          value: false\n          help: \"为所有页面添加 <meta name=\\\"robots\\\" content=\\\"noindex\\\" /> 标签，阻止搜索引擎索引，但不是所有搜索引擎都会遵守\"\n        - $formkit: textarea\n          name: keywords\n          label: \"站点关键词\"\n          help: \"目前主流搜索引擎已经不再使用此字段，所以通常不建议设置，此选项可能在未来版本中被移除\"\n          auto-height: true\n        - $formkit: textarea\n          name: description\n          label: \"站点描述\"\n          help: \"仅对首页生效，其他页面将根据页面类型自动生成描述\"\n          auto-height: true\n    - group: user\n      label: 用户设置\n      formSchema:\n        - $formkit: checkbox\n          name: allowRegistration\n          id: allowRegistration\n          key: allowRegistration\n          label: \"开放注册\"\n          value: false\n        - $formkit: checkbox\n          name: mustVerifyEmailOnRegistration\n          label: \"注册需验证邮箱\"\n          if: \"$get(allowRegistration).value === true\"\n          help: \"需要确保已经正确配置邮件通知器\"\n          value: false\n        - $formkit: textarea\n          name: protectedUsernames\n          label: \"保留用户名与名称\"\n          if: \"$get(allowRegistration).value === true\"\n          help: 保留用户名与名称，限制用户注册或编辑资料时使用，多个请用英文逗号,分隔，留空则不限制\n          value: \"admin,administrator,root,system,superuser,guest,test,demo,user,backup,ftp,www,manager,support,service,api,超级管理员,管理员\"\n        - $formkit: roleSelect\n          name: defaultRole\n          label: \"默认角色\"\n          validation: 'required'\n          if: \"$get(allowRegistration).value === true\"\n          help: 用户注册之后默认为用户分配的角色\n        - $formkit: attachmentPolicySelect\n          name: avatarPolicy\n          label: \"头像存储位置\"\n          value: \"default-policy\"\n          help: 指定用户上传头像的存储策略（已过时，请使用附件配置中的头像附件配置）\n        - $formkit: attachmentPolicySelect\n          name: ucAttachmentPolicy\n          label: \"个人中心附件存储位置\"\n          value: \"default-policy\"\n          help: 指定用户在个人中心上传的附件的存储位置（已过时，请使用附件配置中的个人中心附件配置）\n    - group: attachment\n      label: 附件配置\n      formSchema:\n        - $formkit: group\n          name: console\n          label: 管理端附件配置\n          help: 为管理端直接上传附件的地方设置默认的存储策略和分组\n          children:\n            - $formkit: attachmentPolicySelect\n              name: policyName\n              label: \"存储策略\"\n              auto-select: false\n            - $formkit: attachmentGroupSelect\n              name: groupName\n              label: \"存储组\"\n              auto-select: false\n              clearable: true\n        - $formkit: group\n          name: uc\n          label: 个人中心附件配置\n          help: 为个人中心直接上传附件的地方设置默认的存储策略和分组，处于安全考虑，建议为所选存储策略限制文件类型和大小\n          children:\n            - $formkit: attachmentPolicySelect\n              name: policyName\n              label: \"存储策略\"\n              auto-select: false\n            - $formkit: attachmentGroupSelect\n              name: groupName\n              label: \"存储组\"\n              auto-select: false\n              clearable: true\n        - $formkit: group\n          name: avatar\n          label: 头像附件配置\n          help: 为用户头像设置存储策略，处于安全考虑，建议为所选存储策略限制文件类型和大小\n          children:\n            - $formkit: attachmentPolicySelect\n              name: policyName\n              label: \"存储策略\"\n    - group: comment\n      label: 评论设置\n      formSchema:\n        - $formkit: checkbox\n          name: enable\n          value: true\n          label: \"启用评论\"\n        - $formkit: checkbox\n          name: requireReviewForNew\n          value: true\n          label: \"新评论审核\"\n          help: 开启之后，新评论需要管理员审核后才会显示\n        - $formkit: checkbox\n          name: systemUserOnly\n          value: true\n          label: \"仅允许注册用户评论\"\n    - group: routeRules\n      label: 主题路由设置\n      formSchema:\n        - $formkit: checkbox\n          label: \"关闭主题预览\"\n          value: false\n          name: disableThemePreview\n          help: \"关闭后，未包含主题管理权限的用户将无法通过参数预览未激活的主题\"\n        - $formkit: text\n          label: \"分类页路由前缀\"\n          value: \"categories\"\n          name: categories\n          validation: required | alphanumeric\n        - $formkit: text\n          label: \"标签页路由前缀\"\n          value: \"tags\"\n          name: tags\n          validation: required | alphanumeric\n        - $formkit: text\n          label: \"归档页路由前缀\"\n          value: \"archives\"\n          name: archives\n          validation: required | alphanumeric\n        - $formkit: select\n          label: 文章详情页访问规则\n          value: '/archives/{slug}'\n          options:\n            - label: '/archives/{slug}'\n              value: '/archives/{slug}'\n            - label: '/archives/{name}'\n              value: '/archives/{name}'\n            - label: '/?p={name}'\n              value: '/?p={name}'\n            - label: '/?p={slug}'\n              value: '/?p={slug}'\n            - label: '/{year}/{slug}'\n              value: '/{year:\\d{4}}/{slug}'\n            - label: '/{year}/{month}/{slug}'\n              value: '/{year:\\d{4}}/{month:\\d{2}}/{slug}'\n            - label: '/{year}/{month}/{day}/{slug}'\n              value: '/{year:\\d{4}}/{month:\\d{2}}/{day:\\d{2}}/{slug}'\n            - label: '/categories/{categorySlug}/{slug}'\n              value: '/categories/{categorySlug}/{slug}'\n          name: post\n          validation: required\n    - group: codeInjection\n      label: 代码注入\n      formSchema:\n        - $formkit: code\n          language: html\n          height: 200px\n          label: \"全局 head 标签\"\n          name: globalHead\n          help: \"注入代码到所有页面的 head 标签部分\"\n        - $formkit: code\n          language: html\n          height: 200px\n          label: \"内容页 head 标签\"\n          name: contentHead\n          help: \"注入代码到文章页面和自定义页面的 head 标签部分\"\n        - $formkit: code\n          language: html\n          height: 200px\n          label: \"页脚\"\n          name: footer\n          help: \"注入代码到所有页面的页脚部分\""
  },
  {
    "path": "application/src/main/resources/extensions/user.yaml",
    "content": "apiVersion: v1alpha1\nkind: User\nmetadata:\n  name: anonymousUser\n  labels:\n    halo.run/hidden-user: \"true\"\n  finalizers:\n    - system-protection\nspec:\n  displayName: Anonymous User\n  email: anonymous@example.com\n  disabled: true\n\n---\napiVersion: v1alpha1\nkind: User\nmetadata:\n  name: ghost\n  labels:\n    halo.run/hidden-user: \"true\"\n  finalizers:\n    - system-protection\nspec:\n  displayName: 已删除用户\n  email: ghost@example.com\n  disabled: true\n  bio: 该用户已被删除。\n\n"
  },
  {
    "path": "application/src/main/resources/initial-data.yaml",
    "content": "# 提供了 timestamp、username 变量，用于初始化数据时填充时间戳和用户名\n# 初始化文章关联的分类、标签数据\napiVersion: content.halo.run/v1alpha1\nkind: Category\nmetadata:\n  name: 76514a40-6ef1-4ed9-b58a-e26945bde3ca\nspec:\n  displayName: 默认分类\n  slug: default\n  description: 这是你的默认分类，如不需要，删除即可。\n  cover: \"\"\n  template: \"\"\n  priority: 0\n  children: [ ]\nstatus:\n  permalink: \"/categories/default\"\n\n---\napiVersion: content.halo.run/v1alpha1\nkind: Tag\nmetadata:\n  name: c33ceabb-d8f1-4711-8991-bb8f5c92ad7c\nspec:\n  displayName: Halo\n  slug: halo\n  cover: \"\"\nstatus:\n  permalink: \"/tags/halo\"\n\n---\n# 文章关联的内容\napiVersion: content.halo.run/v1alpha1\nkind: Snapshot\nmetadata:\n  name: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9\n  annotations:\n    content.halo.run/keep-raw: \"true\"\nspec:\n  subjectRef:\n    group: content.halo.run\n    version: v1alpha1\n    kind: Post\n    name: 5152aea5-c2e8-4717-8bba-2263d46e19d5\n  rawType: HTML\n  rawPatch: <h2 id=\"hello-halo\"><strong>Hello\n    Halo</strong></h2><p>如果你看到了这一篇文章，那么证明你已经安装成功了，感谢使用 <a target=\"_blank\"\n    rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run/\">Halo</a>\n    进行创作，希望能够使用愉快。</p><h2\n    id=\"%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5\"><strong>相关链接</strong></h2><ul><li><p>官网：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://www.halo.run\">https://www.halo.run</a></p></li><li><p>文档：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://docs.halo.run\">https://docs.halo.run</a></p></li><li><p>社区：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://bbs.halo.run\">https://bbs.halo.run</a></p></li><li><p>应用市场：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://www.halo.run/store/apps\">https://www.halo.run/store/apps</a></p></li><li><p>开源地址：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://github.com/halo-dev/halo\">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中，有任何问题都可以通过以上链接找寻答案，或者联系我们。</p><blockquote><p>这是一篇自动生成的文章，请删除这篇文章之后开始你的创作吧！</p></blockquote>\n  contentPatch: <h2 id=\"hello-halo\"><strong>Hello\n    Halo</strong></h2><p>如果你看到了这一篇文章，那么证明你已经安装成功了，感谢使用 <a target=\"_blank\"\n    rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run/\">Halo</a>\n    进行创作，希望能够使用愉快。</p><h2\n    id=\"%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5\"><strong>相关链接</strong></h2><ul><li><p>官网：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://www.halo.run\">https://www.halo.run</a></p></li><li><p>文档：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://docs.halo.run\">https://docs.halo.run</a></p></li><li><p>社区：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://bbs.halo.run\">https://bbs.halo.run</a></p></li><li><p>应用市场：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://www.halo.run/store/apps\">https://www.halo.run/store/apps</a></p></li><li><p>开源地址：<a\n    target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n    href=\"https://github.com/halo-dev/halo\">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中，有任何问题都可以通过以上链接找寻答案，或者联系我们。</p><blockquote><p>这是一篇自动生成的文章，请删除这篇文章之后开始你的创作吧！</p></blockquote>\n  lastModifyTime: \"${timestamp}\"\n  owner: \"${username}\"\n  contributors:\n    - \"${username}\"\n\n---\n# 初始化文章数据\napiVersion: content.halo.run/v1alpha1\nkind: Post\nmetadata:\n  name: 5152aea5-c2e8-4717-8bba-2263d46e19d5\nspec:\n  title: Hello Halo\n  slug: hello-halo\n  releaseSnapshot: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9\n  headSnapshot: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9\n  baseSnapshot: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9\n  owner: \"${username}\"\n  template: \"\"\n  cover: \"\"\n  deleted: false\n  publish: true\n  publishTime: \"${timestamp}\"\n  pinned: false\n  allowComment: true\n  visible: PUBLIC\n  priority: 0\n  excerpt:\n    autoGenerate: false\n    raw: 如果你看到了这一篇文章，那么证明你已经安装成功了，感谢使用 Halo 进行创作，希望能够使用愉快。\n  categories:\n    - 76514a40-6ef1-4ed9-b58a-e26945bde3ca\n  tags:\n    - c33ceabb-d8f1-4711-8991-bb8f5c92ad7c\n  htmlMetas: [ ]\nstatus:\n  permalink: /archives/hello-halo\n\n---\n# 自定义页面关联的内容\napiVersion: content.halo.run/v1alpha1\nkind: Snapshot\nmetadata:\n  name: c3f73cc2-194e-4cd8-9092-7386aa50a0e5\n  annotations:\n    content.halo.run/keep-raw: \"true\"\nspec:\n  subjectRef:\n    group: content.halo.run\n    version: v1alpha1\n    kind: SinglePage\n    name: 373a5f79-f44f-441a-9df1-85a4f553ece8\n  rawType: HTML\n  rawPatch: <h2><strong>关于页面</strong></h2><p>这是一个自定义页面，你可以在后台的 <code>页面</code>\n    -&gt; <code>自定义页面</code>\n    找到它，你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面，你可以在后台删除它。</p></blockquote>\n  contentPatch: <h2><strong>关于页面</strong></h2><p>这是一个自定义页面，你可以在后台的 <code>页面</code>\n    -&gt; <code>自定义页面</code>\n    找到它，你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面，你可以在后台删除它。</p></blockquote>\n  lastModifyTime: \"${timestamp}\"\n  owner: \"${username}\"\n  contributors:\n    - \"${username}\"\n\n---\n# 初始化自定义页面数据\napiVersion: content.halo.run/v1alpha1\nkind: SinglePage\nmetadata:\n  name: 373a5f79-f44f-441a-9df1-85a4f553ece8\nspec:\n  title: 关于\n  slug: about\n  template: \"\"\n  cover: \"\"\n  owner: \"${username}\"\n  deleted: false\n  publish: true\n  baseSnapshot: c3f73cc2-194e-4cd8-9092-7386aa50a0e5\n  headSnapshot: c3f73cc2-194e-4cd8-9092-7386aa50a0e5\n  releaseSnapshot: c3f73cc2-194e-4cd8-9092-7386aa50a0e5\n  pinned: false\n  allowComment: true\n  visible: PUBLIC\n  version: 1\n  priority: 0\n  excerpt:\n    autoGenerate: false\n    raw: 这是一个自定义页面，你可以在后台的 页面 -> 自定义页面 找到它，你可以用于新建关于页面、联系我们页面等等。\n  htmlMetas: [ ]\nstatus:\n  permalink: \"/about\"\n\n---\n# 首页菜单项\napiVersion: v1alpha1\nkind: MenuItem\nmetadata:\n  name: 88c3f10b-321c-4092-86a8-70db00251b74\nspec:\n  displayName: 首页\n  href: /\n  children: [ ]\n  priority: 0\n---\n# 关联到文章作为菜单\napiVersion: v1alpha1\nkind: MenuItem\nmetadata:\n  name: c4c814d1-0c2c-456b-8c96-4864965fee94\nspec:\n  displayName: \"Hello Halo\"\n  href: \"/archives/hello-halo\"\n  children: [ ]\n  priority: 1\n  targetRef:\n    group: content.halo.run\n    version: v1alpha1\n    kind: Post\n    name: 5152aea5-c2e8-4717-8bba-2263d46e19d5\n---\n# 关联到标签作为菜单\napiVersion: v1alpha1\nkind: MenuItem\nmetadata:\n  name: 35869bd3-33b5-448b-91ee-cf6517a59644\nspec:\n  displayName: \"Halo\"\n  href: \"/tags/halo\"\n  children: [ ]\n  priority: 2\n  targetRef:\n    group: content.halo.run\n    version: v1alpha1\n    kind: Tag\n    name: c33ceabb-d8f1-4711-8991-bb8f5c92ad7c\n---\n# 关联到自定义页面作为菜单\napiVersion: v1alpha1\nkind: MenuItem\nmetadata:\n  name: b0d041fa-dc99-48f6-a193-8604003379cf\nspec:\n  displayName: \"关于\"\n  href: \"/about\"\n  children: [ ]\n  priority: 3\n  targetRef:\n    group: content.halo.run\n    version: v1alpha1\n    kind: SinglePage\n    name: 373a5f79-f44f-441a-9df1-85a4f553ece8\n---\napiVersion: v1alpha1\nkind: Menu\nmetadata:\n  name: primary\nspec:\n  displayName: 主菜单\n  menuItems:\n    - 88c3f10b-321c-4092-86a8-70db00251b74\n    - c4c814d1-0c2c-456b-8c96-4864965fee94\n    - 35869bd3-33b5-448b-91ee-cf6517a59644\n    - b0d041fa-dc99-48f6-a193-8604003379cf\n"
  },
  {
    "path": "application/src/main/resources/schema-h2.sql",
    "content": "create table if not exists extensions\n(\n    name    varchar(255) not null,\n    data    blob,\n    version bigint,\n    primary key (name)\n);\n"
  },
  {
    "path": "application/src/main/resources/schema-mariadb.sql",
    "content": "create table if not exists extensions\n(\n    name    varchar(255) not null COLLATE utf8mb4_bin,\n    data    longblob,\n    version bigint,\n    primary key (name)\n);\n"
  },
  {
    "path": "application/src/main/resources/schema-mysql.sql",
    "content": "create table if not exists extensions\n(\n    name    varchar(255) not null COLLATE utf8mb4_bin,\n    data    longblob,\n    version bigint,\n    primary key (name)\n);\n"
  },
  {
    "path": "application/src/main/resources/schema-postgresql.sql",
    "content": "create table if not exists extensions\n(\n    name    varchar(255) not null,\n    data    bytea,\n    version bigint,\n    primary key (name)\n);\n"
  },
  {
    "path": "application/src/main/resources/static/halo-tracker.js",
    "content": "!function(){\"use strict\";!function(t){var e=t.screen,r=e.width,n=e.height,a=t.navigator.language,o=t.location,i=t.localStorage,c=t.document,u=t.history,l=o.hostname,s=o.pathname,p=o.search,f=c.currentScript;if(f){var h=function(t,e,r){var n=t[e];return function(){for(var e=[],a=arguments.length;a--;)e[a]=arguments[a];return r.apply(null,e),n.apply(t,e)}},d=function(){return i&&i.getItem(\"haloTracker.disabled\")||T&&function(){var e=t.doNotTrack,r=t.navigator,n=t.external,a=\"msTrackingProtectionEnabled\",o=e||r.doNotTrack||r.msDoNotTrack||n&&a in n&&n[a]();return\"1\"==o||\"yes\"===o}()||j&&!w.includes(l)},g=\"data-\",v=f.getAttribute.bind(f),m=v(g+\"group\")||\"\",k=v(g+\"plural\"),y=v(g+\"name\"),S=v(g+\"host-url\"),b=\"false\"!==v(g+\"auto-track\"),T=v(g+\"do-not-track\"),j=v(g+\"domains\")||\"\",w=j.split(\",\").map((function(t){return t.trim()})),E=(S?S.replace(/\\/$/,\"\"):f.src.split(\"/\").slice(0,-1).join(\"/\"))+\"/apis/api.halo.run/v1alpha1/trackers/counter\",N=r+\"x\"+n,O=\"\"+s+p,x=c.referrer,P=function(t,e){return void 0===t&&(t=O),void 0===e&&(e=x),function(t){if(!d())return fetch(E,{method:\"POST\",body:JSON.stringify(Object.assign({},t)),headers:{\"Content-Type\":\"application/json\"}}).then((function(t){return t.text()})).then((function(t){console.debug(\"Visit count:\",t)}))}((r={group:m,plural:k,name:y,hostname:l,screen:N,language:a,url:O},n={url:t,referrer:e},Object.keys(n).forEach((function(t){void 0!==n[t]&&(r[t]=n[t])})),r));var r,n},V=function(t,e,r){if(r){x=O;var n=r.toString();(O=\"http\"===n.substring(0,4)?\"/\"+n.split(\"/\").splice(3).join(\"/\"):n)!==x&&P()}};if(!t.haloTracker){var A=function(t){return trackEvent(t)};A.trackView=P,t.haloTracker=A}if(b&&!d()){u.pushState=h(u,\"pushState\",V),u.replaceState=h(u,\"replaceState\",V);var C=function(){\"complete\"===c.readyState&&P()};c.addEventListener(\"readystatechange\",C,!0),C()}}}(window)}();\n"
  },
  {
    "path": "application/src/main/resources/static/js/main.js",
    "content": "const Toast = (function () {\n    let container;\n\n    function getContainer() {\n        if (container) return container;\n\n        container = document.createElement(\"div\");\n        container.style.cssText = `\n      position: fixed;\n      top: 20px;\n      left: 50%;\n      transform: translateX(-50%);\n      z-index: 9999;\n    `;\n\n        if (document.body) {\n            document.body.appendChild(container);\n        } else {\n            document.addEventListener(\"DOMContentLoaded\", () => {\n                document.body.appendChild(container);\n            });\n        }\n\n        return container;\n    }\n\n    class ToastMessage {\n        constructor(message, type) {\n            this.message = message;\n            this.type = type;\n            this.element = null;\n            this.create();\n        }\n\n        create() {\n            this.element = document.createElement(\"div\");\n            this.element.textContent = this.message;\n            this.element.style.cssText = `\n        background-color: ${this.type === \"success\" ? \"#4CAF50\" : \"#F44336\"};\n        color: white;\n        padding: 12px 24px;\n        border-radius: 4px;\n        margin-bottom: 10px;\n        box-shadow: 0 2px 5px rgba(0,0,0,0.2);\n        opacity: 0;\n        transition: opacity 0.3s ease-in-out;\n      `;\n            getContainer().appendChild(this.element);\n\n            setTimeout(() => {\n                this.element.style.opacity = \"1\";\n            }, 10);\n\n            setTimeout(() => {\n                this.remove();\n            }, 3000);\n        }\n\n        remove() {\n            this.element.style.opacity = \"0\";\n            setTimeout(() => {\n                const parent = this.element.parentNode;\n                if (parent) {\n                    parent.removeChild(this.element);\n                }\n            }, 300);\n        }\n    }\n\n    function showToast(message, type) {\n        if (document.readyState === \"loading\") {\n            document.addEventListener(\"DOMContentLoaded\", () => {\n                new ToastMessage(message, type);\n            });\n        } else {\n            new ToastMessage(message, type);\n        }\n    }\n\n    return {\n        success: function (message) {\n            showToast(message, \"success\");\n        },\n        error: function (message) {\n            showToast(message, \"error\");\n        },\n    };\n})();\n\nfunction sendVerificationCode(button, sendRequest) {\n    let timer;\n    const countdown = 60;\n    const originalButtonText = button.textContent;\n\n    button.addEventListener(\"click\", () => {\n        button.disabled = true;\n        button.textContent = i18nResources.sendVerificationCodeSending;\n        sendRequest()\n            .then(() => {\n                startCountdown();\n                Toast.success(i18nResources.sendVerificationCodeSuccess);\n            })\n            .catch((e) => {\n                button.disabled = false;\n                button.textContent = originalButtonText;\n                if (e instanceof Error) {\n                    Toast.error(e.message);\n                } else {\n                    Toast.error(i18nResources.sendVerificationCodeFailed);\n                }\n            });\n    });\n\n    function startCountdown() {\n        let remainingTime = countdown;\n        button.disabled = true;\n        button.classList.add(\"disabled\");\n\n        timer = setInterval(() => {\n            if (remainingTime > 0) {\n                button.textContent = `${remainingTime}s`;\n                remainingTime--;\n            } else {\n                clearInterval(timer);\n                button.textContent = originalButtonText;\n                button.disabled = false;\n                button.classList.remove(\"disabled\");\n            }\n        }, 1000);\n    }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n    const passwordContainers = document.querySelectorAll(\".toggle-password-display-flag\");\n\n    passwordContainers.forEach((container) => {\n        const passwordInput = container.querySelector('input[type=\"password\"]');\n        const toggleButton = container.querySelector(\".toggle-password-button\");\n        const displayIcon = container.querySelector(\".password-display-icon\");\n        const hiddenIcon = container.querySelector(\".password-hidden-icon\");\n\n        if (passwordInput && toggleButton && displayIcon && hiddenIcon) {\n            toggleButton.addEventListener(\"click\", () => {\n                if (passwordInput.type === \"password\") {\n                    passwordInput.type = \"text\";\n                    displayIcon.style.display = \"none\";\n                    hiddenIcon.style.display = \"block\";\n                } else {\n                    passwordInput.type = \"password\";\n                    displayIcon.style.display = \"block\";\n                    hiddenIcon.style.display = \"none\";\n                }\n            });\n        }\n    });\n});\n\nfunction setupPasswordConfirmation(passwordId, confirmPasswordId) {\n    const password = document.getElementById(passwordId);\n    const confirmPassword = document.getElementById(confirmPasswordId);\n\n    function validatePasswordMatch() {\n        if (password.value !== confirmPassword.value) {\n            confirmPassword.setCustomValidity(i18nResources.passwordConfirmationFailed);\n        } else {\n            confirmPassword.setCustomValidity(\"\");\n        }\n    }\n\n    password.addEventListener(\"change\", validatePasswordMatch);\n    confirmPassword.addEventListener(\"input\", validatePasswordMatch);\n}\n"
  },
  {
    "path": "application/src/main/resources/static/styles/main.css",
    "content": ".gateway-page {\n    width: 100%;\n    height: 100vh;\n    overflow: auto;\n}\n\n.gateway-wrapper,\n.gateway-wrapper:before,\n.gateway-wrapper:after,\n.gateway-wrapper *,\n.gateway-wrapper :before,\n.gateway-wrapper :after {\n    box-sizing: border-box;\n    border-style: solid;\n    border-width: 0;\n}\n\n.gateway-wrapper {\n    --color-primary: #4ccba0;\n    --color-secondary: #0e1731;\n    --color-link: #1f75cb;\n    --color-text: #374151;\n    --color-border: #d1d5db;\n    --rounded-sm: 0.125em;\n    --rounded-base: 0.25em;\n    --rounded-lg: 0.5em;\n    --spacing-2xl: 1.5em;\n    --spacing-xl: 1.25em;\n    --spacing-lg: 1em;\n    --spacing-md: 0.875em;\n    --spacing-sm: 0.625em;\n    --spacing-xs: 0.5em;\n    --text-xl: 1.25em;\n    --text-2xl: 1.5em;\n    --text-lg: 1.125em;\n    --text-base: 1em;\n    --text-sm: 0.875em;\n    --font-size-base: 16px;\n    --font-size-md: 14px;\n    padding: 5% var(--spacing-lg);\n    font-size: var(--font-size-base);\n    max-width: 28em;\n    margin: 0 auto;\n    font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,\n        Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n    line-height: 1.5;\n}\n\n.halo-form-wrapper {\n    border-radius: var(--rounded-lg);\n    padding: var(--spacing-2xl);\n    background: #fff;\n    border: 1px solid #dfe6ecb3;\n}\n\n.form-title {\n    all: unset;\n    font-size: var(--text-2xl);\n    margin-bottom: var(--spacing-lg);\n    font-weight: 500;\n    display: block;\n}\n\n.halo-form .form-item {\n    margin-bottom: var(--spacing-2xl);\n    flex-direction: column;\n    width: 100%;\n    display: flex;\n}\n\n.halo-form .form-item:last-of-type {\n    margin-bottom: 0;\n}\n\n.halo-form .form-item-group {\n    gap: var(--spacing-lg);\n    margin-bottom: var(--spacing-2xl);\n    align-items: flex-start;\n    display: flex;\n}\n\n.halo-form .form-item-group .form-item {\n    margin-bottom: 0;\n}\n\n.halo-form .form-input {\n    border-radius: var(--rounded-base);\n    border: 1px solid var(--color-border);\n    background: #fff;\n    height: 2.5em;\n    padding: 0 0.75rem;\n}\n\n.halo-form .form-input:focus-within {\n    border-color: var(--color-primary);\n    outline-offset: \"2px\";\n    outline: 2px solid #0000;\n}\n\n.halo-form .form-item input {\n    appearance: none;\n    font-size: var(--text-base);\n    box-shadow: none;\n    background: none;\n    width: 100%;\n    height: 100%;\n    display: block;\n}\n\n.halo-form .form-item select {\n    appearance: none;\n    font-size: var(--text-base);\n    box-shadow: none;\n    width: 100%;\n    height: 100%;\n    display: block;\n    outline: none;\n    border: none;\n    background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m12 13.171l4.95-4.95l1.414 1.415L12 16L5.636 9.636L7.05 8.222z'/%3E%3C/svg%3E\")\n        right 0em center no-repeat;\n}\n\n.halo-form .form-item input:focus {\n    outline: none;\n}\n\n.halo-form .form-input-stack {\n    align-items: center;\n    gap: 0.5em;\n    display: flex;\n}\n\n.halo-form .form-input-stack-icon {\n    color: var(--color-text);\n    cursor: pointer;\n    align-items: center;\n    display: inline-flex;\n}\n\n.halo-form .form-input-stack-select {\n    all: unset;\n    color: var(--color-text);\n    font-size: var(--text-sm);\n    background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m12 13.171l4.95-4.95l1.414 1.415L12 16L5.636 9.636L7.05 8.222z'/%3E%3C/svg%3E\")\n        right 0.3em center no-repeat;\n    align-items: center;\n    padding-right: 1.85em;\n    display: inline-flex;\n}\n\n.halo-form .form-input-stack-text {\n    color: var(--color-text);\n    font-size: var(--text-sm);\n}\n\n.halo-form .form-item label {\n    color: var(--color-text);\n    font-size: var(--text-base);\n    margin-bottom: 0.75rem;\n    font-weight: 500;\n}\n\n.halo-form .form-item .form-label-group {\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 0.75em;\n    display: flex;\n}\n\n.halo-form .form-item .form-label-group label {\n    margin-bottom: 0;\n}\n\n.halo-form .form-item-extra-link {\n    color: var(--color-link);\n    font-size: var(--text-sm);\n    text-decoration: none;\n}\n\n.halo-form .form-item-compact {\n    gap: var(--spacing-sm);\n    margin-bottom: var(--spacing-2xl);\n    align-items: center;\n    display: flex;\n}\n\n.halo-form .form-item-compact label {\n    color: var(--color-text);\n    font-size: var(--text-sm);\n}\n\n.halo-form button[type=\"submit\"] {\n    background: var(--color-secondary);\n    border-radius: var(--rounded-base);\n    color: #fff;\n    cursor: pointer;\n    border: none;\n    height: 2.5em;\n}\n\n.halo-form button[type=\"submit\"]:hover {\n    opacity: 0.8;\n}\n\n.halo-form button[type=\"submit\"]:active {\n    opacity: 0.9;\n}\n\n.halo-form button[disabled] {\n    cursor: not-allowed !important;\n}\n\n.halo-form input[type=\"checkbox\"] {\n    border: 1px solid var(--color-border);\n    border-radius: var(--rounded-sm);\n    appearance: none;\n    print-color-adjust: exact;\n    vertical-align: middle;\n    user-select: none;\n    color: #2563eb;\n    background-color: #fff;\n    background-origin: border-box;\n    flex-shrink: 0;\n    width: 1em;\n    height: 1em;\n    padding: 0;\n    display: inline-block;\n}\n\n.halo-form input[type=\"checkbox\"]:focus {\n    outline-offset: 2px;\n    outline: 2px solid #0000;\n    box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2563eb, 0 0 #0000;\n}\n\n.halo-form input[type=\"checkbox\"]:checked {\n    background-color: currentColor;\n    background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e\");\n    background-position: center;\n    background-repeat: no-repeat;\n    background-size: 100% 100%;\n    border-color: #0000;\n}\n\n.halo-form .form-input-group {\n    gap: var(--spacing-sm);\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n    align-items: center;\n    display: grid;\n    height: 2.5em;\n}\n\n.halo-form .form-input {\n    grid-column: span 2 / span 2;\n}\n\n.halo-form .form-input-group button {\n    border-radius: var(--rounded-base);\n    border: 1px solid var(--color-border);\n    color: var(--color-text);\n    font-size: var(--text-sm);\n    cursor: pointer;\n    background: #fff;\n    grid-column: span 1 / span 1;\n    height: 100%;\n}\n\n.halo-form .form-input-group button:hover {\n    color: #333;\n    background: #f3f4f6;\n}\n\n.halo-form .form-input-group button:active {\n    background: #f9fafb;\n}\n\n.pill-items {\n    all: unset;\n    gap: var(--spacing-sm);\n    flex-wrap: wrap;\n    justify-content: center;\n    margin: 0;\n    display: flex;\n}\n\n.pill-items li {\n    all: unset;\n    border-radius: var(--rounded-lg);\n    border: 1px solid #e5e7eb;\n    transition-property: all;\n    transition-duration: 0.15s;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    overflow: hidden;\n}\n\n.pill-items li button {\n    all: unset;\n    cursor: pointer;\n    width: 100%;\n    height: 100%;\n}\n\n.pill-items li a,\n.pill-items li button {\n    gap: var(--spacing-sm);\n    font-size: var(--text-sm);\n    color: #1f2937;\n    align-items: center;\n    padding: 0.6em 0.9em;\n    text-decoration: none;\n    display: flex;\n}\n\n.pill-items li img {\n    width: 1.5em;\n    height: 1.5em;\n}\n\n.pill-items li:hover {\n    border-color: var(--color-primary);\n    background: #f3f4f6;\n}\n\n.pill-items li:hover {\n    color: #111827;\n}\n\n.pill-items li:focus-within {\n    border-color: var(--color-primary);\n}\n\n.divider-wrapper {\n    color: var(--color-text);\n    font-size: var(--text-sm);\n    gap: var(--spacing-lg);\n    align-items: center;\n    margin: 1.5em 0;\n    display: flex;\n}\n\n.divider-wrapper hr {\n    border: 0;\n    border-top: 1px solid #f3f4f6;\n    flex-grow: 1;\n    overflow: hidden;\n}\n\n.alert {\n    border-radius: var(--rounded-base);\n    margin-bottom: var(--spacing-xl);\n    padding: var(--spacing-md) var(--spacing-xl);\n    font-size: var(--text-sm);\n    color: var(--color-text);\n    border: 1px solid #e5e7eb;\n    position: relative;\n    overflow: hidden;\n}\n\n.alert:before {\n    content: \"\";\n    background: #d1d5db;\n    width: 0.25em;\n    height: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n}\n\n.alert-warning {\n    border-color: #fde047;\n}\n\n.alert-warning:before {\n    background: #ea580c;\n}\n\n.alert-error {\n    border-color: #fca5a5;\n}\n\n.alert-error:before {\n    background: #dc2626;\n}\n\n.alert-success {\n    border-color: #86efac;\n}\n\n.alert-success:before {\n    background: #16a34a;\n}\n\n.alert-info {\n    border-color: #7dd3fc;\n}\n\n.alert-info:before {\n    background: #0284c7;\n}\n\n@media (forced-colors: active) {\n    .halo-form input[type=\"checkbox\"]:checked {\n        appearance: auto;\n    }\n}\n\n@media only screen and (max-width: 768px) {\n    .halo-form .form-item-group {\n        flex-direction: column;\n    }\n}\n\n@media screen and (min-width: 1201px) and (max-width: 1600px) {\n    .gateway-wrapper {\n        font-size: var(--font-size-md);\n    }\n}\n\n@media screen and (min-width: 1601px) {\n    .gateway-wrapper {\n        font-size: var(--font-size-base);\n    }\n}\n\n::-ms-reveal {\n    display: none;\n}\n"
  },
  {
    "path": "application/src/main/resources/templates/challenges/two-factor/totp.html",
    "content": "<!doctype html>\n<html\n    xmlns:th=\"https://www.thymeleaf.org\"\n    th:replace=\"~{gateway_fragments/layout :: layout(title = |#{title} - ${site.title}|, head = ~{::head}, body = ~{::body})}\"\n>\n    <th:block th:fragment=\"body\">\n        <div class=\"gateway-wrapper totp-page-wrapper\">\n            <div th:replace=\"~{gateway_fragments/common::haloLogo}\"></div>\n            <div class=\"halo-form-wrapper\">\n                <h1 class=\"form-title\" th:text=\"#{title}\"></h1>\n                <form th:replace=\"~{gateway_fragments/totp::form}\"></form>\n            </div>\n        </div>\n    </th:block>\n\n    <th:block th:fragment=\"head\">\n        <style>\n            .totp-page-wrapper .cancel-link {\n                color: var(--color-link);\n                font-size: var(--text-sm);\n                text-decoration: none;\n                text-align: center;\n            }\n        </style>\n    </th:block>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/challenges/two-factor/totp.properties",
    "content": "title=两步验证\n"
  },
  {
    "path": "application/src/main/resources/templates/challenges/two-factor/totp_en.properties",
    "content": "title=Two-Factor Authentication\n"
  },
  {
    "path": "application/src/main/resources/templates/challenges/two-factor/totp_es.properties",
    "content": "title=Autenticación en Dos Pasos\n"
  },
  {
    "path": "application/src/main/resources/templates/challenges/two-factor/totp_zh_TW.properties",
    "content": "title=兩步驗證\n"
  },
  {
    "path": "application/src/main/resources/templates/error/error.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\" xmlns:th=\"https://www.thymeleaf.org\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <title th:text=\"${error.status} + ' | ' + ${#strings.defaultString(error.title, 'Internal server error')}\"></title>\n    <style>\n        body {\n            padding: 30px 20px;\n            font-family: -apple-system, BlinkMacSystemFont,\n            \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\",\n            \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n            color: #727272;\n            line-height: 1.6;\n        }\n\n        .container {\n            max-width: 500px;\n            margin: 0 auto;\n        }\n\n        h1 {\n            margin: 0;\n            font-size: 60px;\n            line-height: 1;\n            color: #252427;\n            font-weight: 700;\n            display: inline-block;\n        }\n\n        h2 {\n            margin: 100px 0 0;\n            font-weight: 600;\n            letter-spacing: 0.1em;\n            color: #A299AC;\n            text-transform: uppercase;\n        }\n\n        p {\n            font-size: 16px;\n            margin: 1em 0;\n        }\n\n        @media screen and (min-width: 768px) {\n            body {\n                padding: 50px;\n            }\n        }\n\n        @media screen and (max-width: 480px) {\n            h1 {\n                font-size: 48px;\n            }\n        }\n\n        .title {\n            position: relative;\n        }\n\n        .title::before {\n            content: '';\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            right: 0;\n            height: 2px;\n            background-color: #000;\n            transform-origin: bottom right;\n            transform: scaleX(0);\n            transition: transform 0.5s ease;\n        }\n\n        .title:hover::before {\n            transform-origin: bottom left;\n            transform: scaleX(1);\n        }\n\n        .back-home button {\n            z-index: 1;\n            position: relative;\n            font-size: inherit;\n            font-family: inherit;\n            color: white;\n            padding: 0.5em 1em;\n            outline: none;\n            border: none;\n            background-color: hsl(0, 0%, 0%);\n            overflow: hidden;\n            transition: color 0.4s ease-in-out;\n        }\n\n        .back-home button::before {\n            content: '';\n            z-index: -1;\n            position: absolute;\n            top: 100%;\n            left: 100%;\n            width: 1em;\n            height: 1em;\n            border-radius: 50%;\n            background-color: #fff;\n            transform-origin: center;\n            transform: translate3d(-50%, -50%, 0) scale3d(0, 0, 0);\n            transition: transform 0.45s ease-in-out;\n        }\n\n        .back-home button:hover {\n            cursor: pointer;\n            color: #000;\n        }\n\n        .back-home button:hover::before {\n            transform: translate3d(-50%, -50%, 0) scale3d(15, 15, 15);\n        }\n    </style>\n</head>\n<body>\n\n<div class=\"container\">\n    <h2 th:text=\"${error.status}\"></h2>\n    <h1 class=\"title\" th:text=\"${#strings.defaultString(error.title, 'Internal server error')}\"></h1>\n    <p th:text=\"${#strings.defaultString(error.detail, '未知错误！可能存在的原因：未正确设置主题或主题文件缺失。')}\"></p>\n    <div class=\"back-home\">\n        <button th:onclick=\"window.location.href='[(${site.url})]'\"\n                th:text=\"首页\">\n        </button>\n    </div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/common.html",
    "content": "<th:block th:fragment=\"basicStaticResources\">\n    <th:block th:replace=\"~{gateway_fragments/common::basicStyleResources}\"></th:block>\n    <th:block th:replace=\"~{gateway_fragments/common::basicScriptResources}\"></th:block>\n</th:block>\n\n<th:block th:fragment=\"basicScriptResources\">\n    <th:block th:if=\"${not #strings.isEmpty(publicKey)} and ${fragmentTemplateName} == 'login_local'\">\n        <script src=\"/webjars/jsencrypt/3.5.4/bin/jsencrypt.min.js\" defer></script>\n        <script th:inline=\"javascript\" type=\"text/javascript\">\n            const publicKey = /*[[${publicKey}]]*/ \"\";\n\n            // Encrypt function\n            function encryptPassword(password) {\n                const encrypt = new JSEncrypt();\n                encrypt.setPublicKey(publicKey);\n                return encrypt.encrypt(password);\n            }\n\n            document.addEventListener(\"DOMContentLoaded\", function () {\n                const loginForm = document.getElementById(\"login-form\");\n                loginForm.addEventListener(\"submit\", function (event) {\n                    const plainPasswordInput = document.getElementById(\"plainPassword\");\n                    const passwordInput = document.getElementById(\"password\");\n                    passwordInput.value = encryptPassword(plainPasswordInput.value);\n                });\n            });\n        </script>\n    </th:block>\n\n    <script th:inline=\"javascript\">\n        const i18nResources = {\n            sendVerificationCodeSuccess: `[(#{js.sendVerificationCode.success})]`,\n            sendVerificationCodeFailed: `[(#{js.sendVerificationCode.failed})]`,\n            sendVerificationCodeSending: `[(#{js.sendVerificationCode.sending})]`,\n            passwordConfirmationFailed: `[(#{js.passwordConfirmation.failed})]`,\n        };\n    </script>\n\n    <script src=\"/js/main.js\"></script>\n</th:block>\n\n<th:block th:fragment=\"basicStyleResources\">\n    <link rel=\"stylesheet\" href=\"/webjars/normalize.css/8.0.1/normalize.css\" />\n    <link rel=\"stylesheet\" th:href=\"|/styles/main.css?v=${site.version}|\" />\n</th:block>\n\n<div th:remove=\"tag\" th:fragment=\"languageSwitcher\">\n    <style>\n        .language-switcher {\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            margin: 2em 0;\n            gap: 0.625rem;\n        }\n\n        .language-switcher label {\n            color: var(--color-text);\n            display: inline-flex;\n        }\n\n        .language-switcher select {\n            all: unset;\n            border: 1px solid var(--color-border);\n            font-size: var(--text-sm);\n            height: 2em;\n            border-radius: var(--rounded-lg);\n            outline: none;\n            padding: 0 2em 0 0.5em;\n            display: inline-flex;\n            align-items: center;\n            color: var(--color-text);\n            background-color: var(--color-text);\n            background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m12 13.171l4.95-4.95l1.414 1.415L12 16L5.636 9.636L7.05 8.222z'/%3E%3C/svg%3E\");\n            background-repeat: no-repeat;\n            background-position: right 0.45rem center;\n        }\n\n        .language-switcher select:focus {\n            border-color: var(--color-primary);\n        }\n    </style>\n    <div class=\"language-switcher\">\n        <label for=\"language-select\">\n            <svg viewBox=\"0 0 24 24\" width=\"1.2em\" height=\"1.2em\">\n                <path\n                    fill=\"currentColor\"\n                    d=\"m12.87 15.07l-2.54-2.51l.03-.03A17.5 17.5 0 0 0 14.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35C8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5l3.11 3.11zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2zm-2.62 7l1.62-4.33L19.12 17z\"\n                ></path>\n            </svg>\n        </label>\n        <select id=\"language-select\" onchange=\"changeLanguage()\">\n            <option value=\"en\" th:selected=\"${#locale.toLanguageTag} == 'en'\">English</option>\n            <option value=\"es\" th:selected=\"${#locale.toLanguageTag} == 'es'\">Español</option>\n            <option value=\"zh-CN\" th:selected=\"${#locale.toLanguageTag} == 'zh-CN'\">简体中文</option>\n            <option value=\"zh-TW\" th:selected=\"${#locale.toLanguageTag} == 'zh-TW'\">繁体中文</option>\n        </select>\n        <script type=\"text/javascript\">\n            function changeLanguage() {\n                const selectedLanguage = document.getElementById(\"language-select\").value;\n                const currentURL = new URL(window.location.href);\n                currentURL.searchParams.set(\"language\", selectedLanguage);\n\n                history.replaceState(null, \"\", currentURL.toString());\n\n                window.location.reload();\n            }\n        </script>\n    </div>\n</div>\n\n<div th:remove=\"tag\" th:fragment=\"haloLogo\">\n    <style>\n        .halo-logo {\n            width: 100%;\n            display: flex;\n            justify-content: center;\n            margin-bottom: 2em;\n        }\n\n        .halo-logo img {\n            width: 6em;\n        }\n    </style>\n    <div class=\"halo-logo\">\n        <img src=\"/images/wordmark.svg\" alt=\"Halo\" />\n    </div>\n</div>\n\n<div th:remove=\"tag\" th:fragment=\"socialAuthProviders\">\n    <th:block th:unless=\"${#lists.isEmpty(socialAuthProviders)}\">\n        <div class=\"divider-wrapper\">\n            <hr />\n            <th:block th:text=\"#{socialLogin.label}\"></th:block>\n            <hr />\n        </div>\n        <ul class=\"pill-items\">\n            <li th:each=\"provider : ${socialAuthProviders}\">\n                <form\n                    class=\"social-auth-provider-form\"\n                    th:action=\"|/login/social/${provider.metadata.name}?remember-me=false|\"\n                    method=\"post\"\n                >\n                    <button type=\"submit\">\n                        <img th:src=\"${provider.spec.logo}\" th:alt=\"|${provider.spec.displayName}'s icon|\" />\n                        <span th:text=\"${provider.spec.displayName}\"></span>\n                    </button>\n                </form>\n            </li>\n        </ul>\n    </th:block>\n    <script>\n        document.addEventListener(\"DOMContentLoaded\", function () {\n            const rememberMeCheckbox = document.getElementById(\"remember-me\");\n            const socialAuthProviders = document.querySelectorAll(\".social-auth-provider-form\");\n            \n            rememberMeCheckbox.addEventListener(\"change\", function (e) {\n                const rememberMe = e.target.checked;\n                \n                socialAuthProviders.forEach((form) => {\n                    const url = new URL(form.action, window.location.origin);\n                    url.searchParams.set(\"remember-me\", rememberMe ? \"true\" : \"false\");\n                    form.action = url.pathname + url.search;\n                });\n            });\n        });\n    </script>\n</div>\n\n<div th:remove=\"tag\" th:fragment=\"passwordResetMethods\">\n    <th:block th:unless=\"${#lists.isEmpty(otherMethods)}\">\n        <div class=\"divider-wrapper\">\n            <hr />\n            <th:block th:text=\"#{passwordResetMethods.label}\"></th:block>\n            <hr />\n        </div>\n        <ul class=\"pill-items\">\n            <li th:each=\"method : ${otherMethods}\">\n                <a th:href=\"${method.href}\">\n                    <img th:src=\"${method.icon}\" th:alt=\"|${method.name}'s icon|\" />\n                    <span\n                        th:text=\"${#messages.msgOrNull('passwordResetMethods.' + method.name + '.displayName') ?: method.name}\"\n                    ></span>\n                </a>\n            </li>\n        </ul>\n    </th:block>\n</div>\n\n<div th:remove=\"tag\" th:fragment=\"signupNoticeContent\">\n    <style>\n        .signup-notice-content {\n            font-size: var(--text-sm);\n            color: var(--color-text);\n            text-align: center;\n            margin: 1em 0;\n        }\n\n        .signup-notice-content a {\n            color: var(--color-link);\n            text-decoration: none;\n        }\n    </style>\n    <div th:if=\"${globalInfo.allowRegistration}\" class=\"signup-notice-content\">\n        <div>\n            <th:block th:text=\"#{signupNotice.description}\"></th:block>\n            <a th:href=\"@{/signup}\" th:text=\"#{signupNotice.link}\"></a>\n        </div>\n    </div>\n</div>\n\n<div th:remove=\"tag\" th:fragment=\"loginNoticeContent\">\n    <style>\n        .login-notice-content {\n            font-size: var(--text-sm);\n            color: var(--color-text);\n            text-align: center;\n            margin: 1em 0;\n        }\n\n        .login-notice-content a {\n            color: var(--color-link);\n            text-decoration: none;\n        }\n    </style>\n    <div th:if=\"${globalInfo.allowRegistration}\" class=\"login-notice-content\">\n        <div>\n            <th:block th:text=\"#{loginNotice.description}\"></th:block>\n            <a th:href=\"@{/login}\" th:text=\"#{loginNotice.link}\"></a>\n        </div>\n    </div>\n</div>\n\n<div th:remove=\"tag\" th:fragment=\"returnToSiteContent\">\n    <style>\n        .returntosite-content {\n            font-size: var(--text-sm);\n            text-align: center;\n            margin: 1em 0;\n        }\n\n        .returntosite-content a {\n            color: var(--color-text);\n            display: inline-flex;\n            align-items: center;\n            gap: 0.3em;\n            text-decoration: none;\n        }\n    </style>\n    <div class=\"returntosite-content\">\n        <a th:href=\"@{/}\">\n            <svg viewBox=\"0 0 24 24\" width=\"1.2em\" height=\"1.2em\">\n                <path fill=\"currentColor\" d=\"M21 11H6.83l3.58-3.59L9 6l-6 6l6 6l1.41-1.42L6.83 13H21z\"></path>\n            </svg>\n            <span th:text=\"#{returnToSite}\"></span>\n        </a>\n    </div>\n</div>\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/common.properties",
    "content": "socialLogin.label=社交登录\njs.sendVerificationCode.success=发送成功\njs.sendVerificationCode.failed=发送失败，请稍后再试\njs.sendVerificationCode.sending=发送中...\njs.passwordConfirmation.failed=确认密码不匹配\n\nsignupNotice.description=没有账号？\nsignupNotice.link=立即注册\nloginNotice.description=已有账号，\nloginNotice.link=立即登录\nreturnToSite=返回网站\n\npasswordResetMethods.label=其他重置方式\npasswordResetMethods.email.displayName=通过邮件重置"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/common_en.properties",
    "content": "socialLogin.label=Social Login\njs.sendVerificationCode.success=Sent Successfully\njs.sendVerificationCode.failed=Sending Failed, Please Try Again Later\njs.sendVerificationCode.sending=Sending...\njs.passwordConfirmation.failed=Password confirmation does not match\n\nsignupNotice.description=Don't have an account?\nsignupNotice.link=Sign up\nloginNotice.description=Already have an account,\nloginNotice.link=Login now\nreturnToSite=Return to site\n\npasswordResetMethods.label=Other Reset Methods\npasswordResetMethods.email.displayName=Reset via Email"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/common_es.properties",
    "content": "socialLogin.label=Inicio de Sesión Social\njs.sendVerificationCode.success=Enviado con éxito\njs.sendVerificationCode.failed=Error al enviar, por favor intente nuevamente más tarde\njs.sendVerificationCode.sending=Enviando...\njs.passwordConfirmation.failed=La confirmación de la contraseña no coincide\n\nsignupNotice.description=¿No tienes una cuenta?\nsignupNotice.link=Regístrate ahora\nloginNotice.description=Ya tienes una cuenta,\nloginNotice.link=Inicia sesión ahora\nreturnToSite=Volver al sitio\n\npasswordResetMethods.label=Otros Métodos de Restablecimiento\npasswordResetMethods.email.displayName=Restablecer por Correo Electrónico"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/common_zh_TW.properties",
    "content": "socialLogin.label=社交登入\njs.sendVerificationCode.success=發送成功\njs.sendVerificationCode.failed=發送失敗，請稍後再試\njs.sendVerificationCode.sending=發送中...\njs.passwordConfirmation.failed=確認密碼不匹配\n\nsignupNotice.description=沒有帳號？\nsignupNotice.link=立即註冊\nloginNotice.description=已有帳號，\nloginNotice.link=立即登入\nreturnToSite=返回網站\n\npasswordResetMethods.label=其他重置方式\npasswordResetMethods.email.displayName=通過郵件重置"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/input.html",
    "content": "<div th:remove=\"tag\" th:fragment=\"password(id,name,required,minlength,maxlength,enableToggle)\">\n    <div class=\"form-input\" th:classappend=\"${enableToggle ? 'form-input-stack toggle-password-display-flag' : ''}\">\n        <input\n            th:id=\"${id}\"\n            th:name=\"${name}\"\n            type=\"password\"\n            autocomplete=\"off\"\n            spellcheck=\"false\"\n            autocorrect=\"off\"\n            autocapitalize=\"off\"\n            th:required=\"${required}\"\n            th:minlength=\"${minlength}\"\n            th:maxlength=\"${maxlength}\"\n        />\n\n        <div th:if=\"${enableToggle}\" class=\"form-input-stack-icon toggle-password-button\">\n            <svg class=\"password-hidden-icon\" style=\"display: none\" viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\">\n                <path\n                    fill=\"currentColor\"\n                    d=\"M12 3c5.392 0 9.878 3.88 10.819 9c-.94 5.12-5.427 9-10.819 9c-5.392 0-9.878-3.88-10.818-9C2.122 6.88 6.608 3 12 3Zm0 16a9.005 9.005 0 0 0 8.778-7a9.005 9.005 0 0 0-17.555 0A9.005 9.005 0 0 0 12 19Zm0-2.5a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9Zm0-2a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5Z\"\n                ></path>\n            </svg>\n\n            <svg class=\"password-display-icon\" viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\">\n                <path\n                    fill=\"currentColor\"\n                    d=\"M17.883 19.297A10.949 10.949 0 0 1 12 21c-5.392 0-9.878-3.88-10.818-9A10.982 10.982 0 0 1 4.52 5.935L1.394 2.808l1.414-1.414l19.799 19.798l-1.414 1.415l-3.31-3.31ZM5.936 7.35A8.965 8.965 0 0 0 3.223 12a9.005 9.005 0 0 0 13.201 5.838l-2.028-2.028A4.5 4.5 0 0 1 8.19 9.604L5.936 7.35Zm6.978 6.978l-3.242-3.241a2.5 2.5 0 0 0 3.241 3.241Zm7.893 2.265l-1.431-1.431A8.935 8.935 0 0 0 20.778 12A9.005 9.005 0 0 0 9.552 5.338L7.974 3.76C9.221 3.27 10.58 3 12 3c5.392 0 9.878 3.88 10.819 9a10.947 10.947 0 0 1-2.012 4.593Zm-9.084-9.084a4.5 4.5 0 0 1 4.769 4.769l-4.77-4.77Z\"\n                ></path>\n            </svg>\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/layout.html",
    "content": "<!doctype html>\n<html th:lang=\"${#locale.toLanguageTag}\" th:fragment=\"layout (title,head,body)\">\n    <head>\n        <meta charset=\"UTF-8\" />\n        <meta content=\"IE=edge\" http-equiv=\"X-UA-Compatible\" />\n        <meta content=\"webkit\" name=\"renderer\" />\n        <meta\n            content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\"\n            name=\"viewport\"\n        />\n        <meta content=\"noindex,nofollow\" name=\"robots\" />\n        <title th:text=\"${title}\"></title>\n        <link rel=\"preload\" href=\"/images/wordmark.svg\" as=\"image\" type=\"image/svg+xml\" />\n        <link rel=\"preload\" href=\"/images/logo.png\" as=\"image\" type=\"image/png\" />\n        <link th:unless=\"${#strings.isEmpty(site.favicon)}\" rel=\"icon\" th:href=\"${site.favicon}\" />\n\n        <th:block th:replace=\"~{gateway_fragments/common::basicStaticResources}\"></th:block>\n\n        <th:block th:if=\"${head != null}\">\n            <th:block th:replace=\"${head}\" />\n        </th:block>\n    </head>\n    <body class=\"gateway-page\">\n        <th:block th:replace=\"${body}\" />\n    </body>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/login.html",
    "content": "<form\n    th:fragment=\"form\"\n    class=\"halo-form\"\n    name=\"login-form\"\n    id=\"login-form\"\n    th:action=\"${authProvider.spec.authenticationUrl}\"\n    th:method=\"${authProvider.spec.method}\"\n>\n    <div class=\"alert alert-error\" role=\"alert\" th:if=\"${param.error.size() > 0}\" th:with=\"error = ${param.error[0]}\">\n        <strong th:if=\"${error == 'invalid-credential'}\">\n            <span th:text=\"#{form.error.invalidCredential}\"></span>\n        </strong>\n        <strong th:if=\"${error == 'rate-limit-exceeded'}\">\n            <span th:text=\"#{form.error.rateLimitExceeded}\"></span>\n        </strong>\n        <strong th:if=\"${error == 'account-disabled'}\">\n            <span th:text=\"#{form.error.accountDisabled}\"></span>\n        </strong>\n    </div>\n    <div class=\"alert\" role=\"alert\" th:if=\"${param.logout.size() > 0}\">\n        <strong th:text=\"#{form.messages.logoutSuccess}\"></strong>\n    </div>\n    <div class=\"alert\" role=\"alert\" th:if=\"${param.signup.size() > 0}\">\n        <strong th:text=\"#{form.messages.signupSuccess}\"></strong>\n    </div>\n    <div class=\"alert\" role=\"alert\" th:if=\"${param.oauth2_bind.size() > 0}\">\n        <strong th:text=\"#{form.messages.oauth2Bind}\"></strong>\n    </div>\n    <div class=\"alert\" role=\"alert\" th:if=\"${param.password_reset.size() > 0}\">\n        <strong th:text=\"#{form.messages.passwordReset}\"></strong>\n    </div>\n\n    <div th:replace=\"~{__${fragmentTemplateName}__::form}\"></div>\n\n    <div th:if=\"${authProvider.spec.rememberMeSupport}\" class=\"form-item-compact\">\n        <input type=\"checkbox\" id=\"remember-me\" name=\"remember-me\" value=\"true\" th:checked=\"${rememberMe}\"/>\n        <label for=\"remember-me\" th:text=\"#{form.rememberMe.label}\"></label>\n    </div>\n\n    <div class=\"form-item\">\n        <button type=\"submit\" th:text=\"#{form.submit}\"></button>\n    </div>\n</form>\n\n<div th:remove=\"tag\" th:fragment=\"formAuthProviders\">\n    <th:block th:unless=\"${#lists.isEmpty(formAuthProviders)}\">\n        <div class=\"divider-wrapper\">\n            <hr />\n            <th:block th:text=\"#{otherLogin.label}\"></th:block>\n            <hr />\n        </div>\n        <ul class=\"pill-items\">\n            <li th:each=\"provider : ${formAuthProviders}\">\n                <a th:href=\"'/login?method=' + ${provider.metadata.name}\">\n                    <img th:src=\"${provider.spec.logo}\" th:alt=\"|${provider.spec.displayName}'s icon|\" />\n                    <span\n                        th:text=\"${#messages.msgOrNull('formAuthProviders.' + provider.metadata.name + '.displayName') ?: provider.spec.displayName}\"\n                    ></span>\n                </a>\n            </li>\n        </ul>\n    </th:block>\n</div>"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/login.properties",
    "content": "form.messages.logoutSuccess=登出成功。\nform.messages.signupSuccess=恭喜！注册成功，请立即登录。\nform.messages.oauth2Bind=当前登录未绑定账号，请尝试通过其他方式登录，登录成功后会自动绑定账号。\nform.messages.passwordReset=密码重置成功，请立即登录。\nform.error.invalidCredential=无效的凭证。\nform.error.rateLimitExceeded=请求过于频繁，请稍后再试。\nform.error.accountDisabled=账号已被禁用。\nform.rememberMe.label=保持登录会话\nform.submit=登录\n\notherLogin.label=其他登录方式\n\n# Rule: `formAuthProviders.${provider.metadata.name}.displayName`\nformAuthProviders.local.displayName=账号密码登录"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/login_en.properties",
    "content": "form.messages.logoutSuccess=Logout successfully.\nform.messages.signupSuccess=Congratulations! Sign up successfully, please login now.\nform.messages.oauth2Bind=The current login is not bound to an account. Please try to log in through other methods. After successful login, the account will be automatically bound.\nform.messages.passwordReset=Password reset successfully, please login now.\nform.error.invalidCredential=Invalid credentials.\nform.error.rateLimitExceeded=Too many requests, please try again later.\nform.error.accountDisabled=Account has been disabled.\nform.rememberMe.label=Remember me\nform.submit=Login\n\notherLogin.label=Other Login\n\n# Rule: `formAuthProviders.${provider.metadata.name}.displayName`\nformAuthProviders.local.displayName=Login with credentials"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/login_es.properties",
    "content": "form.messages.logoutSuccess=Cierre de sesión exitoso.\nform.messages.signupSuccess=¡Felicidades! Registro exitoso, por favor inicie sesión de inmediato.\nform.messages.oauth2Bind=El inicio de sesión actual no está vinculado a una cuenta. Intente iniciar sesión a través de otros métodos. Después de un inicio de sesión exitoso, la cuenta se vinculará automáticamente.\nform.messages.passwordReset=¡Felicidades! La contraseña se ha restablecido con éxito.\nform.error.invalidCredential=Credenciales inválidas.\nform.error.rateLimitExceeded=Demasiadas solicitudes, por favor intente nuevamente más tarde.\nform.error.accountDisabled=La cuenta ha sido deshabilitada.\nform.rememberMe.label=Mantener sesión iniciada\nform.submit=Iniciar sesión\n\notherLogin.label=Otras formas de inicio de sesión\n\n# Rule: `formAuthProviders.${provider.metadata.name}.displayName`\nformAuthProviders.local.displayName=Iniciar sesión con credenciales"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/login_zh_TW.properties",
    "content": "form.messages.logoutSuccess=登出成功。\nform.messages.signupSuccess=恭喜！註冊成功，請立即登入。\nform.messages.oauth2Bind=當前登入未綁定至帳戶。請嘗試通過其他方法登入。成功登入後，帳戶將自動綁定。\nform.messages.passwordReset=密碼重置成功。請立即登入。\nform.error.invalidCredential=無效的憑證。\nform.error.rateLimitExceeded=請求過於頻繁，請稍後再試。\nform.error.accountDisabled=帳戶已被停用。\nform.form.rememberMe.label=保持登入會話\nform.form.submit=登入\n\notherLogin.label=其他登入方式\n\n# Rule: `formAuthProviders.${provider.metadata.name}.displayName`\nformAuthProviders.local.displayName=帳號密碼登入"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/logout.html",
    "content": "<form th:fragment=\"form\" class=\"halo-form\" id=\"logout-form\" name=\"logout-form\" th:action=\"@{/logout}\" method=\"post\">\n    <div class=\"user-info\" th:object=\"${user}\">\n        <img th:if=\"*{spec.avatar}\" th:src=\"*{spec.avatar}\" class=\"user-avatar\" />\n        <div class=\"user-details\">\n            <span th:text=\"#{form.currentUser.label}\"></span>\n            <span class=\"user-name\" th:text=\"|*{spec.displayName}(@*{metadata.name})|\"></span>\n        </div>\n    </div>\n    <div class=\"form-item\">\n        <button type=\"submit\" th:text=\"#{form.submit}\"></button>\n    </div>\n\n    <div class=\"form-item\">\n        <a href=\"javascript:history.back()\" class=\"cancel-link\" th:text=\"#{form.cancel}\"> </a>\n    </div>\n</form>\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/logout.properties",
    "content": "form.submit=退出登录\nform.cancel=取消\nform.currentUser.label=当前登录的用户："
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/logout_en.properties",
    "content": "form.submit=Logout\nform.cancel=Cancel\nform.currentUser.label=Currently logged in user:"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/logout_es.properties",
    "content": "form.submit=Cerrar Sesión\nform.cancel=Cancelar\nform.currentUser.label=Usuario actualmente conectado:\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/logout_zh_TW.properties",
    "content": "form.submit=退出登入\nform.cancel=取消\nform.currentUser.label=當前登入的使用者："
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_reset.html",
    "content": "<form\n        th:fragment=\"form\"\n        class=\"halo-form\"\n        th:action=\"@{/password-reset/email/{resetToken}(resetToken=${resetToken})}\"\n        method=\"post\"\n        th:object=\"${form}\"\n>\n    <div class=\"alert alert-error\" role=\"alert\" th:if=\"${error}\">\n        <strong th:if=\"${error == 'rate_limit_exceeded'}\" th:text=\"#{error.rate_limit_exceeded}\"></strong>\n    </div>\n    <div class=\"form-item\">\n        <label for=\"password\" th:text=\"#{form.password.label}\">Password</label>\n        <th:block\n                th:replace=\"~{gateway_fragments/input :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}\"\n        ></th:block>\n        <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('password')}\" th:errors=\"*{password}\"></p>\n    </div>\n    <div class=\"form-item\">\n        <label for=\"confirmPassword\" th:text=\"#{form.confirmPassword.label}\">Confirm Password</label>\n        <th:block\n                th:replace=\"~{gateway_fragments/input :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}\"\n        ></th:block>\n        <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('confirmPassword')}\" th:errors=\"*{confirmPassword}\"></p>\n    </div>\n    <div class=\"form-item\">\n        <div class=\"alert\" th:text=\"#{form.password.tips}\"></div>\n    </div>\n    <div class=\"form-item\">\n        <button type=\"submit\" th:text=\"#{form.submit}\"></button>\n    </div>\n\n    <script th:inline=\"javascript\">\n        document.addEventListener(\"DOMContentLoaded\", function () {\n            setupPasswordConfirmation(\"password\", \"confirmPassword\");\n        });\n    </script>\n</form>"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_reset.properties",
    "content": "form.password.label=密码\nform.confirmPassword.label=确认密码\nform.password.tips=密码只能使用大小写字母 (A-Z, a-z)、数字 (0-9)，以及以下特殊字符: !@#$%^&*.?。\nform.submit=修改密码\nerror.rate_limit_exceeded=您的请求过于频繁，请稍后再试。\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_reset_en.properties",
    "content": "form.password.label=Password\nform.confirmPassword.label=Confirm Password\nform.password.tips=The password can only use uppercase and lowercase letters (A-Z, a-z), numbers (0-9), and the following special characters: !@#$%^&*.?\nform.submit=Change password\nerror.rate_limit_exceeded=Your request is too frequent, please try again later.\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_reset_es.properties",
    "content": "form.password.label=Contraseña\nform.confirmPassword.label=Confirmar Contraseña\nform.password.tips=La contraseña solo puede usar letras mayúsculas y minúsculas (A-Z, a-z), números (0-9) y los siguientes caracteres especiales: !@#$%^&*.?\nform.submit=Cambiar Contraseña\nerror.rate_limit_exceeded=Su solicitud es demasiado frecuente, por favor intente nuevamente más tarde.\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_reset_zh_TW.properties",
    "content": "form.password.label=密碼\nform.confirmPassword.label=確認密碼\nform.password.tips=密碼只能使用大小寫字母 (A-Z, a-z)、數字 (0-9)，以及以下特殊字符: !@#$%^&*.?\nform.submit=修改密碼\nerror.rate_limit_exceeded=您的請求過於頻繁，請稍後再試。\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_send.html",
    "content": "<th:block th:fragment=\"form\">\n    <form th:if=\"${sent}\" method=\"get\" action=\"/login\" class=\"halo-form\">\n        <div class=\"form-item\">\n            <div class=\"alert\" th:text=\"#{form.message.success}\"></div>\n        </div>\n        <div class=\"form-item\">\n            <button type=\"submit\" th:text=\"#{form.sent.submit}\"></button>\n        </div>\n    </form>\n    <form th:unless=\"${sent}\" class=\"halo-form\" th:action=\"@{/password-reset/email}\" method=\"post\" th:object=\"${form}\">\n        <div class=\"alert alert-error\" th:if=\"${param.error.size() > 0}\">\n            <strong th:if=\"${param.error[0] == 'invalid_reset_token'}\" th:text=\"#{error.invalid_reset_token}\"></strong>\n        </div>\n        <div class=\"alert alert-error\" th:if=\"${error}\">\n            <strong th:if=\"${error == 'rate_limit_exceeded'}\" th:text=\"#{error.rate_limit_exceeded}\"></strong>\n        </div>\n        <div class=\"form-item\">\n            <label for=\"email\" th:text=\"#{form.email.label}\"></label>\n            <div class=\"form-input\">\n                <input type=\"email\" id=\"email\" name=\"email\" autofocus required th:field=\"*{email}\"/>\n            </div>\n            <p class=\"alert alert-error\" role=\"alert\" th:if=\"${#fields.hasErrors('email')}\" th:errors=\"*{email}\"></p>\n        </div>\n        <div class=\"form-item\">\n            <button type=\"submit\" th:text=\"#{form.submit}\"></button>\n        </div>\n    </form>\n</th:block>\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_send.properties",
    "content": "form.email.label=电子邮箱\nform.submit=提交\nform.sent.submit=返回到登录页面\nform.message.success=检查您的电子邮件中是否有重置密码的链接。如果几分钟内没有出现，请检查您的垃圾邮件文件夹。\nerror.rate_limit_exceeded=您的请求速度太快。请稍后再试。\nerror.invalid_reset_token=重置密码令牌无效。请重试。\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_send_en.properties",
    "content": "form.email.label=Email\nform.submit=Submit\nform.sent.submit=Return to login\nform.message.success=Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder.\nerror.rate_limit_exceeded=You are making requests too quickly. Please try again later.\nerror.invalid_reset_token=The reset password token is invalid. Please try again.\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_send_es.properties",
    "content": "form.email.label=Correo Electrónico\nform.submit=Enviar\nform.sent.submit=Volver a la Página de Inicio de Sesión\nform.message.success=Revisa tu correo electrónico para ver el enlace de restablecimiento de contraseña. Si no aparece en unos minutos, revisa tu carpeta de spam.\nerror.rate_limit_exceeded=Se ha superado el límite de intentos de restablecimiento de contraseña. Por favor, inténtalo de nuevo más tarde.\nerror.invalid_reset_token=El enlace de restablecimiento de contraseña no es válido o ha expirado. Por favor, solicita un nuevo enlace.\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/password_reset_email_send_zh_TW.properties",
    "content": "form.email.label=電子郵件\nform.submit=提交\nform.sent.submit=返回到登入頁面\nform.message.success=檢查您的電子郵件中是否有重置密碼的連結。如果幾分鐘內沒有出現，請檢查您的垃圾郵件資料夾。\nerror.rate_limit_exceeded=您的請求過於頻繁。請稍後再試。\nerror.invalid_reset_token=重置密碼連結無效。請重試。\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/signup.html",
    "content": "<form\n    th:fragment=\"form\"\n    class=\"halo-form\"\n    name=\"signup-form\"\n    id=\"signup-form\"\n    th:action=\"@{/signup}\"\n    th:object=\"${form}\"\n    method=\"post\"\n>\n    <div class=\"alert alert-error\" role=\"alert\" th:if=\"${error == 'invalid-email-code'}\">\n        <strong>\n            <span th:text=\"#{form.error.invalidEmailCode}\"></span>\n        </strong>\n    </div>\n    <div class=\"alert alert-error\" role=\"alert\" th:if=\"${error == 'rate-limit-exceeded'}\">\n        <strong>\n            <span th:text=\"#{form.error.rateLimitExceeded}\"></span>\n        </strong>\n    </div>\n    <div class=\"alert alert-error\" role=\"alert\" th:if=\"${error == 'duplicate-username'}\">\n        <strong>\n            <span th:text=\"#{form.error.duplicateUsername}\"></span>\n        </strong>\n    </div>\n    <div class=\"alert alert-error\" role=\"alert\" th:if=\"${error == 'restricted-username'}\">\n        <strong>\n            <span th:text=\"#{form.error.protectedUsernames}\"></span>\n        </strong>\n    </div>\n    <div class=\"form-item-group\">\n        <div class=\"form-item\">\n            <label for=\"username\" th:text=\"#{form.username.label}\"></label>\n            <div class=\"form-input\">\n                <input\n                    type=\"text\"\n                    id=\"username\"\n                    name=\"username\"\n                    autocomplete=\"off\"\n                    spellcheck=\"false\"\n                    autocorrect=\"off\"\n                    autocapitalize=\"off\"\n                    autofocus\n                    required\n                    minlength=\"4\"\n                    maxlength=\"63\"\n                    th:field=\"*{username}\"\n                />\n            </div>\n            <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('username')}\" th:errors=\"*{username}\"></p>\n        </div>\n\n        <div class=\"form-item\">\n            <label for=\"displayName\" th:text=\"#{form.displayName.label}\"></label>\n            <div class=\"form-input\">\n                <input\n                    type=\"text\"\n                    id=\"displayName\"\n                    name=\"displayName\"\n                    autocomplete=\"off\"\n                    spellcheck=\"false\"\n                    autocorrect=\"off\"\n                    autocapitalize=\"off\"\n                    required\n                    th:field=\"*{displayName}\"\n                />\n            </div>\n            <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('displayName')}\" th:errors=\"*{displayName}\"></p>\n        </div>\n    </div>\n\n    <div class=\"form-item-group\">\n        <div class=\"form-item\">\n            <label for=\"email\" th:text=\"#{form.email.label}\"></label>\n            <div class=\"form-input\">\n                <input\n                    type=\"email\"\n                    id=\"email\"\n                    name=\"email\"\n                    autocomplete=\"off\"\n                    spellcheck=\"false\"\n                    autocorrect=\"off\"\n                    autocapitalize=\"off\"\n                    required\n                    th:field=\"*{email}\"\n                />\n            </div>\n            <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('email')}\" th:errors=\"*{email}\"></p>\n        </div>\n\n        <div class=\"form-item\" th:if=\"${globalInfo.mustVerifyEmailOnRegistration}\">\n            <label for=\"emailCode\" th:text=\"#{form.emailCode.label}\"></label>\n            <div class=\"form-input-group\">\n                <div class=\"form-input\">\n                    <input type=\"text\" inputmode=\"numeric\" pattern=\"\\d*\" id=\"emailCode\" name=\"emailCode\" required />\n                </div>\n\n                <button id=\"emailCodeSendButton\" type=\"button\" th:text=\"#{form.emailCode.sendButton}\"></button>\n            </div>\n            <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('emailCode')}\" th:errors=\"*{emailCode}\"></p>\n        </div>\n    </div>\n\n    <div class=\"form-item\">\n        <label for=\"password\" th:text=\"#{form.password.label}\"></label>\n        <th:block\n            th:replace=\"~{gateway_fragments/input :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}\"\n        ></th:block>\n        <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('password')}\" th:errors=\"*{password}\"></p>\n    </div>\n\n    <div class=\"form-item\">\n        <label for=\"confirmPassword\" th:text=\"#{form.confirmPassword.label}\"></label>\n        <th:block\n            th:replace=\"~{gateway_fragments/input :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}\"\n        ></th:block>\n        <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('confirmPassword')}\" th:errors=\"*{confirmPassword}\"></p>\n    </div>\n\n    <div class=\"form-item\">\n        <button type=\"submit\" th:text=\"#{form.submit}\"></button>\n    </div>\n\n    <script th:inline=\"javascript\">\n        document.addEventListener(\"DOMContentLoaded\", function () {\n            setupPasswordConfirmation(\"password\", \"confirmPassword\");\n\n            const form = document.getElementById(\"signup-form\");\n            form.addEventListener(\"submit\", function () {\n                const usernameInput = document.getElementById(\"username\");\n                const displayNameInput = document.getElementById(\"displayName\");\n                if (usernameInput) {\n                    usernameInput.value = usernameInput.value.trim();\n                }\n                if (displayNameInput) {\n                    displayNameInput.value = displayNameInput.value.trim();\n                }\n            });\n        });\n    </script>\n\n    <script th:if=\"${globalInfo.mustVerifyEmailOnRegistration}\" th:inline=\"javascript\">\n        document.addEventListener(\"DOMContentLoaded\", function () {\n            const headerName = /*[[${_csrf.headerName}]]*/ \"\";\n            const token = /*[[${_csrf.token}]]*/ \"\";\n            async function sendRequest() {\n                const email = document.getElementById(\"email\").value;\n\n                if (!email) {\n                    throw new Error(/*[[#{form.emailCode.send.emptyValidation}]]*/ \"\");\n                }\n\n                const response = await fetch(\"/signup/send-email-code\", {\n                    method: \"POST\",\n                    body: JSON.stringify({ email: email }),\n                    headers: {\n                        \"Content-Type\": \"application/json\",\n                        [headerName]: token,\n                    },\n                });\n\n                if (!response.ok) {\n                    const json = await response.json();\n                    if (json.errors && json.errors.length) {\n                        throw new Error(json.errors[0]);\n                    }\n                }\n\n                return response;\n            }\n\n            const emailCodeSendButton = document.getElementById(\"emailCodeSendButton\");\n            sendVerificationCode(emailCodeSendButton, sendRequest);\n        });\n    </script>\n</form>\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/signup.properties",
    "content": "form.username.label=用户名\nform.displayName.label=名称\nform.email.label=电子邮箱\nform.emailCode.label=邮箱验证码\nform.emailCode.sendButton=发送\nform.emailCode.send.emptyValidation=请先输入邮箱地址\nform.password.label=密码\nform.confirmPassword.label=确认密码\nform.submit=注册\nform.error.invalidEmailCode=无效的邮箱验证码\nform.error.duplicateUsername=用户名已经被注册\nform.error.rateLimitExceeded=请求过于频繁，请稍后再试\nform.error.protectedUsernames=用户名或名称被禁止注册"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/signup_en.properties",
    "content": "form.username.label=Username\nform.displayName.label=Display Name\nform.email.label=Email\nform.emailCode.label=Email Verification Code\nform.emailCode.sendButton=Send\nform.emailCode.send.emptyValidation=Please enter your email address first\nform.password.label=Password\nform.confirmPassword.label=Confirm Password\nform.submit=Sign Up\nform.error.invalidEmailCode=Invalid Email Verification Code\nform.error.duplicateUsername=Username is already taken\nform.error.rateLimitExceeded=Too many requests, please try again later\nform.error.protectedUsernames=The username or display name is not allowed to be registered\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/signup_es.properties",
    "content": "form.username.label=Nombre de Usuario\nform.displayName.label=Nombre\nform.email.label=Correo Electrónico\nform.emailCode.label=Código de Verificación\nform.emailCode.sendButton=Enviar\nform.emailCode.send.emptyValidation=Por favor, introduce tu dirección de correo electrónico primero\nform.password.label=Contraseña\nform.confirmPassword.label=Confirmar Contraseña\nform.submit=Registrarse\nform.error.invalidEmailCode=Código de verificación del correo inválido\nform.error.duplicateUsername=El nombre de usuario ya está registrado\nform.error.rateLimitExceeded=Demasiadas solicitudes, por favor intente nuevamente más tarde\nform.error.protectedUsernames=Este nombre de usuario o nombre no están permitidos\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/signup_zh_TW.properties",
    "content": "form.username.label=使用者名稱\nform.displayName.label=名稱\nform.email.label=電子郵件\nform.emailCode.label=郵箱驗證碼\nform.emailCode.sendButton=發送\nform.emailCode.send.emptyValidation=請先輸入電子郵件地址\nform.password.label=密碼\nform.confirmPassword.label=確認密碼\nform.submit=註冊\nform.error.invalidEmailCode=無效的郵箱驗證碼\nform.error.duplicateUsername=使用者名稱已經被註冊\nform.error.rateLimitExceeded=請求過於頻繁，請稍後再試\nform.error.protectedUsernames=使用者名稱或名稱被禁止註冊\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/totp.html",
    "content": "<form\n        th:fragment=\"form\"\n        class=\"halo-form\"\n        th:action=\"@{/challenges/two-factor/totp}\"\n        name=\"two-factor-form\"\n        id=\"two-factor-form\"\n        method=\"post\"\n>\n    <div class=\"alert alert-error\" role=\"alert\" th:if=\"${param.error.size() > 0}\">\n        <strong th:text=\"#{form.messages.invalidError}\"></strong>\n    </div>\n    <div class=\"form-item\">\n        <label for=\"code\" th:text=\"#{form.code.label}\"></label>\n        <div class=\"form-input\">\n            <input\n                    type=\"text\"\n                    inputmode=\"numeric\"\n                    id=\"code\"\n                    name=\"code\"\n                    autocomplete=\"one-time-code\"\n                    pattern=\"\\d{6}\"\n                    autofocus\n                    required\n            />\n        </div>\n    </div>\n    <div class=\"form-item\">\n        <button type=\"submit\" th:text=\"#{form.submit}\"></button>\n    </div>\n    <div class=\"form-item\">\n        <a th:href=\"@{/logout}\" class=\"cancel-link\" th:text=\"#{form.cancel}\"></a>\n    </div>\n</form>"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/totp.properties",
    "content": "form.messages.invalidError=错误的验证码\nform.code.label=验证码\nform.submit=验证\nform.cancel=取消\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/totp_en.properties",
    "content": "form.messages.invalidError=Invalid TOTP code\nform.code.label=TOTP Code\nform.submit=Verify\nform.cancel=Cancel\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/totp_es.properties",
    "content": "form.messages.invalidError=Código de verificación incorrecto\nform.code.label=Código de Verificación\nform.submit=Verificar\nform.cancel=Cancelar\n"
  },
  {
    "path": "application/src/main/resources/templates/gateway_fragments/totp_zh_TW.properties",
    "content": "form.messages.invalidError=錯誤的驗證碼\nform.code.label=驗證碼\nform.submit=驗證\nform.cancel=取消\n"
  },
  {
    "path": "application/src/main/resources/templates/login.html",
    "content": "<!doctype html>\n<html\n    xmlns:th=\"https://www.thymeleaf.org\"\n    th:replace=\"~{gateway_fragments/layout :: layout(title = |#{title} - ${site.title}|, head = null, body = ~{::body})}\"\n>\n    <th:block th:fragment=\"body\">\n        <div class=\"gateway-wrapper\">\n            <div th:replace=\"~{gateway_fragments/common::haloLogo}\"></div>\n\n            <div class=\"halo-form-wrapper\">\n                <div th:replace=\"~{gateway_fragments/login::form}\"></div>\n                <div th:replace=\"~{gateway_fragments/login::formAuthProviders}\"></div>\n                <div th:replace=\"~{gateway_fragments/common::socialAuthProviders}\"></div>\n            </div>\n\n            <div th:replace=\"~{gateway_fragments/common::signupNoticeContent}\"></div>\n            <div th:replace=\"~{gateway_fragments/common::returnToSiteContent}\"></div>\n            <div th:replace=\"~{gateway_fragments/common::languageSwitcher}\"></div>\n        </div>\n    </th:block>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/login.properties",
    "content": "title=登录"
  },
  {
    "path": "application/src/main/resources/templates/login_en.properties",
    "content": "title=Login\n"
  },
  {
    "path": "application/src/main/resources/templates/login_es.properties",
    "content": "title=Iniciar Sesión\n"
  },
  {
    "path": "application/src/main/resources/templates/login_local.html",
    "content": "<div th:remove=\"tag\" th:fragment=\"form\">\n    <input type=\"hidden\" name=\"password\" id=\"password\" />\n\n    <div class=\"form-item\">\n        <label for=\"username\" th:text=\"#{form.username.label}\"> </label>\n\n        <div class=\"form-input\">\n            <input\n                id=\"username\"\n                name=\"username\"\n                type=\"text\"\n                autocomplete=\"off\"\n                spellcheck=\"false\"\n                autocorrect=\"off\"\n                autocapitalize=\"off\"\n                required\n                autofocus\n            />\n        </div>\n    </div>\n    <div class=\"form-item\">\n        <div class=\"form-label-group\">\n            <label for=\"plainPassword\" th:text=\"#{form.password.label}\"> </label>\n            <a\n                class=\"form-item-extra-link\"\n                tabindex=\"-1\"\n                th:href=\"@{/password-reset/email}\"\n                th:text=\"#{form.password.forgot}\"\n            >\n            </a>\n        </div>\n\n        <th:block\n            th:replace=\"~{gateway_fragments/input :: password(id = 'plainPassword', name = null, required = 'true', minlength = null, maxlength = 257, enableToggle = true)}\"\n        ></th:block>\n    </div>\n</div>\n"
  },
  {
    "path": "application/src/main/resources/templates/login_local.properties",
    "content": "form.username.label=用户名或邮箱地址\nform.password.label=密码\nform.password.forgot=忘记密码？\n"
  },
  {
    "path": "application/src/main/resources/templates/login_local_en.properties",
    "content": "form.username.label=Username or email address\nform.password.label=Password\nform.password.forgot=Forgot your password?\n"
  },
  {
    "path": "application/src/main/resources/templates/login_local_es.properties",
    "content": "form.username.label=Nombre de usuario o dirección de correo electrónico\nform.password.label=Contraseña\nform.password.forgot=¿Olvidaste tu contraseña?\n"
  },
  {
    "path": "application/src/main/resources/templates/login_local_zh_TW.properties",
    "content": "form.username.label=使用者名稱或電子郵件地址\nform.password.label=密碼\nform.password.forgot=忘記密碼？"
  },
  {
    "path": "application/src/main/resources/templates/login_zh_TW.properties",
    "content": "title=登入\n"
  },
  {
    "path": "application/src/main/resources/templates/logout.html",
    "content": "<!DOCTYPE html>\n<html\n    xmlns:th=\"https://www.thymeleaf.org\"\n    th:replace=\"~{gateway_fragments/layout:: layout(title = |#{title} - ${site.title}|, head = ~{::head}, body = ~{::body})}\"\n>\n    <th:block th:fragment=\"body\">\n        <div class=\"gateway-wrapper logout-page-wrapper\">\n            <div th:replace=\"~{gateway_fragments/common::haloLogo}\"></div>\n            <div class=\"halo-form-wrapper\">\n                <h1 class=\"form-title\" th:text=\"#{form.title}\"></h1>\n                <form th:replace=\"~{gateway_fragments/logout::form}\"></form>\n            </div>\n            <div th:replace=\"~{gateway_fragments/common::languageSwitcher}\"></div>\n        </div>\n    </th:block>\n\n    <th:block th:fragment=\"head\">\n        <style>\n            .logout-page-wrapper .user-info {\n                border-radius: var(--rounded-lg);\n                padding: var(--spacing-xs);\n                gap: var(--spacing-lg);\n                display: flex;\n                margin-bottom: 2em;\n                align-items: center;\n                border: 1px dashed #dfe6ecb3;\n            }\n\n            .logout-page-wrapper .user-avatar {\n                width: 4em;\n                border-radius: 100%;\n            }\n\n            .logout-page-wrapper .user-details {\n                color: var(--color-text);\n                font-size: var(--text-sm);\n                gap: var(--spacing-xs);\n                display: flex;\n                flex-direction: column;\n            }\n\n            .logout-page-wrapper .user-name {\n                font-weight: 600;\n                color: #333;\n            }\n\n            .logout-page-wrapper .cancel-link {\n                color: var(--color-link);\n                font-size: var(--text-sm);\n                text-decoration: none;\n                text-align: center;\n            }\n        </style>\n    </th:block>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/logout.properties",
    "content": "title=退出登录\nform.title=确定要退出登录吗？"
  },
  {
    "path": "application/src/main/resources/templates/logout_en.properties",
    "content": "title=Logout\nform.title=Are you sure want to log out?"
  },
  {
    "path": "application/src/main/resources/templates/logout_es.properties",
    "content": "title=Cerrar Sesión\nform.title=¿Estás seguro de que deseas cerrar sesión?"
  },
  {
    "path": "application/src/main/resources/templates/logout_zh_TW.properties",
    "content": "title=退出登入\nform.title=確定要退出登入嗎？"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/reset.html",
    "content": "<!doctype html>\n<html\n    xmlns:th=\"https://www.thymeleaf.org\"\n    th:replace=\"~{gateway_fragments/layout:: layout(title = |#{title(${username})} - ${site.title}|, head = null, body = ~{::body})}\"\n>\n    <th:block th:fragment=\"body\">\n        <div class=\"gateway-wrapper\">\n            <div th:replace=\"~{gateway_fragments/common::haloLogo}\"></div>\n            <div class=\"halo-form-wrapper\">\n                <h1 class=\"form-title\" th:text=\"#{title(${username})}\"></h1>\n                <form th:replace=\"~{gateway_fragments/password_reset_email_reset::form}\"></form>\n            </div>\n            <div th:replace=\"~{gateway_fragments/common::languageSwitcher}\"></div>\n        </div>\n    </th:block>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/reset.properties",
    "content": "title=为 {0} 修改密码\n"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/reset_en.properties",
    "content": "title=Change password for @{0}\n"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/reset_es.properties",
    "content": "title=Cambiar Contraseña para {0}\n"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/reset_zh_TW.properties",
    "content": "title=為 {0} 修改密碼\n"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/send.html",
    "content": "<!doctype html>\n<html\n    xmlns:th=\"https://www.thymeleaf.org\"\n    th:replace=\"~{gateway_fragments/layout:: layout(title = |#{title} - ${site.title}|, head = null, body = ~{::body})}\"\n>\n    <th:block th:fragment=\"body\">\n        <div class=\"gateway-wrapper\">\n            <div th:replace=\"~{gateway_fragments/common::haloLogo}\"></div>\n            <div class=\"halo-form-wrapper\">\n                <h1 class=\"form-title\" th:text=\"${sent} ? #{sent.title} : #{title}\"></h1>\n                <form th:replace=\"~{gateway_fragments/password_reset_email_send::form}\"></form>\n\n                <div th:replace=\"~{gateway_fragments/common::passwordResetMethods}\"></div>\n            </div>\n\n            <div th:replace=\"~{gateway_fragments/common::languageSwitcher}\"></div>\n        </div>\n    </th:block>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/send.properties",
    "content": "title=重置密码\nsent.title=已发送重置密码的邮件"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/send_en.properties",
    "content": "title=Reset password\nsent.title=Password reset email has been sent"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/send_es.properties",
    "content": "title=Restablecer Contraseña\nsent.title=Correo de Restablecimiento de Contraseña Enviado"
  },
  {
    "path": "application/src/main/resources/templates/password-reset/email/send_zh_TW.properties",
    "content": "title=重置密碼\nsent.title=已發送重置密碼的郵件"
  },
  {
    "path": "application/src/main/resources/templates/setup.html",
    "content": "<!DOCTYPE html>\n<html\n    xmlns:th=\"https://www.thymeleaf.org\"\n    th:replace=\"~{gateway_fragments/layout :: layout(title = |#{title} - Halo|, head = ~{::head}, body = ~{::body})}\"\n>\n    <th:block th:fragment=\"head\">\n        <style>\n            .setup-page-wrapper {\n                max-width: 35em;\n            }\n        </style>\n    </th:block>\n    <th:block th:fragment=\"body\">\n        <div class=\"gateway-wrapper setup-page-wrapper\">\n            <div th:replace=\"~{gateway_fragments/common::haloLogo}\"></div>\n\n            <div class=\"halo-form-wrapper\">\n                <h1 class=\"form-title\" th:text=\"#{title}\"></h1>\n                <div class=\"alert alert-error\" role=\"alert\" th:if=\"${usingH2database}\">\n                    <strong th:text=\"#{form.messages.h2.title}\"></strong>\n                    <br />\n                    <span th:text=\"#{form.messages.h2.content}\"> </span>\n                </div>\n                <form th:object=\"${form}\" th:action=\"@{/system/setup}\" class=\"halo-form\" method=\"post\">\n\n                    <div class=\"form-item\">\n                        <label for=\"language\" th:text=\"#{form.language.label}\"></label>\n                        <div class=\"form-input\">\n                            <select name=\"language\" id=\"language\">\n                                <option value=\"en\" th:selected=\"${#locale.toLanguageTag} == 'en'\">English</option>\n                                <option value=\"es\" th:selected=\"${#locale.toLanguageTag} == 'es'\">Español</option>\n                                <option value=\"zh-CN\" th:selected=\"${#locale.toLanguageTag} == 'zh-CN'\">简体中文</option>\n                                <option value=\"zh-TW\" th:selected=\"${#locale.toLanguageTag} == 'zh-TW'\">繁体中文</option>\n                            </select>\n                        </div>\n                    </div>\n\n                    <div class=\"form-item\">\n                        <label for=\"externalUrl\" th:text=\"#{form.externalUrl.label}\"></label>\n                        <div class=\"form-input\">\n                            <input name=\"externalUrl\"\n                                   type=\"url\"\n                                   th:field=\"*{externalUrl}\"\n                                   autocomplete=\"off\"\n                                   spellcheck=\"false\"\n                                   autocapitalize=\"off\"\n                                   required/>\n                        </div>\n                        <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('externalUrl')}\" th:errors=\"*{externalUrl}\"></p>\n                    </div>\n\n                    <div class=\"form-item\">\n                        <label for=\"siteTitle\" th:text=\"#{form.siteTitle.label}\"></label>\n                        <div class=\"form-input\">\n                            <input\n                                name=\"siteTitle\"\n                                id=\"siteTitle\"\n                                type=\"text\"\n                                th:field=\"*{siteTitle}\"\n                                autocomplete=\"off\"\n                                spellcheck=\"false\"\n                                autocorrect=\"off\"\n                                autocapitalize=\"off\"\n                                maxlength=\"80\"\n                                required\n                                autofocus\n                            />\n                        </div>\n                        <p\n                            class=\"alert alert-error\"\n                            th:if=\"${#fields.hasErrors('siteTitle')}\"\n                            th:errors=\"*{siteTitle}\"\n                        ></p>\n                    </div>\n\n                    <div class=\"form-item\">\n                        <label for=\"username\" th:text=\"#{form.username.label}\"></label>\n                        <div class=\"form-input\">\n                            <input\n                                name=\"username\"\n                                id=\"username\"\n                                type=\"text\"\n                                th:field=\"*{username}\"\n                                autocomplete=\"off\"\n                                spellcheck=\"false\"\n                                autocorrect=\"off\"\n                                autocapitalize=\"off\"\n                                maxlength=\"63\"\n                                minlength=\"4\"\n                                required\n                            />\n                        </div>\n                        <p\n                            class=\"alert alert-error\"\n                            th:if=\"${#fields.hasErrors('username')}\"\n                            th:errors=\"*{username}\"\n                        ></p>\n                    </div>\n\n                    <div class=\"form-item\">\n                        <label for=\"email\" th:text=\"#{form.email.label}\"></label>\n                        <div class=\"form-input\">\n                            <input\n                                name=\"email\"\n                                id=\"email\"\n                                type=\"email\"\n                                th:field=\"*{email}\"\n                                autocomplete=\"off\"\n                                spellcheck=\"false\"\n                                autocorrect=\"off\"\n                                autocapitalize=\"off\"\n                                required\n                            />\n                        </div>\n                        <p class=\"alert alert-error\" th:if=\"${#fields.hasErrors('email')}\" th:errors=\"*{email}\"></p>\n                    </div>\n\n                    <div class=\"form-item\">\n                        <label for=\"password\" th:text=\"#{form.password.label}\"></label>\n                        <th:block\n                            th:replace=\"~{gateway_fragments/input :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}\"\n                        ></th:block>\n                        <p\n                            class=\"alert alert-error\"\n                            th:if=\"${#fields.hasErrors('password')}\"\n                            th:errors=\"*{password}\"\n                        ></p>\n                    </div>\n\n                    <div class=\"form-item\">\n                        <label for=\"confirmPassword\" th:text=\"#{form.confirmPassword.label}\"></label>\n                        <th:block\n                            th:replace=\"~{gateway_fragments/input :: password(id = 'confirmPassword', name = null, required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}\"\n                        ></th:block>\n                    </div>\n\n                    <div class=\"form-item\">\n                        <button type=\"submit\" th:text=\"#{form.submit}\">初始化</button>\n                    </div>\n                </form>\n            </div>\n        </div>\n\n        <script th:inline=\"javascript\">\n            document.addEventListener(\"DOMContentLoaded\", function () {\n                setupPasswordConfirmation(\"password\", \"confirmPassword\");\n            });\n\n            document.getElementById(\"language\").addEventListener(\"change\", function () {\n                const selectedLanguage = this.value;\n                const currentURL = new URL(window.location.href);\n                currentURL.searchParams.set(\"language\", selectedLanguage);\n                history.replaceState(null, \"\", currentURL.toString());\n                window.location.reload();\n            });\n        </script>\n    </th:block>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/setup.properties",
    "content": "title=系统初始化\nform.language.label=语言\nform.siteTitle.label=站点标题\nform.username.label=用户名\nform.email.label=电子邮箱\nform.password.label=密码\nform.confirmPassword.label=确认密码\nform.externalUrl.label=外部访问地址\nform.submit=初始化\nform.messages.h2.title=警告：正在使用 H2 数据库\nform.messages.h2.content=H2 数据库仅适用于开发环境和测试环境，不推荐在生产环境中使用，H2 非常容易因为操作不当导致数据文件损坏。如果必须要使用，请按时进行数据备份。"
  },
  {
    "path": "application/src/main/resources/templates/setup_en.properties",
    "content": "title=Setup\nform.language.label=Language\nform.siteTitle.label=Site title\nform.username.label=Username\nform.email.label=Email\nform.password.label=Password\nform.confirmPassword.label=Confirm Password\nform.externalUrl.label=External URL\nform.submit=Setup\nform.messages.h2.title=Warning: Using H2 Database\nform.messages.h2.content=The H2 database is only suitable for development and testing environments. It is not recommended for production environments, as H2 is very prone to data file corruption due to improper operations. If you must use it, please back up your data regularly."
  },
  {
    "path": "application/src/main/resources/templates/setup_es.properties",
    "content": "title=Configuración\nform.language.label=Idioma\nform.siteTitle.label=Título del Sitio\nform.username.label=Nombre de Usuario\nform.email.label=Correo Electrónico\nform.password.label=Contraseña\nform.confirmPassword.label=Confirmar Contraseña\nform.externalUrl.label=URL Externa\nform.submit=Configurar\nform.messages.h2.title=Advertencia: Usando la base de datos H2\nform.messages.h2.content=La base de datos H2 solo es adecuada para entornos de desarrollo y prueba. No se recomienda su uso en entornos de producción, ya que H2 es muy susceptible a la corrupción de archivos de datos debido a un manejo inadecuado. Si debe usarla, realice copias de seguridad de los datos regularmente."
  },
  {
    "path": "application/src/main/resources/templates/setup_zh_TW.properties",
    "content": "title=系統初始化\nform.language.label=語言\nform.siteTitle.label=站點標題\nform.username.label=使用者名稱\nform.email.label=電子郵件\nform.password.label=密碼\nform.confirmPassword.label=確認密碼\nform.externalUrl.label=外部訪問地址\nform.submit=初始化\nform.messages.h2.title=警告：正在使用 H2 資料庫\nform.messages.h2.content=H2 資料庫僅適用於開發環境和測試環境，不建議在生產環境中使用，H2 非常容易因為操作不當導致資料檔案損壞。如果必須要使用，請按時進行資料備份。"
  },
  {
    "path": "application/src/main/resources/templates/signup.html",
    "content": "<!doctype html>\n<html\n    xmlns:th=\"https://www.thymeleaf.org\"\n    th:replace=\"~{gateway_fragments/layout :: layout(title = |#{title} - ${site.title}|, head = ~{::head}, body = ~{::body})}\"\n>\n    <th:block th:fragment=\"head\">\n        <style>\n            .signup-page-wrapper {\n                max-width: 38em;\n            }\n        </style>\n    </th:block>\n    <th:block th:fragment=\"body\">\n        <div class=\"gateway-wrapper signup-page-wrapper\">\n            <div th:replace=\"~{gateway_fragments/common::haloLogo}\"></div>\n            <div class=\"halo-form-wrapper\">\n                <h1 class=\"form-title\" th:text=\"#{title}\"></h1>\n                <form th:replace=\"~{gateway_fragments/signup::form}\"></form>\n                <div th:replace=\"~{gateway_fragments/common::socialAuthProviders}\"></div>\n            </div>\n            <div th:replace=\"~{gateway_fragments/common::loginNoticeContent}\"></div>\n            <div th:replace=\"~{gateway_fragments/common::returnToSiteContent}\"></div>\n            <div th:replace=\"~{gateway_fragments/common::languageSwitcher}\"></div>\n        </div>\n    </th:block>\n</html>\n"
  },
  {
    "path": "application/src/main/resources/templates/signup.properties",
    "content": "title=注册"
  },
  {
    "path": "application/src/main/resources/templates/signup_en.properties",
    "content": "title=Sign Up"
  },
  {
    "path": "application/src/main/resources/templates/signup_es.properties",
    "content": "title=Registrarse"
  },
  {
    "path": "application/src/main/resources/templates/signup_zh_TW.properties",
    "content": "title=註冊"
  },
  {
    "path": "application/src/main/resources/thumbnailator.properties",
    "content": "# See https://github.com/coobird/thumbnailator/issues/69 for more\nthumbnailator.conserveMemoryWorkaround=true\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/ApplicationTests.java",
    "content": "package run.halo.app;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@SpringBootTest\nclass ApplicationTests {\n\n    @Test\n    void contextLoads() {\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/PathPrefixPredicateTest.java",
    "content": "package run.halo.app;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.method.HandlerTypePredicate;\n\n/**\n * Test case for api path prefix predicate.\n *\n * @author guqing\n * @date 2022-04-13\n */\npublic class PathPrefixPredicateTest {\n\n    @Test\n    public void prefixPredicate() {\n        boolean falseResult = HandlerTypePredicate.forAnnotation(RestController.class)\n            .and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName()))\n            .test(getClass());\n        assertThat(falseResult).isFalse();\n\n        boolean result = HandlerTypePredicate.forAnnotation(RestController.class)\n            .and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName()))\n            .test(TestController.class);\n        assertThat(result).isTrue();\n    }\n\n    @RestController(\"controller-for-test\")\n    @RequestMapping(\"/test-prefix\")\n    class TestController {\n\n    }\n\n\n    @Test\n    void urlTest() {\n        URI uri = URI.create(\"https:///path\");\n        System.out.println(uri);\n        System.out.println(uri.getPath());\n        System.out.println(URI.create(\"/\"));\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/XForwardHeaderTest.java",
    "content": "package run.halo.app;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.GET;\nimport static org.springframework.web.reactive.function.server.RouterFunctions.route;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.TestConfiguration;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.test.StepVerifier;\n\n@SpringBootTest(webEnvironment = RANDOM_PORT,\n    properties = \"server.forward-headers-strategy=native\")\nclass XForwardHeaderTest {\n\n    @LocalServerPort\n    int port;\n\n    @Test\n    void shouldGetCorrectProtoFromXForwardHeaders() {\n        var response = WebClient.create(\"http://localhost:\" + port)\n            .get().uri(\"/print-uri\")\n            .header(\"X-Forwarded-Proto\", \"https\")\n            .header(\"X-Forwarded-Host\", \"halo.run\")\n            .header(\"X-Forwarded-Port\", \"6666\")\n            .retrieve()\n            .toEntity(String.class);\n        StepVerifier.create(response)\n            .assertNext(entity -> {\n                assertEquals(HttpStatus.OK, entity.getStatusCode());\n                assertEquals(\"\\\"https://halo.run:6666/print-uri\\\"\", entity.getBody());\n            })\n            .verifyComplete();\n    }\n\n    @TestConfiguration\n    static class Configuration {\n\n        @Bean\n        RouterFunction<ServerResponse> printUri() {\n            return route(GET(\"/print-uri\"),\n                request -> {\n                    var uri = request.exchange().getRequest().getURI();\n                    return ServerResponse.ok().bodyValue(uri);\n                });\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/config/CorsTest.java",
    "content": "package run.halo.app.config;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass CorsTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @Nested\n    class RequestCorsEnabledApi {\n\n        @Test\n        @WithMockUser\n        void shouldNotResponseAllowOriginHeaderWithSameOrigin() {\n            webClient.get().uri(\"http://localhost:3000/apis/cors-enabled\")\n                .header(HttpHeaders.ORIGIN, \"http://localhost:3000\")\n                .header(HttpHeaders.AUTHORIZATION, \"fake-authorization\")\n                .header(\"FakeHeader\", \"fake-header-value\")\n                .accept(MediaType.APPLICATION_JSON)\n                .exchange()\n                .expectHeader()\n                .doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);\n        }\n\n        @Test\n        @WithMockUser\n        void shouldResponseAllowOriginHeaderWithDifferentOrigin() {\n            webClient.get().uri(\"http://localhost:3000/apis/cors-enabled\")\n                .header(HttpHeaders.ORIGIN, \"https://another.website\")\n                .header(HttpHeaders.AUTHORIZATION, \"fake-authorization\")\n                // .header(\"ForbiddenHeader\", \"fake value\")\n                .accept(MediaType.APPLICATION_JSON)\n                .exchange()\n                .expectHeader()\n                .exists(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);\n        }\n\n        @Test\n        @WithMockUser\n        void shouldResponseAllowOriginHeaderWithForbiddenHeader() {\n            webClient.get().uri(\"http://localhost:3000/apis/cors-enabled\")\n                .header(HttpHeaders.ORIGIN, \"https://another.website\")\n                .header(HttpHeaders.AUTHORIZATION, \"fake-authorization\")\n                .header(\"FakeHeader\", \"fake-header-value\")\n                // .header(\"ForbiddenHeader\", \"fake value\")\n                .accept(MediaType.APPLICATION_JSON)\n                .exchange()\n                .expectHeader()\n                .exists(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);\n        }\n    }\n\n    @Nested\n    class RequestCorsDisabledApi {\n\n        @Test\n        @WithMockUser\n        void shouldNotResponseAllowOriginHeaderWithDifferentOrigin() {\n            webClient.get().uri(\"http://localhost:3000/cors-disabled\")\n                .header(HttpHeaders.ORIGIN, \"https://another.website\")\n                .header(HttpHeaders.AUTHORIZATION, \"fake-authorization\")\n                .accept(MediaType.APPLICATION_JSON)\n                .exchange()\n                .expectHeader()\n                .doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);\n        }\n\n        @Test\n        @WithMockUser\n        void shouldNotResponseAllowOriginHeaderWithSameOrigin() {\n            webClient.get().uri(\"http://localhost:3000/cors-disabled\")\n                .header(HttpHeaders.ORIGIN, \"http://localhost:3000\")\n                .header(HttpHeaders.AUTHORIZATION, \"fake-authorization\")\n                .header(\"FakeHeader\", \"fake-header-value\")\n                .accept(MediaType.APPLICATION_JSON)\n                .exchange()\n                .expectHeader()\n                .doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/config/ExtensionConfigurationTest.java",
    "content": "package run.halo.app.config;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.anySet;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.test.context.bean.override.mockito.MockitoBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.store.ExtensionStoreRepository;\n\n@DirtiesContext\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass ExtensionConfigurationTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @Autowired\n    SchemeManager schemeManager;\n\n    @MockitoBean\n    RoleService roleService;\n\n    @BeforeEach\n    void setUp() {\n        // disable authorization\n        var rule = new Role.PolicyRule.Builder()\n            .apiGroups(\"*\")\n            .resources(\"*\")\n            .verbs(\"*\")\n            .build();\n        var role = new Role();\n        role.setMetadata(new Metadata());\n        role.getMetadata().setName(\"supper-role\");\n        role.setRules(List.of(rule));\n        when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));\n        // register scheme\n        schemeManager.register(FakeExtension.class);\n\n        webClient = webClient.mutateWith(csrf());\n    }\n\n    @AfterEach\n    void cleanUp(@Autowired ExtensionStoreRepository repository) {\n        var gvk = Scheme.buildFromType(FakeExtension.class).groupVersionKind();\n        repository.deleteAll().block();\n        schemeManager.fetch(GroupVersionKind.fromExtension(FakeExtension.class))\n            .ifPresent(scheme -> schemeManager.unregister(scheme));\n    }\n\n    @Test\n    @WithMockUser\n    void shouldReturnNotFoundWhenSchemeNotRegistered() {\n        // unregister the Extension if necessary\n        schemeManager.fetch(Scheme.buildFromType(FakeExtension.class).groupVersionKind())\n            .ifPresent(schemeManager::unregister);\n\n        webClient.get()\n            .uri(\"/apis/fake.halo.run/v1alpha1/fakes\")\n            .exchange()\n            .expectStatus().isNotFound();\n\n        webClient.get()\n            .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n            .exchange()\n            .expectStatus().isNotFound();\n\n        webClient.post()\n            .uri(\"/apis/fake.halo.run/v1alpha1/fakes\")\n            .bodyValue(new FakeExtension())\n            .exchange()\n            .expectStatus().isNotFound();\n\n        webClient.put()\n            .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n            .bodyValue(new FakeExtension())\n            .exchange()\n            .expectStatus().isNotFound();\n\n        webClient.delete()\n            .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n            .exchange()\n            .expectStatus().isNotFound();\n    }\n\n    @Nested\n    @DisplayName(\"After creating extension\")\n    class AfterCreatingExtension {\n\n        @Autowired\n        ExtensionClient extClient;\n\n        FakeExtension createdFake;\n\n        @BeforeEach\n        void setUp() {\n            var metadata = new Metadata();\n            metadata.setName(\"my-fake\");\n            metadata.setLabels(Map.of(\"label-key\", \"label-value\"));\n            var fake = new FakeExtension();\n            fake.setMetadata(metadata);\n\n            webClient.get()\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/{}\", metadata.getName())\n                .exchange()\n                .expectStatus().isNotFound();\n\n            createdFake = webClient.post()\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes\")\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(fake)\n                .exchange()\n                .expectStatus().isCreated()\n                .expectHeader().location(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n                .expectBody(FakeExtension.class)\n                .consumeWith(result -> {\n                    var gotFake = result.getResponseBody();\n                    assertNotNull(gotFake);\n                    assertEquals(\"my-fake\", gotFake.getMetadata().getName());\n                    assertNotNull(gotFake.getMetadata().getVersion());\n                    assertNotNull(gotFake.getMetadata().getCreationTimestamp());\n                })\n                .returnResult()\n                .getResponseBody();\n        }\n\n        @Test\n        @WithMockUser\n        void shouldDeleteExtensionWhenSchemeRegistered() {\n            webClient.delete()\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/{name}\",\n                    createdFake.getMetadata().getName())\n                .exchange()\n                .expectStatus().isOk()\n                .expectHeader().contentType(MediaType.APPLICATION_JSON)\n                .expectBody(FakeExtension.class)\n                .consumeWith(result -> {\n                    var deletedFake = result.getResponseBody();\n                    assertNotNull(deletedFake);\n                    assertNotNull(deletedFake.getMetadata().getDeletionTimestamp());\n                    assertTrue(deletedFake.getMetadata().getDeletionTimestamp()\n                        .isBefore(Instant.now()));\n                });\n        }\n\n        @Test\n        @WithMockUser\n        void shouldListExtensionsWhenSchemeRegistered() {\n            webClient.get().uri(\"/apis/fake.halo.run/v1alpha1/fakes\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody().jsonPath(\"$.items.length()\").isEqualTo(1);\n        }\n\n        @Test\n        @WithMockUser\n        void shouldListExtensionsWithMatchedSelectors() {\n            webClient.get().uri(uriBuilder -> uriBuilder\n                    .path(\"/apis/fake.halo.run/v1alpha1/fakes\")\n                    .queryParam(\"labelSelector\", \"label-key=label-value\")\n                    .queryParam(\"fieldSelector\", \"name=my-fake\")\n                    .build())\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody().jsonPath(\"$.items.length()\").isEqualTo(1);\n        }\n\n        @Test\n        @WithMockUser\n        void shouldListExtensionsWithMismatchedSelectors() {\n            webClient.get().uri(uriBuilder -> uriBuilder\n                    .path(\"/apis/fake.halo.run/v1alpha1/fakes\")\n                    .queryParam(\"labelSelector\", \"label-key=invalid-label-value\")\n                    .queryParam(\"fieldSelector\", \"name=invalid-name\")\n                    .build())\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody().jsonPath(\"$.items.length()\").isEqualTo(0);\n        }\n\n        @Test\n        @WithMockUser\n        void shouldUpdateExtensionWhenSchemeRegistered() {\n            var name = createdFake.getMetadata().getName();\n            FakeExtension fakeToUpdate = getFakeExtension(name);\n            fakeToUpdate.getMetadata().setLabels(Map.of(\"updated\", \"true\"));\n\n            webClient.put()\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/{name}\", name)\n                .bodyValue(fakeToUpdate)\n                .exchange()\n                .expectStatus().isOk()\n                .expectHeader().contentType(MediaType.APPLICATION_JSON)\n                .expectBody(FakeExtension.class)\n                .consumeWith(result -> {\n                    var updatedFake = result.getResponseBody();\n                    assertNotNull(updatedFake);\n                    assertNotEquals(fakeToUpdate.getMetadata().getVersion(),\n                        updatedFake.getMetadata().getVersion());\n                    assertEquals(Map.of(\"updated\", \"true\"),\n                        updatedFake.getMetadata().getLabels());\n                });\n        }\n\n        @Test\n        @WithMockUser\n        void shouldGetExtensionWhenSchemeRegistered() {\n            var name = createdFake.getMetadata().getName();\n            webClient.get()\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/{name}\", name)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(FakeExtension.class)\n                .consumeWith(result -> {\n                    var gotFake = result.getResponseBody();\n                    assertNotNull(gotFake);\n                    assertEquals(name, gotFake.getMetadata().getName());\n                    assertNotNull(gotFake.getMetadata().getVersion());\n                    assertNotNull(gotFake.getMetadata().getCreationTimestamp());\n                });\n        }\n\n        FakeExtension getFakeExtension(String name) {\n            return webClient.get()\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/{name}\", name)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(FakeExtension.class)\n                .returnResult()\n                .getResponseBody();\n        }\n\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/config/HaloConfigurationTest.java",
    "content": "package run.halo.app.config;\n\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.annotation.DirtiesContext;\nimport run.halo.app.search.SearchEngine;\nimport run.halo.app.search.lucene.LuceneSearchEngine;\n\nclass HaloConfigurationTest {\n\n    @Nested\n    @SpringBootTest\n    class LuceneSearchEngineDisabled {\n\n        @Test\n        void shouldNotCreateLuceneSearchEngineBean(\n            @Autowired ObjectProvider<SearchEngine> searchEngines) {\n            var searchEngine = searchEngines.getIfAvailable();\n            assertNull(searchEngine);\n        }\n    }\n\n    @Nested\n    @SpringBootTest(properties = \"halo.search-engine.lucene.enabled=true\")\n    @DirtiesContext\n    class LuceneSearchEngineEnabled {\n\n        @Test\n        void shouldCreateLuceneSearchEngineBean(\n            @Autowired ObjectProvider<SearchEngine> searchEngines) {\n            var searchEngine = searchEngines.getIfAvailable();\n            assertNotNull(searchEngine);\n            assertInstanceOf(LuceneSearchEngine.class, searchEngine);\n        }\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/config/SecurityConfigTest.java",
    "content": "package run.halo.app.config;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY;\n\nimport org.apache.commons.lang3.RandomStringUtils;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass SecurityConfigTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @Test\n    void shouldNotIncludeSubdomainForHstsHeader() {\n        webClient.get()\n            .uri(builder -> builder.scheme(\"https\").path(\"/fake\").build())\n            .accept(MediaType.TEXT_HTML)\n            .exchange()\n            .expectHeader()\n            .value(STRICT_TRANSPORT_SECURITY,\n                hsts -> assertFalse(hsts.contains(\"includeSubDomains\")));\n\n        webClient.get()\n            .uri(builder -> builder.scheme(\"https\").path(\"/apis/fake\").build())\n            .accept(MediaType.APPLICATION_JSON)\n            .exchange()\n            .expectHeader()\n            .value(STRICT_TRANSPORT_SECURITY,\n                hsts -> assertFalse(hsts.contains(\"includeSubDomains\")));\n    }\n\n    @Test\n    void shouldAllowPasswordLengthMoreThan72(@Autowired PasswordEncoder passwordEncoder) {\n        var encoded = passwordEncoder.encode(RandomStringUtils.secure().nextAlphanumeric(73));\n        assertNotNull(encoded);\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/config/ServerCodecTest.java",
    "content": "package run.halo.app.config;\n\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static org.springframework.web.reactive.function.server.RequestPredicates.contentType;\nimport static org.springframework.web.reactive.function.server.RouterFunctions.route;\n\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.TestConfiguration;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.core.ParameterizedTypeReference;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\n@Import(ServerCodecTest.TestConfig.class)\nclass ServerCodecTest {\n\n    static final String INSTANT = \"2022-06-09T10:57:30Z\";\n\n    static final String LOCAL_DATE_TIME = \"2022-06-10T10:57:30\";\n\n    @Autowired\n    WebTestClient webClient;\n\n    @Test\n    @WithMockUser\n    void timeSerializationTest() {\n        webClient.get().uri(\"/fake/api/times\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.instant\").value(equalTo(INSTANT))\n            .jsonPath(\"$.localDateTime\").value(equalTo(LOCAL_DATE_TIME))\n        ;\n    }\n\n    @Test\n    @WithMockUser\n    void timeDeserializationTest() {\n        webClient\n            .mutateWith(csrf())\n            .post().uri(\"/fake/api/time/report\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .accept(MediaType.APPLICATION_JSON)\n            .bodyValue(Map.of(\"now\", Instant.parse(INSTANT)))\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody(new ParameterizedTypeReference<Map<String, Instant>>() {\n            }).isEqualTo(Map.of(\"now\", Instant.parse(INSTANT)))\n        ;\n    }\n\n    @TestConfiguration(proxyBeanMethods = false)\n    static class TestConfig {\n\n        @Bean\n        RouterFunction<ServerResponse> timesRouter() {\n            return route().GET(\"/fake/api/times\", request -> {\n                var times = Map.of(\"instant\", Instant.parse(INSTANT),\n                    \"localDateTime\", LocalDateTime.parse(LOCAL_DATE_TIME));\n                return ServerResponse\n                    .ok()\n                    .contentType(MediaType.APPLICATION_JSON)\n                    .bodyValue(times);\n            }).build();\n        }\n\n        @Bean\n        RouterFunction<ServerResponse> reportTime() {\n            final var type = new ParameterizedTypeReference<Map<String, Instant>>() {\n            };\n            return route().POST(\"/fake/api/time/report\",\n                    contentType(MediaType.APPLICATION_JSON).and(accept(MediaType.APPLICATION_JSON)),\n                    request -> ServerResponse.ok()\n                        .body(request.bodyToMono(type), type))\n                .build();\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/config/WebFluxConfigTest.java",
    "content": "package run.halo.app.config;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Set;\nimport org.hamcrest.core.StringStartsWith;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.TestConfiguration;\nimport org.springframework.boot.test.web.server.LocalServerPort;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.reactive.socket.WebSocketHandler;\nimport org.springframework.web.reactive.socket.WebSocketMessage;\nimport org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.endpoint.WebSocketEndpoint;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.Metadata;\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@Import({\n    WebFluxConfigTest.WebSocketSupportTest.TestWebSocketConfiguration.class,\n    WebFluxConfigTest.ServerWebExchangeContextFilterTest.TestConfig.class,\n    WebFluxConfigTest.UrlHandlerFilterTest.TestConfig.class\n})\n@AutoConfigureWebTestClient\nclass WebFluxConfigTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @MockitoSpyBean\n    RoleService roleService;\n\n    @LocalServerPort\n    int port;\n\n    @Nested\n    class WebSocketSupportTest {\n\n        @Test\n        void shouldInitializeWebSocketEndpoint() {\n            var role = new Role();\n            var metadata = new Metadata();\n            metadata.setName(\"fake-role\");\n            role.setMetadata(metadata);\n            role.setRules(List.of(new Role.PolicyRule.Builder()\n                .apiGroups(\"fake.halo.run\")\n                .verbs(\"watch\")\n                .resources(\"resources\")\n                .build()));\n            when(roleService.listDependenciesFlux(Set.of(\"anonymous\"))).thenReturn(Flux.just(role));\n            var webSocketClient = new ReactorNettyWebSocketClient();\n            webSocketClient.execute(\n                    URI.create(\"ws://localhost:\" + port + \"/apis/fake.halo.run/v1alpha1/resources\"),\n                    session -> {\n                        var send = session.send(Flux.just(session.textMessage(\"halo\")));\n                        var receive = session.receive().map(WebSocketMessage::getPayloadAsText)\n                            .next()\n                            .doOnNext(message -> assertEquals(\"HALO\", message));\n                        return send.and(receive);\n                    })\n                .as(StepVerifier::create)\n                .verifyComplete();\n        }\n\n        @TestConfiguration\n        static class TestWebSocketConfiguration {\n\n            @Bean\n            WebSocketEndpoint fakeWebSocketEndpoint() {\n                return new FakeWebSocketEndpoint();\n            }\n\n        }\n\n        static class FakeWebSocketEndpoint implements WebSocketEndpoint {\n\n            @Override\n            public String urlPath() {\n                return \"/resources\";\n            }\n\n            @Override\n            public GroupVersion groupVersion() {\n                return GroupVersion.parseAPIVersion(\"fake.halo.run/v1alpha1\");\n            }\n\n            @Override\n            public WebSocketHandler handler() {\n                return session -> {\n                    var messages = session.receive()\n                        .map(message -> session.textMessage(\n                            message.getPayloadAsText().toUpperCase())\n                        );\n                    return session.send(messages).then(session.close());\n                };\n            }\n        }\n\n    }\n\n    @Nested\n    class UiPageRequest {\n\n        @WithMockUser\n        @ParameterizedTest\n        @ValueSource(strings = {\n            \"/console\",\n            \"/console/index\",\n            \"/console/index.html\",\n            \"/console/dashboard\",\n            \"/console/fake\"\n        })\n        void shouldRequestConsoleIndex(String uri) {\n            webClient.get().uri(uri)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(String.class).value(StringStartsWith.startsWith(\"console index\"));\n        }\n\n        @WithMockUser\n        @ParameterizedTest\n        @ValueSource(strings = {\n            \"/uc\",\n            \"/uc/index\",\n            \"/uc/index.html\",\n            \"/uc/profile\",\n            \"/uc/fake\"\n        })\n        void shouldRequestUcIndex(String uri) {\n            webClient.get().uri(uri)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(String.class).value(StringStartsWith.startsWith(\"uc index\"));\n        }\n\n        @Test\n        void shouldRedirectToLoginPageIfUnauthenticated() {\n            webClient.get().uri(\"/console\")\n                .exchange()\n                .expectStatus().isFound()\n                .expectHeader().location(\"/login?authentication_required\");\n        }\n\n        @Test\n        @WithMockUser\n        void shouldRequestUiAssetsCorrectly() {\n            webClient.get().uri(\"/ui-assets/fake.txt\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(String.class).value(StringStartsWith.startsWith(\"fake.\"));\n        }\n\n        @Test\n        @WithMockUser\n        void shouldResponseNotFoundWhenAssetsNotExist() {\n            webClient.get().uri(\"/ui-assets/not-found.txt\")\n                .exchange()\n                .expectStatus().isNotFound();\n        }\n    }\n\n    @Nested\n    class StaticResourcesTest {\n\n        @Test\n        void shouldRespond404WhenThemeResourceNotFound() {\n            webClient.get().uri(\"/themes/fake-theme/assets/favicon.ico\")\n                .exchange()\n                .expectStatus().isNotFound();\n        }\n    }\n\n\n    @Nested\n    class ServerWebExchangeContextFilterTest {\n\n        @TestConfiguration\n        static class TestConfig {\n\n            @Bean\n            RouterFunction<ServerResponse> assertServerWebExchangeRoute() {\n                return RouterFunctions.route()\n                    .GET(\"/assert-server-web-exchange\",\n                        request -> Mono.deferContextual(contextView -> {\n                            var exchange = ServerWebExchangeContextFilter.getExchange(contextView);\n                            assertTrue(exchange.isPresent());\n                            return ServerResponse.ok().build();\n                        }))\n                    .build();\n            }\n\n        }\n\n        @Test\n        void shouldGetExchangeFromContextView() {\n            webClient.get().uri(\"/assert-server-web-exchange\")\n                .exchange()\n                .expectStatus().isOk();\n        }\n\n    }\n\n    @Nested\n    class UrlHandlerFilterTest {\n\n        @TestConfiguration\n        static class TestConfig {\n\n            @Bean\n            RouterFunction<ServerResponse> urlHandlerFilterTestRoute() {\n                return RouterFunctions.route()\n                    .GET(\"/fake\", request -> ServerResponse.ok().bodyValue(\"ok\"))\n                    .build();\n            }\n\n        }\n\n        @Test\n        void shouldHandleUrlWithTrailingSlash() {\n            webClient.get().uri(\"/fake/\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(String.class).isEqualTo(\"ok\");\n        }\n\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/CategoryPostCountUpdaterTest.java",
    "content": "package run.halo.app.content;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.verify;\n\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport reactor.util.retry.Retry;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.SchemeManager;\n\n/**\n * Tests for {@link CategoryPostCountUpdater}.\n *\n * @author guqing\n * @since 2.15.0\n */\n@SpringBootTest\nclass CategoryPostCountUpdaterTest {\n\n\n    private final List<Post> storedPosts = posts();\n\n    private final List<Category> storedCategories = categories();\n\n    @Autowired\n    SchemeManager schemeManager;\n\n    @MockitoSpyBean\n    ExtensionClient client;\n\n    @Autowired\n    ReactiveExtensionClient reactiveClient;\n\n    private CategoryPostCountUpdater.CategoryPostCountService categoryPostCountService;\n\n    Mono<Extension> deleteImmediately(Extension extension) {\n        var name = extension.getMetadata().getName();\n        var scheme = schemeManager.get(extension.getClass());\n        return reactiveClient.fetch(scheme.type(), name)\n            .flatMap(reactiveClient::delete)\n            .flatMap(deleting -> reactiveClient.fetch(scheme.type(), name)\n                .flatMap(e -> Mono.error(new IllegalStateException(\"Extension still exists\")))\n                .retryWhen(Retry.backoff(10, Duration.ofMillis(100))\n                    .filter(IllegalStateException.class::isInstance)\n                )\n                .thenReturn(deleting)\n            );\n    }\n\n    @BeforeEach\n    void setUp() {\n        categoryPostCountService =\n            new CategoryPostCountUpdater.CategoryPostCountService(client);\n        Flux.fromIterable(storedPosts)\n            .flatMap(post -> reactiveClient.create(post))\n            .as(StepVerifier::create)\n            .expectNextCount(storedPosts.size())\n            .verifyComplete();\n\n        Flux.fromIterable(storedCategories)\n            .flatMap(category -> reactiveClient.create(category))\n            .as(StepVerifier::create)\n            .expectNextCount(storedCategories.size())\n            .verifyComplete();\n    }\n\n    @AfterEach\n    void tearDown() {\n        Flux.fromIterable(storedPosts)\n            .flatMap(this::deleteImmediately)\n            .as(StepVerifier::create)\n            .expectNextCount(storedPosts.size())\n            .verifyComplete();\n\n        Flux.fromIterable(storedCategories)\n            .flatMap(this::deleteImmediately)\n            .as(StepVerifier::create)\n            .expectNextCount(storedCategories.size())\n            .verifyComplete();\n    }\n\n    @Test\n    void reconcileStatusPostForCategoryA() {\n        categoryPostCountService.recalculatePostCount(Set.of(\"category-A\"));\n\n        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);\n        verify(client).update(captor.capture());\n        var value = captor.getValue();\n        assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(1);\n        assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);\n    }\n\n    @Test\n    void reconcileStatusPostForCategoryB() {\n        categoryPostCountService.recalculatePostCount(Set.of(\"category-B\"));\n\n        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);\n        verify(client).update(captor.capture());\n        var category = captor.getValue();\n        assertThat(category.getStatusOrDefault().getPostCount()).isEqualTo(1);\n        assertThat(category.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);\n    }\n\n    @Test\n    void reconcileStatusPostForCategoryC() {\n        categoryPostCountService.recalculatePostCount(Set.of(\"category-C\"));\n\n        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);\n        verify(client).update(captor.capture());\n        var value = captor.getValue();\n        assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(2);\n        assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);\n    }\n\n    @Test\n    void reconcileStatusPostForCategoryD() {\n        categoryPostCountService.recalculatePostCount(Set.of(\"category-D\"));\n\n        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);\n        verify(client).update(captor.capture());\n        var value = captor.getValue();\n        assertThat(value.getStatusOrDefault().postCount).isEqualTo(1);\n        assertThat(value.getStatusOrDefault().visiblePostCount).isEqualTo(0);\n    }\n\n    private List<Category> categories() {\n        /*\n         * |-A(post-4)\n         *   |-B(post-3)\n         *   |-|-C(post-2,post-1)\n         *   |-D(post-1)\n         */\n        Category categoryA = category(\"category-A\");\n        categoryA.getSpec().setChildren(List.of(\"category-B\", \"category-D\"));\n\n        Category categoryB = category(\"category-B\");\n        categoryB.getSpec().setChildren(List.of(\"category-C\"));\n\n        Category categoryC = category(\"category-C\");\n        Category categoryD = category(\"category-D\");\n        return List.of(categoryA, categoryB, categoryC, categoryD);\n    }\n\n    private Category category(String name) {\n        Category category = new Category();\n        Metadata metadata = new Metadata();\n        metadata.setName(name);\n        category.setMetadata(metadata);\n        category.setSpec(new Category.CategorySpec());\n        category.setStatus(new Category.CategoryStatus());\n\n        category.getSpec().setDisplayName(\"display-name\");\n        category.getSpec().setSlug(\"slug\");\n        category.getSpec().setPriority(0);\n        return category;\n    }\n\n    private List<Post> posts() {\n        /*\n         * |-A(post-4)\n         *   |-B(post-3)\n         *   |-|-C(post-2,post-1)\n         *   |-D(post-1)\n         */\n        Post post1 = fakePost();\n        post1.getMetadata().setName(\"post-1\");\n        post1.getSpec().setCategories(List.of(\"category-D\", \"category-C\"));\n        post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n\n        Post post2 = fakePost();\n        post2.getMetadata().setName(\"post-2\");\n        post2.getSpec().setCategories(List.of(\"category-C\"));\n        post2.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n\n        Post post3 = fakePost();\n        post3.getMetadata().setName(\"post-3\");\n        post3.getSpec().setCategories(List.of(\"category-B\"));\n        post3.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n\n        Post post4 = fakePost();\n        post4.getMetadata().setName(\"post-4\");\n        post4.getSpec().setCategories(List.of(\"category-A\"));\n        post4.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n        return List.of(post1, post2, post3, post4);\n    }\n\n    Post fakePost() {\n        var post = TestPost.postV1();\n        post.getSpec().setAllowComment(true);\n        post.getSpec().setDeleted(false);\n        post.getSpec().setExcerpt(new Post.Excerpt());\n        post.getSpec().getExcerpt().setAutoGenerate(false);\n        post.getSpec().setPinned(false);\n        post.getSpec().setPriority(0);\n        post.getSpec().setPublish(false);\n        post.getSpec().setSlug(\"fake-post\");\n        return post;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/ContentRequestTest.java",
    "content": "package run.halo.app.content;\n\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link ContentRequest}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass ContentRequestTest {\n    private ContentRequest contentRequest;\n\n    @BeforeEach\n    void setUp() {\n        Ref ref = new Ref();\n        ref.setKind(Post.KIND);\n        ref.setGroup(\"content.halo.run\");\n        ref.setName(\"test-post\");\n        contentRequest = new ContentRequest(ref, \"snapshot-1\", null, \"\"\"\n            Four score and seven\n            years ago our fathers\n\n            brought forth on this continent\n            \"\"\",\n            \"\"\"\n                <p>Four score and seven</p>\n                <p>years ago our fathers</p>\n                <br/>\n                <p>brought forth on this continent</p>\n                \"\"\",\n            \"MARKDOWN\");\n    }\n\n    @Test\n    void toSnapshot() throws JSONException {\n        String expectedContentPath =\n            \"<p>Four score and seven</p>\\\\n<p>years ago our fathers</p>\\\\n<br/>\\\\n<p>brought forth \"\n                + \"on this continent</p>\\\\n\";\n        String expectedRawPatch =\n            \"Four score and seven\\\\nyears ago our fathers\\\\n\\\\nbrought forth on this continent\\\\n\";\n        Snapshot snapshot = contentRequest.toSnapshot();\n        snapshot.getMetadata().setName(\"7b149646-ac60-4a5c-98ee-78b2dd0631b2\");\n        JSONAssert.assertEquals(JsonUtils.objectToJson(snapshot),\n            \"\"\"\n                {\n                    \"spec\": {\n                        \"subjectRef\": {\n                            \"kind\": \"Post\",\n                            \"group\": \"content.halo.run\",\n                            \"name\": \"test-post\"\n                        },\n                        \"rawType\": \"MARKDOWN\",\n                        \"rawPatch\": \"%s\",\n                        \"contentPatch\": \"%s\"\n                    },\n                    \"apiVersion\": \"content.halo.run/v1alpha1\",\n                    \"kind\": \"Snapshot\",\n                    \"metadata\": {\n                        \"name\": \"7b149646-ac60-4a5c-98ee-78b2dd0631b2\",\n                        \"annotations\": {}\n                    }\n                }\n                \"\"\".formatted(expectedRawPatch, expectedContentPath),\n            true);\n    }\n\n    @Test\n    void rawPatchFrom() throws JSONException {\n        String s = contentRequest.rawPatchFrom(\"\"\"\n            Four score and seven\n            years ago our fathers\n            \"\"\");\n        JSONAssert.assertEquals(s,\n            \"\"\"\n                   [\n                    {\n                        \"source\": {\n                            \"position\": 3,\n                            \"lines\": []\n                        },\n                        \"target\": {\n                            \"position\": 3,\n                            \"lines\": [\n                                \"brought forth on this continent\",\n                                \"\"\n                            ]\n                        },\n                        \"type\": \"INSERT\"\n                    }\n                ]\n                \"\"\", true);\n    }\n\n    @Test\n    void contentPatchFrom() throws JSONException {\n        String s = contentRequest.contentPatchFrom(\"\"\"\n            <p>Four score and seven</p>\n            <p>years ago our fathers</p>\n            \"\"\");\n        JSONAssert.assertEquals(s, \"\"\"\n            [\n                 {\n                     \"source\": {\n                         \"position\": 2,\n                         \"lines\": []\n                     },\n                     \"target\": {\n                         \"position\": 2,\n                         \"lines\": [\n                             \"<br/>\",\n                             \"<p>brought forth on this continent</p>\"\n                         ]\n                     },\n                     \"type\": \"INSERT\"\n                 }\n            ]\n            \"\"\", true);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/PostIntegrationTests.java",
    "content": "package run.halo.app.content;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.anySet;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.bean.override.mockito.MockitoBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataOperator;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Integration tests for {@link PostService}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@SpringBootTest\n@AutoConfigureWebTestClient\n@WithMockUser(username = \"fake-user\", password = \"fake-password\", roles = \"fake-super-role\")\npublic class PostIntegrationTests {\n\n    @Autowired\n    private WebTestClient webTestClient;\n\n    @MockitoBean\n    RoleService roleService;\n\n    @BeforeEach\n    void setUp() {\n        var rule = new Role.PolicyRule.Builder()\n            .apiGroups(\"*\")\n            .resources(\"*\")\n            .verbs(\"*\")\n            .build();\n        var role = new Role();\n        role.setMetadata(new Metadata());\n        role.getMetadata().setName(\"super-role\");\n        role.setRules(List.of(rule));\n        when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));\n        webTestClient = webTestClient.mutateWith(csrf());\n    }\n\n    @Test\n    void draftPost() {\n        webTestClient.post()\n            .uri(\"/apis/api.console.halo.run/v1alpha1/posts\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .bodyValue(postDraftRequest())\n            .exchange()\n            .expectBody(Post.class)\n            .value(post -> {\n                MetadataOperator metadata = post.getMetadata();\n                Post.PostSpec spec = post.getSpec();\n                assertThat(spec.getTitle()).isEqualTo(\"无标题文章\");\n                assertThat(metadata.getCreationTimestamp()).isNotNull();\n                assertThat(metadata.getName()).startsWith(\"post-\");\n                assertThat(spec.getHeadSnapshot()).isNotNull();\n                assertThat(spec.getHeadSnapshot()).isEqualTo(spec.getBaseSnapshot());\n                assertThat(spec.getOwner()).isEqualTo(\"fake-user\");\n\n                assertThat(post.getStatus()).isNotNull();\n                assertThat(post.getStatus().getPhase()).isEqualTo(\"DRAFT\");\n                assertThat(post.getStatus().getConditions().peek().getType()).isEqualTo(\"DRAFT\");\n            });\n    }\n\n    @Test\n    void draftPostAsPublish() {\n        PostRequest postRequest = postDraftRequest();\n        postRequest.post().getSpec().setPublish(true);\n        webTestClient.post()\n            .uri(\"/apis/api.console.halo.run/v1alpha1/posts\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .bodyValue(postRequest)\n            .exchange()\n            .expectBody(Post.class)\n            .value(post -> {\n                assertThat(post.getSpec().getReleaseSnapshot()).isNotNull();\n                assertThat(post.getSpec().getReleaseSnapshot())\n                    .isEqualTo(post.getSpec().getHeadSnapshot());\n                assertThat(post.getSpec().getHeadSnapshot())\n                    .isEqualTo(post.getSpec().getBaseSnapshot());\n            });\n    }\n\n    PostRequest postDraftRequest() {\n        String s = \"\"\"\n            {\n                \"post\": {\n                    \"spec\": {\n                        \"title\": \"无标题文章\",\n                        \"slug\": \"41c2ad39-21b4-45e4-a36b-5768245a0555\",\n                        \"template\": \"\",\n                        \"cover\": \"\",\n                        \"deleted\": false,\n                        \"publish\": true,\n                        \"publishTime\": \"\",\n                        \"pinned\": false,\n                        \"allowComment\": true,\n                        \"visible\": \"PUBLIC\",\n                        \"version\": 1,\n                        \"priority\": 0,\n                        \"excerpt\": {\n                            \"autoGenerate\": true,\n                            \"raw\": \"\"\n                        },\n                        \"categories\": [],\n                        \"tags\": [],\n                        \"htmlMetas\": []\n                    },\n                    \"apiVersion\": \"content.halo.run/v1alpha1\",\n                    \"kind\": \"Post\",\n                    \"metadata\": {\n                        \"name\": \"\",\n                        \"generateName\": \"post-\"\n                    }\n                },\n                \"content\": {\n                    \"raw\": \"<p>hello world</p>\",\n                    \"content\": \"<p>hello world</p>\",\n                    \"rawType\": \"HTML\"\n                }\n            }\n            \"\"\";\n        return JsonUtils.jsonToObject(s, PostRequest.class);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/TestPost.java",
    "content": "package run.halo.app.content;\n\nimport java.time.Instant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataUtil;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\npublic class TestPost {\n    public static Post postV1() {\n        Post post = new Post();\n        post.setKind(Post.KIND);\n        post.setApiVersion(getApiVersion(Post.class));\n        Metadata metadata = new Metadata();\n        metadata.setName(\"post-A\");\n        metadata.setVersion(1L);\n        post.setMetadata(metadata);\n\n        Post.PostSpec postSpec = new Post.PostSpec();\n        post.setSpec(postSpec);\n\n        postSpec.setTitle(\"post-A\");\n        postSpec.setBaseSnapshot(snapshotV1().getMetadata().getName());\n        postSpec.setHeadSnapshot(\"base-snapshot\");\n        postSpec.setReleaseSnapshot(null);\n\n        return post;\n    }\n\n    public static Snapshot snapshotV1() {\n        Snapshot snapshot = new Snapshot();\n        snapshot.setKind(Snapshot.KIND);\n        snapshot.setApiVersion(getApiVersion(Snapshot.class));\n        Metadata metadata = new Metadata();\n        metadata.setName(\"snapshot-A\");\n        metadata.setVersion(1L);\n        metadata.setCreationTimestamp(Instant.now());\n        snapshot.setMetadata(metadata);\n        MetadataUtil.nullSafeAnnotations(snapshot).put(Snapshot.KEEP_RAW_ANNO, \"true\");\n        Snapshot.SnapShotSpec spec = new Snapshot.SnapShotSpec();\n        snapshot.setSpec(spec);\n\n        Snapshot.addContributor(snapshot, \"guqing\");\n        spec.setRawType(\"MARKDOWN\");\n        spec.setRawPatch(\"A\");\n        spec.setContentPatch(\"<p>A</p>\");\n\n        return snapshot;\n    }\n\n    public static Snapshot snapshotV2() {\n        Snapshot snapshot = new Snapshot();\n        snapshot.setKind(Snapshot.KIND);\n        snapshot.setApiVersion(getApiVersion(Snapshot.class));\n        Metadata metadata = new Metadata();\n        metadata.setCreationTimestamp(Instant.now().plusSeconds(10));\n        metadata.setName(\"snapshot-B\");\n        snapshot.setMetadata(metadata);\n        Snapshot.SnapShotSpec spec = new Snapshot.SnapShotSpec();\n        snapshot.setSpec(spec);\n        Snapshot.addContributor(snapshot, \"guqing\");\n        spec.setRawType(\"MARKDOWN\");\n        spec.setRawPatch(PatchUtils.diffToJsonPatch(\"A\", \"B\"));\n        spec.setContentPatch(PatchUtils.diffToJsonPatch(\"<p>A</p>\", \"<p>B</p>\"));\n\n        return snapshot;\n    }\n\n    public static Snapshot snapshotV3() {\n        Snapshot snapshotV3 = snapshotV2();\n        snapshotV3.getMetadata().setName(\"snapshot-C\");\n        snapshotV3.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(20));\n        Snapshot.SnapShotSpec spec = snapshotV3.getSpec();\n        Snapshot.addContributor(snapshotV3, \"guqing\");\n        spec.setRawType(\"MARKDOWN\");\n        spec.setRawPatch(PatchUtils.diffToJsonPatch(\"B\", \"C\"));\n        spec.setContentPatch(PatchUtils.diffToJsonPatch(\"<p>B</p>\", \"<p>C</p>\"));\n\n        return snapshotV3;\n    }\n\n    public static String getApiVersion(Class<? extends AbstractExtension> extension) {\n        GVK annotation = extension.getAnnotation(GVK.class);\n        return annotation.group() + \"/\" + annotation.version();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/CommentEmailOwnerTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link CommentEmailOwner}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass CommentEmailOwnerTest {\n\n    @Test\n    void constructorTest() throws JSONException {\n        CommentEmailOwner commentEmailOwner =\n            new CommentEmailOwner(\"example@example.com\", \"avatar\", \"displayName\", \"website\");\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"email\": \"example@example.com\",\n                    \"avatar\": \"avatar\",\n                    \"displayName\": \"displayName\",\n                    \"website\": \"website\"\n                }\n                \"\"\",\n            JsonUtils.objectToJson(commentEmailOwner),\n            true);\n    }\n\n    @Test\n    void toCommentOwner() throws JSONException {\n        CommentEmailOwner commentEmailOwner =\n            new CommentEmailOwner(\"example@example.com\", \"avatar\", \"displayName\", \"website\");\n        Comment.CommentOwner commentOwner = commentEmailOwner.toCommentOwner();\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"kind\": \"Email\",\n                    \"name\": \"example@example.com\",\n                    \"displayName\": \"displayName\",\n                    \"annotations\": {\n                        \"website\": \"website\",\n                        \"avatar\": \"avatar\"\n                    }\n                }\n                \"\"\",\n            JsonUtils.objectToJson(commentOwner),\n            true);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/CommentNotificationReasonPublisherTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.event.post.CommentCreatedEvent;\nimport run.halo.app.event.post.ReplyCreatedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.ExternalLinkProcessor;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.notification.NotificationReasonEmitter;\nimport run.halo.app.notification.ReasonPayload;\nimport run.halo.app.notification.UserIdentity;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Tests for {@link CommentNotificationReasonPublisher}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CommentNotificationReasonPublisherTest {\n\n    @Mock\n    private ExtensionClient client;\n\n    @Mock\n    CommentNotificationReasonPublisher.NewCommentOnPostReasonPublisher\n        newCommentOnPostReasonPublisher;\n\n    @Mock\n    CommentNotificationReasonPublisher.NewCommentOnPageReasonPublisher\n        newCommentOnPageReasonPublisher;\n\n    @Mock\n    CommentNotificationReasonPublisher.NewReplyReasonPublisher newReplyReasonPublisher;\n\n    @InjectMocks\n    private CommentNotificationReasonPublisher reasonPublisher;\n\n\n    @Test\n    void onNewCommentTest() {\n        var comment = mock(Comment.class);\n        var spyReasonPublisher = spy(reasonPublisher);\n\n        doReturn(true).when(spyReasonPublisher).isPostComment(eq(comment));\n\n        var event = new CommentCreatedEvent(this, comment);\n        spyReasonPublisher.onNewComment(event);\n\n        verify(newCommentOnPostReasonPublisher).publishReasonBy(eq(comment));\n\n        doReturn(false).when(spyReasonPublisher).isPostComment(eq(comment));\n        doReturn(true).when(spyReasonPublisher).isPageComment(eq(comment));\n\n        spyReasonPublisher.onNewComment(event);\n        verify(newCommentOnPageReasonPublisher).publishReasonBy(eq(comment));\n    }\n\n    @Test\n    void onNewReplyTest() {\n        var reply = mock(Reply.class);\n        var spec = mock(Reply.ReplySpec.class);\n        when(reply.getSpec()).thenReturn(spec);\n        when(spec.getCommentName()).thenReturn(\"fake-comment\");\n\n        var spyReasonPublisher = spy(reasonPublisher);\n        var comment = mock(Comment.class);\n\n        when(client.fetch(eq(Comment.class), eq(\"fake-comment\")))\n            .thenReturn(Optional.of(comment));\n\n        var event = new ReplyCreatedEvent(this, reply);\n        spyReasonPublisher.onNewReply(event);\n\n        verify(newReplyReasonPublisher).publishReasonBy(eq(reply), eq(comment));\n        verify(spec).getCommentName();\n        verify(client).fetch(eq(Comment.class), eq(\"fake-comment\"));\n    }\n\n    @Test\n    void isPostCommentTest() {\n        var comment = createComment();\n        comment.getSpec()\n            .setSubjectRef(Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class)));\n\n        assertThat(reasonPublisher.isPostComment(comment)).isTrue();\n\n        comment.getSpec()\n            .setSubjectRef(Ref.of(\"fake-post\", GroupVersionKind.fromExtension(SinglePage.class)));\n\n        assertThat(reasonPublisher.isPostComment(comment)).isFalse();\n    }\n\n    @Test\n    void isPageComment() {\n        var comment = createComment();\n        comment.getSpec()\n            .setSubjectRef(Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class)));\n\n        assertThat(reasonPublisher.isPageComment(comment)).isFalse();\n\n        comment.getSpec()\n            .setSubjectRef(Ref.of(\"fake-post\", GroupVersionKind.fromExtension(SinglePage.class)));\n\n        assertThat(reasonPublisher.isPageComment(comment)).isTrue();\n    }\n\n    @Nested\n    @ExtendWith(MockitoExtension.class)\n    class CommentContentConverterTest {\n        @Mock\n        ExternalUrlSupplier externalUrlSupplier;\n\n        @Mock\n        ExternalLinkProcessor externalLinkProcessor;\n\n        @InjectMocks\n        CommentNotificationReasonPublisher.CommentContentConverter commentContentConverter;\n\n        @Test\n        void shouldConvertRelativeImageLinksToAbsolute() {\n            var content =\n                \"<p>Test content <img src=\\\"/upload/image.jpg\\\" alt=\\\"Test image\\\" /></p>\";\n            \n            when(externalLinkProcessor.processLink(\"/upload/image.jpg\"))\n                .thenReturn(\"https://example.com/upload/image.jpg\");\n\n            var result = commentContentConverter.convertRelativeLinks(content);\n\n            assertThat(result).contains(\"https://example.com/upload/image.jpg\");\n            assertThat(result).contains(\"Test content\");\n            verify(externalLinkProcessor).processLink(\"/upload/image.jpg\");\n        }\n\n        @Test\n        void shouldHandleRelativeImageLinksWithoutLeadingSlash() {\n            var content = \"<p><img src=\\\"upload/image.jpg\\\" /></p>\";\n            \n            when(externalLinkProcessor.processLink(\"upload/image.jpg\"))\n                .thenReturn(\"https://example.com/upload/image.jpg\");\n\n            var result = commentContentConverter.convertRelativeLinks(content);\n\n            assertThat(result).contains(\"https://example.com/upload/image.jpg\");\n            verify(externalLinkProcessor).processLink(\"upload/image.jpg\");\n        }\n\n        @Test\n        void shouldNotConvertAbsoluteImageLinks() {\n            var content = \"<p><img src=\\\"https://cdn.example.com/image.jpg\\\" /></p>\";\n            \n            when(externalLinkProcessor.processLink(\"https://cdn.example.com/image.jpg\"))\n                .thenReturn(\"https://cdn.example.com/image.jpg\");\n\n            var result = commentContentConverter.convertRelativeLinks(content);\n\n            assertThat(result).contains(\"https://cdn.example.com/image.jpg\");\n            verify(externalLinkProcessor).processLink(\"https://cdn.example.com/image.jpg\");\n        }\n\n        @Test\n        void shouldHandleMultipleImages() {\n            var content = \"<p>\"\n                + \"<img src=\\\"/img1.jpg\\\" />\"\n                + \"<img src=\\\"/img2.jpg\\\" />\"\n                + \"<img src=\\\"https://example.com/img3.jpg\\\" />\"\n                + \"</p>\";\n            \n            when(externalLinkProcessor.processLink(\"/img1.jpg\"))\n                .thenReturn(\"https://example.com/img1.jpg\");\n            when(externalLinkProcessor.processLink(\"/img2.jpg\"))\n                .thenReturn(\"https://example.com/img2.jpg\");\n            when(externalLinkProcessor.processLink(\"https://example.com/img3.jpg\"))\n                .thenReturn(\"https://example.com/img3.jpg\");\n\n            var result = commentContentConverter.convertRelativeLinks(content);\n\n            assertThat(result).contains(\"https://example.com/img1.jpg\");\n            assertThat(result).contains(\"https://example.com/img2.jpg\");\n            assertThat(result).contains(\"https://example.com/img3.jpg\");\n            verify(externalLinkProcessor).processLink(\"/img1.jpg\");\n            verify(externalLinkProcessor).processLink(\"/img2.jpg\");\n            verify(externalLinkProcessor).processLink(\"https://example.com/img3.jpg\");\n        }\n\n        @Test\n        void shouldHandleContentWithoutImages() {\n            var content = \"<p>This is a comment content without images</p>\";\n\n            var result = commentContentConverter.convertRelativeLinks(content);\n\n            assertThat(result).contains(\"This is a comment content without images\");\n            assertThat(result).doesNotContain(\"img\");\n        }\n\n        @Test\n        void shouldHandleEmptyContent() {\n            var content = \"\";\n\n            var result = commentContentConverter.convertRelativeLinks(content);\n\n            assertThat(result).isEmpty();\n        }\n\n        @Test\n        void shouldHandleComplexHtmlContent() {\n            var content = \"\"\"\n                <div>\n                    <h1>Title</h1>\n                    <p>Paragraph content</p>\n                    <img src=\"/images/photo1.png\" alt=\"Photo 1\" />\n                    <p>More text</p>\n                    <img src=\"assets/photo2.jpg\" />\n                </div>\n                \"\"\";\n            \n            when(externalLinkProcessor.processLink(\"/images/photo1.png\"))\n                .thenReturn(\"https://example.com/images/photo1.png\");\n            when(externalLinkProcessor.processLink(\"assets/photo2.jpg\"))\n                .thenReturn(\"https://example.com/assets/photo2.jpg\");\n\n            var result = commentContentConverter.convertRelativeLinks(content);\n\n            assertThat(result).contains(\"https://example.com/images/photo1.png\");\n            assertThat(result).contains(\"https://example.com/assets/photo2.jpg\");\n            assertThat(result).contains(\"Title\");\n            assertThat(result).contains(\"Paragraph content\");\n            verify(externalLinkProcessor).processLink(\"/images/photo1.png\");\n            verify(externalLinkProcessor).processLink(\"assets/photo2.jpg\");\n        }\n    }\n\n    @Nested\n    @ExtendWith(MockitoExtension.class)\n    class NewCommentOnPostReasonPublisherTest {\n        @Mock\n        ExtensionClient client;\n\n        @Mock\n        NotificationReasonEmitter emitter;\n\n        @Mock\n        ExtensionGetter extensionGetter;\n\n        @Mock\n        ExternalLinkProcessor externalLinkProcessor;\n\n        @Mock\n        CommentNotificationReasonPublisher.CommentContentConverter commentContentConverter;\n\n        @InjectMocks\n        CommentNotificationReasonPublisher.NewCommentOnPostReasonPublisher\n            newCommentOnPostReasonPublisher;\n\n        @Test\n        void publishReasonByTest() {\n            final var comment = createComment();\n            comment.getSpec().getOwner().setDisplayName(\"fake-display-name\");\n            comment.getSpec().setContent(\"fake-comment-content\");\n\n            var post = mock(Post.class);\n            final var spec = mock(Post.PostSpec.class);\n            var metadata = new Metadata();\n            metadata.setName(\"fake-post\");\n            when(post.getMetadata()).thenReturn(metadata);\n            when(post.getStatusOrDefault()).thenReturn(new Post.PostStatus());\n            when(post.getSpec()).thenReturn(spec);\n            when(spec.getTitle()).thenReturn(\"fake-title\");\n\n            when(client.fetch(eq(Post.class), eq(metadata.getName())))\n                .thenReturn(Optional.of(post));\n\n            when(commentContentConverter.convertRelativeLinks(eq(\"fake-comment-content\")))\n                .thenReturn(\"fake-comment-content\");\n\n            when(emitter.emit(eq(\"new-comment-on-post\"), any()))\n                .thenReturn(Mono.empty());\n\n            newCommentOnPostReasonPublisher.publishReasonBy(comment);\n\n            verify(client).fetch(eq(Post.class), eq(metadata.getName()));\n            verify(emitter).emit(eq(\"new-comment-on-post\"), assertArg(consumer -> {\n                var builder = ReasonPayload.builder();\n                consumer.accept(builder);\n                var reasonPayload = builder.build();\n                var reasonSubject = Reason.Subject.builder()\n                    .apiVersion(post.getApiVersion())\n                    .kind(post.getKind())\n                    .name(post.getMetadata().getName())\n                    .title(post.getSpec().getTitle())\n                    .build();\n                assertThat(reasonPayload.getSubject()).isEqualTo(reasonSubject);\n\n                assertThat(reasonPayload.getAuthor())\n                    .isEqualTo(\n                        UserIdentity.anonymousWithEmail(comment.getSpec().getOwner().getName()));\n\n                assertThat(reasonPayload.getAttributes()).containsAllEntriesOf(Map.of(\n                    \"postName\", post.getMetadata().getName(),\n                    \"postTitle\", post.getSpec().getTitle(),\n                    \"commenter\", comment.getSpec().getOwner().getDisplayName(),\n                    \"content\", comment.getSpec().getContent(),\n                    \"commentName\", comment.getMetadata().getName()\n                ));\n            }));\n        }\n\n        @Test\n        void doNotEmitReasonTest() {\n            final var comment = createComment();\n            var commentOwner = new Comment.CommentOwner();\n            commentOwner.setKind(User.KIND);\n            commentOwner.setName(\"fake-user\");\n            comment.getSpec().setOwner(commentOwner);\n\n            var post = new Post();\n            post.setMetadata(new Metadata());\n            post.getMetadata().setName(\"fake-post\");\n            post.setSpec(new Post.PostSpec());\n            post.getSpec().setOwner(\"fake-user\");\n\n            // the username is the same as the comment owner\n            assertThat(newCommentOnPostReasonPublisher.doNotEmitReason(comment, post)).isTrue();\n\n            // not the same username\n            commentOwner.setName(\"other\");\n            assertThat(newCommentOnPostReasonPublisher.doNotEmitReason(comment, post)).isFalse();\n\n            // the comment owner is email and the same as the post-owner user email\n            commentOwner.setKind(Comment.CommentOwner.KIND_EMAIL);\n            commentOwner.setName(\"example@example.com\");\n            var user = new User();\n            user.setSpec(new User.UserSpec());\n            user.getSpec().setEmail(\"example@example.com\");\n            when(client.fetch(eq(User.class), eq(\"fake-user\")))\n                .thenReturn(Optional.of(user));\n\n            assertThat(newCommentOnPostReasonPublisher.doNotEmitReason(comment, post)).isTrue();\n\n            // the comment owner is email and not the same as the post-owner user email\n            user.getSpec().setEmail(\"fake@example.com\");\n            assertThat(newCommentOnPostReasonPublisher.doNotEmitReason(comment, post)).isFalse();\n        }\n    }\n\n    @Nested\n    @ExtendWith(MockitoExtension.class)\n    class NewCommentOnPageReasonPublisherTest {\n        @Mock\n        ExtensionClient client;\n\n        @Mock\n        NotificationReasonEmitter emitter;\n\n        @Mock\n        ExternalLinkProcessor externalLinkProcessor;\n\n        @Mock\n        CommentNotificationReasonPublisher.CommentContentConverter commentContentConverter;\n\n        @InjectMocks\n        CommentNotificationReasonPublisher.NewCommentOnPageReasonPublisher\n            newCommentOnPageReasonPublisher;\n\n        @Test\n        void publishReasonByTest() {\n            final var comment = createComment();\n            comment.getSpec().getOwner().setDisplayName(\"fake-display-name\");\n            comment.getSpec().setContent(\"fake-comment-content\");\n            comment.getSpec().setSubjectRef(\n                Ref.of(\"fake-page\", GroupVersionKind.fromExtension(SinglePage.class)));\n\n            var page = mock(SinglePage.class);\n            final var spec = mock(SinglePage.SinglePageSpec.class);\n            var metadata = new Metadata();\n            metadata.setName(\"fake-page\");\n            when(page.getMetadata()).thenReturn(metadata);\n            when(page.getStatusOrDefault()).thenReturn(new SinglePage.SinglePageStatus());\n            when(page.getSpec()).thenReturn(spec);\n            when(spec.getTitle()).thenReturn(\"fake-title\");\n\n            when(client.fetch(eq(SinglePage.class), eq(metadata.getName())))\n                .thenReturn(Optional.of(page));\n\n            when(commentContentConverter.convertRelativeLinks(eq(\"fake-comment-content\")))\n                .thenReturn(\"fake-comment-content\");\n\n            when(emitter.emit(eq(\"new-comment-on-single-page\"), any()))\n                .thenReturn(Mono.empty());\n\n            newCommentOnPageReasonPublisher.publishReasonBy(comment);\n\n            verify(client).fetch(eq(SinglePage.class), eq(metadata.getName()));\n            verify(emitter).emit(eq(\"new-comment-on-single-page\"), assertArg(consumer -> {\n                var builder = ReasonPayload.builder();\n                consumer.accept(builder);\n                var reasonPayload = builder.build();\n                var reasonSubject = Reason.Subject.builder()\n                    .apiVersion(page.getApiVersion())\n                    .kind(page.getKind())\n                    .name(page.getMetadata().getName())\n                    .title(page.getSpec().getTitle())\n                    .build();\n                assertThat(reasonPayload.getSubject()).isEqualTo(reasonSubject);\n\n                assertThat(reasonPayload.getAuthor())\n                    .isEqualTo(\n                        UserIdentity.anonymousWithEmail(comment.getSpec().getOwner().getName()));\n\n                assertThat(reasonPayload.getAttributes()).containsAllEntriesOf(Map.of(\n                    \"pageName\", page.getMetadata().getName(),\n                    \"pageTitle\", page.getSpec().getTitle(),\n                    \"commenter\", comment.getSpec().getOwner().getDisplayName(),\n                    \"content\", comment.getSpec().getContent(),\n                    \"commentName\", comment.getMetadata().getName()\n                ));\n            }));\n        }\n\n        @Test\n        void doNotEmitReasonTest() {\n            final var comment = createComment();\n            var commentOwner = new Comment.CommentOwner();\n            commentOwner.setKind(User.KIND);\n            commentOwner.setName(\"fake-user\");\n            comment.getSpec().setOwner(commentOwner);\n\n            var page = new SinglePage();\n            page.setMetadata(new Metadata());\n            page.getMetadata().setName(\"fake-page\");\n            page.setSpec(new SinglePage.SinglePageSpec());\n            page.getSpec().setOwner(\"fake-user\");\n\n            // the username is the same as the comment owner\n            assertThat(newCommentOnPageReasonPublisher.doNotEmitReason(comment, page)).isTrue();\n\n            // not the same username\n            commentOwner.setName(\"other\");\n            assertThat(newCommentOnPageReasonPublisher.doNotEmitReason(comment, page)).isFalse();\n\n            // the comment owner is email and the same as the page-owner user email\n            commentOwner.setKind(Comment.CommentOwner.KIND_EMAIL);\n            commentOwner.setName(\"example@example.com\");\n            var user = new User();\n            user.setSpec(new User.UserSpec());\n            user.getSpec().setEmail(\"example@example.com\");\n            when(client.fetch(eq(User.class), eq(\"fake-user\")))\n                .thenReturn(Optional.of(user));\n\n            assertThat(newCommentOnPageReasonPublisher.doNotEmitReason(comment, page)).isTrue();\n\n            // the comment owner is email and not the same as the post-owner user email\n            user.getSpec().setEmail(\"fake@example.com\");\n            assertThat(newCommentOnPageReasonPublisher.doNotEmitReason(comment, page)).isFalse();\n        }\n    }\n\n    @Nested\n    @ExtendWith(MockitoExtension.class)\n    class NewReplyReasonPublisherTest {\n\n        @Mock\n        ExtensionClient client;\n\n        @Mock\n        NotificationReasonEmitter notificationReasonEmitter;\n\n        @Mock\n        ExtensionGetter extensionGetter;\n\n        @Mock\n        CommentNotificationReasonPublisher.CommentContentConverter commentContentConverter;\n\n        @InjectMocks\n        CommentNotificationReasonPublisher.NewReplyReasonPublisher newReplyReasonPublisher;\n\n        @Test\n        void publishReasonByTest() {\n            when(extensionGetter.getExtensions(CommentSubject.class))\n                .thenReturn(Flux.empty());\n            var reply = createReply(\"fake-reply\");\n\n            reply.getSpec().setQuoteReply(\"fake-quote-reply\");\n            var quoteReply = createReply(\"fake-quote-reply\");\n\n            when(client.fetch(eq(Reply.class), eq(\"fake-quote-reply\")))\n                .thenReturn(Optional.of(quoteReply));\n\n            var spyNewReplyReasonPublisher = spy(newReplyReasonPublisher);\n\n            var comment = createComment();\n            comment.getSpec().setContent(\"fake-comment-content\");\n\n            doReturn(false).when(spyNewReplyReasonPublisher)\n                .doNotEmitReason(any(), any(), any());\n            \n            // Mock commentContentConverter for all content conversions\n            when(commentContentConverter.convertRelativeLinks(eq(\"fake-comment-content\")))\n                .thenReturn(\"fake-comment-content\");\n            when(commentContentConverter.convertRelativeLinks(eq(\"fake-reply-content\")))\n                .thenReturn(\"fake-reply-content\");\n            \n            when(notificationReasonEmitter.emit(any(), any()))\n                .thenReturn(Mono.empty());\n\n            // execute target method\n            spyNewReplyReasonPublisher.publishReasonBy(reply, comment);\n\n            verify(notificationReasonEmitter)\n                .emit(eq(NotificationReasonConst.SOMEONE_REPLIED_TO_YOU), assertArg(consumer -> {\n                    var builder = ReasonPayload.builder();\n                    consumer.accept(builder);\n                    var reasonPayload = builder.build();\n                    var reasonSubject = Reason.Subject.builder()\n                        .apiVersion(quoteReply.getApiVersion())\n                        .kind(quoteReply.getKind())\n                        .name(quoteReply.getMetadata().getName())\n                        .title(quoteReply.getSpec().getContent())\n                        .build();\n                    assertThat(reasonPayload.getSubject()).isEqualTo(reasonSubject);\n\n                    assertThat(reasonPayload.getAuthor())\n                        .isEqualTo(\n                            UserIdentity.of(reply.getSpec().getOwner().getName()));\n\n                    assertThat(reasonPayload.getAttributes()).containsAllEntriesOf(Map.of(\n                        \"commentContent\", comment.getSpec().getContent(),\n                        \"isQuoteReply\", true,\n                        \"quoteContent\", quoteReply.getSpec().getContent(),\n                        \"commentName\", comment.getMetadata().getName(),\n                        \"replier\", reply.getSpec().getOwner().getDisplayName(),\n                        \"content\", reply.getSpec().getContent(),\n                        \"replyName\", reply.getMetadata().getName()\n                    ));\n                }));\n        }\n\n        @Test\n        void doNotEmitReasonTest() {\n            final var currentReply = createReply(\"current\");\n            currentReply.getSpec().setQuoteReply(\"quote\");\n            final var quoteReply = createReply(\"quote\");\n            final var comment = createComment();\n\n            assertThat(newReplyReasonPublisher\n                .doNotEmitReason(currentReply, quoteReply, comment)).isTrue();\n\n            currentReply.getSpec().getOwner().setName(\"other\");\n            assertThat(newReplyReasonPublisher\n                .doNotEmitReason(currentReply, quoteReply, comment)).isFalse();\n\n            currentReply.getSpec().setQuoteReply(null);\n            assertThat(newReplyReasonPublisher\n                .doNotEmitReason(currentReply, quoteReply, comment)).isFalse();\n\n            currentReply.getSpec().setOwner(comment.getSpec().getOwner());\n            assertThat(newReplyReasonPublisher\n                .doNotEmitReason(currentReply, quoteReply, comment)).isTrue();\n        }\n\n        static Reply createReply(String name) {\n            var reply = new Reply();\n            reply.setMetadata(new Metadata());\n            reply.getMetadata().setName(name);\n            reply.setSpec(new Reply.ReplySpec());\n            reply.getSpec().setCommentName(\"fake-comment\");\n            var owner = new Comment.CommentOwner();\n            owner.setKind(User.KIND);\n            owner.setName(\"fake-user\");\n            owner.setDisplayName(\"fake-display-name\");\n            reply.getSpec().setOwner(owner);\n            reply.getSpec().setContent(\"fake-reply-content\");\n            return reply;\n        }\n    }\n\n    static Comment createComment() {\n        var comment = new Comment();\n        comment.setMetadata(new Metadata());\n        comment.getMetadata().setName(\"fake-comment\");\n        comment.setSpec(new Comment.CommentSpec());\n        var commentOwner = new Comment.CommentOwner();\n        commentOwner.setKind(Comment.CommentOwner.KIND_EMAIL);\n        commentOwner.setName(\"example@example.com\");\n        comment.getSpec().setOwner(commentOwner);\n        comment.getSpec().setSubjectRef(\n            Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class)));\n        return comment;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/CommentRequestTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link CommentRequest}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass CommentRequestTest {\n\n    @Test\n    void constructor() throws JSONException {\n        CommentRequest commentRequest = createCommentRequest();\n\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"subjectRef\": {\n                        \"group\": \"fake.halo.run\",\n                        \"version\": \"v1alpha1\",\n                        \"kind\": \"Fake\",\n                        \"name\": \"fake\"\n                    },\n                    \"raw\": \"raw\",\n                    \"content\": \"content\",\n                    \"allowNotification\": true\n                }\n                \"\"\",\n            JsonUtils.objectToJson(commentRequest),\n            true);\n    }\n\n    @Test\n    void toComment() throws JSONException {\n        CommentRequest commentRequest = createCommentRequest();\n        Comment comment = commentRequest.toComment();\n        assertThat(comment.getMetadata().getName()).isNotNull();\n\n        comment.getMetadata().setName(\"fake\");\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"spec\": {\n                        \"raw\": \"raw\",\n                        \"content\": \"content\",\n                        \"allowNotification\": true,\n                        \"subjectRef\": {\n                            \"group\": \"fake.halo.run\",\n                            \"version\": \"v1alpha1\",\n                            \"kind\": \"Fake\",\n                            \"name\": \"fake\"\n                        }\n                    },\n                    \"apiVersion\": \"content.halo.run/v1alpha1\",\n                    \"kind\": \"Comment\",\n                    \"metadata\": {\n                        \"name\": \"fake\"\n                    }\n                }\n                \"\"\",\n            JsonUtils.objectToJson(comment),\n            true);\n    }\n\n    private static CommentRequest createCommentRequest() {\n        CommentRequest commentRequest = new CommentRequest();\n        commentRequest.setRaw(\"raw\");\n        commentRequest.setContent(\"content\");\n        commentRequest.setAllowNotification(true);\n\n        FakeExtension fakeExtension = new FakeExtension();\n        fakeExtension.setMetadata(new Metadata());\n        fakeExtension.getMetadata().setName(\"fake\");\n        commentRequest.setSubjectRef(Ref.of(fakeExtension));\n        return commentRequest;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/CommentServiceImplIntegrationTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionStoreUtil;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.store.ReactiveExtensionStoreClient;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Integration tests for {@link CommentServiceImpl}.\n *\n * @author guqing\n * @since 2.15.0\n */\nclass CommentServiceImplIntegrationTest {\n\n    @Nested\n    @DirtiesContext\n    @SpringBootTest\n    class CommentRemoveTest {\n        private final List<Comment> storedComments = createComments(350);\n\n        @Autowired\n        private SchemeManager schemeManager;\n\n        @MockitoSpyBean\n        private ReactiveExtensionClient reactiveClient;\n\n        @Autowired\n        private ReactiveExtensionStoreClient storeClient;\n\n        @MockitoSpyBean\n        private CommentServiceImpl commentService;\n\n        Mono<Extension> deleteImmediately(Extension extension) {\n            var name = extension.getMetadata().getName();\n            var scheme = schemeManager.get(extension.getClass());\n\n            // delete from db\n            var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);\n            return storeClient.delete(storeName, extension.getMetadata().getVersion())\n                .thenReturn(extension);\n        }\n\n        @BeforeEach\n        void setUp() {\n            Flux.fromIterable(storedComments)\n                .flatMap(post -> reactiveClient.create(post))\n                .as(StepVerifier::create)\n                .expectNextCount(storedComments.size())\n                .verifyComplete();\n        }\n\n        @AfterEach\n        void tearDown() {\n            Flux.fromIterable(storedComments)\n                .flatMap(this::deleteImmediately)\n                .as(StepVerifier::create)\n                .expectNextCount(storedComments.size())\n                .verifyComplete();\n        }\n\n        @Test\n        void commentBatchDeletionTest() {\n            Ref ref = Ref.of(\"67\",\n                GroupVersionKind.fromAPIVersionAndKind(\"content.halo.run/v1alpha1\", \"SinglePage\"));\n            commentService.removeBySubject(ref)\n                .as(StepVerifier::create)\n                .verifyComplete();\n\n            verify(reactiveClient, times(storedComments.size())).delete(any(Comment.class));\n            verify(commentService, times(2)).listCommentsByRef(eq(ref), any());\n\n            commentService.listCommentsByRef(ref, PageRequestImpl.ofSize(1))\n                .as(StepVerifier::create)\n                .consumeNextWith(result -> {\n                    assertThat(result.getTotal()).isEqualTo(0);\n                })\n                .verifyComplete();\n        }\n\n        List<Comment> createComments(int size) {\n            List<Comment> comments = new ArrayList<>(size);\n            for (int i = 0; i < size; i++) {\n                var comment = createComment();\n                comment.getMetadata().setName(\"comment-\" + i);\n                comments.add(comment);\n            }\n            return comments;\n        }\n    }\n\n    Comment createComment() {\n        return JsonUtils.jsonToObject(\"\"\"\n              {\n                \"spec\": {\n                    \"raw\": \"fake-raw\",\n                    \"content\": \"fake-content\",\n                    \"owner\": {\n                        \"kind\": \"User\",\n                        \"name\": \"fake-user\"\n                    },\n                    \"userAgent\": \"\",\n                    \"ipAddress\": \"\",\n                    \"approvedTime\": \"2024-02-28T09:15:16.095Z\",\n                    \"creationTime\": \"2024-02-28T06:23:42.923294424Z\",\n                    \"priority\": 0,\n                    \"top\": false,\n                    \"allowNotification\": false,\n                    \"approved\": true,\n                    \"hidden\": false,\n                    \"subjectRef\": {\n                        \"group\": \"content.halo.run\",\n                        \"version\": \"v1alpha1\",\n                        \"kind\": \"SinglePage\",\n                        \"name\": \"67\"\n                    },\n                    \"lastReadTime\": \"2024-02-29T03:39:04.230Z\"\n                },\n                \"apiVersion\": \"content.halo.run/v1alpha1\",\n                \"kind\": \"Comment\",\n                \"metadata\": {\n                    \"generateName\": \"comment-\"\n                }\n            }\n            \"\"\", Comment.class);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/CommentServiceImplTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.counter.MeterUtils;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.security.authorization.AuthorityUtils;\n\n/**\n * Tests for {@link CommentServiceImpl}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith({SpringExtension.class, MockitoExtension.class})\nclass CommentServiceImplTest {\n\n    @Mock\n    SystemConfigFetcher environmentFetcher;\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    UserService userService;\n\n    @Mock\n    RoleService roleService;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    @InjectMocks\n    CommentServiceImpl commentService;\n\n    @Mock\n    CounterService counterService;\n\n    private static User createUser(String name) {\n        User user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(name);\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setAvatar(name + \"-avatar\");\n        user.getSpec().setDisplayName(name + \"-displayName\");\n        user.getSpec().setEmail(name + \"-email\");\n        return user;\n    }\n\n    @Test\n    void listComment() {\n        var comments = new ListResult<Comment>(1, 10, 3, comments());\n        when(client.listBy(eq(Comment.class), any(ListOptions.class), any(PageRequest.class)))\n            .thenReturn(Mono.just(comments));\n\n        PostCommentSubject postCommentSubject = Mockito.mock(PostCommentSubject.class);\n        when(extensionGetter.getExtensions(CommentSubject.class))\n            .thenReturn(Flux.just(postCommentSubject));\n\n        when(postCommentSubject.supports(any())).thenReturn(true);\n        when(postCommentSubject.get(eq(\"fake-post\"))).thenReturn(Mono.just(post()));\n\n        when(userService.getUserOrGhost(any()))\n            .thenReturn(Mono.just(ghostUser()));\n        // when(userService.getUserOrGhost(\"A-owner\"))\n        //     .thenReturn(Mono.just(createUser(\"A-owner\")));\n        when(userService.getUserOrGhost(\"B-owner\"))\n            .thenReturn(Mono.just(createUser(\"B-owner\")));\n\n        ServerWebExchange exchange = mock(ServerWebExchange.class);\n        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();\n        MockServerRequest request = MockServerRequest.builder()\n            .queryParams(queryParams)\n            .exchange(exchange)\n            .build();\n        ServerHttpRequest httpRequest = mock(ServerHttpRequest.class);\n        when(exchange.getRequest()).thenReturn(httpRequest);\n        when(httpRequest.getQueryParams()).thenReturn(queryParams);\n        final var listResultMono = commentService.listComment(new CommentQuery(request));\n        Counter counterA = new Counter();\n        counterA.setUpvote(3);\n        String commentACounter = MeterUtils.nameOf(Comment.class, \"A\");\n        when(counterService.getByName(eq(commentACounter))).thenReturn(Mono.just(counterA));\n\n        Counter counterB = new Counter();\n        counterB.setUpvote(9);\n        String commentBCounter = MeterUtils.nameOf(Comment.class, \"B\");\n        when(counterService.getByName(eq(commentBCounter))).thenReturn(Mono.just(counterB));\n\n        Counter counterC = new Counter();\n        counterC.setUpvote(0);\n        String commentCCounter = MeterUtils.nameOf(Comment.class, \"C\");\n        when(counterService.getByName(eq(commentCCounter))).thenReturn(Mono.just(counterC));\n\n        StepVerifier.create(listResultMono)\n            .consumeNextWith(result -> {\n                try {\n                    JSONAssert.assertEquals(expectListResultJson(),\n                        JsonUtils.objectToJson(result),\n                        true);\n                } catch (JSONException e) {\n                    throw new RuntimeException(e);\n                }\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    @WithMockUser(username = \"B-owner\")\n    void create() throws JSONException {\n        var commentSetting = getCommentSetting();\n        when(environmentFetcher.fetchComment()).thenReturn(Mono.just(commentSetting));\n        when(roleService.contains(Set.of(\"USER\"),\n            Set.of(AuthorityUtils.COMMENT_MANAGEMENT_ROLE_NAME)))\n            .thenReturn(Mono.just(false));\n\n        CommentRequest commentRequest = new CommentRequest();\n        commentRequest.setRaw(\"fake-raw\");\n        commentRequest.setContent(\"fake-content\");\n        commentRequest.setAllowNotification(true);\n        commentRequest.setSubjectRef(Ref.of(post()));\n\n        ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);\n\n        when(client.fetch(eq(User.class), eq(\"B-owner\")))\n            .thenReturn(Mono.just(createUser(\"B-owner\")));\n        Comment commentToCreate = commentRequest.toComment();\n        commentToCreate.getMetadata().setName(\"fake\");\n        Mono<Comment> commentMono = commentService.create(commentToCreate);\n        when(client.create(any())).thenReturn(Mono.empty());\n        StepVerifier.create(commentMono)\n            .verifyComplete();\n\n        verify(client, times(1)).create(captor.capture());\n        Comment comment = captor.getValue();\n        comment.getSpec().setCreationTime(null);\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"spec\": {\n                        \"raw\": \"fake-raw\",\n                        \"content\": \"fake-content\",\n                        \"owner\": {\n                            \"kind\": \"User\",\n                            \"name\": \"B-owner\",\n                            \"displayName\": \"B-owner-displayName\"\n                        },\n                        \"priority\": 0,\n                        \"top\": false,\n                        \"allowNotification\": true,\n                        \"approved\": false,\n                        \"hidden\": false,\n                        \"subjectRef\": {\n                            \"group\": \"content.halo.run\",\n                            \"version\": \"v1alpha1\",\n                            \"kind\": \"Post\",\n                            \"name\": \"fake-post\"\n                        }\n                    },\n                    \"apiVersion\": \"content.halo.run/v1alpha1\",\n                    \"kind\": \"Comment\",\n                    \"metadata\": {\n                        \"name\": \"fake\"\n                    }\n                }\n                \"\"\",\n            JsonUtils.objectToJson(comment),\n            true);\n    }\n\n    private List<Comment> comments() {\n        Comment a = comment(\"A\");\n        a.getSpec().getOwner().setKind(Comment.CommentOwner.KIND_EMAIL);\n        a.getSpec().getOwner()\n            .setAnnotations(Map.of(Comment.CommentOwner.AVATAR_ANNO, \"avatar\",\n                Comment.CommentOwner.WEBSITE_ANNO, \"website\"));\n        return List.of(a, comment(\"B\"), comment(\"C\"));\n    }\n\n    private Comment comment(String name) {\n        Comment comment = new Comment();\n        comment.setMetadata(new Metadata());\n        comment.getMetadata().setName(name);\n\n        comment.setSpec(new Comment.CommentSpec());\n        Comment.CommentOwner commentOwner = new Comment.CommentOwner();\n        commentOwner.setKind(User.KIND);\n        commentOwner.setDisplayName(\"displayName\");\n        commentOwner.setName(name + \"-owner\");\n        comment.getSpec().setOwner(commentOwner);\n\n        comment.getSpec().setSubjectRef(Ref.of(post()));\n\n        comment.setStatus(new Comment.CommentStatus());\n        return comment;\n    }\n\n    private Post post() {\n        Post post = TestPost.postV1();\n        post.getMetadata().setName(\"fake-post\");\n        return post;\n    }\n\n    private static SystemSetting.Comment getCommentSetting() {\n        SystemSetting.Comment commentSetting = new SystemSetting.Comment();\n        commentSetting.setEnable(true);\n        commentSetting.setSystemUserOnly(true);\n        commentSetting.setRequireReviewForNew(true);\n        return commentSetting;\n    }\n\n    User ghostUser() {\n        User user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(\"ghost\");\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setDisplayName(\"Ghost\");\n        user.getSpec().setEmail(\"\");\n        return user;\n    }\n\n    private String expectListResultJson() {\n        return \"\"\"\n            {\n                \"page\": 1,\n                \"size\": 10,\n                \"total\": 3,\n                \"totalPages\": 1,\n                \"items\": [\n                    {\n                        \"comment\": {\n                            \"spec\": {\n                                \"owner\": {\n                                    \"kind\": \"Email\",\n                                    \"name\": \"A-owner\",\n                                    \"displayName\": \"displayName\",\n                                    \"annotations\": {\n                                        \"website\": \"website\",\n                                        \"avatar\": \"avatar\"\n                                    }\n                                },\n                                \"subjectRef\": {\n                                    \"group\": \"content.halo.run\",\n                                    \"version\": \"v1alpha1\",\n                                    \"kind\": \"Post\",\n                                    \"name\": \"fake-post\"\n                                }\n                            },\n                            \"status\": {},\n                            \"apiVersion\": \"content.halo.run/v1alpha1\",\n                            \"kind\": \"Comment\",\n                            \"metadata\": {\n                                \"name\": \"A\"\n                            }\n                        },\n                        \"owner\": {\n                            \"kind\": \"Email\",\n                            \"name\": \"A-owner\",\n                            \"displayName\": \"displayName\",\n                            \"avatar\": \"avatar\",\n                            \"email\": \"A-owner\"\n                        },\n                        \"subject\": {\n                            \"spec\": {\n                                \"title\": \"post-A\",\n                                \"headSnapshot\": \"base-snapshot\",\n                                \"baseSnapshot\": \"snapshot-A\"\n                            },\n                            \"apiVersion\": \"content.halo.run/v1alpha1\",\n                            \"kind\": \"Post\",\n                            \"metadata\": {\n                                \"name\": \"fake-post\",\n                                \"version\": 1\n                            }\n                        },\n                        \"stats\": {\n                            \"upvote\": 3\n                        }\n                    },\n                    {\n                        \"comment\": {\n                            \"spec\": {\n                                \"owner\": {\n                                    \"kind\": \"User\",\n                                    \"name\": \"B-owner\",\n                                    \"displayName\": \"displayName\"\n                                },\n                                \"subjectRef\": {\n                                    \"group\": \"content.halo.run\",\n                                    \"version\": \"v1alpha1\",\n                                    \"kind\": \"Post\",\n                                    \"name\": \"fake-post\"\n                                }\n                            },\n                            \"status\": {},\n                            \"apiVersion\": \"content.halo.run/v1alpha1\",\n                            \"kind\": \"Comment\",\n                            \"metadata\": {\n                                \"name\": \"B\"\n                            }\n                        },\n                        \"owner\": {\n                            \"kind\": \"User\",\n                            \"name\": \"B-owner\",\n                            \"displayName\": \"B-owner-displayName\",\n                            \"avatar\": \"B-owner-avatar\",\n                            \"email\": \"B-owner-email\"\n                        },\n                        \"subject\": {\n                            \"spec\": {\n                                \"title\": \"post-A\",\n                                \"headSnapshot\": \"base-snapshot\",\n                                \"baseSnapshot\": \"snapshot-A\"\n                            },\n                            \"apiVersion\": \"content.halo.run/v1alpha1\",\n                            \"kind\": \"Post\",\n                            \"metadata\": {\n                                \"name\": \"fake-post\",\n                                \"version\": 1\n                            }\n                        },\n                        \"stats\": {\n                           \"upvote\": 9\n                        }\n                    },\n                    {\n                        \"comment\": {\n                            \"spec\": {\n                                \"owner\": {\n                                    \"kind\": \"User\",\n                                    \"name\": \"C-owner\",\n                                    \"displayName\": \"displayName\"\n                                },\n                                \"subjectRef\": {\n                                    \"group\": \"content.halo.run\",\n                                    \"version\": \"v1alpha1\",\n                                    \"kind\": \"Post\",\n                                    \"name\": \"fake-post\"\n                                }\n                            },\n                            \"status\": {},\n                            \"apiVersion\": \"content.halo.run/v1alpha1\",\n                            \"kind\": \"Comment\",\n                            \"metadata\": {\n                                \"name\": \"C\"\n                            }\n                        },\n                        \"owner\": {\n                            \"kind\": \"User\",\n                            \"name\": \"ghost\",\n                            \"displayName\": \"Ghost\",\n                            \"email\": \"\"\n                        },\n                        \"subject\": {\n                            \"spec\": {\n                                \"title\": \"post-A\",\n                                \"headSnapshot\": \"base-snapshot\",\n                                \"baseSnapshot\": \"snapshot-A\"\n                            },\n                            \"apiVersion\": \"content.halo.run/v1alpha1\",\n                            \"kind\": \"Post\",\n                            \"metadata\": {\n                                \"name\": \"fake-post\",\n                                \"version\": 1\n                            }\n                        },\n                        \"stats\": {\n                            \"upvote\": 0\n                        }\n                    }\n                ],\n                \"first\": true,\n                \"last\": true,\n                \"hasNext\": false,\n                \"hasPrevious\": false\n            }\n            \"\"\";\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/PostCommentSubjectTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\n\n/**\n * Tests for {@link PostCommentSubject}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostCommentSubjectTest {\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private PostCommentSubject postCommentSubject;\n\n    @Test\n    void get() {\n        when(client.fetch(eq(Post.class), any()))\n            .thenReturn(Mono.empty());\n        when(client.fetch(eq(Post.class), eq(\"fake-post\")))\n            .thenReturn(Mono.just(TestPost.postV1()));\n\n        postCommentSubject.get(\"fake-post\")\n            .as(StepVerifier::create)\n            .expectNext(TestPost.postV1())\n            .verifyComplete();\n\n        postCommentSubject.get(\"fake-post2\")\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void supports() {\n        Post post = new Post();\n        post.setMetadata(new Metadata());\n        post.getMetadata().setName(\"test\");\n        boolean supports = postCommentSubject.supports(Ref.of(post));\n        assertThat(supports).isTrue();\n\n        FakeExtension fakeExtension = new FakeExtension();\n        fakeExtension.setMetadata(new Metadata());\n        fakeExtension.getMetadata().setName(\"test\");\n        supports = postCommentSubject.supports(Ref.of(fakeExtension));\n        assertThat(supports).isFalse();\n    }\n\n    @Test\n    void shouldSupportRefWithoutVersion() {\n        var ref = new Ref();\n        ref.setName(\"fake-post\");\n        ref.setGroup(Constant.GROUP);\n        ref.setKind(Post.KIND);\n        assertTrue(postCommentSubject.supports(ref));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/ReplyNotificationSubscriptionHelperTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.content.comment.ReplyNotificationSubscriptionHelper.identityFrom;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.notification.UserIdentity;\n\n/**\n * Tests for {@link ReplyNotificationSubscriptionHelper}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ReplyNotificationSubscriptionHelperTest {\n\n    @Mock\n    NotificationCenter notificationCenter;\n\n    @InjectMocks\n    ReplyNotificationSubscriptionHelper notificationSubscriptionHelper;\n\n    @Test\n    void subscribeNewReplyReasonForCommentTest() {\n        var comment = createComment();\n        var spyNotificationSubscriptionHelper = spy(notificationSubscriptionHelper);\n\n        doNothing().when(spyNotificationSubscriptionHelper).subscribeReply(any(UserIdentity.class));\n\n        spyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment);\n\n        verify(spyNotificationSubscriptionHelper).subscribeReply(\n            eq(ReplyNotificationSubscriptionHelper.identityFrom(\n                comment.getSpec().getOwner()))\n        );\n    }\n\n    @Test\n    void subscribeNewReplyReasonForReplyTest() {\n        var reply = new Reply();\n        reply.setMetadata(new Metadata());\n        reply.getMetadata().setName(\"fake-reply\");\n        reply.setSpec(new Reply.ReplySpec());\n        reply.getSpec().setCommentName(\"fake-comment\");\n        var owner = new Comment.CommentOwner();\n        owner.setKind(User.KIND);\n        owner.setName(\"fake-user\");\n        reply.getSpec().setOwner(owner);\n\n        var spyNotificationSubscriptionHelper = spy(notificationSubscriptionHelper);\n\n        doNothing().when(spyNotificationSubscriptionHelper).subscribeReply(any(UserIdentity.class));\n\n        spyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply);\n\n        verify(spyNotificationSubscriptionHelper).subscribeReply(\n            eq(ReplyNotificationSubscriptionHelper.identityFrom(\n                reply.getSpec().getOwner()))\n        );\n    }\n\n    @Test\n    void subscribeReplyTest() {\n        var comment = createComment();\n        var identity = ReplyNotificationSubscriptionHelper.identityFrom(\n            comment.getSpec().getOwner());\n\n        when(notificationCenter.subscribe(any(), any())).thenReturn(Mono.empty());\n\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(identity.name());\n\n        notificationSubscriptionHelper.subscribeReply(identity);\n\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(NotificationReasonConst.SOMEONE_REPLIED_TO_YOU);\n        interestReason.setExpression(\"props.repliedOwner == '%s'\".formatted(subscriber.getName()));\n        verify(notificationCenter).subscribe(eq(subscriber), eq(interestReason));\n    }\n\n    @Nested\n    class IdentityTest {\n\n        @Test\n        void identityFromTest() {\n            var owner = new Comment.CommentOwner();\n            owner.setKind(User.KIND);\n            owner.setName(\"fake-user\");\n\n            assertThat(identityFrom(owner))\n                .isEqualTo(UserIdentity.of(owner.getName()));\n\n            owner.setKind(Comment.CommentOwner.KIND_EMAIL);\n            owner.setName(\"example@example.com\");\n            assertThat(identityFrom(owner))\n                .isEqualTo(UserIdentity.anonymousWithEmail(owner.getName()));\n        }\n    }\n\n    static Comment createComment() {\n        var comment = new Comment();\n        comment.setMetadata(new Metadata());\n        comment.getMetadata().setName(\"fake-comment\");\n        comment.setSpec(new Comment.CommentSpec());\n        var commentOwner = new Comment.CommentOwner();\n        commentOwner.setKind(Comment.CommentOwner.KIND_EMAIL);\n        commentOwner.setName(\"example@example.com\");\n        comment.getSpec().setOwner(commentOwner);\n        comment.getSpec().setSubjectRef(\n            Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class)));\n        return comment;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/ReplyServiceImplIntegrationTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionStoreUtil;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.store.ReactiveExtensionStoreClient;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Integration tests for {@link ReplyServiceImpl}.\n *\n * @author guqing\n * @since 2.15.0\n */\nclass ReplyServiceImplIntegrationTest {\n\n    @Nested\n    @DirtiesContext\n    @SpringBootTest\n    class ReplyRemoveTest {\n        private final List<Reply> storedReplies = createReplies(320);\n\n        private List<Reply> createReplies(int size) {\n            List<Reply> replies = new ArrayList<>(size);\n            for (int i = 0; i < size; i++) {\n                var reply = JsonUtils.jsonToObject(fakeReplyJson(), Reply.class);\n                reply.getMetadata().setName(\"reply-\" + i);\n                replies.add(reply);\n            }\n            return replies;\n        }\n\n        @Autowired\n        private SchemeManager schemeManager;\n\n        @MockitoSpyBean\n        private ReactiveExtensionClient reactiveClient;\n\n        @Autowired\n        private ReactiveExtensionStoreClient storeClient;\n\n        @MockitoSpyBean\n        private ReplyServiceImpl replyService;\n\n        Mono<Extension> deleteImmediately(Extension extension) {\n            var name = extension.getMetadata().getName();\n            var scheme = schemeManager.get(extension.getClass());\n\n            // delete from db\n            var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);\n            return storeClient.delete(storeName, extension.getMetadata().getVersion())\n                .thenReturn(extension);\n        }\n\n        @BeforeEach\n        void setUp() {\n            Flux.fromIterable(storedReplies)\n                .flatMap(post -> reactiveClient.create(post))\n                .as(StepVerifier::create)\n                .expectNextCount(storedReplies.size())\n                .verifyComplete();\n        }\n\n        @AfterEach\n        void tearDown() {\n            Flux.fromIterable(storedReplies)\n                .flatMap(this::deleteImmediately)\n                .as(StepVerifier::create)\n                .expectNextCount(storedReplies.size())\n                .verifyComplete();\n        }\n\n        @Test\n        void removeAllByComment() {\n            String commentName = \"fake-comment\";\n            replyService.removeAllByComment(commentName)\n                .as(StepVerifier::create)\n                .verifyComplete();\n\n            verify(reactiveClient, times(storedReplies.size())).delete(any(Reply.class));\n            verify(replyService, times(2)).listRepliesByComment(eq(commentName), any());\n\n            replyService.listRepliesByComment(commentName, PageRequestImpl.ofSize(1))\n                .as(StepVerifier::create)\n                .consumeNextWith(result -> assertThat(result.getTotal()).isEqualTo(0))\n                .verifyComplete();\n        }\n    }\n\n    String fakeReplyJson() {\n        return \"\"\"\n                {\n                    \"metadata\":{\n                        \"name\":\"fake-reply\"\n                    },\n                    \"spec\":{\n                        \"raw\":\"fake-raw\",\n                        \"content\":\"fake-content\",\n                        \"owner\":{\n                            \"kind\":\"User\",\n                            \"name\":\"fake-user\",\n                            \"displayName\":\"fake-display-name\"\n                        },\n                        \"creationTime\": \"2024-03-11T06:23:42.923294424Z\",\n                        \"ipAddress\":\"\",\n                        \"approved\": true,\n                        \"hidden\": false,\n                        \"allowNotification\": false,\n                        \"top\": false,\n                        \"priority\": 0,\n                        \"commentName\":\"fake-comment\"\n                    },\n                    \"owner\":{\n                        \"kind\":\"User\",\n                        \"displayName\":\"fake-display-name\"\n                    },\n                    \"stats\":{\n                        \"upvote\":0\n                    }\n                }\n            \"\"\";\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/comment/SinglePageCommentSubjectTest.java",
    "content": "package run.halo.app.content.comment;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\n\n/**\n * Tests for {@link SinglePageCommentSubject}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SinglePageCommentSubjectTest {\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private SinglePageCommentSubject singlePageCommentSubject;\n\n    @Test\n    void get() {\n        when(client.fetch(eq(SinglePage.class), any()))\n            .thenReturn(Mono.empty());\n\n        SinglePage singlePage = new SinglePage();\n        singlePage.setMetadata(new Metadata());\n        singlePage.getMetadata().setName(\"fake-single-page\");\n\n        when(client.fetch(eq(SinglePage.class), eq(\"fake-single-page\")))\n            .thenReturn(Mono.just(singlePage));\n\n        singlePageCommentSubject.get(\"fake-single-page\")\n            .as(StepVerifier::create)\n            .expectNext(singlePage)\n            .verifyComplete();\n\n        singlePageCommentSubject.get(\"fake-single-page-2\")\n            .as(StepVerifier::create)\n            .verifyComplete();\n\n        verify(client, times(1)).fetch(eq(SinglePage.class), eq(\"fake-single-page\"));\n    }\n\n    @Test\n    void supports() {\n        SinglePage singlePage = new SinglePage();\n        singlePage.setMetadata(new Metadata());\n        singlePage.getMetadata().setName(\"test\");\n        boolean supports = singlePageCommentSubject.supports(Ref.of(singlePage));\n        assertThat(supports).isTrue();\n\n        FakeExtension fakeExtension = new FakeExtension();\n        fakeExtension.setMetadata(new Metadata());\n        fakeExtension.getMetadata().setName(\"test\");\n        supports = singlePageCommentSubject.supports(Ref.of(fakeExtension));\n        assertThat(supports).isFalse();\n    }\n\n\n    @Test\n    void shouldSupportRefWithoutVersion() {\n        var ref = new Ref();\n        ref.setName(\"fake-post\");\n        ref.setGroup(Constant.GROUP);\n        ref.setKind(SinglePage.KIND);\n        assertTrue(singlePageCommentSubject.supports(ref));\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/permalinks/CategoryPermalinkPolicyTest.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\n\n/**\n * Tests for {@link CategoryPermalinkPolicy}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CategoryPermalinkPolicyTest {\n\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    private CategoryPermalinkPolicy categoryPermalinkPolicy;\n\n    @BeforeEach\n    void setUp() {\n        categoryPermalinkPolicy =\n            new CategoryPermalinkPolicy(externalUrlSupplier, environmentFetcher);\n    }\n\n    @Test\n    void permalink() {\n        Category category = new Category();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"category-test\");\n        category.setMetadata(metadata);\n        Category.CategorySpec categorySpec = new Category.CategorySpec();\n        categorySpec.setSlug(\"slug-test\");\n        category.setSpec(categorySpec);\n\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n        String permalink = categoryPermalinkPolicy.permalink(category);\n        assertThat(permalink).isEqualTo(\"/categories/slug-test\");\n\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"http://exmaple.com\"));\n        permalink = categoryPermalinkPolicy.permalink(category);\n        assertThat(permalink).isEqualTo(\"http://exmaple.com/categories/slug-test\");\n        String path = URI.create(permalink).getPath();\n        assertThat(path).isEqualTo(\"/categories/slug-test\");\n\n        category.getSpec().setSlug(\"中文 slug\");\n        permalink = categoryPermalinkPolicy.permalink(category);\n        assertThat(permalink).isEqualTo(\"http://exmaple.com/categories/%E4%B8%AD%E6%96%87%20slug\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/permalinks/PostPermalinkPolicyTest.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport java.text.DecimalFormat;\nimport java.text.NumberFormat;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationContext;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.content.PostService;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.utils.PathUtils;\n\n/**\n * Tests for {@link PostPermalinkPolicy}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostPermalinkPolicyTest {\n    private static final NumberFormat NUMBER_FORMAT = new DecimalFormat(\"00\");\n\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @Mock\n    private PostService postService;\n\n    private PostPermalinkPolicy postPermalinkPolicy;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n        lenient().when(postService.listCategories(any())).thenReturn(Flux.empty());\n        postPermalinkPolicy =\n            new PostPermalinkPolicy(environmentFetcher, externalUrlSupplier, postService);\n    }\n\n    @Test\n    void permalink() {\n        Post post = TestPost.postV1();\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(post);\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, \"/{year}/{month}/{day}/{slug}\");\n        post.getMetadata().setName(\"test-post\");\n        post.getSpec().setSlug(\"test-post-slug\");\n        Instant now = Instant.now();\n        post.getSpec().setPublishTime(now);\n\n        ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault());\n        String year = String.valueOf(zonedDateTime.getYear());\n        String month = NUMBER_FORMAT.format(zonedDateTime.getMonthValue());\n        String day = NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth());\n\n        String permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink)\n            .isEqualTo(PathUtils.combinePath(year, month, day, post.getSpec().getSlug()));\n\n        // pattern {month}/{day}/{slug}\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, \"/{month}/{day}/{slug}\");\n        permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink)\n            .isEqualTo(PathUtils.combinePath(month, day, post.getSpec().getSlug()));\n\n        // pattern /?p={name}\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, \"/?p={name}\");\n        permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink).isEqualTo(\"/?p=test-post\");\n\n        // pattern /posts/{slug}\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, \"/posts/{slug}\");\n        permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink).isEqualTo(\"/posts/test-post-slug\");\n\n        // pattern /posts/{name}\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, \"/posts/{name}\");\n        permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink).isEqualTo(\"/posts/test-post\");\n    }\n\n    @Test\n    void permalinkForCategory() {\n        Post post = TestPost.postV1();\n        post.getSpec().setCategories(List.of(\"test-category\"));\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(post);\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, \"/{categorySlug}/{slug}\");\n        post.getMetadata().setName(\"test-post\");\n        post.getSpec().setSlug(\"test-post-slug\");\n        Instant now = Instant.now();\n        post.getSpec().setPublishTime(now);\n\n        var category = createCategory(\"test-category\", \"test-category-slug\");\n        when(postService.listCategories(post.getSpec().getCategories()))\n            .thenReturn(Flux.just(category));\n        var permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink).isEqualTo(\"/test-category-slug/test-post-slug\");\n    }\n\n    @Test\n    void permalinkWithExternalUrl() {\n        Post post = TestPost.postV1();\n        Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(post);\n        annotations.put(Constant.PERMALINK_PATTERN_ANNO, \"/{year}/{month}/{day}/{slug}\");\n        post.getMetadata().setName(\"test-post\");\n        post.getSpec().setSlug(\"test-post-slug\");\n        Instant now = Instant.parse(\"2022-11-01T02:40:06.806310Z\");\n        post.getSpec().setPublishTime(now);\n\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"http://example.com\"));\n\n        String permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink).isEqualTo(\"http://example.com/2022/11/01/test-post-slug\");\n\n        post.getSpec().setSlug(\"中文 slug\");\n        permalink = postPermalinkPolicy.permalink(post);\n        assertThat(permalink).isEqualTo(\"http://example.com/2022/11/01/%E4%B8%AD%E6%96%87%20slug\");\n    }\n\n    private Category createCategory(String name, String slug) {\n        Category category = new Category();\n        Metadata metadata = new Metadata();\n        metadata.setName(name);\n        category.setMetadata(metadata);\n        category.setSpec(new Category.CategorySpec());\n        category.setStatus(new Category.CategoryStatus());\n\n        category.getSpec().setDisplayName(\"display-name\");\n        category.getSpec().setSlug(slug);\n        category.getSpec().setPriority(0);\n        return category;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/content/permalinks/TagPermalinkPolicyTest.java",
    "content": "package run.halo.app.content.permalinks;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationContext;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\n\n/**\n * Tests for {@link TagPermalinkPolicy}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass TagPermalinkPolicyTest {\n\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    private TagPermalinkPolicy tagPermalinkPolicy;\n\n    @BeforeEach\n    void setUp() {\n        tagPermalinkPolicy = new TagPermalinkPolicy(externalUrlSupplier, environmentFetcher);\n    }\n\n    @Test\n    void permalink() {\n        Tag tag = new Tag();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"test-tag\");\n        tag.setMetadata(metadata);\n        Tag.TagSpec tagSpec = new Tag.TagSpec();\n        tagSpec.setSlug(\"test-slug\");\n        tag.setSpec(tagSpec);\n\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n\n        String permalink = tagPermalinkPolicy.permalink(tag);\n        assertThat(permalink).isEqualTo(\"/tags/test-slug\");\n\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"http://example.com\"));\n\n        permalink = tagPermalinkPolicy.permalink(tag);\n        assertThat(permalink).isEqualTo(\"http://example.com/tags/test-slug\");\n\n        tag.getSpec().setSlug(\"中文slug\");\n        permalink = tagPermalinkPolicy.permalink(tag);\n        assertThat(permalink).isEqualTo(\"http://example.com/tags/%E4%B8%AD%E6%96%87slug\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/PolicyConfigChangeDetectorTest.java",
    "content": "package run.halo.app.core.attachment;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\n\n/**\n * Tests for {@link PolicyConfigChangeDetector}.\n *\n * @author guqing\n * @since 2.20.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PolicyConfigChangeDetectorTest {\n\n    @Mock\n    private PolicyConfigChangeDetector.AttachmentUpdateTrigger updateTrigger;\n\n    @Mock\n    private ExtensionClient client;\n\n    @InjectMocks\n    private PolicyConfigChangeDetector policyConfigChangeDetector;\n\n    @Test\n    void reconcileTest() {\n        final var spyDetector = spy(policyConfigChangeDetector);\n\n        var configMap = new ConfigMap();\n        configMap.setMetadata(new Metadata());\n        configMap.getMetadata().setLabels(Map.of(Policy.POLICY_OWNER_LABEL, \"fake-policy\"));\n\n        when(client.fetch(eq(ConfigMap.class), eq(\"fake-config\")))\n            .thenReturn(Optional.of(configMap));\n        when(client.listAllNames(same(Attachment.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(List.of(\"fake-attachment\"));\n\n        spyDetector.reconcile(new Reconciler.Request(\"fake-config\"));\n\n        verify(updateTrigger).addAll(List.of(\"fake-attachment\"));\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/endpoint/LocalAttachmentUploadHandlerTest.java",
    "content": "package run.halo.app.core.attachment.endpoint;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.params.provider.Arguments.arguments;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Path;\nimport java.time.Clock;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DefaultDataBufferFactory;\nimport org.springframework.http.MediaType;\nimport reactor.core.publisher.Flux;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.core.attachment.thumbnail.LocalThumbnailService;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Constant;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;\nimport run.halo.app.core.extension.attachment.endpoint.UploadOption;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.ExternalUrlSupplier;\n\n@ExtendWith(MockitoExtension.class)\nclass LocalAttachmentUploadHandlerTest {\n\n    @InjectMocks\n    LocalAttachmentUploadHandler uploadHandler;\n\n    @Mock\n    AttachmentRootGetter attachmentRootGetter;\n\n    @Mock\n    ExternalUrlSupplier externalUrlSupplier;\n\n    @Mock\n    LocalThumbnailService localThumbnailService;\n\n    @TempDir\n    Path attachmentRoot;\n\n    static Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());\n\n    @BeforeEach\n    void setUp() {\n        uploadHandler.setClock(clock);\n        lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(\"/\"));\n    }\n\n    public static Stream<Arguments> testUploadWithRenameStrategy() {\n        return Stream.of(arguments(\n                \"Random file name with length 10\",\n                \"\"\"\n                    {\n                      \"alwaysRenameFilename\": true,\n                      \"renameStrategy\": {\n                        \"method\": \"RANDOM\",\n                        \"randomLength\": 10\n                      }\n                    }\n                    \"\"\",\n                (Consumer<Attachment>) attachment -> {\n                    var displayName = attachment.getSpec().getDisplayName();\n                    assertTrue(displayName.startsWith(\"halo-\"));\n                    assertTrue(displayName.endsWith(\".png\"));\n                    // halo-xxxxxx.png\n                    assertEquals(4 + 10 + 5, displayName.length());\n                    // fake-content\n                    assertEquals(12L, attachment.getSpec().getSize());\n                }),\n            arguments(\n                \"Random file name with length 10 but without original filename\",\n                \"\"\"\n                    {\n                      \"alwaysRenameFilename\": true,\n                      \"renameStrategy\": {\n                        \"method\": \"RANDOM\",\n                        \"randomLength\": 10,\n                        \"excludeOriginalFilename\": true\n                      }\n                    }\n                    \"\"\",\n                (Consumer<Attachment>) attachment -> {\n                    var displayName = attachment.getSpec().getDisplayName();\n                    assertFalse(displayName.startsWith(\"halo-\"));\n                    assertTrue(displayName.endsWith(\".png\"));\n                    // halo-xxxxxx.png\n                    assertEquals(10 + 4, displayName.length());\n                    // fake-content\n                    assertEquals(12L, attachment.getSpec().getSize());\n                }),\n            arguments(\n                \"Rename filename with UUID but exclude original filename\",\n                \"\"\"\n                    {\n                      \"alwaysRenameFilename\": true,\n                      \"renameStrategy\": {\n                        \"method\": \"UUID\",\n                        \"excludeOriginalFilename\": true\n                      }\n                    }\n                    \"\"\",\n                (Consumer<Attachment>) attachment -> {\n                    var displayName = attachment.getSpec().getDisplayName();\n                    assertFalse(displayName.startsWith(\"halo-\"));\n                    assertTrue(displayName.endsWith(\".png\"));\n                    // xxxxxx.png\n                    assertEquals(36 + 4, displayName.length());\n                    // fake-content\n                    assertEquals(12L, attachment.getSpec().getSize());\n                }\n            ),\n            arguments(\n                \"Rename filename with UUID\",\n                \"\"\"\n                    {\n                      \"alwaysRenameFilename\": true,\n                      \"renameStrategy\": {\n                        \"method\": \"UUID\",\n                        \"excludeOriginalFilename\": false\n                      }\n                    }\n                    \"\"\",\n                (Consumer<Attachment>) attachment -> {\n                    var displayName = attachment.getSpec().getDisplayName();\n                    assertTrue(displayName.startsWith(\"halo-\"));\n                    assertTrue(displayName.endsWith(\".png\"));\n                    // xxxxxx.png\n                    assertEquals(5 + 36 + 4, displayName.length());\n                    // fake-content\n                    assertEquals(12L, attachment.getSpec().getSize());\n                }\n            ),\n            arguments(\n                \"Rename filename with timestamp but without original filename\",\n                \"\"\"\n                    {\n                        \"alwaysRenameFilename\": true,\n                        \"renameStrategy\": {\n                            \"method\": \"TIMESTAMP\",\n                            \"excludeOriginalFilename\": true\n                        }\n                    }\n                    \"\"\",\n                (Consumer<Attachment>) attachment -> {\n                    var expect = clock.instant().toEpochMilli() + \".png\";\n                    assertEquals(expect, attachment.getSpec().getDisplayName());\n                }\n            ),\n            arguments(\n                \"Rename filename with timestamp\",\n                \"\"\"\n                    {\n                        \"alwaysRenameFilename\": true,\n                        \"renameStrategy\": {\n                            \"method\": \"TIMESTAMP\"\n                        }\n                    }\n                    \"\"\",\n                (Consumer<Attachment>) attachment -> {\n                    var expect = \"halo-\" + clock.instant().toEpochMilli() + \".png\";\n                    assertEquals(expect, attachment.getSpec().getDisplayName());\n                }\n            )\n        );\n    }\n\n    @ParameterizedTest(name = \"{0}\")\n    @MethodSource\n    void testUploadWithRenameStrategy(String name, String config, Consumer<Attachment> assertion) {\n        assertNotNull(uploadHandler);\n        var dataBufferFactory = new DefaultDataBufferFactory();\n        var dataBuffer = dataBufferFactory.allocateBuffer(1024);\n        dataBuffer.write(\"fake content\".getBytes(StandardCharsets.UTF_8));\n        var content = Flux.<DataBuffer>just(dataBuffer);\n\n        var policy = new Policy();\n        var policySpec = new Policy.PolicySpec();\n        policy.setSpec(policySpec);\n        policySpec.setTemplateName(\"local\");\n\n        var configMap = new ConfigMap();\n        configMap.setData(Map.of(\"default\", config));\n\n        var uploadOption =\n            UploadOption.from(\"halo.png\", content, MediaType.IMAGE_PNG, policy, configMap);\n\n        when(attachmentRootGetter.get()).thenReturn(attachmentRoot);\n        uploadHandler.upload(uploadOption)\n            .as(StepVerifier::create)\n            .assertNext(attachment -> {\n                assertion.accept(attachment);\n                assertNotNull(attachment.getStatus().getPermalink());\n                assertNotNull(attachment.getStatus().getThumbnails());\n            })\n            .verifyComplete();\n\n    }\n\n    @Test\n    void shouldGetPermalinkWhenUriContainsIllegalChars() {\n        var attachment = new Attachment();\n        attachment.setMetadata(new Metadata());\n        attachment.getMetadata().setAnnotations(Map.of(\n            Constant.URI_ANNO_KEY, \"/path/with space.png\"\n        ));\n        var permalink = uploadHandler.doGetPermalink(attachment);\n        assertTrue(permalink.isPresent());\n        assertEquals(\"/path/with%20space.png\", permalink.get().toASCIIString());\n    }\n\n    @Test\n    void shouldDeleteWithThumbnails() {\n        var deleteContext = Mockito.mock(AttachmentHandler.DeleteContext.class);\n        when(deleteContext.policy()).thenReturn(createPolicy(\"local\"));\n        var attachment =\n            createAttachment(Map.of(Constant.LOCAL_REL_PATH_ANNO_KEY, \"path/to/file.png\"));\n        when(deleteContext.attachment()).thenReturn(attachment);\n        when(attachmentRootGetter.get()).thenReturn(attachmentRoot);\n        uploadHandler.delete(deleteContext)\n            .as(StepVerifier::create)\n            .expectNext(attachment)\n            .verifyComplete();\n\n        verify(this.localThumbnailService).delete(attachmentRoot\n            .resolve(\"path\")\n            .resolve(\"to\")\n            .resolve(\"file.png\")\n        );\n    }\n\n    Attachment createAttachment(Map<String, String> annotations) {\n        var attachment = new Attachment();\n        attachment.setMetadata(new Metadata());\n        attachment.getMetadata().setName(\"fake-attachment\");\n        attachment.getMetadata().setAnnotations(annotations);\n        return attachment;\n    }\n\n    Policy createPolicy(String templateName) {\n        var policy = new Policy();\n        policy.setMetadata(new Metadata());\n        policy.getMetadata().setName(\"fake-policy\");\n        policy.setSpec(new Policy.PolicySpec());\n        policy.getSpec().setTemplateName(templateName);\n        return policy;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/endpoint/PolicyEndpointTest.java",
    "content": "package run.halo.app.core.attachment.endpoint;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.HashMap;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.transaction.ReactiveTransaction;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\nimport tools.jackson.databind.json.JsonMapper;\n\n@ExtendWith(MockitoExtension.class)\nclass PolicyEndpointTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Spy\n    JsonMapper jsonMapper = JsonMapper.shared();\n\n    @Mock\n    ReactiveTransactionManager txManager;\n\n    @InjectMocks\n    PolicyEndpoint endpoint;\n\n    WebTestClient webClient;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .build();\n    }\n\n    @Test\n    void shouldRespondNotFoundIfPolicyNotFound() {\n        // Implement test logic here\n        var policyScheme = Scheme.buildFromType(Policy.class);\n        when(client.get(Policy.class, \"fake-policy\"))\n            .thenReturn(Mono.error(() -> new ExtensionNotFoundException(\n                policyScheme.groupVersionKind(), \"fake-policy\")\n            ));\n        webClient.get().uri(\"/policies/fake-policy/configs/fake-group\")\n            .exchange()\n            .expectStatus().isNotFound();\n    }\n\n    @Test\n    void shouldRespondNullIfNoConfigFound() {\n        when(client.get(Policy.class, \"fake-policy\")).thenReturn(Mono.fromSupplier(() -> {\n            var policy = new Policy();\n            policy.setSpec(new Policy.PolicySpec());\n            policy.getSpec().setConfigMapName(\"fake-config-map\");\n            return policy;\n        }));\n\n        when(client.fetch(ConfigMap.class, \"fake-config-map\"))\n            .thenReturn(Mono.empty());\n\n        webClient.get().uri(\"/policies/fake-policy/configs/fake-group\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(String.class)\n            .isEqualTo(\"null\");\n    }\n\n    @Test\n    void shouldRespondNullIfGroupNotFound() {\n        when(client.get(Policy.class, \"fake-policy\")).thenReturn(Mono.fromSupplier(() -> {\n            var policy = new Policy();\n            policy.setSpec(new Policy.PolicySpec());\n            policy.getSpec().setConfigMapName(\"fake-config-map\");\n            return policy;\n        }));\n\n        when(client.fetch(ConfigMap.class, \"fake-config-map\"))\n            .thenReturn(Mono.fromSupplier(() -> {\n                var cm = new ConfigMap();\n                cm.setData(new HashMap<>());\n                return cm;\n            }));\n\n        webClient.get().uri(\"/policies/fake-policy/configs/fake-group\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(String.class)\n            .isEqualTo(\"null\");\n    }\n\n    @Test\n    void shouldRespondConfigIfGroupFound() {\n        when(client.get(Policy.class, \"fake-policy\")).thenReturn(Mono.fromSupplier(() -> {\n            var policy = new Policy();\n            policy.setSpec(new Policy.PolicySpec());\n            policy.getSpec().setConfigMapName(\"fake-config-map\");\n            return policy;\n        }));\n\n        when(client.fetch(ConfigMap.class, \"fake-config-map\"))\n            .thenReturn(Mono.fromSupplier(() -> {\n                var cm = new ConfigMap();\n                cm.setData(new HashMap<>());\n                cm.getData().put(\"fake-group\", \"\"\"\n                    {\n                      \"halo\": \"awesome\"\n                    }\"\"\");\n                return cm;\n            }));\n\n        webClient.get().uri(\"/policies/fake-policy/configs/fake-group\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody()\n            .jsonPath(\"$.halo\").isEqualTo(\"awesome\");\n    }\n\n\n    @Test\n    void shouldUpdateConfigIfPresent() {\n        when(client.get(Policy.class, \"fake-policy\")).thenReturn(Mono.fromSupplier(() -> {\n            var policy = new Policy();\n            policy.setSpec(new Policy.PolicySpec());\n            policy.getSpec().setConfigMapName(\"fake-config-map\");\n            return policy;\n        }));\n\n        var cm = new ConfigMap();\n        cm.setMetadata(new Metadata());\n        cm.getMetadata().setName(\"fake-config-map\");\n        cm.getMetadata().setVersion(1L);\n        cm.setData(new HashMap<>());\n        cm.getData().put(\"fake-group\", \"\"\"\n            {\n              \"halo\": \"awesome\"\n            }\"\"\");\n        when(client.fetch(ConfigMap.class, \"fake-config-map\"))\n            .thenReturn(Mono.just(cm));\n\n        var tx = mock(ReactiveTransaction.class);\n        when(txManager.getReactiveTransaction(any())).thenReturn(Mono.just(tx));\n        when(txManager.commit(tx)).thenReturn(Mono.empty());\n\n        when(client.update(cm)).thenReturn(Mono.just(cm));\n\n        var body = \"\"\"\n            {\n              \"halo\": \"nice\",\n              \"key\": \"value\"\n            }\"\"\";\n\n        webClient.put().uri(\"/policies/fake-policy/configs/fake-group\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .bodyValue(body)\n            .exchange()\n            .expectStatus().isNoContent();\n\n        verify(client).<ConfigMap>update(assertArg(gotCm -> {\n            var data = gotCm.getData();\n            JSONAssert.assertEquals(body, data.get(\"fake-group\"), true);\n        }));\n    }\n\n    @Test\n    void shouldCreateConfigIfAbsent() {\n        var policy = new Policy();\n        policy.setSpec(new Policy.PolicySpec());\n        when(client.get(Policy.class, \"fake-policy\")).thenReturn(Mono.just(policy));\n\n\n        var tx = mock(ReactiveTransaction.class);\n        when(txManager.getReactiveTransaction(any())).thenReturn(Mono.just(tx));\n        when(txManager.commit(tx)).thenReturn(Mono.empty());\n\n        var cm = new ConfigMap();\n        cm.setMetadata(new Metadata());\n        cm.getMetadata().setName(\"fake-config-map\");\n        cm.getMetadata().setVersion(1L);\n        cm.setData(new HashMap<>());\n        cm.getData().put(\"fake-group\", \"\"\"\n            {\n              \"halo\": \"nice\",\n              \"key\": \"value\"\n            }\\\n            \"\"\");\n        when(client.create(any(ConfigMap.class))).thenReturn(Mono.just(cm));\n        when(client.update(policy)).thenReturn(Mono.just(policy));\n\n        var body = \"\"\"\n            {\n              \"halo\": \"nice\",\n              \"key\": \"value\"\n            }\"\"\";\n\n        webClient.put().uri(\"/policies/fake-policy/configs/fake-group\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .bodyValue(body)\n            .exchange()\n            .expectStatus().isNoContent();\n\n        verify(client).<ConfigMap>create(assertArg(gotCm -> {\n            var data = gotCm.getData();\n            JSONAssert.assertEquals(body, data.get(\"fake-group\"), true);\n        }));\n\n        verify(client).<Policy>update(assertArg(\n            gotPolicy -> assertEquals(\"fake-config-map\", gotPolicy.getSpec().getConfigMapName())\n        ));\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/impl/AttachmentRootGetterImplTest.java",
    "content": "package run.halo.app.core.attachment.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.infra.properties.HaloProperties;\n\n/**\n * Tests for {@link AttachmentRootGetterImpl}.\n *\n * @author guqing\n * @since 2.19.0\n */\n@ExtendWith(MockitoExtension.class)\nclass AttachmentRootGetterImplTest {\n    @Mock\n    private HaloProperties haloProperties;\n\n    @InjectMocks\n    private AttachmentRootGetterImpl localAttachmentDirGetter;\n\n    @Test\n    void get() {\n        var rootPath = Path.of(\"/tmp\");\n        when(haloProperties.getWorkDir()).thenReturn(rootPath);\n        var dir = localAttachmentDirGetter.get();\n        assertThat(dir).isEqualTo(rootPath.resolve(\"attachments\"));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/thumbnail/DefaultLocalThumbnailServiceTest.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.google.common.util.concurrent.MoreExecutors;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.util.ResourceUtils;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.attachment.AttachmentRootGetter;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.infra.properties.AttachmentProperties;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultLocalThumbnailServiceTest {\n\n    @Mock\n    AttachmentRootGetter attachmentRootGetter;\n\n    @Mock\n    HaloProperties haloProperties;\n\n    @Mock\n    AttachmentProperties.ThumbnailProperties thumbnailProperties;\n\n    DefaultLocalThumbnailService generator;\n\n    Path source;\n\n    @TempDir\n    Path attachmentRoot;\n\n    @BeforeEach\n    void setUp() throws IOException {\n        var attachmentProperties = mock(AttachmentProperties.class);\n        when(attachmentProperties.getThumbnail()).thenReturn(thumbnailProperties);\n        when(haloProperties.getAttachment()).thenReturn(attachmentProperties);\n        when(thumbnailProperties.isDisabled()).thenReturn(false);\n        when(thumbnailProperties.getConcurrentThreads()).thenReturn(1);\n        var imagePath =\n            ResourceUtils.getFile(\"classpath:static/images/halo-logo-401x401.png\").toPath();\n        lenient().when(attachmentRootGetter.get()).thenReturn(attachmentRoot);\n        this.source = attachmentRoot.resolve(\"static\").resolve(\"hal-logo-401x401.png\");\n        Files.createDirectories(this.source.getParent());\n        Files.copy(imagePath, this.source);\n\n        this.generator =\n            new DefaultLocalThumbnailService(this.attachmentRootGetter, this.haloProperties);\n        var executorService = MoreExecutors.newDirectExecutorService();\n        this.generator.setExecutorService(executorService);\n    }\n\n    @AfterEach\n    void cleanUp() throws Exception {\n        this.generator.destroy();\n    }\n\n    @Test\n    void shouldGenerateThumbnail() {\n        this.generator.generate(source, ThumbnailSize.S)\n            .as(StepVerifier::create)\n            .assertNext(resource -> {\n                assertTrue(resource.isReadable());\n                assertDoesNotThrow(() -> {\n                    var thumbnailSize = resource.contentLength();\n                    var sourceSize = Files.size(source);\n                    assertTrue(thumbnailSize < sourceSize);\n                });\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReplaceWithSourceIfSizeIsLarger() {\n        this.generator.generate(source, ThumbnailSize.M)\n            .as(StepVerifier::create)\n            .assertNext(resource -> {\n                assertTrue(resource.isReadable());\n                assertDoesNotThrow(() -> {\n                    var thumbnailSize = resource.contentLength();\n                    var sourceSize = Files.size(source);\n                    assertEquals(thumbnailSize, sourceSize);\n                });\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldDisableThumbnailGeneration() {\n        when(thumbnailProperties.isDisabled()).thenReturn(true);\n        this.generator.generate(source, ThumbnailSize.S)\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/thumbnail/DefaultThumbnailServiceTest.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Flux;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ExternalUrlSupplier;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultThumbnailServiceTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    ExternalUrlSupplier externalUrlSupplier;\n\n    @InjectMocks\n    DefaultThumbnailService thumbnailService;\n\n    @Test\n    void shouldGetThumbnailDirectlyIfPermalinkIsRelative() {\n        thumbnailService.get(URI.create(\"/images/fake.png\"), ThumbnailSize.M)\n            .as(StepVerifier::create)\n            .expectNext(URI.create(\"/images/fake.png?width=800\"))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetThumbnailDirectlyIfPermalinkContainsSpecialChars() {\n        thumbnailService.get(URI.create(\"/images/中文.png\"), ThumbnailSize.M)\n            .as(StepVerifier::create)\n            .expectNext(URI.create(\"/images/%E4%B8%AD%E6%96%87.png?width=800\"))\n            .verifyComplete();\n\n        thumbnailService.get(URI.create(\"/images/space%20space.png\"), ThumbnailSize.M)\n            .as(StepVerifier::create)\n            .expectNext(URI.create(\"/images/space%20space.png?width=800\"))\n            .verifyComplete();\n\n        thumbnailService.get(URI.create(\"/images/percent%2f.png\"), ThumbnailSize.M)\n            .as(StepVerifier::create)\n            .expectNext(URI.create(\"/images/percent%2f.png?width=800\"))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetThumbnailDirectlyIfPermalinkIsInSite() throws MalformedURLException {\n        when(externalUrlSupplier.getRaw()).thenReturn(URI.create(\"https://www.halo.run\").toURL());\n        thumbnailService.get(URI.create(\"https://www.halo.run/images/fake.png\"), ThumbnailSize.M)\n            .as(StepVerifier::create)\n            .expectNext(URI.create(\"https://www.halo.run/images/fake.png?width=800\"))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetEmptyThumbnailIfNoAttachmentsFound() throws MalformedURLException {\n        when(externalUrlSupplier.getRaw()).thenReturn(URI.create(\"https://www.halo.run\").toURL());\n        Mockito.when(\n                client.listAll(same(Attachment.class), isA(ListOptions.class), isA(Sort.class))\n            )\n            .thenReturn(Flux.empty());\n        thumbnailService.get(URI.create(\"https://fake.halo.run/fake.png\"))\n            .as(StepVerifier::create)\n            .expectNext(Map.of())\n            .verifyComplete();\n\n        // Only invoke once due to caching\n        verify(client).listAll(same(Attachment.class), isA(ListOptions.class), isA(Sort.class));\n    }\n\n    @Test\n    void shouldGetThumbnailsIfAttachmentsFound() throws MalformedURLException {\n        when(externalUrlSupplier.getRaw()).thenReturn(URI.create(\"https://www.halo.run\").toURL());\n        Mockito.when(\n                client.listAll(same(Attachment.class), isA(ListOptions.class), isA(Sort.class))\n            )\n            .thenReturn(Flux.just(\n                createAttachment(\"fake-png\", \"https://fake.halo.run/fake.png\",\n                    Map.of(\"s\", \"/fake.png?width=400\")),\n                createAttachment(\"fake-png\", \"https://fake.halo.run/fake.png\",\n                    Map.of(\"m\", \"/fake.png?width=800\"))\n            ));\n\n        thumbnailService.get(URI.create(\"https://fake.halo.run/fake.png\"))\n            .as(StepVerifier::create)\n            .expectNext(Map.of(ThumbnailSize.S, URI.create(\"/fake.png?width=400\")))\n            .verifyComplete();\n\n        // Only invoke once due to caching\n        verify(client).listAll(same(Attachment.class), isA(ListOptions.class), isA(Sort.class));\n    }\n\n    Attachment createAttachment(String name, String permalink, Map<String, String> thumbnails) {\n        var attachment = new Attachment();\n        attachment.setMetadata(new Metadata());\n        attachment.getMetadata().setName(name);\n        attachment.setSpec(new Attachment.AttachmentSpec());\n        attachment.setStatus(new Attachment.AttachmentStatus());\n        attachment.getStatus().setPermalink(permalink);\n        attachment.getStatus().setThumbnails(thumbnails);\n        return attachment;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/thumbnail/ThumbnailImgTagPostProcessorTest.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\nimport static org.thymeleaf.templatemode.TemplateMode.HTML;\n\nimport java.net.URI;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.StandardModelFactory;\nimport org.thymeleaf.model.AttributeValueQuotes;\nimport org.thymeleaf.model.IModelFactory;\nimport org.thymeleaf.spring6.SpringTemplateEngine;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.attachment.ThumbnailSize;\n\n@ExtendWith(MockitoExtension.class)\nclass ThumbnailImgTagPostProcessorTest {\n\n    @Mock\n    ThumbnailService thumbnailService;\n\n    @Mock\n    ITemplateContext templateContext;\n\n    @InjectMocks\n    ThumbnailImgTagPostProcessor processor;\n\n    IModelFactory modelFactory;\n\n    @BeforeEach\n    void setUp() {\n        var templateEngine = new SpringTemplateEngine();\n        this.modelFactory = new StandardModelFactory(templateEngine.getConfiguration(), HTML);\n    }\n\n    @Test\n    void shouldReturnEmptyIfImgTagWithoutSrc() {\n        var imgTag = modelFactory.createStandaloneElementTag(\"img\");\n\n        processor.process(templateContext, imgTag)\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnEmptyIfImgTagWithSrcSet() {\n        var imgTag = modelFactory.createStandaloneElementTag(\n            \"img\",\n            Map.of(\"src\", \"/halo.png\",\n                \"srcset\", \"fake-srcset\"),\n            AttributeValueQuotes.DOUBLE,\n            true,\n            true);\n\n        processor.process(templateContext, imgTag)\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnEmptyIfNotImgTag() {\n        var imgTag = modelFactory.createStandaloneElementTag(\"not-a-img\");\n\n        processor.process(templateContext, imgTag)\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnEmptyIfNoThumbnailsFound() {\n        var imgTag = modelFactory.createStandaloneElementTag(\"img\", \"src\", \"/halo.png\");\n\n        when(thumbnailService.get(URI.create(\"/halo.png\")))\n            .thenReturn(Mono.just(Map.of()));\n\n        processor.process(templateContext, imgTag)\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnTagIfImgTagWithSrc() {\n        var imgTag = modelFactory.createStandaloneElementTag(\"img\", \"src\", \"/halo.png\");\n\n        when(templateContext.getModelFactory()).thenReturn(modelFactory);\n        when(thumbnailService.get(URI.create(\"/halo.png\")))\n            .thenReturn(Mono.just(Map.of(ThumbnailSize.S, URI.create(\"/halo.png?width=400\"))));\n\n        processor.process(templateContext, imgTag)\n            .as(StepVerifier::create)\n            .assertNext(tag -> {\n                var srcset = tag.getAttribute(\"srcset\");\n                assertEquals(\"/halo.png?width=400 400w\", srcset.getValue());\n                assertTrue(tag.hasAttribute(\"sizes\"));\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnTagIfImgTagWithSrcAndSizes() {\n        var imgTag = modelFactory.createStandaloneElementTag(\n            \"img\",\n            Map.of(\"src\", \"/halo.png\",\n                \"sizes\", \"fake-sizes\"),\n            AttributeValueQuotes.DOUBLE,\n            true,\n            true);\n\n        when(templateContext.getModelFactory()).thenReturn(modelFactory);\n        when(thumbnailService.get(URI.create(\"/halo.png\")))\n            .thenReturn(Mono.just(Map.of(ThumbnailSize.S, URI.create(\"/halo.png?width=400\"))));\n\n        processor.process(templateContext, imgTag)\n            .as(StepVerifier::create)\n            .assertNext(tag -> {\n                assertEquals(\"/halo.png?width=400 400w\", tag.getAttribute(\"srcset\").getValue());\n                assertEquals(\"fake-sizes\", tag.getAttribute(\"sizes\").getValue());\n            })\n            .verifyComplete();\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/thumbnail/ThumbnailResourceTransformerTest.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.core.io.Resource;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.web.reactive.resource.ResourceTransformerChain;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.attachment.ThumbnailSize;\n\n@ExtendWith(MockitoExtension.class)\nclass ThumbnailResourceTransformerTest {\n\n    @Mock\n    LocalThumbnailService localThumbnailService;\n\n    @Mock\n    ResourceTransformerChain transformerChain;\n\n    @Mock\n    Resource resource;\n\n    @InjectMocks\n    ThumbnailResourceTransformer thumbnailResourceTransformer;\n\n    @Test\n    void shouldNotTransformWithoutWidthQuery() {\n        var exchange = MockServerWebExchange.builder(MockServerHttpRequest.get(\"/halo.png\").build())\n            .build();\n        when(this.transformerChain.transform(exchange, this.resource))\n            .thenReturn(Mono.just(this.resource));\n\n        thumbnailResourceTransformer.transform(exchange, this.resource, this.transformerChain)\n            .as(StepVerifier::create)\n            .expectNext(this.resource)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldNotTransformWithNonFileResource() {\n        var exchange = MockServerWebExchange.builder(\n                MockServerHttpRequest.get(\"/halo.png\").queryParam(\"width\", \"400\").build())\n            .build();\n        when(this.resource.isFile()).thenReturn(false);\n        when(this.transformerChain.transform(exchange, this.resource))\n            .thenReturn(Mono.just(this.resource));\n\n        thumbnailResourceTransformer.transform(exchange, this.resource, this.transformerChain)\n            .as(StepVerifier::create)\n            .expectNext(this.resource)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldNotTransformWithUnsupportedImageType() {\n        var exchange = MockServerWebExchange.builder(\n                MockServerHttpRequest.get(\"/halo.svg\").queryParam(\"width\", \"400\").build())\n            .build();\n        when(this.resource.isFile()).thenReturn(true);\n        when(this.resource.getFilename()).thenReturn(\"halo.svg\");\n        when(this.transformerChain.transform(exchange, this.resource))\n            .thenReturn(Mono.just(this.resource));\n\n        thumbnailResourceTransformer.transform(exchange, this.resource, this.transformerChain)\n            .as(StepVerifier::create)\n            .expectNext(this.resource)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnSourceIfEmptyGeneration() throws IOException {\n        var exchange = MockServerWebExchange.builder(\n                MockServerHttpRequest.get(\"/halo.png\").queryParam(\"width\", \"400\").build())\n            .build();\n        var attachmentRoot = Path.of(\"attachments\").toAbsolutePath();\n        var sourcePath = attachmentRoot.resolve(\"upload\").resolve(\"halo.png\");\n        when(this.resource.isFile()).thenReturn(true);\n        when(this.resource.getFilename()).thenReturn(sourcePath.getFileName().toString());\n        when(this.resource.getFile()).thenReturn(sourcePath.toFile());\n        thumbnailResourceTransformer = spy(thumbnailResourceTransformer);\n\n        when(localThumbnailService.generate(sourcePath, ThumbnailSize.S)).thenReturn(Mono.empty());\n\n        when(this.transformerChain.transform(eq(exchange), isA(Resource.class)))\n            .thenAnswer(invocation -> Mono.just(invocation.getArgument(1)));\n\n        thumbnailResourceTransformer.transform(exchange, this.resource, this.transformerChain)\n            .as(StepVerifier::create)\n            .assertNext(resource -> assertDoesNotThrow(\n                () -> assertEquals(sourcePath.toUri(), resource.getURI())\n            ))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnIfThumbnailExists() throws IOException {\n        var exchange = MockServerWebExchange.builder(\n                MockServerHttpRequest.get(\"/halo.png\").queryParam(\"width\", \"400\").build())\n            .build();\n\n        var attachmentRoot = Path.of(\"attachments\").toAbsolutePath();\n        var sourcePath = attachmentRoot.resolve(\"upload\").resolve(\"halo.png\");\n        when(this.resource.isFile()).thenReturn(true);\n        when(this.resource.getFilename()).thenReturn(sourcePath.getFileName().toString());\n        when(this.resource.getFile()).thenReturn(sourcePath.toFile());\n        thumbnailResourceTransformer = spy(thumbnailResourceTransformer);\n\n        var generatedResource = mock(Resource.class);\n        when(this.localThumbnailService.generate(sourcePath, ThumbnailSize.S))\n            .thenReturn(Mono.just(generatedResource));\n        when(this.transformerChain.transform(exchange, generatedResource))\n            .thenReturn(Mono.just(generatedResource));\n\n        thumbnailResourceTransformer.transform(exchange, this.resource, this.transformerChain)\n            .as(StepVerifier::create)\n            .expectNext(generatedResource)\n            .verifyComplete();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/attachment/thumbnail/ThumbnailUtilsTest.java",
    "content": "package run.halo.app.core.attachment.thumbnail;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.springframework.http.MediaType;\nimport run.halo.app.core.attachment.ThumbnailSize;\n\nclass ThumbnailUtilsTest {\n\n    @ParameterizedTest\n    @ValueSource(strings = {\n        \"image/jpg\", \"image/jpeg\", \"image/png\", \"image/bmp\", \"image/vnd.wap.wbmp\",\n    })\n    void shouldBeSupportedImageType(String mimeType) {\n        Assertions.assertTrue(ThumbnailUtils.isSupportedImage(MediaType.parseMediaType(mimeType)));\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\n        \"image/svg+xml\", \"image/gif\", \"image/webp\", \"image/x-icon\", \"image/avif\", \"image/tiff\",\n        \"application/json\", \"text/plain\", \"application/octet-stream\"\n    })\n    void shouldNotBeSupportedImageType(String mimeType) {\n        assertFalse(ThumbnailUtils.isSupportedImage(MediaType.parseMediaType(mimeType)));\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\n        \"jpg\", \"jpeg\", \"png\", \"bmp\", \"wbmp\", \"JPG\", \"JPEG\", \"PNG\", \"BMP\", \"WBMP\"\n    })\n    void shouldBeSupportedImageSuffix(String suffix) {\n        assertTrue(ThumbnailUtils.isSupportedImage(suffix));\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\n        \"svg\", \"avif\", \"gif\", \"webp\", \"x-icon\", \"tiff\", \"json\", \"txt\", \"\", \" \", \"  \", \".jpg\"\n    })\n    void shouldNotBeSupportedImageSuffix(String suffix) {\n        assertFalse(ThumbnailUtils.isSupportedImage(suffix));\n    }\n\n    @Test\n    void shouldBuildSrcSetWithUriWithSpecialCharacters() {\n        var permalink = URI.create(\"/中文.png\").toASCIIString();\n        var srcsetMap = ThumbnailUtils.buildSrcsetMap(URI.create(permalink));\n        assertEquals(\"/%E4%B8%AD%E6%96%87.png?width=400\",\n            srcsetMap.get(ThumbnailSize.S).toString());\n        assertEquals(\"/%E4%B8%AD%E6%96%87.png?width=800\",\n            srcsetMap.get(ThumbnailSize.M).toString());\n        assertEquals(\"/%E4%B8%AD%E6%96%87.png?width=1200\",\n            srcsetMap.get(ThumbnailSize.L).toString());\n        assertEquals(\"/%E4%B8%AD%E6%96%87.png?width=1600\",\n            srcsetMap.get(ThumbnailSize.XL).toString());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/counter/MeterUtilsTest.java",
    "content": "package run.halo.app.core.counter;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.Meter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport io.micrometer.core.instrument.search.RequiredSearch;\nimport io.micrometer.core.instrument.simple.SimpleMeterRegistry;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.core.extension.content.Post;\n\n/**\n * Tests for {@link MeterUtils}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass MeterUtilsTest {\n\n    @Test\n    void nameOf() {\n        String s = MeterUtils.nameOf(Post.class, \"fake-post\");\n        assertThat(s).isEqualTo(\"posts.content.halo.run/fake-post\");\n    }\n\n    @Test\n    void testNameOf() {\n        String s = MeterUtils.nameOf(\"content.halo.run\", \"posts\", \"fake-post\");\n        assertThat(s).isEqualTo(\"posts.content.halo.run/fake-post\");\n    }\n\n    @Test\n    void visitCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        MeterUtils.visitCounter(meterRegistry, \"posts.content.halo.run/fake-post\")\n            .increment();\n        RequiredSearch requiredSearch = meterRegistry.get(\"posts.content.halo.run/fake-post\");\n        assertThat(requiredSearch.counter().count()).isEqualTo(1);\n        Meter.Id id = requiredSearch.counter().getId();\n        assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.VISIT_SCENE);\n        assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))\n            .isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());\n    }\n\n    @Test\n    void upvoteCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        MeterUtils.upvoteCounter(meterRegistry, \"posts.content.halo.run/fake-post\")\n            .increment(2);\n        RequiredSearch requiredSearch = meterRegistry.get(\"posts.content.halo.run/fake-post\");\n        assertThat(requiredSearch.counter().count()).isEqualTo(2);\n        Meter.Id id = requiredSearch.counter().getId();\n        assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.UPVOTE_SCENE);\n        assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))\n            .isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());\n    }\n\n    @Test\n    void totalCommentCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        MeterUtils.totalCommentCounter(meterRegistry, \"content.halo.run.posts.fake-post\")\n            .increment(3);\n        RequiredSearch requiredSearch = meterRegistry.get(\"content.halo.run.posts.fake-post\");\n        assertThat(requiredSearch.counter().count()).isEqualTo(3);\n        Meter.Id id = requiredSearch.counter().getId();\n        assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.TOTAL_COMMENT_SCENE);\n        assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))\n            .isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());\n    }\n\n    @Test\n    void approvedCommentCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        MeterUtils.approvedCommentCounter(meterRegistry, \"posts.content.halo.run/fake-post\")\n            .increment(2);\n        RequiredSearch requiredSearch = meterRegistry.get(\"posts.content.halo.run/fake-post\");\n        assertThat(requiredSearch.counter().count()).isEqualTo(2);\n        Meter.Id id = requiredSearch.counter().getId();\n        assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.APPROVED_COMMENT_SCENE);\n        assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))\n            .isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());\n    }\n\n    @Test\n    void isVisitCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        Counter visitCounter =\n            MeterUtils.visitCounter(meterRegistry, \"posts.content.halo.run/fake-post\");\n        assertThat(MeterUtils.isVisitCounter(visitCounter)).isTrue();\n    }\n\n    @Test\n    void isUpvoteCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        Counter upvoteCounter =\n            MeterUtils.upvoteCounter(meterRegistry, \"posts.content.halo.run/fake-post\");\n        assertThat(MeterUtils.isUpvoteCounter(upvoteCounter)).isTrue();\n        assertThat(MeterUtils.isVisitCounter(upvoteCounter)).isFalse();\n    }\n\n    @Test\n    void isDownvoteCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        Counter downvoteCounter =\n            MeterUtils.downvoteCounter(meterRegistry, \"posts.content.halo.run/fake-post\");\n        assertThat(MeterUtils.isDownvoteCounter(downvoteCounter)).isTrue();\n        assertThat(MeterUtils.isVisitCounter(downvoteCounter)).isFalse();\n    }\n\n    @Test\n    void isTotalCommentCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        Counter totalCommentCounter =\n            MeterUtils.totalCommentCounter(meterRegistry, \"posts.content.halo.run/fake-post\");\n        assertThat(MeterUtils.isTotalCommentCounter(totalCommentCounter)).isTrue();\n        assertThat(MeterUtils.isVisitCounter(totalCommentCounter)).isFalse();\n    }\n\n    @Test\n    void isApprovedCommentCounter() {\n        MeterRegistry meterRegistry = new SimpleMeterRegistry();\n        Counter approvedCommentCounter =\n            MeterUtils.approvedCommentCounter(meterRegistry, \"posts.content.halo.run/fake-post\");\n        assertThat(MeterUtils.isApprovedCommentCounter(approvedCommentCounter)).isTrue();\n        assertThat(MeterUtils.isVisitCounter(approvedCommentCounter)).isFalse();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/WebSocketHandlerMappingTest.java",
    "content": "package run.halo.app.core.endpoint;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.web.reactive.socket.WebSocketHandler;\nimport org.springframework.web.reactive.socket.WebSocketSession;\nimport run.halo.app.extension.GroupVersion;\n\n@ExtendWith(MockitoExtension.class)\nclass WebSocketHandlerMappingTest {\n\n    @InjectMocks\n    WebSocketHandlerMapping handlerMapping;\n\n    @Test\n    void shouldRegisterEndpoint() {\n        var endpoint = new FakeWebSocketEndpoint();\n        handlerMapping.register(List.of(endpoint));\n        assertTrue(handlerMapping.getEndpointMap().containsValue(endpoint));\n    }\n\n    @Test\n    void shouldUnregisterEndpoint() {\n        var endpoint = new FakeWebSocketEndpoint();\n        handlerMapping.register(List.of(endpoint));\n        assertTrue(handlerMapping.getEndpointMap().containsValue(endpoint));\n        handlerMapping.unregister(List.of(endpoint));\n        assertFalse(handlerMapping.getEndpointMap().containsValue(endpoint));\n    }\n\n    static class FakeWebSocketEndpoint implements WebSocketEndpoint {\n\n        @Override\n        public String urlPath() {\n            return \"/resources\";\n        }\n\n        @Override\n        public GroupVersion groupVersion() {\n            return GroupVersion.parseAPIVersion(\"fake.halo.run/v1alpha1\");\n        }\n\n        @Override\n        public WebSocketHandler handler() {\n            return WebSocketSession::close;\n        }\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/console/EmailVerificationCodeTest.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;\n\nimport io.github.resilience4j.ratelimiter.RateLimiterConfig;\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport java.time.Duration;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.validation.Validator;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.user.service.EmailVerificationService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Tests for a part of {@link UserEndpoint} about sending email verification code.\n *\n * @author guqing\n * @see UserEndpoint\n * @see EmailVerificationService\n * @since 2.11.0\n */\n@ExtendWith({SpringExtension.class, MockitoExtension.class})\n@WithMockUser(username = \"fake-user\", password = \"fake-password\")\nclass EmailVerificationCodeTest {\n\n    WebTestClient webClient;\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    EmailVerificationService emailVerificationService;\n\n    @Mock\n    UserService userService;\n\n    @Mock\n    RateLimiterRegistry rateLimiterRegistry;\n\n    @Mock\n    Validator validator;\n\n    @InjectMocks\n    UserEndpoint endpoint;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .apply(springSecurity())\n            .build();\n    }\n\n    @Test\n    void sendEmailVerificationCode() {\n        var config = RateLimiterConfig.custom()\n            .limitRefreshPeriod(Duration.ofSeconds(10))\n            .limitForPeriod(1)\n            .build();\n        var sendCodeRateLimiter = RateLimiterRegistry.of(config)\n            .rateLimiter(\"send-email-verification-code-fake-user:hi@halo.run\");\n        when(rateLimiterRegistry.rateLimiter(\n            \"send-email-verification-code-fake-user:hi@halo.run\",\n            \"send-email-verification-code\")\n        ).thenReturn(sendCodeRateLimiter);\n\n        var user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(\"fake-user\");\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setEmail(\"hi@halo.run\");\n        when(emailVerificationService.sendVerificationCode(anyString(), anyString()))\n            .thenReturn(Mono.empty());\n        webClient.post()\n            .uri(\"/users/-/send-email-verification-code\")\n            .bodyValue(Map.of(\"email\", \"hi@halo.run\"))\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n        // request again to trigger rate limit\n        webClient.post()\n            .uri(\"/users/-/send-email-verification-code\")\n            .bodyValue(Map.of(\"email\", \"hi@halo.run\"))\n            .exchange()\n            .expectStatus()\n            .isEqualTo(HttpStatus.TOO_MANY_REQUESTS);\n    }\n\n    @Test\n    void verifyEmail() {\n        var config = RateLimiterConfig.custom()\n            .limitRefreshPeriod(Duration.ofSeconds(10))\n            .limitForPeriod(1)\n            .build();\n\n        var verifyEmailRateLimiter = RateLimiterRegistry.of(config)\n            .rateLimiter(\"verify-email-fake-user\");\n        when(rateLimiterRegistry.rateLimiter(\"verify-email-fake-user\", \"verify-email\"))\n            .thenReturn(verifyEmailRateLimiter);\n\n        when(emailVerificationService.verify(anyString(), anyString()))\n            .thenReturn(Mono.empty());\n        when(userService.confirmPassword(anyString(), anyString()))\n            .thenReturn(Mono.just(true));\n        webClient.post()\n            .uri(\"/users/-/verify-email\")\n            .bodyValue(Map.of(\"code\", \"fake-code-1\", \"password\", \"123456\"))\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n        // request again to trigger rate limit\n        webClient.post()\n            .uri(\"/users/-/verify-email\")\n            .bodyValue(Map.of(\"code\", \"fake-code-2\", \"password\", \"123456\"))\n            .exchange()\n            .expectStatus()\n            .isEqualTo(HttpStatus.TOO_MANY_REQUESTS);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/console/PluginEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;\nimport static org.springframework.web.reactive.function.BodyInserters.fromMultipartData;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.core.io.ByteArrayResource;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.http.CacheControl;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.MultipartBodyBuilder;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.user.service.SettingConfigService;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.plugin.PluginService;\n\n@Slf4j\n@ExtendWith(MockitoExtension.class)\nclass PluginEndpointTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    SystemVersionSupplier systemVersionSupplier;\n\n    @Mock\n    PluginService pluginService;\n\n    @Mock\n    SettingConfigService settingConfigService;\n\n    @Spy\n    WebProperties webProperties = new WebProperties();\n\n    @InjectMocks\n    PluginEndpoint endpoint;\n\n    @Nested\n    class PluginListTest {\n\n        @Test\n        void shouldListEmptyPluginsWhenNoPlugins() {\n            when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))\n                .thenReturn(Mono.just(ListResult.emptyResult()));\n\n            bindToRouterFunction(endpoint.endpoint())\n                .build()\n                .get().uri(\"/plugins\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(0)\n                .jsonPath(\"$.total\").isEqualTo(0);\n        }\n\n        @Test\n        void shouldListPluginsWhenPluginPresent() {\n            var plugins = List.of(\n                createPlugin(\"fake-plugin-1\"),\n                createPlugin(\"fake-plugin-2\"),\n                createPlugin(\"fake-plugin-3\")\n            );\n            var expectResult = new ListResult<>(plugins);\n            when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n\n            bindToRouterFunction(endpoint.endpoint())\n                .build()\n                .get().uri(\"/plugins\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(3)\n                .jsonPath(\"$.total\").isEqualTo(3);\n        }\n\n        @Test\n        void shouldFilterPluginsWhenKeywordProvided() {\n            var expectPlugin =\n                createPlugin(\"fake-plugin-2\", \"expected display name\", \"\", false);\n            var unexpectedPlugin1 =\n                createPlugin(\"fake-plugin-1\", \"first fake display name\", \"\", false);\n            var unexpectedPlugin2 =\n                createPlugin(\"fake-plugin-3\", \"second fake display name\", \"\", false);\n            var plugins = List.of(\n                expectPlugin\n            );\n            var expectResult = new ListResult<>(plugins);\n            when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n\n            bindToRouterFunction(endpoint.endpoint())\n                .build()\n                .get().uri(\"/plugins?keyword=Expected\")\n                .exchange()\n                .expectStatus().isOk();\n        }\n\n        @Test\n        void shouldFilterPluginsWhenEnabledProvided() {\n            var expectPlugin =\n                createPlugin(\"fake-plugin-2\", \"expected display name\", \"\", true);\n            var plugins = List.of(\n                expectPlugin\n            );\n            var expectResult = new ListResult<>(plugins);\n\n            when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n\n            bindToRouterFunction(endpoint.endpoint())\n                .build()\n                .get().uri(\"/plugins?enabled=true\")\n                .exchange()\n                .expectStatus().isOk();\n        }\n\n        @Test\n        void shouldSortPluginsWhenCreationTimestampSet() {\n            var expectPlugin =\n                createPlugin(\"fake-plugin-2\", \"expected display name\", \"\", true);\n            var expectResult = new ListResult<>(List.of(expectPlugin));\n            when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n\n            bindToRouterFunction(endpoint.endpoint())\n                .build()\n                .get().uri(\"/plugins?sort=creationTimestamp,desc\")\n                .exchange()\n                .expectStatus().isOk();\n        }\n    }\n\n    @Nested\n    class PluginUpgradeTest {\n\n        WebTestClient webClient;\n\n        Path tempDirectory;\n\n        Path plugin002;\n\n        @BeforeEach\n        void setUp() throws URISyntaxException, IOException {\n            webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n                .build();\n\n            lenient().when(systemVersionSupplier.get()).thenReturn(Version.parse(\"0.0.0\"));\n            tempDirectory = Files.createTempDirectory(\"halo-test-plugin-upgrade-\");\n            plugin002 = tempDirectory.resolve(\"plugin-0.0.2.jar\");\n\n            var plugin002Uri = requireNonNull(\n                getClass().getClassLoader().getResource(\"plugin/plugin-0.0.2\")).toURI();\n\n            FileUtils.jar(Paths.get(plugin002Uri), tempDirectory.resolve(\"plugin-0.0.2.jar\"));\n        }\n\n        @AfterEach\n        void cleanUp() {\n            FileUtils.deleteRecursivelyAndSilently(tempDirectory);\n        }\n\n        @Test\n        void shouldResponseBadRequestIfNoPluginInstalledBefore() {\n            var bodyBuilder = new MultipartBodyBuilder();\n            bodyBuilder.part(\"file\", new FileSystemResource(plugin002))\n                .contentType(MediaType.MULTIPART_FORM_DATA);\n\n            when(pluginService.upgrade(eq(\"fake-plugin\"), isA(Path.class)))\n                .thenReturn(Mono.error(new ServerWebInputException(\"plugin not found\")));\n\n            webClient.post().uri(\"/plugins/fake-plugin/upgrade\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .body(fromMultipartData(bodyBuilder.build()))\n                .exchange()\n                .expectStatus().isBadRequest();\n\n            verify(pluginService).upgrade(eq(\"fake-plugin\"), isA(Path.class));\n        }\n\n    }\n\n    @Nested\n    class UpdatePluginConfigTest {\n        WebTestClient webClient;\n\n        @BeforeEach\n        void setUp() {\n            webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build();\n        }\n\n        @Test\n        void updateWhenConfigMapNameIsNull() {\n            Plugin plugin = createPlugin(\"fake-plugin\");\n            plugin.getSpec().setConfigMapName(null);\n\n            when(client.fetch(eq(Plugin.class), eq(\"fake-plugin\"))).thenReturn(Mono.just(plugin));\n            webClient.put()\n                .uri(\"/plugins/fake-plugin/json-config\")\n                .exchange()\n                .expectStatus().isBadRequest();\n        }\n\n        @Test\n        void updateJsonConfigTest() {\n            Plugin plugin = createPlugin(\"fake-plugin\");\n            plugin.getSpec().setConfigMapName(\"fake-config-map\");\n\n            when(client.fetch(eq(Plugin.class), eq(\"fake-plugin\"))).thenReturn(Mono.just(plugin));\n            when(settingConfigService.upsertConfig(eq(\"fake-config-map\"), any()))\n                .thenReturn(Mono.empty());\n\n            webClient.put()\n                .uri(\"/plugins/fake-plugin/json-config\")\n                .body(Mono.just(Map.of()), Map.class)\n                .exchange()\n                .expectStatus().is2xxSuccessful();\n        }\n    }\n\n    @Nested\n    class PluginConfigAndSettingFetchTest {\n        WebTestClient webClient;\n\n        @BeforeEach\n        void setUp() {\n            webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n                .build();\n        }\n\n        @Test\n        void fetchSetting() {\n            Plugin plugin = createPlugin(\"fake\");\n            plugin.getSpec().setSettingName(\"fake-setting\");\n\n            when(client.fetch(eq(Setting.class), eq(\"fake-setting\")))\n                .thenReturn(Mono.just(new Setting()));\n\n            when(client.fetch(eq(Plugin.class), eq(\"fake\"))).thenReturn(Mono.just(plugin));\n            webClient.get()\n                .uri(\"/plugins/fake/setting\")\n                .exchange()\n                .expectStatus().isOk();\n\n            verify(client).fetch(eq(Setting.class), eq(\"fake-setting\"));\n            verify(client).fetch(eq(Plugin.class), eq(\"fake\"));\n        }\n\n        @Test\n        void fetchJsonConfig() {\n            Plugin plugin = createPlugin(\"fake\");\n            plugin.getSpec().setConfigMapName(\"fake-config\");\n\n            when(settingConfigService.fetchConfig(eq(\"fake-config\")))\n                .thenReturn(Mono.empty());\n            when(client.fetch(eq(Plugin.class), eq(\"fake\"))).thenReturn(Mono.just(plugin));\n            webClient.get()\n                .uri(\"/plugins/fake/json-config\")\n                .exchange()\n                .expectStatus().isOk();\n\n            verify(settingConfigService).fetchConfig(eq(\"fake-config\"));\n            verify(client).fetch(eq(Plugin.class), eq(\"fake\"));\n        }\n    }\n\n    Plugin createPlugin(String name) {\n        return createPlugin(name, \"fake display name\", \"fake description\", null);\n    }\n\n    Plugin createPlugin(String name, String displayName, String description, Boolean enabled) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        metadata.setCreationTimestamp(Instant.now());\n        var spec = new Plugin.PluginSpec();\n        spec.setDisplayName(displayName);\n        spec.setDescription(description);\n        spec.setEnabled(enabled);\n        var plugin = new Plugin();\n        plugin.setMetadata(metadata);\n        plugin.setSpec(spec);\n        return plugin;\n    }\n\n    Plugin createPlugin(String name, Instant creationTimestamp) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        metadata.setCreationTimestamp(creationTimestamp);\n        var spec = new Plugin.PluginSpec();\n        var plugin = new Plugin();\n        plugin.setMetadata(metadata);\n        plugin.setSpec(spec);\n        return plugin;\n    }\n\n    @Nested\n    class BundleResourceEndpointTest {\n\n        private long lastModified;\n\n        WebTestClient webClient;\n\n        @BeforeEach\n        void setUp() {\n            webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build();\n            long currentTimeMillis = System.currentTimeMillis();\n            // We should ignore milliseconds here\n            // See https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1 for more.\n            this.lastModified = currentTimeMillis - currentTimeMillis % 1_000;\n        }\n\n        @Test\n        void shouldBeRedirectedWhileFetchingBundleJsWithoutVersion() {\n            when(pluginService.generateBundleVersion()).thenReturn(Mono.just(\"fake-version\"));\n            webClient.get().uri(\"/plugins/-/bundle.js\")\n                .exchange()\n                .expectStatus().is3xxRedirection()\n                .expectHeader().cacheControl(CacheControl.noStore())\n                .expectHeader().location(\n                    \"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js?v=fake-version\");\n        }\n\n        @Test\n        void shouldBeRedirectedWhileFetchingBundleCssWithoutVersion() {\n            when(pluginService.generateBundleVersion()).thenReturn(Mono.just(\"fake-version\"));\n            webClient.get().uri(\"/plugins/-/bundle.css\")\n                .exchange()\n                .expectStatus().is3xxRedirection()\n                .expectHeader().cacheControl(CacheControl.noStore())\n                .expectHeader().location(\n                    \"/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css?v=fake-version\");\n        }\n\n        @Test\n        void shouldFetchBundleCssWithCacheControl() {\n            var cache = webProperties.getResources().getCache();\n            cache.setUseLastModified(true);\n            var cachecontrol = cache.getCachecontrol();\n            cachecontrol.setNoCache(true);\n            endpoint.afterPropertiesSet();\n\n            when(pluginService.getCssBundle(\"fake-version\"))\n                .thenReturn(Mono.fromSupplier(() -> mockResource(\"fake-css\")));\n            webClient.get().uri(\"/plugins/-/bundle.css?v=fake-version\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectHeader().cacheControl(CacheControl.noCache())\n                .expectHeader().contentType(\"text/css\")\n                .expectHeader().lastModified(lastModified)\n                .expectBody(String.class).isEqualTo(\"fake-css\");\n        }\n\n        @Test\n        void shouldFetchBundleJsWithCacheControl() {\n            var cache = webProperties.getResources().getCache();\n            cache.setUseLastModified(true);\n            var cachecontrol = cache.getCachecontrol();\n            cachecontrol.setNoStore(true);\n            endpoint.afterPropertiesSet();\n\n            when(pluginService.getJsBundle(\"fake-version\"))\n                .thenReturn(Mono.fromSupplier(() -> mockResource(\"fake-js\")));\n            webClient.get().uri(\"/plugins/-/bundle.js?v=fake-version\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectHeader().cacheControl(CacheControl.noStore())\n                .expectHeader().contentType(\"text/javascript\")\n                .expectHeader().lastModified(lastModified)\n                .expectBody(String.class).isEqualTo(\"fake-js\");\n        }\n\n        @Test\n        void shouldFetchBundleCss() {\n            when(pluginService.getCssBundle(\"fake-version\"))\n                .thenReturn(Mono.fromSupplier(() -> mockResource(\"fake-css\")));\n            webClient.get().uri(\"/plugins/-/bundle.css?v=fake-version\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectHeader().cacheControl(CacheControl.empty())\n                .expectHeader().contentType(\"text/css\")\n                .expectHeader().lastModified(-1)\n                .expectBody(String.class).isEqualTo(\"fake-css\");\n        }\n\n        @Test\n        void shouldFetchBundleJs() {\n            when(pluginService.getJsBundle(\"fake-version\"))\n                .thenReturn(Mono.fromSupplier(() -> mockResource(\"fake-js\")));\n            webClient.get().uri(\"/plugins/-/bundle.js?v=fake-version\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectHeader().cacheControl(CacheControl.empty())\n                .expectHeader().contentType(\"text/javascript\")\n                .expectHeader().lastModified(-1)\n                .expectBody(String.class).isEqualTo(\"fake-js\");\n        }\n\n        Resource mockResource(String content) {\n            var resource = new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8));\n            resource = spy(resource);\n            try {\n                doReturn(lastModified).when(resource).lastModified();\n            } catch (IOException e) {\n                // should never happen\n                throw new RuntimeException(e);\n            }\n            return resource;\n        }\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/console/PostEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.ContentUpdateParam;\nimport run.halo.app.content.PostRequest;\nimport run.halo.app.content.PostService;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Post.PostSpec;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Tests for @{@link PostEndpoint}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostEndpointTest {\n\n    @Mock\n    PostService postService;\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    ApplicationEventPublisher eventPublisher;\n\n    @InjectMocks\n    PostEndpoint postEndpoint;\n\n    WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        postEndpoint.setMaxAttemptsWaitForPublish(3);\n        webTestClient = WebTestClient\n            .bindToRouterFunction(postEndpoint.endpoint())\n            .build();\n    }\n\n    @Test\n    void draftPost() {\n        when(postService.draftPost(any())).thenReturn(Mono.just(TestPost.postV1()));\n        webTestClient.post()\n            .uri(\"/posts\")\n            .bodyValue(postRequest(TestPost.postV1()))\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody(Post.class)\n            .value(post -> assertThat(post).isEqualTo(TestPost.postV1()));\n    }\n\n    @Test\n    void updatePost() {\n        when(postService.updatePost(any())).thenReturn(Mono.just(TestPost.postV1()));\n\n        webTestClient.put()\n            .uri(\"/posts/post-A\")\n            .bodyValue(postRequest(TestPost.postV1()))\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody(Post.class)\n            .value(post -> assertThat(post).isEqualTo(TestPost.postV1()));\n    }\n\n    @Test\n    void publishRetryOnOptimisticLockingFailure() {\n        var post = new Post();\n        post.setMetadata(new Metadata());\n        post.getMetadata().setName(\"post-1\");\n        post.setSpec(new PostSpec());\n        when(client.get(eq(Post.class), eq(\"post-1\"))).thenReturn(Mono.just(post));\n\n        when(client.update(any(Post.class)))\n            .thenReturn(Mono.error(new OptimisticLockingFailureException(\"fake-error\")));\n\n        // Send request\n        webTestClient.put()\n            .uri(\"/posts/{name}/publish?async=false\", \"post-1\")\n            .exchange()\n            .expectStatus()\n            .is5xxServerError();\n\n        // Verify WebClient retry behavior\n        verify(client, times(6)).get(eq(Post.class), eq(\"post-1\"));\n        verify(client, times(6)).update(any(Post.class));\n    }\n\n    @Test\n    void publishSuccess() {\n        var post = new Post();\n        post.setMetadata(new Metadata());\n        post.getMetadata().setName(\"post-1\");\n        post.setSpec(new PostSpec());\n\n        var publishedPost = new Post();\n        var publishedMetadata = new Metadata();\n        publishedMetadata.setAnnotations(Map.of(Post.LAST_RELEASED_SNAPSHOT_ANNO, \"my-release\"));\n        publishedPost.setMetadata(publishedMetadata);\n        var publishedPostSpec = new PostSpec();\n        publishedPostSpec.setReleaseSnapshot(\"my-release\");\n        publishedPost.setSpec(publishedPostSpec);\n\n        when(client.get(eq(Post.class), eq(\"post-1\")))\n            .thenReturn(Mono.just(post))\n            .thenReturn(Mono.just(publishedPost));\n\n        when(client.update(any(Post.class)))\n            .thenReturn(Mono.just(post));\n\n        // Send request\n        webTestClient.put()\n            .uri(\"/posts/{name}/publish?async=false\", \"post-1\")\n            .exchange()\n            .expectStatus()\n            .is2xxSuccessful();\n\n        // Verify WebClient retry behavior\n        verify(client, times(2)).get(eq(Post.class), eq(\"post-1\"));\n        verify(client).update(any(Post.class));\n    }\n\n    @Test\n    void shouldFailIfWaitTimeoutForPublishedStatus() {\n        var post = new Post();\n        post.setMetadata(new Metadata());\n        post.getMetadata().setName(\"post-1\");\n        post.setSpec(new PostSpec());\n\n        var publishedPost = new Post();\n        var publishedMetadata = new Metadata();\n        publishedMetadata.setAnnotations(\n            Map.of(Post.LAST_RELEASED_SNAPSHOT_ANNO, \"old-my-release\"));\n        publishedPost.setMetadata(publishedMetadata);\n        var publishedPostSpec = new PostSpec();\n        publishedPostSpec.setReleaseSnapshot(\"my-release\");\n        publishedPost.setSpec(publishedPostSpec);\n\n        when(client.get(eq(Post.class), eq(\"post-1\")))\n            .thenReturn(Mono.just(post))\n            .thenReturn(Mono.just(publishedPost));\n\n        when(client.update(any(Post.class)))\n            .thenReturn(Mono.just(post));\n\n        // Send request\n        webTestClient.put()\n            .uri(\"/posts/{name}/publish?async=false\", \"post-1\")\n            .exchange()\n            .expectStatus()\n            .is5xxServerError();\n\n        // Verify WebClient retry behavior\n        verify(client, times(5)).get(eq(Post.class), eq(\"post-1\"));\n        verify(client).update(any(Post.class));\n    }\n\n    PostRequest postRequest(Post post) {\n        return new PostRequest(post, new ContentUpdateParam(null, \"B\", \"<p>B</p>\", \"MARKDOWN\"));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/console/SinglePageEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Tests for @{@link SinglePageEndpoint}.\n *\n * @author guqing\n * @since 2.3.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SinglePageEndpointTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    SinglePageEndpoint singlePageEndpoint;\n\n    WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        webTestClient = WebTestClient\n            .bindToRouterFunction(singlePageEndpoint.endpoint())\n            .build();\n    }\n\n    @Test\n    void publishRetryOnOptimisticLockingFailure() {\n        var page = new SinglePage();\n        page.setMetadata(new Metadata());\n        page.getMetadata().setName(\"page-1\");\n        page.setSpec(new SinglePage.SinglePageSpec());\n        when(client.get(eq(SinglePage.class), eq(\"page-1\"))).thenReturn(Mono.just(page));\n\n        when(client.update(any(SinglePage.class)))\n            .thenReturn(Mono.error(new OptimisticLockingFailureException(\"fake-error\")));\n\n        // Send request\n        webTestClient.put()\n            .uri(\"/singlepages/{name}/publish?async=false\", \"page-1\")\n            .exchange()\n            .expectStatus()\n            .is5xxServerError();\n\n        // Verify WebClient retry behavior\n        verify(client, times(6)).get(eq(SinglePage.class), eq(\"page-1\"));\n        verify(client, times(6)).update(any(SinglePage.class));\n    }\n\n    @Test\n    void publishSuccess() {\n        var page = new SinglePage();\n        page.setMetadata(new Metadata());\n        page.getMetadata().setName(\"page-1\");\n        page.setSpec(new SinglePage.SinglePageSpec());\n\n        when(client.get(eq(SinglePage.class), eq(\"page-1\"))).thenReturn(Mono.just(page));\n        when(client.fetch(eq(SinglePage.class), eq(\"page-1\"))).thenReturn(Mono.empty());\n\n        when(client.update(any(SinglePage.class))).thenReturn(Mono.just(page));\n\n        // Send request\n        webTestClient.put()\n            .uri(\"/singlepages/{name}/publish?async=false\", \"page-1\")\n            .exchange()\n            .expectStatus()\n            .is2xxSuccessful();\n\n        // Verify WebClient retry behavior\n        verify(client, times(1)).get(eq(SinglePage.class), eq(\"page-1\"));\n        verify(client, times(1)).update(any(SinglePage.class));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/console/TagEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;\nimport static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;\n\nimport java.time.Instant;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Tag endpoint test.\n *\n * @author LIlGG\n */\n@ExtendWith(MockitoExtension.class)\nclass TagEndpointTest {\n    @Mock\n    ReactiveExtensionClient client;\n\n    @InjectMocks\n    TagEndpoint tagEndpoint;\n\n    WebTestClient webClient;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(tagEndpoint.endpoint())\n            .apply(springSecurity())\n            .build();\n    }\n\n    @Nested\n    class TagListTest {\n\n        @Test\n        void shouldListEmptyTagsWhenNoTags() {\n            when(client.listBy(same(Tag.class), any(), any(PageRequest.class)))\n                .thenReturn(Mono.just(ListResult.emptyResult()));\n\n            bindToRouterFunction(tagEndpoint.endpoint())\n                .build()\n                .get().uri(\"/tags\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(0)\n                .jsonPath(\"$.total\").isEqualTo(0);\n        }\n\n        @Test\n        void shouldListTagsWhenTagPresent() {\n            var tags = List.of(\n                createTag(\"fake-tag-1\"),\n                createTag(\"fake-tag-2\")\n            );\n            var expectResult = new ListResult<>(tags);\n            when(client.listBy(same(Tag.class), any(), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n\n            bindToRouterFunction(tagEndpoint.endpoint())\n                .build()\n                .get().uri(\"/tags\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(2)\n                .jsonPath(\"$.total\").isEqualTo(2);\n        }\n\n        Tag createTag(String name) {\n            return createTag(name, \"fake display name\");\n        }\n\n        Tag createTag(String name, String displayName) {\n            var metadata = new Metadata();\n            metadata.setName(name);\n            metadata.setCreationTimestamp(Instant.now());\n            var spec = new Tag.TagSpec();\n            spec.setDisplayName(displayName);\n            spec.setSlug(name);\n            var tag = new Tag();\n            tag.setMetadata(metadata);\n            tag.setSpec(spec);\n            return tag;\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/console/UserEndpointIntegrationTest.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.mockito.ArgumentMatchers.anySet;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.test.context.bean.override.mockito.MockitoBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\n@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)\n@WithMockUser(username = \"fake-user\", password = \"fake-password\", roles = \"fake-super-role\")\npublic class UserEndpointIntegrationTest {\n    @Autowired\n    WebTestClient webClient;\n\n    @Autowired\n    ReactiveExtensionClient client;\n\n    @MockitoBean\n    RoleService roleService;\n\n    @BeforeEach\n    void setUp() {\n        var rule = new Role.PolicyRule.Builder()\n            .apiGroups(\"*\")\n            .resources(\"*\")\n            .verbs(\"*\")\n            .build();\n        var role = new Role();\n        role.setMetadata(new Metadata());\n        role.getMetadata().setName(\"fake-super-role\");\n        role.setRules(List.of(rule));\n        when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));\n        webClient = webClient.mutateWith(csrf());\n    }\n\n    @Nested\n    class UserListTest {\n        @Test\n        void shouldFilterUsersWhenDisplayNameKeywordProvided() {\n            var expectUser =\n                createUser(\"fake-user-2\", \"expected display name\");\n            var unexpectedUser1 =\n                createUser(\"fake-user-1\", \"first fake display name\");\n            var unexpectedUser2 =\n                createUser(\"fake-user-3\", \"second fake display name\");\n\n            client.create(expectUser).block();\n            client.create(unexpectedUser1).block();\n            client.create(unexpectedUser2).block();\n\n            when(roleService.list(anySet())).thenReturn(Flux.empty());\n            when(roleService.getRolesByUsernames(\n                List.of(\"fake-user-2\")\n            )).thenReturn(Mono.just(Map.of(\"fake-user-2\", Set.of(\"fake-super-role\"))));\n\n            webClient.get().uri(\"/apis/api.console.halo.run/v1alpha1/users?keyword=Expected\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(1)\n                .jsonPath(\"$.items[0].user.metadata.name\").isEqualTo(\"fake-user-2\");\n\n        }\n\n        @Test\n        void shouldFilterUsersWhenUserNameKeywordProvided() {\n            var expectUser =\n                createUser(\"fake-user\", \"expected display name\");\n            var unexpectedUser1 =\n                createUser(\"fake-user-1\", \"first fake display name\");\n            var unexpectedUser2 =\n                createUser(\"fake-user-3\", \"second fake display name\");\n\n            client.create(expectUser).block();\n            client.create(unexpectedUser1).block();\n            client.create(unexpectedUser2).block();\n\n            when(roleService.list(anySet())).thenReturn(Flux.empty());\n            when(roleService.getRolesByUsernames(List.of(\"fake-user\")))\n                .thenReturn(Mono.just(Map.of(\"fake-user\", Set.of(\"fake-super-role\"))));\n\n            webClient.get().uri(\"/apis/api.console.halo.run/v1alpha1/users?keyword=fake-user\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(1)\n                .jsonPath(\"$.items[0].user.metadata.name\").isEqualTo(\"fake-user\");\n        }\n    }\n\n    User createUser(String name, String displayName) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        metadata.setCreationTimestamp(Instant.now());\n        var spec = new User.UserSpec();\n        spec.setEmail(\"fake-email\");\n        spec.setDisplayName(displayName);\n        var user = new User();\n        user.setMetadata(metadata);\n        user.setSpec(spec);\n        return user;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/console/UserEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.console;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anySet;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.MultipartBodyBuilder;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.service.AttachmentService;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.exception.UserNotFoundException;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@ExtendWith(MockitoExtension.class)\nclass UserEndpointTest {\n\n    WebTestClient webClient;\n\n    @Mock\n    RoleService roleService;\n\n    @Mock\n    AttachmentService attachmentService;\n\n    @Mock\n    SystemConfigFetcher environmentFetcher;\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    UserService userService;\n\n    @InjectMocks\n    UserEndpoint endpoint;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .apply(springSecurity())\n            .build()\n            .mutateWith(mockUser(\"fake-user\").password(\"fake-password\").roles(\"fake-super-role\"));\n    }\n\n    @Nested\n    class UserListTest {\n\n        @Test\n        void shouldListEmptyUsersWhenNoUsers() {\n            when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of()));\n            when(roleService.list(any())).thenReturn(Flux.empty());\n            when(client.listBy(same(User.class), any(), any(PageRequest.class)))\n                .thenReturn(Mono.just(ListResult.emptyResult()));\n\n            webClient.get().uri(\"/users\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(0)\n                .jsonPath(\"$.total\").isEqualTo(0);\n        }\n\n        @Test\n        void shouldListUsersWhenUserPresent() {\n            var users = List.of(\n                createUser(\"fake-user-1\"),\n                createUser(\"fake-user-2\"),\n                createUser(\"fake-user-3\")\n            );\n            var expectResult = new ListResult<>(users);\n            when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of()));\n            when(roleService.list(anySet())).thenReturn(Flux.empty());\n            when(client.listBy(same(User.class), any(), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n\n            webClient.get().uri(\"/users\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.items.length()\").isEqualTo(3)\n                .jsonPath(\"$.total\").isEqualTo(3);\n        }\n\n        @Test\n        void shouldFilterUsersWhenRoleProvided() {\n            var expectUser =\n                JsonUtils.jsonToObject(\"\"\"\n                    {\n                        \"apiVersion\": \"v1alpha1\",\n                        \"kind\": \"User\",\n                        \"metadata\": {\n                            \"name\": \"alice\",\n                            \"annotations\": {\n                                \"rbac.authorization.halo.run/role-names\": \"[\\\\\"guest\\\\\"]\"\n                            }\n                        }\n                    }\n                    \"\"\", User.class);\n            var users = List.of(\n                expectUser\n            );\n            var expectResult = new ListResult<>(users);\n            when(client.listBy(same(User.class), any(), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n            when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of()));\n            when(roleService.list(anySet())).thenReturn(Flux.empty());\n\n            webClient.get().uri(\"/users?role=guest\")\n                .exchange()\n                .expectStatus().isOk();\n        }\n\n        @Test\n        void shouldSortUsersWhenCreationTimestampSet() {\n            var expectUser =\n                createUser(\"fake-user-2\", \"expected display name\");\n            var expectResult = new ListResult<>(List.of(expectUser));\n            when(client.listBy(same(User.class), any(), any(PageRequest.class)))\n                .thenReturn(Mono.just(expectResult));\n            when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of()));\n            when(roleService.list(anySet())).thenReturn(Flux.empty());\n\n            webClient.get().uri(\"/users?sort=creationTimestamp,desc\")\n                .exchange()\n                .expectStatus().isOk();\n        }\n\n        User createUser(String name) {\n            return createUser(name, \"fake display name\");\n        }\n\n        User createUser(String name, String displayName) {\n            var metadata = new Metadata();\n            metadata.setName(name);\n            metadata.setCreationTimestamp(Instant.now());\n            var spec = new User.UserSpec();\n            spec.setDisplayName(displayName);\n            var user = new User();\n            user.setMetadata(metadata);\n            user.setSpec(spec);\n            return user;\n        }\n\n    }\n\n    @Nested\n    @DisplayName(\"GetUserDetail\")\n    class GetUserDetailTest {\n\n        @Test\n        void shouldResponseErrorIfUserNotFound() {\n            when(userService.getUser(\"fake-user\"))\n                .thenReturn(Mono.error(new UserNotFoundException(\"fake-user\")));\n            webClient.get().uri(\"/users/-\")\n                .exchange()\n                .expectStatus().isNotFound();\n\n            verify(userService).getUser(eq(\"fake-user\"));\n        }\n\n        @Test\n        void shouldGetCurrentUserDetail() {\n            var metadata = new Metadata();\n            metadata.setName(\"fake-user\");\n            var user = new User();\n            user.setMetadata(metadata);\n            when(userService.getUser(\"fake-user\")).thenReturn(Mono.just(user));\n            Role role = new Role();\n            role.setMetadata(new Metadata());\n            role.getMetadata().setName(\"fake-super-role\");\n            role.setRules(List.of());\n            when(roleService.list(Set.of(\"fake-super-role\"), true)).thenReturn(Flux.just(role));\n            webClient.get().uri(\"/users/-\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectHeader().contentType(MediaType.APPLICATION_JSON)\n                .expectBody(UserEndpoint.DetailedUser.class)\n                .isEqualTo(new UserEndpoint.DetailedUser(user, List.of(role)));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"UpdateProfile\")\n    class UpdateProfileTest {\n\n        @Test\n        void shouldUpdateProfileCorrectly() {\n            var currentUser = createUser(\"fake-user\");\n            var updatedUser = createUser(\"fake-user\");\n            var requestUser = createUser(\"fake-user\");\n\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(currentUser));\n            when(client.update(currentUser)).thenReturn(Mono.just(updatedUser));\n\n            webClient.put().uri(\"/users/-\")\n                .bodyValue(requestUser)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(User.class)\n                .isEqualTo(updatedUser);\n\n            verify(client).get(User.class, \"fake-user\");\n            verify(client).update(currentUser);\n        }\n\n        @Test\n        void shouldGetErrorIfUsernameMismatch() {\n            var currentUser = createUser(\"fake-user\");\n            var requestUser = createUser(\"another-fake-user\");\n\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(currentUser));\n\n            webClient.put().uri(\"/users/-\")\n                .bodyValue(requestUser)\n                .exchange()\n                .expectStatus().isBadRequest();\n\n            verify(client).get(User.class, \"fake-user\");\n            verify(client, never()).update(currentUser);\n        }\n\n        User createUser(String name) {\n            var spec = new User.UserSpec();\n            spec.setEmail(\"hi@halo.run\");\n            spec.setBio(\"Fake bio\");\n            spec.setDisplayName(\"Faker\");\n            spec.setPassword(\"fake-password\");\n\n            var metadata = new Metadata();\n            metadata.setName(name);\n\n            var user = new User();\n            user.setSpec(spec);\n            user.setMetadata(metadata);\n            return user;\n        }\n    }\n\n    @Nested\n    @DisplayName(\"ChangePassword\")\n    class ChangePasswordTest {\n\n        @Test\n        void shouldUpdateMyPasswordCorrectly() {\n            var user = new User();\n            when(userService.updateWithRawPassword(\"fake-user\", \"new-password\"))\n                .thenReturn(Mono.just(user));\n            when(userService.confirmPassword(\"fake-user\", \"old-password\"))\n                .thenReturn(Mono.just(true));\n            webClient.put().uri(\"/users/-/password\")\n                .bodyValue(\n                    new UserEndpoint.ChangeOwnPasswordRequest(\"old-password\", \"new-password\"))\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(User.class)\n                .isEqualTo(user);\n\n            verify(userService, times(1)).updateWithRawPassword(\"fake-user\", \"new-password\");\n        }\n\n        @Test\n        void shouldUpdateOtherPasswordCorrectly() {\n            var user = new User();\n            when(userService.updateWithRawPassword(\"another-fake-user\", \"new-password\"))\n                .thenReturn(Mono.just(user));\n            webClient.put()\n                .uri(\"/users/another-fake-user/password\")\n                .bodyValue(\n                    new UserEndpoint.ChangeOwnPasswordRequest(\"old-password\", \"new-password\"))\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(User.class)\n                .isEqualTo(user);\n\n            verify(userService, times(1)).updateWithRawPassword(\"another-fake-user\",\n                \"new-password\");\n        }\n\n    }\n\n    @Nested\n    @DisplayName(\"GrantPermission\")\n    class GrantPermissionEndpointTest {\n\n        @Test\n        void shouldGetBadRequestIfRequestBodyIsEmpty() {\n            webClient.post().uri(\"/users/fake-user/permissions\")\n                .contentType(MediaType.APPLICATION_JSON)\n                .exchange()\n                .expectStatus().isBadRequest();\n\n            // Why one more time to verify? Because the SuperAdminInitializer will fetch admin user.\n            verify(client, never()).fetch(same(User.class), eq(\"fake-user\"));\n            verify(client, never()).fetch(same(Role.class), eq(\"fake-role\"));\n        }\n\n        @Test\n        void shouldGrantPermission() {\n            when(userService.grantRoles(\"fake-user\", Set.of(\"fake-role\"))).thenReturn(Mono.empty());\n\n            webClient.post().uri(\"/users/fake-user/permissions\")\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(new UserEndpoint.GrantRequest(Set.of(\"fake-role\")))\n                .exchange()\n                .expectStatus().isOk();\n        }\n\n        @Test\n        void shouldGetPermission() {\n            Role roleA = JsonUtils.jsonToObject(\"\"\"\n                {\n                    \"apiVersion\": \"v1alpha1\",\n                    \"kind\": \"Role\",\n                    \"metadata\": {\n                        \"name\": \"test-A\",\n                        \"annotations\": {\n                            \"rbac.authorization.halo.run/ui-permissions\": \\\n                            \"[\\\\\"permission-A\\\\\", \\\\\"permission-A\\\\\"]\"\n                        }\n                    },\n                    \"rules\": []\n                }\n                \"\"\", Role.class);\n            when(roleService.listPermissions(eq(Set.of(\"test-A\")))).thenReturn(Flux.just(roleA));\n            when(roleService.getRolesByUsername(\"fake-user\")).thenReturn(Flux.just(\"test-A\"));\n            when(roleService.list(Set.of(\"test-A\"), true)).thenReturn(Flux.just(roleA));\n\n            webClient.get().uri(\"/users/fake-user/permissions\")\n                .exchange()\n                .expectStatus()\n                .isOk()\n                .expectBody(UserEndpoint.UserPermission.class)\n                .value(userPermission -> {\n                    assertEquals(List.of(roleA), userPermission.getRoles());\n                    assertEquals(List.of(roleA), userPermission.getPermissions());\n                    assertEquals(List.of(\"permission-A\"), userPermission.getUiPermissions());\n                });\n        }\n    }\n\n    @Test\n    void createWhenNameDuplicate() {\n        when(userService.createUser(any(User.class), anySet()))\n            .thenReturn(Mono.just(new User()));\n        var userRequest = new UserEndpoint.CreateUserRequest(\"fake-user\",\n            \"fake-email\",\n            \"\",\n            \"\",\n            \"\",\n            \"\",\n            \"\",\n            Map.of(),\n            Set.of());\n        webClient.post().uri(\"/users\")\n            .bodyValue(userRequest)\n            .exchange()\n            .expectStatus().isOk();\n    }\n\n    @Nested\n    class AvatarUploadTest {\n        @Test\n        void respondWithErrorIfTypeNotPNG() {\n\n            var multipartBodyBuilder = new MultipartBodyBuilder();\n            multipartBodyBuilder.part(\"file\", \"fake-file\")\n                .contentType(MediaType.IMAGE_JPEG)\n                .filename(\"fake-filename.jpg\");\n\n            when(environmentFetcher.fetch(\n                SystemSetting.Attachment.GROUP, SystemSetting.Attachment.class\n            )).thenReturn(Mono.fromSupplier(() -> SystemSetting.Attachment.builder()\n                .avatar(null)\n                .build())\n            );\n            when(environmentFetcher.fetch(\n                SystemSetting.User.GROUP, SystemSetting.User.class)\n            ).thenReturn(Mono.empty());\n\n            webClient\n                .post()\n                .uri(\"/users/-/avatar\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))\n                .exchange()\n                .expectStatus()\n                .is4xxClientError();\n        }\n\n        @Test\n        void shouldUploadSuccessfully() {\n            var currentUser = createUser(\"fake-user\");\n\n            Attachment attachment = new Attachment();\n            Metadata metadata = new Metadata();\n            metadata.setName(\"fake-attachment\");\n            attachment.setMetadata(metadata);\n\n            var multipartBodyBuilder = new MultipartBodyBuilder();\n            multipartBodyBuilder.part(\"file\", \"fake-file\")\n                .contentType(MediaType.IMAGE_PNG)\n                .filename(\"fake-filename.png\");\n\n\n            when(environmentFetcher.fetch(\n                SystemSetting.Attachment.GROUP, SystemSetting.Attachment.class\n            )).thenReturn(Mono.fromSupplier(() -> SystemSetting.Attachment.builder()\n                .avatar(null)\n                .build())\n            );\n            when(environmentFetcher.fetch(\n                SystemSetting.User.GROUP, SystemSetting.User.class)\n            ).thenReturn(Mono.empty());\n\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(currentUser));\n            when(attachmentService.upload(eq(\"default-policy\"), anyString(), anyString(),\n                any(), any(MediaType.IMAGE_PNG.getClass()))).thenReturn(Mono.just(attachment));\n\n            when(client.update(currentUser)).thenReturn(Mono.just(currentUser));\n\n            webClient.post()\n                .uri(\"/users/-/avatar\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))\n                .exchange()\n                .expectStatus()\n                .isOk()\n                .expectBody(User.class).isEqualTo(currentUser);\n\n            verify(client).get(User.class, \"fake-user\");\n            verify(client).update(currentUser);\n        }\n\n        @Test\n        void shouldUseFallbackSetting() {\n            var currentUser = createUser(\"fake-user\");\n\n            Attachment attachment = new Attachment();\n            Metadata metadata = new Metadata();\n            metadata.setName(\"fake-attachment\");\n            attachment.setMetadata(metadata);\n\n            var multipartBodyBuilder = new MultipartBodyBuilder();\n            multipartBodyBuilder.part(\"file\", \"fake-file\")\n                .contentType(MediaType.IMAGE_PNG)\n                .filename(\"fake-filename.png\");\n\n            when(environmentFetcher.fetch(\n                SystemSetting.Attachment.GROUP, SystemSetting.Attachment.class\n            )).thenReturn(Mono.fromSupplier(() -> SystemSetting.Attachment.builder()\n                .avatar(null)\n                .build())\n            );\n            when(environmentFetcher.fetch(\n                SystemSetting.User.GROUP, SystemSetting.User.class)\n            ).thenReturn(Mono.fromSupplier(() -> {\n                var us = new SystemSetting.User();\n                us.setAvatarPolicy(\"fake-avatar-policy\");\n                return us;\n            }));\n\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(currentUser));\n            when(attachmentService.upload(eq(\"fake-avatar-policy\"), anyString(), anyString(),\n                any(), any(MediaType.IMAGE_PNG.getClass()))).thenReturn(Mono.just(attachment));\n\n            when(client.update(currentUser)).thenReturn(Mono.just(currentUser));\n\n            webClient.post()\n                .uri(\"/users/-/avatar\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))\n                .exchange()\n                .expectStatus()\n                .isOk()\n                .expectBody(User.class).isEqualTo(currentUser);\n\n            verify(client).get(User.class, \"fake-user\");\n            verify(client).update(currentUser);\n        }\n\n        User createUser(String name) {\n            var spec = new User.UserSpec();\n            spec.setEmail(\"hi@halo.run\");\n            spec.setBio(\"Fake bio\");\n            spec.setDisplayName(\"Faker\");\n            spec.setAvatar(\"fake-avatar.png\");\n            spec.setPassword(\"fake-password\");\n\n            var metadata = new Metadata();\n            metadata.setName(name);\n            metadata.setAnnotations(new HashMap<>());\n\n            var user = new User();\n            user.setSpec(spec);\n            user.setMetadata(metadata);\n            return user;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/CategoryQueryEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\n\n/**\n * Tests for {@link CategoryQueryEndpoint}.\n *\n * @author guqing\n * @since 2.5.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CategoryQueryEndpointTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private PostPublicQueryService postPublicQueryService;\n    private CategoryQueryEndpoint endpoint;\n    private WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        endpoint = new CategoryQueryEndpoint(client, postPublicQueryService);\n        RouterFunction<ServerResponse> routerFunction = endpoint.endpoint();\n        webTestClient = WebTestClient.bindToRouterFunction(routerFunction).build();\n    }\n\n    @Test\n    void listCategories() {\n        ListResult<Category> listResult = new ListResult<>(List.of());\n        when(client.listBy(eq(Category.class), any(ListOptions.class), any(PageRequest.class)))\n            .thenReturn(Mono.just(listResult));\n\n        webTestClient.get()\n            .uri(\"/categories?page=1&size=10\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.total\").isEqualTo(listResult.getTotal())\n            .jsonPath(\"$.items\").isArray();\n    }\n\n    @Test\n    void getByName() {\n        Category category = new Category();\n        category.setMetadata(new Metadata());\n        category.getMetadata().setName(\"test\");\n        when(client.get(eq(Category.class), eq(\"test\"))).thenReturn(Mono.just(category));\n\n        webTestClient.get()\n            .uri(\"/categories/test\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.metadata.name\").isEqualTo(category.getMetadata().getName());\n    }\n\n    @Test\n    void listPostsByCategoryName() {\n        ListResult<ListedPostVo> listResult = new ListResult<>(List.of());\n        when(postPublicQueryService.list(any(), any(PageRequest.class)))\n            .thenReturn(Mono.just(listResult));\n\n        webTestClient.get()\n            .uri(\"/categories/test/posts?page=1&size=10\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.total\").isEqualTo(listResult.getTotal())\n            .jsonPath(\"$.items\").isArray();\n    }\n\n    @Test\n    void groupVersion() {\n        GroupVersion groupVersion = endpoint.groupVersion();\n        assertThat(groupVersion.toString()).isEqualTo(\"api.content.halo.run/v1alpha1\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/CommentFinderEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport io.github.resilience4j.ratelimiter.RateLimiter;\nimport io.github.resilience4j.ratelimiter.RateLimiterConfig;\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport java.time.Duration;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.comment.CommentRequest;\nimport run.halo.app.content.comment.CommentService;\nimport run.halo.app.content.comment.ReplyRequest;\nimport run.halo.app.content.comment.ReplyService;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.theme.finders.CommentFinder;\nimport run.halo.app.theme.finders.CommentPublicQueryService;\n\n/**\n * Tests for {@link CommentFinderEndpoint}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CommentFinderEndpointTest {\n    @Mock\n    private CommentFinder commentFinder;\n\n    @Mock\n    private CommentPublicQueryService commentPublicQueryService;\n\n    @Mock\n    private CommentService commentService;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @Mock\n    private ReplyService replyService;\n\n    @Mock\n    private RateLimiterRegistry rateLimiterRegistry;\n\n    @InjectMocks\n    private CommentFinderEndpoint commentFinderEndpoint;\n\n    private WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(environmentFetcher.fetchComment()).thenReturn(Mono.empty());\n        webTestClient = WebTestClient\n            .bindToRouterFunction(commentFinderEndpoint.endpoint())\n            .build();\n    }\n\n    @Test\n    void listComments() {\n        when(commentPublicQueryService.list(any(), any(PageRequest.class)))\n            .thenReturn(Mono.just(new ListResult<>(1, 10, 0, List.of())));\n\n        Ref ref = new Ref();\n        ref.setGroup(\"content.halo.run\");\n        ref.setVersion(\"v1alpha1\");\n        ref.setKind(\"Post\");\n        ref.setName(\"test\");\n        webTestClient.get()\n            .uri(uriBuilder -> uriBuilder.path(\"/comments\")\n                .queryParam(\"group\", ref.getGroup())\n                .queryParam(\"version\", ref.getVersion())\n                .queryParam(\"kind\", ref.getKind())\n                .queryParam(\"name\", ref.getName())\n                .queryParam(\"page\", 1)\n                .queryParam(\"size\", 10)\n                .build())\n            .exchange()\n            .expectStatus()\n            .isOk();\n        ArgumentCaptor<Ref> refCaptor = ArgumentCaptor.forClass(Ref.class);\n        verify(commentPublicQueryService, times(1))\n            .list(refCaptor.capture(), any(PageRequest.class));\n        Ref value = refCaptor.getValue();\n        assertThat(value).isEqualTo(ref);\n    }\n\n    @Test\n    void getComment() {\n        when(commentPublicQueryService.getByName(any()))\n            .thenReturn(null);\n\n        webTestClient.get()\n            .uri(\"/comments/test-comment\")\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n        verify(commentPublicQueryService, times(1)).getByName(eq(\"test-comment\"));\n    }\n\n    @Test\n    void listCommentReplies() {\n        when(commentPublicQueryService.listReply(any(), anyInt(), anyInt()))\n            .thenReturn(Mono.just(new ListResult<>(2, 20, 0, List.of())));\n\n        webTestClient.get()\n            .uri(uriBuilder -> uriBuilder.path(\"/comments/test-comment/reply\")\n                .queryParam(\"page\", 2)\n                .queryParam(\"size\", 20)\n                .build())\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n        verify(commentPublicQueryService, times(1)).listReply(eq(\"test-comment\"), eq(2), eq(20));\n    }\n\n    @Test\n    void createComment() {\n        when(commentService.create(any())).thenReturn(Mono.empty());\n\n        RateLimiterConfig config = RateLimiterConfig.custom()\n            .limitForPeriod(10)\n            .limitRefreshPeriod(Duration.ofSeconds(1))\n            .timeoutDuration(Duration.ofSeconds(10))\n            .build();\n        RateLimiter rateLimiter = RateLimiter.of(\"comment-creation-from-ip-\" + \"0:0:0:0:0:0:0:0\",\n            config);\n        when(rateLimiterRegistry.rateLimiter(anyString(), anyString())).thenReturn(rateLimiter);\n\n        final CommentRequest commentRequest = new CommentRequest();\n        Ref ref = new Ref();\n        ref.setGroup(\"content.halo.run\");\n        ref.setVersion(\"v1alpha1\");\n        ref.setKind(\"Post\");\n        ref.setName(\"test-post\");\n        commentRequest.setSubjectRef(ref);\n        commentRequest.setContent(\"content\");\n        commentRequest.setRaw(\"raw\");\n        commentRequest.setAllowNotification(false);\n        webTestClient.post()\n            .uri(\"/comments\")\n            .bodyValue(commentRequest)\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n        ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);\n        verify(commentService, times(1)).create(captor.capture());\n        Comment value = captor.getValue();\n        assertThat(value.getSpec().getIpAddress()).isNotNull();\n        assertThat(value.getSpec().getUserAgent()).isNotNull();\n        assertThat(value.getSpec().getSubjectRef()).isEqualTo(ref);\n    }\n\n    @Test\n    void createReply() {\n        when(replyService.create(any(), any())).thenReturn(Mono.empty());\n\n        final ReplyRequest replyRequest = new ReplyRequest();\n        replyRequest.setRaw(\"raw\");\n        replyRequest.setContent(\"content\");\n        replyRequest.setAllowNotification(true);\n\n        when(rateLimiterRegistry.rateLimiter(\"comment-creation-from-ip-127.0.0.1\",\n            \"comment-creation\"))\n            .thenReturn(RateLimiter.ofDefaults(\"comment-creation\"));\n\n        webTestClient.post()\n            .uri(\"/comments/test-comment/reply\")\n            .header(\"X-Forwarded-For\", \"127.0.0.1\")\n            .bodyValue(replyRequest)\n            .exchange()\n            .expectStatus()\n            .isOk();\n\n        ArgumentCaptor<Reply> captor = ArgumentCaptor.forClass(Reply.class);\n        verify(replyService, times(1)).create(eq(\"test-comment\"), captor.capture());\n        Reply value = captor.getValue();\n        assertThat(value.getSpec().getIpAddress()).isNotNull();\n        assertThat(value.getSpec().getUserAgent()).isNotNull();\n        assertThat(value.getSpec().getQuoteReply()).isNull();\n\n        verify(rateLimiterRegistry).rateLimiter(\"comment-creation-from-ip-127.0.0.1\",\n            \"comment-creation\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/MenuQueryEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport lombok.NonNull;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Menu;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.theme.finders.MenuFinder;\nimport run.halo.app.theme.finders.vo.MenuItemVo;\nimport run.halo.app.theme.finders.vo.MenuVo;\n\n/**\n * Tests for {@link MenuQueryEndpoint}.\n *\n * @author guqing\n * @since 2.5.0\n */\n@ExtendWith(MockitoExtension.class)\nclass MenuQueryEndpointTest {\n\n    @Mock\n    private MenuFinder menuFinder;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @InjectMocks\n    private MenuQueryEndpoint endpoint;\n\n    private WebTestClient webClient;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build();\n    }\n\n    @Test\n    void getPrimaryMenu() {\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake-primary\");\n        MenuVo menuVo = MenuVo.builder()\n            .metadata(metadata)\n            .spec(new Menu.Spec())\n            .menuItems(List.of(MenuItemVo.from(createMenuItem(\"item1\"))))\n            .build();\n        when(menuFinder.getByName(eq(\"fake-primary\")))\n            .thenReturn(Mono.just(menuVo));\n\n        SystemSetting.Menu menuSetting = new SystemSetting.Menu();\n        menuSetting.setPrimary(\"fake-primary\");\n        when(environmentFetcher.fetch(eq(SystemSetting.Menu.GROUP), eq(SystemSetting.Menu.class)))\n            .thenReturn(Mono.just(menuSetting));\n\n        webClient.get().uri(\"/menus/-\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.metadata.name\").isEqualTo(\"fake-primary\")\n            .jsonPath(\"$.menuItems[0].metadata.name\").isEqualTo(\"item1\");\n\n        verify(menuFinder).getByName(eq(\"fake-primary\"));\n        verify(environmentFetcher).fetch(eq(SystemSetting.Menu.GROUP),\n            eq(SystemSetting.Menu.class));\n    }\n\n    @NonNull\n    private static MenuItem createMenuItem(String name) {\n        MenuItem menuItem = new MenuItem();\n        menuItem.setMetadata(new Metadata());\n        menuItem.getMetadata().setName(name);\n        menuItem.setSpec(new MenuItem.MenuItemSpec());\n        menuItem.getSpec().setDisplayName(name);\n        return menuItem;\n    }\n\n    @Test\n    void getMenuByName() {\n        Metadata metadata = new Metadata();\n        metadata.setName(\"test-menu\");\n        MenuVo menuVo = MenuVo.builder()\n            .metadata(metadata)\n            .spec(new Menu.Spec())\n            .menuItems(List.of(MenuItemVo.from(createMenuItem(\"item2\"))))\n            .build();\n        when(menuFinder.getByName(eq(\"test-menu\")))\n            .thenReturn(Mono.just(menuVo));\n\n        webClient.get().uri(\"/menus/test-menu\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.metadata.name\").isEqualTo(\"test-menu\")\n            .jsonPath(\"$.menuItems[0].metadata.name\").isEqualTo(\"item2\");\n\n        verify(menuFinder).getByName(eq(\"test-menu\"));\n    }\n\n    @Test\n    void groupVersion() {\n        GroupVersion groupVersion = endpoint.groupVersion();\n        assertThat(groupVersion.toString()).isEqualTo(\"api.halo.run/v1alpha1\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/PluginQueryEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.theme.finders.PluginFinder;\n\n/**\n * Tests for {@link PluginQueryEndpoint}.\n *\n * @author guqing\n * @since 2.5.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PluginQueryEndpointTest {\n\n    @Mock\n    private PluginFinder pluginFinder;\n\n    @InjectMocks\n    private PluginQueryEndpoint endpoint;\n\n    private WebTestClient webClient;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build();\n    }\n\n    @Test\n    void available() {\n        when(pluginFinder.available(\"fake-plugin\")).thenReturn(true);\n        webClient.get().uri(\"/plugins/fake-plugin/available\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$\").isEqualTo(true);\n    }\n\n    @Test\n    void groupVersion() {\n        GroupVersion groupVersion = endpoint.groupVersion();\n        assertThat(groupVersion.toString()).isEqualTo(\"api.plugin.halo.run/v1alpha1\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/PostQueryEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.NavigationPostVo;\nimport run.halo.app.theme.finders.vo.PostVo;\n\n/**\n * Tests for {@link PostQueryEndpoint}.\n *\n * @author guqing\n * @since 2.5.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostQueryEndpointTest {\n\n    private WebTestClient webClient;\n\n    @Mock\n    private PostFinder postFinder;\n\n    @Mock\n    private PostPublicQueryService postPublicQueryService;\n\n    @InjectMocks\n    private PostQueryEndpoint endpoint;\n\n    @BeforeEach\n    public void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .build();\n    }\n\n    @Test\n    public void listPosts() {\n        ListResult<ListedPostVo> result = new ListResult<>(List.of());\n        when(postPublicQueryService.list(any(), any(PageRequest.class)))\n            .thenReturn(Mono.just(result));\n\n        webClient.get().uri(\"/posts\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.items\").isArray();\n\n        verify(postPublicQueryService).list(any(), any(PageRequest.class));\n    }\n\n    @Test\n    public void getPostByName() {\n        Metadata metadata = new Metadata();\n        metadata.setName(\"test\");\n        PostVo post = PostVo.builder()\n            .metadata(metadata)\n            .build();\n        when(postFinder.getByName(anyString())).thenReturn(Mono.just(post));\n\n        webClient.get().uri(\"/posts/{name}\", \"test\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.metadata.name\").isEqualTo(\"test\");\n\n        verify(postFinder).getByName(anyString());\n    }\n\n    @Test\n    public void testGetPostNavigationByName() {\n        Metadata metadata = new Metadata();\n        metadata.setName(\"test\");\n        NavigationPostVo navigation = NavigationPostVo.builder()\n            .next(ListedPostVo.builder().metadata(metadata).build())\n            .build();\n        when(postFinder.cursor(anyString()))\n            .thenReturn(Mono.just(navigation));\n\n        webClient.get().uri(\"/posts/{name}/navigation\", \"test\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.next.metadata.name\").isEqualTo(\"test\");\n\n        verify(postFinder).cursor(anyString());\n    }\n\n    @Test\n    void groupVersion() {\n        GroupVersion groupVersion = endpoint.groupVersion();\n        assertThat(groupVersion.toString()).isEqualTo(\"api.content.halo.run/v1alpha1\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/PublicApiUtilsTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.GroupVersion;\n\n/**\n * Tests for {@link PublicApiUtils}.\n *\n * @author guqing\n * @since 2.5.0\n */\nclass PublicApiUtilsTest {\n\n    @Test\n    void groupVersion() {\n        GroupVersion groupVersion = PublicApiUtils.groupVersion(new FakExtension());\n        assertThat(groupVersion.toString()).isEqualTo(\"api.halo.run/v1alpha1\");\n\n        groupVersion = PublicApiUtils.groupVersion(new FakeGroupExtension());\n        assertThat(groupVersion.toString()).isEqualTo(\"api.fake.halo.run/v1\");\n    }\n\n    @Test\n    void containsElement() {\n        assertThat(PublicApiUtils.containsElement(null, null)).isFalse();\n        assertThat(PublicApiUtils.containsElement(null, \"test\")).isFalse();\n        assertThat(PublicApiUtils.containsElement(List.of(\"test\"), null)).isFalse();\n        assertThat(PublicApiUtils.containsElement(List.of(\"test\"), \"test\")).isTrue();\n        assertThat(PublicApiUtils.containsElement(List.of(\"test\"), \"test1\")).isFalse();\n    }\n\n    @GVK(group = \"fake.halo.run\", version = \"v1\", kind = \"FakeGroupExtension\", plural =\n        \"fakegroupextensions\", singular = \"fakegroupextension\")\n    static class FakeGroupExtension extends AbstractExtension {\n\n    }\n\n    @GVK(group = \"\", version = \"v1alpha1\", kind = \"FakeExtension\", plural =\n        \"fakeextensions\", singular = \"fakeextension\")\n    static class FakExtension extends AbstractExtension {\n\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/SinglePageQueryEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.GroupVersion;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.theme.finders.SinglePageFinder;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * Tests for {@link SinglePageQueryEndpoint}.\n *\n * @author guqing\n * @since 2.5.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SinglePageQueryEndpointTest {\n\n    @Mock\n    private SinglePageFinder singlePageFinder;\n\n    @InjectMocks\n    private SinglePageQueryEndpoint endpoint;\n\n    private WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        webTestClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build();\n    }\n\n    @Test\n    void getByName() {\n        SinglePageVo singlePage = SinglePageVo.builder()\n            .metadata(metadata(\"fake-page\"))\n            .spec(new SinglePage.SinglePageSpec())\n            .build();\n\n        when(singlePageFinder.getByName(eq(\"fake-page\")))\n            .thenReturn(Mono.just(singlePage));\n\n        webTestClient.get()\n            .uri(\"/singlepages/fake-page\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().contentType(MediaType.APPLICATION_JSON)\n            .expectBody()\n            .jsonPath(\"$.metadata.name\").isEqualTo(\"fake-page\");\n\n        verify(singlePageFinder).getByName(\"fake-page\");\n    }\n\n    Metadata metadata(String name) {\n        Metadata metadata = new Metadata();\n        metadata.setName(name);\n        return metadata;\n    }\n\n    @Test\n    void groupVersion() {\n        GroupVersion groupVersion = endpoint.groupVersion();\n        assertThat(groupVersion.toString()).isEqualTo(\"api.content.halo.run/v1alpha1\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/theme/ThumbnailEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.theme;\n\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.ThumbnailSize;\nimport run.halo.app.core.attachment.thumbnail.ThumbnailService;\n\n/**\n * Tests for {@link ThumbnailEndpoint}.\n *\n * @author guqing\n * @since 2.19.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ThumbnailEndpointTest {\n\n    WebTestClient webClient;\n\n    @Mock\n    private ThumbnailService thumbnailService;\n\n    @InjectMocks\n    private ThumbnailEndpoint endpoint;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .build();\n    }\n\n    @Test\n    void thumbnailUriNotAccessible() {\n        when(thumbnailService.get(URI.create(\"/myavatar.png\"), ThumbnailSize.L))\n            .thenReturn(Mono.empty());\n        webClient.get()\n            .uri(\"/thumbnails/-/via-uri?size=l&uri=/myavatar.png\")\n            .exchange()\n            .expectHeader().location(\"/myavatar.png\")\n            .expectStatus().is3xxRedirection();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/uc/AnnotationSettingEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.plugin.PluginService;\nimport run.halo.app.theme.service.ThemeService;\n\n@ExtendWith(MockitoExtension.class)\nclass AnnotationSettingEndpointTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    PluginService pluginService;\n\n    @Mock\n    ThemeService themeService;\n\n    @InjectMocks\n    AnnotationSettingEndpoint endpoint;\n\n    WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        webTestClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .build();\n    }\n\n    @Test\n    void shouldFetchAnnotationSettings() {\n        when(themeService.fetchActivatedThemeName()).thenReturn(Mono.just(\"fake-theme\"));\n        when(pluginService.getStartedPluginNames()).thenReturn(Flux.just(\"plugin-1\", \"plugin-2\"));\n\n        var annotationSetting = new AnnotationSetting();\n        annotationSetting.setMetadata(new Metadata());\n        annotationSetting.getMetadata().setName(\"fake-annotation\");\n        when(\n            client.listAll(same(AnnotationSetting.class), any(ListOptions.class), any(Sort.class))\n        ).thenReturn(Flux.just(annotationSetting));\n\n        webTestClient.get()\n            .uri(\"/annotationsettings?targetRef={targetRef}\", \"content.halo.run/Post\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBodyList(AnnotationSetting.class)\n            .isEqualTo(List.of(annotationSetting));\n\n        verify(client).listAll(\n            same(AnnotationSetting.class),\n            assertArg(listOptions -> {\n                var condition = listOptions.toCondition();\n                assertEquals(\"\"\"\n                    (\\\n                    (metadata.labels['theme.halo.run/theme-name'] = 'fake-theme' \\\n                    OR metadata.labels['plugin.halo.run/plugin-name'] IN ('plugin-1', 'plugin-2')\\\n                    ) \\\n                    AND spec.targetRef = content.halo.run/Post\\\n                    )\"\"\", condition.toString());\n            }),\n            any(Sort.class));\n    }\n\n    @Test\n    void shouldFetchAnnotationSettingsWithoutActivatedTheme() {\n        when(themeService.fetchActivatedThemeName()).thenReturn(Mono.empty());\n        when(pluginService.getStartedPluginNames()).thenReturn(Flux.just(\"plugin-1\", \"plugin-2\"));\n\n        var annotationSetting = new AnnotationSetting();\n        annotationSetting.setMetadata(new Metadata());\n        annotationSetting.getMetadata().setName(\"fake-annotation\");\n        when(\n            client.listAll(same(AnnotationSetting.class), any(ListOptions.class), any(Sort.class))\n        ).thenReturn(Flux.just(annotationSetting));\n\n        webTestClient.get()\n            .uri(\"/annotationsettings?targetRef={targetRef}\", \"content.halo.run/Post\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBodyList(AnnotationSetting.class)\n            .isEqualTo(List.of(annotationSetting));\n\n        verify(client).listAll(\n            same(AnnotationSetting.class),\n            assertArg(listOptions -> {\n                var condition = listOptions.toCondition();\n                assertEquals(\"\"\"\n                    (\\\n                    metadata.labels['plugin.halo.run/plugin-name'] IN ('plugin-1', 'plugin-2') \\\n                    AND spec.targetRef = content.halo.run/Post\\\n                    )\"\"\", condition.toString());\n            }),\n            any(Sort.class));\n    }\n\n    @Test\n    void shouldFetchAnnotationSettingsWithoutStartedPlugins() {\n        when(themeService.fetchActivatedThemeName()).thenReturn(Mono.just(\"fake-theme\"));\n        when(pluginService.getStartedPluginNames()).thenReturn(Flux.empty());\n\n        var annotationSetting = new AnnotationSetting();\n        annotationSetting.setMetadata(new Metadata());\n        annotationSetting.getMetadata().setName(\"fake-annotation\");\n        when(\n            client.listAll(same(AnnotationSetting.class), any(ListOptions.class), any(Sort.class))\n        ).thenReturn(Flux.just(annotationSetting));\n\n        webTestClient.get()\n            .uri(\"/annotationsettings?targetRef={targetRef}\", \"content.halo.run/Post\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBodyList(AnnotationSetting.class)\n            .isEqualTo(List.of(annotationSetting));\n\n        verify(client).listAll(\n            same(AnnotationSetting.class),\n            assertArg(listOptions -> {\n                var condition = listOptions.toCondition();\n                assertEquals(\"\"\"\n                    (\\\n                    metadata.labels['theme.halo.run/theme-name'] = 'fake-theme' \\\n                    AND spec.targetRef = content.halo.run/Post\\\n                    )\"\"\", condition.toString());\n            }),\n            any(Sort.class));\n    }\n\n    @Test\n    void shouldFetchAnnotationSettingsWithoutActivatedThemeAndStartedPlugins() {\n        when(themeService.fetchActivatedThemeName()).thenReturn(Mono.empty());\n        when(pluginService.getStartedPluginNames()).thenReturn(Flux.empty());\n\n        var annotationSetting = new AnnotationSetting();\n        annotationSetting.setMetadata(new Metadata());\n        annotationSetting.getMetadata().setName(\"fake-annotation\");\n        when(\n            client.listAll(same(AnnotationSetting.class), any(ListOptions.class), any(Sort.class))\n        ).thenReturn(Flux.just(annotationSetting));\n\n        webTestClient.get()\n            .uri(\"/annotationsettings?targetRef={targetRef}\", \"content.halo.run/Post\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBodyList(AnnotationSetting.class)\n            .isEqualTo(List.of(annotationSetting));\n\n        verify(client).listAll(\n            same(AnnotationSetting.class),\n            assertArg(listOptions -> {\n                var condition = listOptions.toCondition();\n                assertEquals(\"\"\"\n                    (\\\n                    EMPTY AND spec.targetRef = content.halo.run/Post\\\n                    )\"\"\", condition.toString());\n            }),\n            any(Sort.class));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/endpoint/uc/UcUserPreferenceEndpointTest.java",
    "content": "package run.halo.app.core.endpoint.uc;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockAuthentication;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;\n\nimport java.util.HashMap;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.authentication.AnonymousAuthenticationToken;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport tools.jackson.databind.JsonNode;\nimport tools.jackson.databind.json.JsonMapper;\nimport tools.jackson.databind.node.NullNode;\nimport tools.jackson.databind.node.ObjectNode;\n\n@ExtendWith(MockitoExtension.class)\nclass UcUserPreferenceEndpointTest {\n\n    WebTestClient webClient;\n\n    @InjectMocks\n    UcUserPreferenceEndpoint endpoint;\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Spy\n    JsonMapper mapper = JsonMapper.shared();\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .apply(springSecurity())\n            .build();\n    }\n\n    @Test\n    void testGroupVersion() {\n        var gv = endpoint.groupVersion();\n        assertEquals(\"uc.api.halo.run\", gv.group());\n        assertEquals(\"v1alpha1\", gv.version());\n    }\n\n    @Test\n    void shouldNotGetPreferenceWhenUnauthenticated() {\n        webClient.mutate()\n            .apply(mockAuthentication(new AnonymousAuthenticationToken(\n                \"key\", \"anonymousUser\", createAuthorityList(\"ROLE_ANONYMOUS\")\n            )))\n            .build()\n            .get()\n            .uri(\"/user-preferences/fake\")\n            .exchange()\n            .expectStatus()\n            .isForbidden();\n    }\n\n    @Test\n    void shouldGetNullPreferenceWhenAuthenticatedAndConfigMapAbsent() {\n        when(client.fetch(ConfigMap.class, \"user-preferences-faker\")).thenReturn(Mono.empty());\n        webClient.mutate()\n            .apply(mockUser(\"faker\"))\n            .build()\n            .get()\n            .uri(\"/user-preferences/fake\")\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody(JsonNode.class)\n            .value(n -> {\n                assertInstanceOf(NullNode.class, n);\n            });\n    }\n\n    @Test\n    void shouldGetPreferenceWhenAuthenticatedAndConfigMapPresent() {\n        var cm = new ConfigMap();\n        cm.setData(new HashMap<>());\n        cm.getData().put(\"fake\", \"\"\"\n            {\n              \"key\": \"value\"\n            }\\\n            \"\"\");\n        when(client.fetch(ConfigMap.class, \"user-preferences-faker\"))\n            .thenReturn(Mono.just(cm));\n        webClient.mutate()\n            .apply(mockUser(\"faker\"))\n            .build()\n            .get()\n            .uri(\"/user-preferences/fake\")\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody(ObjectNode.class)\n            .value(node -> assertEquals(\"value\", node.get(\"key\").asString()));\n    }\n\n    @Test\n    void shouldNotCreatePreferenceWhenUnauthenticated() {\n        webClient.mutate()\n            .apply(mockAuthentication(new AnonymousAuthenticationToken(\n                \"key\", \"anonymousUser\", createAuthorityList(\"ROLE_ANONYMOUS\")\n            )))\n            .build()\n            .put()\n            .uri(\"/user-preferences/faker\")\n            .exchange()\n            .expectStatus()\n            .isForbidden();\n    }\n\n    @Test\n    void shouldCreatePreferenceWithoutConfigMap() {\n        when(client.fetch(ConfigMap.class, \"user-preferences-faker\")).thenReturn(Mono.empty());\n        when(client.create(any(ConfigMap.class))).thenReturn(Mono.just(new ConfigMap()));\n        webClient.mutate()\n            .apply(mockUser(\"faker\"))\n            .build()\n            .put()\n            .uri(\"/user-preferences/fake\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .bodyValue(\"\"\"\n                {\n                  \"key\": \"value\"\n                }\\\n                \"\"\")\n            .exchange()\n            .expectStatus()\n            .isNoContent();\n\n        verify(client).<ConfigMap>create(assertArg(cm -> JSONAssert.assertEquals(\n            \"\"\"\n                {\"key\":\"value\"}\\\n                \"\"\",\n            cm.getData().get(\"fake\"),\n            true\n        )));\n        verify(client, never()).update(any());\n    }\n\n    @Test\n    void shouldUpdatePreferenceWhenConfigMapExists() {\n        var cm = new ConfigMap();\n        cm.setMetadata(new Metadata());\n        cm.getMetadata().setName(\"user-preferences-faker\");\n        cm.getMetadata().setVersion(1L);\n        cm.setData(new HashMap<>());\n        cm.getData().put(\"fake1\", \"\"\"\n            {\n              \"key1\": \"value1\"\n            }\\\n            \"\"\");\n        cm.getData().put(\"fake2\", \"\"\"\n            {\n              \"key2\": \"value2\"\n            }\n            \"\"\");\n        when(client.fetch(ConfigMap.class, \"user-preferences-faker\")).thenReturn(Mono.just(cm));\n        when(client.update(any(ConfigMap.class))).thenReturn(Mono.just(cm));\n        webClient.mutate()\n            .apply(mockUser(\"faker\"))\n            .build()\n            .put()\n            .uri(\"/user-preferences/fake1\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .bodyValue(\"\"\"\n                {\n                  \"newKey\": \"newValue\"\n                }\\\n                \"\"\")\n            .exchange()\n            .expectStatus()\n            .isNoContent();\n\n        verify(client).<ConfigMap>update(assertArg(cmToUpdate -> {\n            JSONAssert.assertEquals(\n                \"\"\"\n                    {\"newKey\":\"newValue\"}\\\n                    \"\"\",\n                cmToUpdate.getData().get(\"fake1\"),\n                true\n            );\n            JSONAssert.assertEquals(\n                \"\"\"\n                    {\"key2\":\"value2\"}\\\n                    \"\"\",\n                cmToUpdate.getData().get(\"fake2\"),\n                true\n            );\n        }));\n\n        verify(client, never()).create(any());\n    }\n\n    @Test\n    void shouldNotUpdatePreferenceWhenNotChange() {\n        var cm = new ConfigMap();\n        cm.setMetadata(new Metadata());\n        cm.getMetadata().setName(\"user-preferences-faker\");\n        cm.getMetadata().setVersion(1L);\n        cm.setData(new HashMap<>());\n        cm.getData().put(\"fake\", \"\"\"\n            {\"key\":\"value\"}\\\n            \"\"\");\n        when(client.fetch(ConfigMap.class, \"user-preferences-faker\")).thenReturn(Mono.just(cm));\n        webClient.mutate()\n            .apply(mockUser(\"faker\"))\n            .build()\n            .put()\n            .uri(\"/user-preferences/fake\")\n            .contentType(MediaType.APPLICATION_JSON)\n            .bodyValue(\"\"\"\n                {\"key\":\"value\"}\\\n                \"\"\")\n            .exchange()\n            .expectStatus()\n            .isNoContent();\n\n        verify(client, never()).update(any(ConfigMap.class));\n        verify(client, never()).create(any(ConfigMap.class));\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/extension/PostTest.java",
    "content": "package run.halo.app.core.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map;\nimport java.util.function.Function;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mockito;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.MetadataOperator;\n\nclass PostTest {\n\n    @Test\n    void staticIsPublishedTest() {\n        var test = (Function<Map<String, String>, Boolean>) (labels) -> {\n            var metadata = Mockito.mock(MetadataOperator.class);\n            when(metadata.getLabels()).thenReturn(labels);\n            return Post.isPublished(metadata);\n        };\n        assertEquals(false, test.apply(Map.of()));\n        assertEquals(false, test.apply(Map.of(\"content.halo.run/published\", \"false\")));\n        assertEquals(false, test.apply(Map.of(\"content.halo.run/published\", \"False\")));\n        assertEquals(false, test.apply(Map.of(\"content.halo.run/published\", \"0\")));\n        assertEquals(false, test.apply(Map.of(\"content.halo.run/published\", \"1\")));\n        assertEquals(false, test.apply(Map.of(\"content.halo.run/published\", \"T\")));\n        assertEquals(false, test.apply(Map.of(\"content.halo.run/published\", \"\")));\n        assertEquals(true, test.apply(Map.of(\"content.halo.run/published\", \"true\")));\n        assertEquals(true, test.apply(Map.of(\"content.halo.run/published\", \"True\")));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/extension/RoleBindingTest.java",
    "content": "package run.halo.app.core.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.time.Instant;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.Metadata;\n\nclass RoleBindingTest {\n\n    @Test\n    void shouldContainUser() {\n        var subject = new RoleBinding.Subject();\n        subject.setName(\"fake-name\");\n        subject.setApiGroup(\"\");\n        subject.setKind(\"User\");\n\n        var binding = new RoleBinding();\n        binding.setMetadata(new Metadata());\n        binding.setSubjects(List.of(subject));\n        assertTrue(RoleBinding.containsUser(\"fake-name\").test(binding));\n        assertFalse(RoleBinding.containsUser(\"non-exist-fake-name\").test(binding));\n    }\n\n    @Test\n    void shouldNotContainUserWhenBindingIsDeleted() {\n        var subject = new RoleBinding.Subject();\n        subject.setName(\"fake-name\");\n        subject.setApiGroup(\"\");\n        subject.setKind(\"User\");\n\n        var binding = new RoleBinding();\n        var metadata = new Metadata();\n        metadata.setDeletionTimestamp(Instant.now());\n        binding.setMetadata(metadata);\n        binding.setSubjects(List.of(subject));\n        assertFalse(RoleBinding.containsUser(\"fake-name\").test(binding));\n        assertFalse(RoleBinding.containsUser(\"non-exist-fake-name\").test(binding));\n    }\n\n    @Test\n    void subjectToStringTest() {\n        assertEquals(\"User/fake-name\", createSubject(\"fake-name\", \"\", \"User\").toString());\n        assertEquals(\n            \"fake.group/User/fake-name\",\n            createSubject(\"fake-name\", \"fake.group\", \"User\").toString()\n        );\n    }\n\n    RoleBinding.Subject createSubject(String name, String apiGroup, String kind) {\n        var subject = new RoleBinding.Subject();\n        subject.setName(name);\n        subject.setApiGroup(apiGroup);\n        subject.setKind(kind);\n        return subject;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/extension/SettingTest.java",
    "content": "package run.halo.app.core.extension;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.security.util.InMemoryResource;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\n\n/**\n * Tests for {@link Setting}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass SettingTest {\n\n    @Test\n    void setting() throws JSONException {\n        String settingYaml = \"\"\"\n            apiVersion: v1alpha1\n            kind: Setting\n            metadata:\n              name: setting-name\n            spec:\n              forms:\n                - group: basic\n                  label: 基本设置\n                  formSchema:\n                    - $el: h1\n                      children: Register\n                    - $formkit: text\n                      help: This will be used for your account.\n                      label: Email\n                      name: email\n                      validation: required|email\n                - group: sns\n                  label: 社交资料\n                  formSchema:\n                    - $formkit: text\n                      help: This will be used for your theme.\n                      label: color\n                      name: color\n                      validation: required\n            \"\"\";\n        var unstructureds = new YamlUnstructuredLoader(\n            new InMemoryResource(settingYaml.getBytes(UTF_8), \"In-memory setting YAML\"))\n            .load();\n        assertThat(unstructureds).hasSize(1);\n        Unstructured unstructured = unstructureds.get(0);\n\n        Setting setting = Unstructured.OBJECT_MAPPER.convertValue(unstructured, Setting.class);\n        assertThat(setting).isNotNull();\n        JSONAssert.assertEquals(\"\"\"\n                {\n                     \"spec\": {\n                         \"forms\": [\n                             {\n                                 \"group\": \"basic\",\n                                 \"label\": \"基本设置\",\n                                 \"formSchema\": [\n                                     {\n                                         \"$el\": \"h1\",\n                                         \"children\": \"Register\"\n                                     },\n                                     {\n                                         \"$formkit\": \"text\",\n                                         \"help\": \"This will be used for your account.\",\n                                         \"label\": \"Email\",\n                                         \"name\": \"email\",\n                                         \"validation\": \"required|email\"\n                                     }\n                                 ]\n                             },\n                             {\n                                 \"group\": \"sns\",\n                                 \"label\": \"社交资料\",\n                                 \"formSchema\": [\n                                     {\n                                         \"$formkit\": \"text\",\n                                         \"help\": \"This will be used for your theme.\",\n                                         \"label\": \"color\",\n                                         \"name\": \"color\",\n                                         \"validation\": \"required\"\n                                     }\n                                 ]\n                             }\n                         ]\n                     },\n                     \"apiVersion\": \"v1alpha1\",\n                     \"kind\": \"Setting\",\n                     \"metadata\": {\n                         \"name\": \"setting-name\"\n                     }\n                }\n                \"\"\",\n            JsonUtils.objectToJson(setting), false);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/extension/TestRole.java",
    "content": "package run.halo.app.core.extension;\n\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Roles to test.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class TestRole {\n\n    public static Role getRoleManage() {\n        return JsonUtils.jsonToObject(\"\"\"\n            {\n                 \"apiVersion\": \"v1alpha1\",\n                 \"kind\": \"Role\",\n                 \"metadata\": {\n                     \"name\": \"role-template-apple-manage\"\n                 },\n                 \"rules\": [{\n                     \"resources\": [\"apples\"],\n                     \"verbs\": [\"create\"]\n                 }]\n             }\n            \"\"\", Role.class);\n    }\n\n    public static Role getRoleView() {\n        return JsonUtils.jsonToObject(\"\"\"\n            {\n                 \"apiVersion\": \"v1alpha1\",\n                 \"kind\": \"Role\",\n                 \"metadata\": {\n                     \"name\": \"role-template-apple-view\"\n                 },\n                 \"rules\": [{\n                     \"resources\": [\"apples\"],\n                     \"verbs\": [\"list\"]\n                 }]\n             }\n            \"\"\", Role.class);\n    }\n\n    public static Role getRoleOther() {\n        return JsonUtils.jsonToObject(\"\"\"\n            {\n                \"apiVersion\": \"v1alpha1\",\n                \"kind\": \"Role\",\n                \"metadata\": {\n                    \"name\": \"role-template-apple-other\"\n                },\n                \"rules\": [{\n                    \"resources\": [\"apples\"],\n                    \"verbs\": [\"update\"]\n                }]\n            }\n            \"\"\", Role.class);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/extension/ThemeTest.java",
    "content": "package run.halo.app.core.extension;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.List;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.security.util.InMemoryResource;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\n\n/**\n * Tests for {@link Theme}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass ThemeTest {\n\n    @Test\n    void constructor() throws JSONException {\n        Theme theme = new Theme();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"test-theme\");\n        theme.setMetadata(metadata);\n\n\n        Theme.ThemeSpec themeSpec = new Theme.ThemeSpec();\n        theme.setSpec(themeSpec);\n        themeSpec.setDisplayName(\"test-theme\");\n\n        Theme.Author author = new Theme.Author();\n        author.setName(\"test-author\");\n        author.setWebsite(\"https://test.com\");\n        themeSpec.setAuthor(author);\n\n        themeSpec.setRepo(\"https://test.com\");\n        themeSpec.setLogo(\"https://test.com\");\n        themeSpec.setHomepage(\"https://test.com\");\n        themeSpec.setDescription(\"test-description\");\n        themeSpec.setConfigMapName(\"test-config-map\");\n        themeSpec.setSettingName(\"test-setting\");\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"spec\": {\n                        \"displayName\": \"test-theme\",\n                        \"author\": {\n                            \"name\": \"test-author\",\n                            \"website\": \"https://test.com\"\n                        },\n                        \"description\": \"test-description\",\n                        \"logo\": \"https://test.com\",\n                        \"homepage\": \"https://test.com\",\n                        \"repo\": \"https://test.com\",\n                        \"version\": \"*\",\n                        \"requires\": \"*\",\n                        \"settingName\": \"test-setting\",\n                        \"configMapName\": \"test-config-map\"\n                    },\n                    \"apiVersion\": \"theme.halo.run/v1alpha1\",\n                    \"kind\": \"Theme\",\n                    \"metadata\": {\n                        \"name\": \"test-theme\"\n                    }\n                }\n                \"\"\",\n            JsonUtils.objectToJson(theme),\n            true);\n\n        themeSpec.setVersion(\"1.0.0\");\n        themeSpec.setRequires(\"2.0.0\");\n        assertThat(themeSpec.getVersion()).isEqualTo(\"1.0.0\");\n        assertThat(themeSpec.getRequires()).isEqualTo(\"2.0.0\");\n    }\n\n    @Test\n    void themeCustomTemplate() throws JSONException {\n        String themeYaml = \"\"\"\n            apiVersion: theme.halo.run/v1alpha1\n            kind: Theme\n            metadata:\n              name: guqing-higan\n            spec:\n              displayName: higan\n              customTemplates:\n                post:\n                  - name: post-template-1\n                    description: description for post-template-1\n                    screenshot: foo.png\n                    file: post_template_1.html\n                  - name: post-template-2\n                    description: description for post-template-2\n                    screenshot: bar.png\n                    file: post_template_2.html\n                category:\n                  - name: category-template-1\n                    description: description for category-template-1\n                    screenshot: foo.png\n                    file: category_template_1.html\n                page:\n                  - name: page-template-1\n                    description: description for page-template-1\n                    screenshot: foo.png\n                    file: page_template_1.html\n            \"\"\";\n        List<Unstructured> unstructuredList =\n            new YamlUnstructuredLoader(new InMemoryResource(themeYaml)).load();\n        assertThat(unstructuredList).hasSize(1);\n        Theme theme = Unstructured.OBJECT_MAPPER.convertValue(unstructuredList.get(0), Theme.class);\n        assertThat(theme).isNotNull();\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"post\": [\n                        {\n                            \"name\": \"post-template-1\",\n                            \"description\": \"description for post-template-1\",\n                            \"screenshot\": \"foo.png\",\n                            \"file\": \"post_template_1.html\"\n                        },\n                        {\n                            \"name\": \"post-template-2\",\n                            \"description\": \"description for post-template-2\",\n                            \"screenshot\": \"bar.png\",\n                            \"file\": \"post_template_2.html\"\n                        }\n                    ],\n                    \"category\": [\n                        {\n                            \"name\": \"category-template-1\",\n                            \"description\": \"description for category-template-1\",\n                            \"screenshot\": \"foo.png\",\n                            \"file\": \"category_template_1.html\"\n                        }],\n                    \"page\": [\n                        {\n                            \"name\": \"page-template-1\",\n                            \"description\": \"description for page-template-1\",\n                            \"screenshot\": \"foo.png\",\n                            \"file\": \"page_template_1.html\"\n                        }]\n                }\n                \"\"\",\n            JsonUtils.objectToJson(theme.getSpec().getCustomTemplates()),\n            true);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpointTest.java",
    "content": "package run.halo.app.core.extension.attachment.endpoint;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;\n\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpStatusCode;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.http.client.MultipartBodyBuilder;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.attachment.AttachmentLister;\nimport run.halo.app.core.attachment.endpoint.AttachmentEndpoint;\nimport run.halo.app.core.extension.attachment.Attachment;\nimport run.halo.app.core.extension.attachment.Group;\nimport run.halo.app.core.extension.attachment.Policy;\nimport run.halo.app.core.extension.attachment.Policy.PolicySpec;\nimport run.halo.app.core.user.service.impl.DefaultAttachmentService;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ReactiveUrlDataBufferFetcher;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n@ExtendWith(MockitoExtension.class)\nclass AttachmentEndpointTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    @Mock\n    ReactiveUrlDataBufferFetcher dataBufferFetcher;\n\n    @Mock\n    AttachmentLister attachmentLister;\n\n    AttachmentEndpoint endpoint;\n\n    WebTestClient webClient;\n\n    @BeforeEach\n    void setUp() {\n        var attachmentService =\n            new DefaultAttachmentService(client, extensionGetter, dataBufferFetcher);\n        endpoint = new AttachmentEndpoint(attachmentService, attachmentLister);\n        webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .apply(springSecurity())\n            .build();\n    }\n\n    @Nested\n    class UploadTest {\n\n        @Test\n        void shouldResponseErrorIfNotLogin() {\n            var policySpec = new PolicySpec();\n            policySpec.setConfigMapName(\"fake-configmap\");\n            var policyMetadata = new Metadata();\n            policyMetadata.setName(\"fake-policy\");\n            var policy = new Policy();\n            policy.setSpec(policySpec);\n            policy.setMetadata(policyMetadata);\n\n            var cm = new ConfigMap();\n            var cmMetadata = new Metadata();\n            cmMetadata.setName(\"fake-configmap\");\n            cm.setData(Map.of());\n\n            var handler = mock(AttachmentHandler.class);\n            var metadata = new Metadata();\n            metadata.setName(\"fake-attachment\");\n            var attachment = new Attachment();\n            attachment.setMetadata(metadata);\n\n            var builder = new MultipartBodyBuilder();\n            builder.part(\"policyName\", \"fake-policy\");\n            builder.part(\"groupName\", \"fake-group\");\n            builder.part(\"file\", \"fake-file\")\n                .contentType(MediaType.TEXT_PLAIN)\n                .filename(\"fake-filename\");\n            webClient\n                .post()\n                .uri(\"/attachments/upload\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .body(BodyInserters.fromMultipartData(builder.build()))\n                .exchange()\n                .expectStatus().isUnauthorized();\n\n            verify(client, never()).get(Policy.class, \"fake-policy\");\n            verify(client, never()).get(ConfigMap.class, \"fake-configmap\");\n            verify(client, never()).create(attachment);\n            verify(extensionGetter, never()).getExtensions(AttachmentHandler.class);\n            verify(handler, never()).upload(any());\n        }\n\n        @Test\n        void shouldResponseErrorIfNoBodyProvided() {\n            webClient\n                .mutateWith(mockUser(\"fake-user\").password(\"fake-password\"))\n                .post()\n                .uri(\"/attachments/upload\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .exchange()\n                .expectStatus().is5xxServerError();\n        }\n\n        @Test\n        void shouldResponseErrorIfPolicyNameIsMissing() {\n            var builder = new MultipartBodyBuilder();\n            builder.part(\"file\", \"fake-file\")\n                .contentType(MediaType.TEXT_PLAIN)\n                .filename(\"fake-filename\");\n            webClient\n                .mutateWith(mockUser(\"fake-user\").password(\"fake-password\"))\n                .post()\n                .uri(\"/attachments/upload\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .body(BodyInserters.fromMultipartData(builder.build()))\n                .exchange()\n                .expectStatus().isBadRequest();\n        }\n\n        @Test\n        void shouldUploadSuccessfully() {\n            var policySpec = new PolicySpec();\n            policySpec.setConfigMapName(\"fake-configmap\");\n            var policyMetadata = new Metadata();\n            policyMetadata.setName(\"fake-policy\");\n            var policy = new Policy();\n            policy.setSpec(policySpec);\n            policy.setMetadata(policyMetadata);\n\n            var cm = new ConfigMap();\n            var cmMetadata = new Metadata();\n            cmMetadata.setName(\"fake-configmap\");\n            cm.setData(Map.of());\n\n            var group = new Group();\n            group.setMetadata(new Metadata());\n            group.getMetadata().setName(\"fake-group\");\n\n            when(client.get(Policy.class, \"fake-policy\")).thenReturn(Mono.just(policy));\n            when(client.get(ConfigMap.class, \"fake-configmap\")).thenReturn(Mono.just(cm));\n            when(client.get(Group.class, \"fake-group\")).thenReturn(Mono.just(group));\n\n            var handler = mock(AttachmentHandler.class);\n            var metadata = new Metadata();\n            metadata.setName(\"fake-attachment\");\n            var attachment = new Attachment();\n            attachment.setMetadata(metadata);\n\n            when(handler.upload(any())).thenReturn(Mono.just(attachment));\n            when(extensionGetter.getExtensions(AttachmentHandler.class))\n                .thenReturn(Flux.just(handler));\n            when(client.create(attachment)).thenReturn(Mono.just(attachment));\n\n            var builder = new MultipartBodyBuilder();\n            builder.part(\"policyName\", \"fake-policy\");\n            builder.part(\"groupName\", \"fake-group\");\n            builder.part(\"file\", \"fake-file\")\n                .contentType(MediaType.TEXT_PLAIN)\n                .filename(\"fake-filename\");\n            webClient\n                .mutateWith(mockUser(\"fake-user\").password(\"fake-password\"))\n                .post()\n                .uri(\"/attachments/upload\")\n                .contentType(MediaType.MULTIPART_FORM_DATA)\n                .body(BodyInserters.fromMultipartData(builder.build()))\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.metadata.name\").isEqualTo(\"fake-attachment\")\n                .jsonPath(\"$.spec.ownerName\").isEqualTo(\"fake-user\")\n                .jsonPath(\"$.spec.policyName\").isEqualTo(\"fake-policy\")\n                .jsonPath(\"$.spec.groupName\").isEqualTo(\"fake-group\")\n            ;\n\n            verify(client).get(Policy.class, \"fake-policy\");\n            verify(client).get(ConfigMap.class, \"fake-configmap\");\n            verify(client).get(Group.class, \"fake-group\");\n            verify(client).create(attachment);\n            verify(handler).upload(assertArg(context -> {\n                assertEquals(policy, context.policy());\n                assertEquals(cm, context.configMap());\n                assertEquals(group, context.group());\n            }));\n        }\n    }\n\n    @Nested\n    class SearchTest {\n\n        @Test\n        void shouldListUngroupedAttachments() {\n            when(attachmentLister.listBy(any())).thenReturn(Mono.just(ListResult.emptyResult()));\n\n            webClient\n                .get()\n                .uri(\"/attachments\")\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"items.length()\").isEqualTo(0);\n        }\n\n        @Test\n        void searchAttachmentWhenGroupIsEmpty() {\n            when(attachmentLister.listBy(any())).thenReturn(Mono.just(ListResult.emptyResult()));\n\n            webClient\n                .get()\n                .uri(\"/attachments\")\n                .exchange()\n                .expectStatus().isOk();\n\n            verify(attachmentLister).listBy(any());\n        }\n    }\n\n    @Nested\n    class ExternalTransferTest {\n        @Test\n        void shouldResponseErrorIfNoPermalinkProvided() {\n            webClient\n                .mutateWith(mockUser(\"fake-user\").password(\"fake-password\"))\n                .post()\n                .uri(\"/attachments/-/upload-from-url\")\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(Map.of(\"policyName\", \"fake-policy\"))\n                .exchange()\n                .expectStatus().isBadRequest();\n        }\n\n        @Test\n        void shouldTransferSuccessfully() {\n            var policySpec = new PolicySpec();\n            policySpec.setConfigMapName(\"fake-configmap\");\n            var policyMetadata = new Metadata();\n            policyMetadata.setName(\"fake-policy\");\n            var policy = new Policy();\n            policy.setSpec(policySpec);\n            policy.setMetadata(policyMetadata);\n\n            var cm = new ConfigMap();\n            var cmMetadata = new Metadata();\n            cmMetadata.setName(\"fake-configmap\");\n            cm.setData(Map.of());\n\n            when(client.get(Policy.class, \"fake-policy\")).thenReturn(Mono.just(policy));\n            when(client.get(ConfigMap.class, \"fake-configmap\")).thenReturn(Mono.just(cm));\n\n            var handler = mock(AttachmentHandler.class);\n            var metadata = new Metadata();\n            metadata.setName(\"fake-attachment\");\n            var attachment = new Attachment();\n            attachment.setMetadata(metadata);\n\n            ResponseEntity<Void> response = new ResponseEntity<>(HttpStatusCode.valueOf(200));\n            HttpHeaders headers = response.getHeaders();\n            DataBuffer dataBuffer = mock(DataBuffer.class);\n\n            when(handler.upload(any())).thenReturn(Mono.just(attachment));\n            when(dataBufferFetcher.head(any())).thenReturn(Mono.just(headers));\n            when(dataBufferFetcher.fetch(any())).thenReturn(Flux.just(dataBuffer));\n            when(extensionGetter.getExtensions(AttachmentHandler.class))\n                .thenReturn(Flux.just(handler));\n            when(client.create(attachment)).thenReturn(Mono.just(attachment));\n\n            var fakeValue =\n                Map.of(\"policyName\", \"fake-policy\", \"url\",\n                    \"http://localhost:8090/fake-url.jpg\");\n            webClient\n                .mutateWith(mockUser(\"fake-user\").password(\"fake-password\"))\n                .post()\n                .uri(\"/attachments/-/upload-from-url\")\n                .contentType(MediaType.APPLICATION_JSON)\n                .bodyValue(fakeValue)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody()\n                .jsonPath(\"$.metadata.name\").isEqualTo(\"fake-attachment\")\n                .jsonPath(\"$.spec.ownerName\").isEqualTo(\"fake-user\")\n                .jsonPath(\"$.spec.policyName\").isEqualTo(\"fake-policy\")\n            ;\n\n            verify(client).get(Policy.class, \"fake-policy\");\n            verify(client).get(ConfigMap.class, \"fake-configmap\");\n            verify(client).create(attachment);\n            verify(dataBufferFetcher).head(any());\n            verify(dataBufferFetcher).fetch(any());\n            verify(handler).upload(any());\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/CommentReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.comment.ReplyService;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.reconciler.CommentReconciler;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.controller.Reconciler;\n\n/**\n * Tests for {@link CommentReconciler}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CommentReconcilerTest {\n\n    @Mock\n    private ExtensionClient client;\n\n    @Mock\n    SchemeManager schemeManager;\n\n    @Mock\n    ReplyService replyService;\n\n    @InjectMocks\n    private CommentReconciler commentReconciler;\n\n    private final Instant now = Instant.now();\n\n    @Test\n    void reconcileDelete() {\n        Comment comment = new Comment();\n        comment.setMetadata(new Metadata());\n        comment.getMetadata().setName(\"test\");\n        comment.getMetadata().setDeletionTimestamp(Instant.now());\n        Set<String> finalizers = new HashSet<>();\n        finalizers.add(CommentReconciler.FINALIZER_NAME);\n        comment.getMetadata().setFinalizers(finalizers);\n        comment.setSpec(new Comment.CommentSpec());\n        comment.getSpec().setSubjectRef(getRef());\n        comment.getSpec().setLastReadTime(now.plusSeconds(5));\n        comment.setStatus(new Comment.CommentStatus());\n\n        when(client.fetch(eq(Comment.class), eq(\"test\")))\n            .thenReturn(Optional.of(comment));\n\n        when(replyService.removeAllByComment(eq(comment.getMetadata().getName())))\n            .thenReturn(Mono.empty());\n        when(client.listBy(eq(Comment.class), any(ListOptions.class), isA(PageRequest.class)))\n            .thenReturn(ListResult.emptyResult());\n\n        Reconciler.Result reconcile = commentReconciler.reconcile(new Reconciler.Request(\"test\"));\n        assertThat(reconcile.reEnqueue()).isFalse();\n        assertThat(reconcile.retryAfter()).isNull();\n\n        verify(replyService).removeAllByComment(eq(comment.getMetadata().getName()));\n\n        ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);\n        verify(client, times(1)).update(captor.capture());\n        Comment value = captor.getValue();\n        assertThat(value.getMetadata().getFinalizers()\n            .contains(CommentReconciler.FINALIZER_NAME)).isFalse();\n    }\n\n    @Test\n    void compatibleCreationTime() {\n        Comment comment = new Comment();\n        comment.setMetadata(new Metadata());\n        comment.getMetadata().setName(\"fake-comment\");\n        comment.setSpec(new Comment.CommentSpec());\n        comment.getSpec().setApprovedTime(Instant.now());\n        comment.getSpec().setCreationTime(null);\n\n        commentReconciler.compatibleCreationTime(comment);\n\n        assertThat(comment.getSpec().getCreationTime())\n            .isEqualTo(comment.getSpec().getApprovedTime());\n    }\n\n    private static Ref getRef() {\n        Ref ref = new Ref();\n        ref.setGroup(\"content.halo.run\");\n        ref.setVersion(\"v1alpha1\");\n        ref.setKind(\"Post\");\n        ref.setName(\"fake-post\");\n        return ref;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/MenuItemReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Duration;\nimport java.util.Optional;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.core.extension.MenuItem.MenuItemSpec;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.reconciler.MenuItemReconciler;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.controller.Reconciler.Request;\n\n@ExtendWith(MockitoExtension.class)\nclass MenuItemReconcilerTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @InjectMocks\n    MenuItemReconciler reconciler;\n\n    @Nested\n    class WhenCategoryRefSet {\n\n        @Test\n        void shouldNotUpdateMenuItemIfCategoryNotFound() {\n            Supplier<MenuItem> menuItemSupplier = () -> createMenuItem(\"fake-name\", spec -> {\n                spec.setTargetRef(Ref.of(\"fake-category\", Category.GVK));\n            });\n\n            when(client.fetch(MenuItem.class, \"fake-name\"))\n                .thenReturn(Optional.of(menuItemSupplier.get()));\n            when(client.fetch(Category.class, \"fake-category\")).thenReturn(Optional.empty());\n\n            var result = reconciler.reconcile(new Request(\"fake-name\"));\n\n            assertTrue(result.reEnqueue());\n            assertEquals(Duration.ofMinutes(1), result.retryAfter());\n            verify(client).fetch(MenuItem.class, \"fake-name\");\n            verify(client).fetch(Category.class, \"fake-category\");\n            verify(client, never()).update(isA(MenuItem.class));\n        }\n\n        @Test\n        void shouldUpdateMenuItemIfCategoryFound() {\n            Supplier<MenuItem> menuItemSupplier = () -> createMenuItem(\"fake-name\", spec -> {\n                spec.setTargetRef(Ref.of(\"fake-category\", Category.GVK));\n            });\n\n            when(client.fetch(MenuItem.class, \"fake-name\"))\n                .thenReturn(Optional.of(menuItemSupplier.get()))\n                .thenReturn(Optional.of(menuItemSupplier.get()));\n            when(client.fetch(Category.class, \"fake-category\"))\n                .thenReturn(Optional.of(createCategory()));\n\n            var result = reconciler.reconcile(new Request(\"fake-name\"));\n\n            assertTrue(result.reEnqueue());\n            assertEquals(Duration.ofMinutes(1), result.retryAfter());\n            verify(client, times(2)).fetch(MenuItem.class, \"fake-name\");\n            verify(client).fetch(Category.class, \"fake-category\");\n            verify(client).<MenuItem>update(argThat(menuItem -> {\n                var status = menuItem.getStatus();\n                return status.getHref().equals(\"fake://permalink\")\n                    && status.getDisplayName().equals(\"Fake Category\");\n            }));\n        }\n\n        Category createCategory() {\n            var metadata = new Metadata();\n            metadata.setName(\"fake-category\");\n\n            var spec = new Category.CategorySpec();\n            spec.setDisplayName(\"Fake Category\");\n            var status = new Category.CategoryStatus();\n            status.setPermalink(\"fake://permalink\");\n\n            var category = new Category();\n            category.setMetadata(metadata);\n            category.setSpec(spec);\n            category.setStatus(status);\n            return category;\n        }\n    }\n\n    @Nested\n    class WhenSinglePageRefSet {\n\n        @Test\n        void shouldUpdateMenuItemIfPageFound() {\n            Supplier<MenuItem> menuItemSupplier = () -> createMenuItem(\"fake-name\",\n                spec -> spec.setTargetRef(Ref.of(\"fake-page\", SinglePage.GVK)));\n\n            when(client.fetch(MenuItem.class, \"fake-name\"))\n                .thenReturn(Optional.of(menuItemSupplier.get()))\n                .thenReturn(Optional.of(menuItemSupplier.get()));\n\n            when(client.fetch(SinglePage.class, \"fake-page\"))\n                .thenReturn(Optional.of(createSinglePage()));\n\n            var result = reconciler.reconcile(new Request(\"fake-name\"));\n            assertTrue(result.reEnqueue());\n            assertEquals(Duration.ofMinutes(1), result.retryAfter());\n            verify(client, times(2)).fetch(MenuItem.class, \"fake-name\");\n            verify(client).fetch(SinglePage.class, \"fake-page\");\n            verify(client).<MenuItem>update(argThat(menuItem -> {\n                var status = menuItem.getStatus();\n                return status.getHref().equals(\"fake://permalink\")\n                    && status.getDisplayName().equals(\"fake-title\");\n            }));\n        }\n\n        SinglePage createSinglePage() {\n            var metadata = new Metadata();\n            metadata.setName(\"fake-page\");\n\n            var spec = new SinglePage.SinglePageSpec();\n            spec.setTitle(\"fake-title\");\n            var status = new SinglePage.SinglePageStatus();\n            status.setPermalink(\"fake://permalink\");\n\n            var singlePage = new SinglePage();\n            singlePage.setMetadata(metadata);\n            singlePage.setSpec(spec);\n            singlePage.setStatus(status);\n            return singlePage;\n        }\n    }\n\n    @Nested\n    class WhenOtherRefsNotSet {\n\n        @Test\n        void shouldNotRequeueIfHrefNotSet() {\n            var menuItem = createMenuItem(\"fake-name\", spec -> {\n                spec.setHref(null);\n                spec.setDisplayName(\"Fake display name\");\n            });\n            when(client.fetch(MenuItem.class, \"fake-name\")).thenReturn(Optional.of(menuItem));\n\n            var result = reconciler.reconcile(new Request(\"fake-name\"));\n            assertFalse(result.reEnqueue());\n\n            verify(client).fetch(MenuItem.class, \"fake-name\");\n            verify(client, never()).update(menuItem);\n        }\n\n        @Test\n        void shouldNotRequeueIfDisplayNameNotSet() {\n            var menuItem = createMenuItem(\"fake-name\", spec -> {\n                spec.setHref(\"/fake\");\n                spec.setDisplayName(null);\n            });\n            when(client.fetch(MenuItem.class, \"fake-name\")).thenReturn(Optional.of(menuItem));\n            var result = reconciler.reconcile(new Request(\"fake-name\"));\n            assertFalse(result.reEnqueue());\n\n            verify(client).fetch(MenuItem.class, \"fake-name\");\n            verify(client, never()).update(menuItem);\n        }\n\n        @Test\n        void shouldReconcileIfHrefAndDisplayNameSet() {\n            Supplier<MenuItem> menuItemSupplier = () -> createMenuItem(\"fake-name\", spec -> {\n                spec.setHref(\"/fake\");\n                spec.setDisplayName(\"Fake display name\");\n            });\n\n            when(client.fetch(MenuItem.class, \"fake-name\"))\n                .thenReturn(Optional.of(menuItemSupplier.get()))\n                .thenReturn(Optional.of(menuItemSupplier.get()));\n\n            var result = reconciler.reconcile(new Request(\"fake-name\"));\n            assertFalse(result.reEnqueue());\n\n            verify(client, times(2)).fetch(MenuItem.class, \"fake-name\");\n            verify(client).update(argThat(ext -> {\n                if (!(ext instanceof MenuItem menuItem)) {\n                    return false;\n                }\n                return menuItem.getStatus().getHref().equals(\"/fake\")\n                    && menuItem.getStatus().getDisplayName().equals(\"Fake display name\");\n            }));\n        }\n    }\n\n    MenuItem createMenuItem(String name, Consumer<MenuItemSpec> specCustomizer) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        var menuItem = new MenuItem();\n        menuItem.setMetadata(metadata);\n        var spec = new MenuItemSpec();\n        if (specCustomizer != null) {\n            specCustomizer.accept(spec);\n        }\n        menuItem.setSpec(spec);\n        return menuItem;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/PluginReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.plugin.PluginConst.PLUGIN_PATH;\nimport static run.halo.app.plugin.PluginConst.RELOAD_ANNO;\nimport static run.halo.app.plugin.PluginConst.RUNTIME_MODE_ANNO;\n\nimport java.io.IOException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Clock;\nimport java.time.Instant;\nimport java.time.ZoneOffset;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport org.jspecify.annotations.Nullable;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.DefaultPluginDescriptor;\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginWrapper;\nimport org.pf4j.RuntimeMode;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport reactor.core.scheduler.Schedulers;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.Reconciler.Request;\nimport run.halo.app.extension.controller.RequeueException;\nimport run.halo.app.infra.Condition;\nimport run.halo.app.infra.ConditionStatus;\nimport run.halo.app.plugin.PluginProperties;\nimport run.halo.app.plugin.PluginService;\nimport run.halo.app.plugin.SpringPluginManager;\n\n/**\n * Tests for {@link PluginReconciler}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PluginReconcilerTest {\n\n    @Mock\n    SpringPluginManager pluginManager;\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    PluginProperties pluginProperties;\n\n    @Mock\n    PluginService pluginService;\n\n    @InjectMocks\n    PluginReconciler reconciler;\n\n    Clock clock = Clock.fixed(Instant.parse(\"2024-01-09T12:00:00Z\"), ZoneOffset.UTC);\n\n    String finalizer = \"plugin-protection\";\n    String name = \"fake-plugin\";\n\n    String reverseProxyName = \"fake-plugin-system-generated-reverse-proxy\";\n\n    String settingName = \"fake-setting\";\n\n    String configMapName = \"fake-configmap\";\n\n    @BeforeEach\n    void setUp() {\n        reconciler.setClock(clock);\n        reconciler.setScheduler(Schedulers.immediate());\n    }\n\n    @Test\n    void shouldNotRequeueIfPluginNotFound() {\n        when(client.fetch(Plugin.class, \"fake-plugin\")).thenReturn(Optional.empty());\n        var result = reconciler.reconcile(new Request(\"fake-plugin\"));\n        assertFalse(result.reEnqueue());\n        verify(client).fetch(Plugin.class, \"fake-plugin\");\n    }\n\n    @Nested\n    class WhenNotDeleting {\n\n        @TempDir\n        Path tempPath;\n\n        @BeforeEach\n        void setUp() throws IOException {\n            lenient().when(pluginService.getRequiredDependencies(any(), any()))\n                .thenReturn(List.of());\n            Files.createFile(tempPath.resolve(\"fake-plugin-1.2.3.jar\"));\n        }\n\n        @Test\n        void shouldNotStartPluginWithDevModeInNonDevEnv() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var spec = plugin.getSpec();\n                spec.setVersion(\"1.2.3\");\n                spec.setLogo(\"fake-logo.svg\");\n                spec.setEnabled(true);\n                plugin.getMetadata()\n                    .setAnnotations(new HashMap<>(Map.of(RUNTIME_MODE_ANNO, \"dev\",\n                        PLUGIN_PATH, \"fake-path\")));\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n\n            var result = reconciler.reconcile(new Request(name));\n            assertFalse(result.reEnqueue());\n\n            var status = fakePlugin.getStatus();\n            assertEquals(Plugin.Phase.UNKNOWN, status.getPhase());\n            var condition = status.getConditions().peekFirst();\n            assertEquals(Condition.builder()\n                .type(PluginReconciler.ConditionType.INITIALIZED)\n                .status(ConditionStatus.FALSE)\n                .reason(PluginReconciler.ConditionReason.INVALID_RUNTIME_MODE)\n                .message(\"\"\"\n                    Cannot run the plugin with development mode in non-development environment.\\\n                    \"\"\")\n                .build(), condition);\n\n            verify(client).update(fakePlugin);\n            verify(client).fetch(Plugin.class, name);\n            verify(pluginProperties).getRuntimeMode();\n            verify(pluginManager, never()).loadPlugin(any(Path.class));\n            verify(pluginManager, never()).startPlugin(name);\n        }\n\n        @Test\n        void shouldStartInDevMode() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var spec = plugin.getSpec();\n                spec.setVersion(\"1.2.3\");\n                spec.setLogo(\"fake-logo.svg\");\n                spec.setEnabled(true);\n                plugin.getMetadata()\n                    .setAnnotations(new HashMap<>(Map.of(RUNTIME_MODE_ANNO, \"dev\",\n                        PLUGIN_PATH, \"fake-path\")));\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(pluginManager.getPlugin(name))\n                .thenReturn(null)\n                .thenReturn(mockPluginWrapper(PluginState.RESOLVED));\n\n            when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);\n            when(pluginProperties.getRuntimeMode()).thenReturn(RuntimeMode.DEVELOPMENT);\n\n            var result = reconciler.reconcile(new Request(name));\n            assertTrue(result.reEnqueue());\n            assertEquals(Paths.get(\"fake-path\").toUri(), fakePlugin.getStatus().getLoadLocation());\n\n            verify(pluginManager).startPlugin(name);\n        }\n\n        @Test\n        void shouldThrowExceptionIfNoPluginPathProvidedInDevMode() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var spec = plugin.getSpec();\n                spec.setVersion(\"1.2.3\");\n                spec.setLogo(\"fake-logo.svg\");\n                spec.setEnabled(true);\n                plugin.getMetadata()\n                    .setAnnotations(new HashMap<>(Map.of(RUNTIME_MODE_ANNO, \"dev\")));\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(pluginManager.getPlugin(name))\n                // loading plugin\n                .thenReturn(null);\n            when(pluginProperties.getRuntimeMode()).thenReturn(RuntimeMode.DEVELOPMENT);\n\n            var result = reconciler.reconcile(new Request(name));\n            assertFalse(result.reEnqueue());\n        }\n\n        @Test\n        void shouldReloadIfReloadAnnotationPresent() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var spec = plugin.getSpec();\n                spec.setVersion(\"1.2.3\");\n                spec.setLogo(\"fake-logo.svg\");\n                spec.setEnabled(true);\n                plugin.getMetadata().setAnnotations(new HashMap<>(Map.of(RELOAD_ANNO, \"true\")));\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));\n            var pluginWrapper = mockPluginWrapper(PluginState.RESOLVED);\n            when(pluginManager.getPlugin(name)).thenReturn(pluginWrapper);\n            when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);\n            when(pluginManager.getUnresolvedPlugins()).thenReturn(List.of(pluginWrapper));\n            when(pluginManager.getResolvedPlugins()).thenReturn(List.of());\n\n            var result = reconciler.reconcile(new Request(name));\n            assertTrue(result.reEnqueue());\n\n            verify(pluginManager).unloadPlugin(name);\n            var loadLocation = Paths.get(fakePlugin.getStatus().getLoadLocation());\n            verify(pluginManager).loadPlugin(loadLocation);\n        }\n\n        @Test\n        void shouldReportIfFailedToStartPlugin() throws IOException {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var spec = plugin.getSpec();\n                spec.setVersion(\"1.2.3\");\n                spec.setLogo(\"fake-logo.svg\");\n                spec.setEnabled(true);\n                spec.setSettingName(settingName);\n                spec.setConfigMapName(configMapName);\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));\n            when(pluginManager.getPlugin(name))\n                // loading plugin\n                .thenReturn(null)\n                // get setting extension\n                .thenReturn(mockPluginWrapperForSetting())\n                .thenReturn(mockPluginWrapperForStaticResources())\n                .thenReturn(\n                    mockPluginWrapper(PluginState.FAILED, new IllegalStateException(\"Fake error\"))\n                );\n            var result = reconciler.reconcile(new Request(name));\n            assertFalse(result.reEnqueue());\n\n            verify(client).update(fakePlugin);\n            var status = fakePlugin.getStatus();\n            assertEquals(Plugin.Phase.FAILED, status.getPhase());\n            var condition = status.getConditions().peekFirst();\n            assertEquals(PluginReconciler.ConditionType.READY, condition.getType());\n            assertEquals(ConditionStatus.FALSE, condition.getStatus());\n            assertEquals(PluginReconciler.ConditionReason.START_ERROR, condition.getReason());\n            assertTrue(condition.getMessage().contains(\"Fake error\"));\n\n            verify(pluginManager, never()).startPlugin(name);\n        }\n\n        @Test\n        void shouldEnablePluginIfEnabled() throws IOException {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var spec = plugin.getSpec();\n                spec.setVersion(\"1.2.3\");\n                spec.setLogo(\"fake-logo.svg\");\n                spec.setEnabled(true);\n                spec.setSettingName(settingName);\n                spec.setConfigMapName(configMapName);\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));\n            when(pluginManager.getPlugin(name))\n                // loading plugin\n                .thenReturn(null)\n                // get setting extension\n                .thenReturn(mockPluginWrapperForSetting())\n                .thenReturn(mockPluginWrapperForStaticResources())\n                // before starting\n                .thenReturn(mockPluginWrapper(PluginState.STARTED))\n                // sync plugin state\n                .thenReturn(mockPluginWrapper(PluginState.STARTED));\n\n            var result = reconciler.reconcile(new Request(name));\n\n            assertFalse(result.reEnqueue());\n            assertTrue(fakePlugin.getMetadata().getFinalizers().contains(finalizer));\n\n            assertEquals(\"fake-plugin-1.2.3.jar\",\n                fakePlugin.getMetadata().getAnnotations().get(PLUGIN_PATH));\n            var loadLocation = Paths.get(fakePlugin.getStatus().getLoadLocation());\n            assertEquals(tempPath.resolve(\"fake-plugin-1.2.3.jar\"), loadLocation);\n            assertEquals(\"/plugins/fake-plugin/assets/fake-logo.svg?version=1.2.3\",\n                fakePlugin.getStatus().getLogo());\n            assertEquals(\"/plugins/fake-plugin/assets/console/main.js?version=1.2.3\",\n                fakePlugin.getStatus().getEntry());\n            assertEquals(\"/plugins/fake-plugin/assets/console/style.css?version=1.2.3\",\n                fakePlugin.getStatus().getStylesheet());\n            assertEquals(Plugin.Phase.STARTED, fakePlugin.getStatus().getPhase());\n            assertEquals(PluginState.STARTED, fakePlugin.getStatus().getLastProbeState());\n            assertNotNull(fakePlugin.getStatus().getLastStartTime());\n\n            var condition = fakePlugin.getStatus().getConditions().peek();\n            assertEquals(PluginReconciler.ConditionType.READY, condition.getType());\n            assertEquals(ConditionStatus.TRUE, condition.getStatus());\n            assertEquals(clock.instant(), condition.getLastTransitionTime());\n\n            verify(pluginManager, never()).startPlugin(name);\n            verify(pluginManager).loadPlugin(loadLocation);\n            verify(pluginManager, times(5)).getPlugin(name);\n            verify(client).update(fakePlugin);\n            verify(client).fetch(Setting.class, settingName);\n            verify(client).create(any(Setting.class));\n            verify(client).fetch(ConfigMap.class, configMapName);\n            verify(client).create(any(ConfigMap.class));\n            verify(client).fetch(ReverseProxy.class, reverseProxyName);\n            verify(client).create(any(ReverseProxy.class));\n        }\n\n        @Test\n        void shouldDisablePluginIfDisabled() throws IOException {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var spec = plugin.getSpec();\n                spec.setVersion(\"1.2.3\");\n                spec.setLogo(\"fake-logo.svg\");\n                spec.setEnabled(false);\n                spec.setSettingName(settingName);\n                spec.setConfigMapName(configMapName);\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));\n\n            when(pluginManager.getPlugin(name))\n                // loading plugin\n                .thenReturn(null)\n                // get setting files.\n                .thenReturn(mockPluginWrapperForSetting())\n                // resolving static resources\n                .thenReturn(mockPluginWrapperForStaticResources())\n                // before disabling plugin\n                .thenReturn(mock(PluginWrapper.class))\n                // sync plugin state\n                .thenReturn(mockPluginWrapper(PluginState.DISABLED));\n\n            var result = reconciler.reconcile(new Request(\"fake-plugin\"));\n\n            assertFalse(result.reEnqueue());\n            assertTrue(fakePlugin.getMetadata().getFinalizers().contains(finalizer));\n\n            assertEquals(\"fake-plugin-1.2.3.jar\",\n                fakePlugin.getMetadata().getAnnotations().get(PLUGIN_PATH));\n            var loadLocation = Paths.get(fakePlugin.getStatus().getLoadLocation());\n            assertEquals(tempPath.resolve(\"fake-plugin-1.2.3.jar\"), loadLocation);\n            assertEquals(\"/plugins/fake-plugin/assets/fake-logo.svg?version=1.2.3\",\n                fakePlugin.getStatus().getLogo());\n            assertEquals(\"/plugins/fake-plugin/assets/console/main.js?version=1.2.3\",\n                fakePlugin.getStatus().getEntry());\n            assertEquals(\"/plugins/fake-plugin/assets/console/style.css?version=1.2.3\",\n                fakePlugin.getStatus().getStylesheet());\n            assertEquals(Plugin.Phase.DISABLED, fakePlugin.getStatus().getPhase());\n            assertEquals(PluginState.DISABLED, fakePlugin.getStatus().getLastProbeState());\n\n            verify(pluginManager).disablePlugin(name);\n            verify(pluginManager).loadPlugin(loadLocation);\n            verify(pluginManager, times(5)).getPlugin(name);\n            verify(client).update(fakePlugin);\n            verify(client).fetch(Setting.class, settingName);\n            verify(client).create(any(Setting.class));\n            verify(client).fetch(ConfigMap.class, configMapName);\n            verify(client).create(any(ConfigMap.class));\n            verify(client).fetch(ReverseProxy.class, reverseProxyName);\n            verify(client).create(any(ReverseProxy.class));\n        }\n\n        PluginWrapper mockPluginWrapperForSetting() throws IOException {\n            var pluginWrapper = mock(PluginWrapper.class);\n\n            var pluginRootResource =\n                new DefaultResourceLoader().getResource(\"classpath:plugin/plugin-0.0.1/\");\n            var classLoader = new URLClassLoader(new URL[] {pluginRootResource.getURL()}, null);\n            when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);\n            lenient().when(pluginWrapper.getDescriptor()).thenReturn(new DefaultPluginDescriptor());\n            return pluginWrapper;\n        }\n\n        PluginWrapper mockPluginWrapperForStaticResources() {\n            // check\n            var pluginWrapper = mock(PluginWrapper.class);\n            var pluginClassLoader = mock(ClassLoader.class);\n            when(pluginClassLoader.getResource(\"console/main.js\")).thenReturn(\n                mock(URL.class));\n            when(pluginClassLoader.getResource(\"console/style.css\")).thenReturn(\n                mock(URL.class));\n            when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader);\n            lenient().when(pluginWrapper.getDescriptor()).thenReturn(new DefaultPluginDescriptor());\n            return pluginWrapper;\n        }\n\n        PluginWrapper mockPluginWrapper(PluginState state) {\n            return mockPluginWrapper(state, null);\n        }\n\n        PluginWrapper mockPluginWrapper(PluginState state, @Nullable Throwable t) {\n            var pluginWrapper = mock(PluginWrapper.class);\n            lenient().when(pluginWrapper.getPluginState()).thenReturn(state);\n            lenient().when(pluginWrapper.getDescriptor()).thenReturn(new DefaultPluginDescriptor());\n            lenient().when(pluginWrapper.getFailedException()).thenReturn(t);\n            return pluginWrapper;\n        }\n\n    }\n\n    @Nested\n    class WhenDeleting {\n\n        @Test\n        void shouldDoNothingWithoutFinalizer() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var metadata = plugin.getMetadata();\n                metadata.setDeletionTimestamp(clock.instant());\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n\n            var result = reconciler.reconcile(new Request(name));\n            assertFalse(result.reEnqueue());\n            verify(client).fetch(Plugin.class, name);\n            verify(client, never()).update(fakePlugin);\n            verify(pluginManager, never()).getPlugin(name);\n            verify(pluginManager, never()).deletePlugin(name);\n        }\n\n        @Test\n        void shouldCleanUpResourceFully() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var metadata = plugin.getMetadata();\n                metadata.setDeletionTimestamp(clock.instant());\n                metadata.setFinalizers(new HashSet<>(Set.of(finalizer)));\n                plugin.getStatus().setLastProbeState(PluginState.STARTED);\n                plugin.getSpec().setConfigMapName(\"fake-configmap\");\n                plugin.getSpec().setSettingName(\"fake-setting\");\n            });\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(client.fetch(Setting.class, \"fake-setting\"))\n                .thenReturn(Optional.empty());\n            when(client.fetch(ReverseProxy.class, reverseProxyName))\n                .thenReturn(Optional.empty());\n\n            when(pluginManager.getPlugin(name))\n                .thenReturn(mock(PluginWrapper.class))\n                .thenReturn(null);\n\n            var result = reconciler.reconcile(new Request(name));\n\n            assertFalse(result.reEnqueue());\n            // make sure the finalizer is removed.\n            assertFalse(fakePlugin.getMetadata().getFinalizers().contains(finalizer));\n            assertNull(fakePlugin.getStatus().getLastProbeState());\n            verify(pluginManager, times(2)).getPlugin(name);\n            verify(pluginManager).deletePlugin(name);\n            verify(client).fetch(Plugin.class, name);\n            verify(client).fetch(Setting.class, \"fake-setting\");\n            verify(client).fetch(ReverseProxy.class, reverseProxyName);\n            verify(client).update(fakePlugin);\n        }\n\n        @Test\n        void shouldDeleteSettingAndRequeueIfExists() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var metadata = plugin.getMetadata();\n                metadata.setDeletionTimestamp(clock.instant());\n                metadata.setFinalizers(new HashSet<>(Set.of(finalizer)));\n                plugin.getStatus().setLastProbeState(PluginState.STARTED);\n                plugin.getSpec().setSettingName(settingName);\n            });\n\n            var fakeSetting = createSetting(settingName);\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(client.fetch(Setting.class, settingName))\n                .thenReturn(Optional.of(fakeSetting));\n            when(client.fetch(ReverseProxy.class, reverseProxyName))\n                .thenReturn(Optional.empty());\n\n            var exception = assertThrows(\n                RequeueException.class,\n                () -> reconciler.reconcile(new Request(name))\n            );\n            assertEquals(Reconciler.Result.requeue(null), exception.getResult());\n            assertEquals(\"Waiting for setting fake-setting to be deleted.\", exception.getMessage());\n\n            // make sure the finalizer is removed.\n            assertFalse(fakePlugin.getMetadata().getFinalizers().contains(finalizer));\n            assertEquals(PluginState.STARTED, fakePlugin.getStatus().getLastProbeState());\n            verify(pluginManager, never()).getPlugin(name);\n            verify(pluginManager, never()).deletePlugin(name);\n            verify(client).fetch(Plugin.class, name);\n            verify(client).fetch(ReverseProxy.class, reverseProxyName);\n            verify(client).fetch(Setting.class, settingName);\n            verify(client).delete(fakeSetting);\n            verify(client, never()).update(fakePlugin);\n        }\n\n        @Test\n        void shouldDeleteReverseProxyAndRequeueIfExists() {\n            var fakePlugin = createPlugin(name, plugin -> {\n                var metadata = plugin.getMetadata();\n                metadata.setDeletionTimestamp(clock.instant());\n                metadata.setFinalizers(new HashSet<>(Set.of(finalizer)));\n                plugin.getStatus().setLastProbeState(PluginState.STARTED);\n                plugin.getSpec().setSettingName(settingName);\n            });\n\n            var reverseProxy = createReverseProxy(reverseProxyName);\n\n            when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));\n            when(client.fetch(ReverseProxy.class, reverseProxyName))\n                .thenReturn(Optional.of(reverseProxy));\n\n            var exception = assertThrows(RequeueException.class,\n                () -> reconciler.reconcile(new Request(name)),\n                \"Waiting for setting fake-setting to be deleted.\");\n            assertEquals(Reconciler.Result.requeue(null), exception.getResult());\n            assertEquals(\"Waiting for reverse proxy \" + reverseProxyName + \" to be deleted.\",\n                exception.getMessage());\n\n            // make sure the finalizer is removed.\n            assertFalse(fakePlugin.getMetadata().getFinalizers().contains(finalizer));\n            assertEquals(PluginState.STARTED, fakePlugin.getStatus().getLastProbeState());\n            verify(pluginManager, never()).getPlugin(name);\n            verify(pluginManager, never()).deletePlugin(name);\n            verify(client).fetch(Plugin.class, name);\n            verify(client).fetch(ReverseProxy.class, reverseProxyName);\n            verify(client).delete(reverseProxy);\n            verify(client, never()).fetch(Setting.class, settingName);\n            verify(client, never()).update(fakePlugin);\n        }\n\n    }\n\n    Setting createSetting(String name) {\n        var setting = new Setting();\n        var metadata = new Metadata();\n        metadata.setName(name);\n        setting.setMetadata(metadata);\n        return setting;\n    }\n\n    ReverseProxy createReverseProxy(String name) {\n        var reverseProxy = new ReverseProxy();\n        var metadata = new Metadata();\n        metadata.setName(name);\n        reverseProxy.setMetadata(metadata);\n        return reverseProxy;\n    }\n\n    Plugin createPlugin(String name, Consumer<Plugin> pluginConsumer) {\n        var plugin = new Plugin();\n        var metadata = new Metadata();\n        plugin.setMetadata(metadata);\n        metadata.setName(name);\n        plugin.setSpec(new Plugin.PluginSpec());\n        plugin.setStatus(new Plugin.PluginStatus());\n        pluginConsumer.accept(plugin);\n        return plugin;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/PostReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationEventPublisher;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.ExcerptGenerator;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.content.PostService;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.content.permalinks.PostPermalinkPolicy;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.core.reconciler.PostReconciler;\nimport run.halo.app.event.post.PostPublishedEvent;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Tests for {@link PostReconciler}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostReconcilerTest {\n\n    @Mock\n    private ExtensionClient client;\n\n    @Mock\n    private PostPermalinkPolicy postPermalinkPolicy;\n\n    @Mock\n    private PostService postService;\n\n    @Mock\n    private ApplicationEventPublisher eventPublisher;\n\n    @Mock\n    private NotificationCenter notificationCenter;\n\n    @Mock\n    private ExtensionGetter extensionGetter;\n\n    @InjectMocks\n    private PostReconciler postReconciler;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(notificationCenter.subscribe(any(), any())).thenReturn(Mono.empty());\n    }\n\n    @Test\n    void reconcile() {\n        String name = \"post-A\";\n        Post post = TestPost.postV1();\n        post.getSpec().setPublish(false);\n        post.getSpec().setHeadSnapshot(\"post-A-head-snapshot\");\n        when(client.fetch(eq(Post.class), eq(name)))\n            .thenReturn(Optional.of(post));\n        when(postService.getContent(eq(post.getSpec().getReleaseSnapshot()),\n            eq(post.getSpec().getBaseSnapshot())))\n            .thenReturn(Mono.empty());\n\n        Snapshot snapshotV1 = TestPost.snapshotV1();\n        Snapshot snapshotV2 = TestPost.snapshotV2();\n        snapshotV1.getSpec().setContributors(Set.of(\"guqing\"));\n        snapshotV2.getSpec().setContributors(Set.of(\"guqing\", \"zhangsan\"));\n        when(client.listAll(eq(Snapshot.class), any(), any()))\n            .thenReturn(List.of(snapshotV1, snapshotV2));\n\n        ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);\n        postReconciler.reconcile(new Reconciler.Request(name));\n\n        verify(client, times(1)).update(captor.capture());\n\n        verify(postPermalinkPolicy, times(1)).permalink(any());\n\n        Post value = captor.getValue();\n        assertThat(value.getStatus().getExcerpt()).isEmpty();\n        assertThat(value.getStatus().getContributors()).isEqualTo(List.of(\"guqing\", \"zhangsan\"));\n    }\n\n    @Test\n    void shouldGenerateBlankExcerptWhenContentIsNull() {\n        var name = \"post-A\";\n        Post post = TestPost.postV1();\n        post.getSpec().setPublish(true);\n        post.getSpec().setHeadSnapshot(\"post-A-head-snapshot\");\n        post.getSpec().setReleaseSnapshot(\"post-fake-released-snapshot\");\n        when(client.fetch(eq(Post.class), eq(name)))\n            .thenReturn(Optional.of(post));\n        when(postService.getContent(eq(post.getSpec().getReleaseSnapshot()),\n            eq(post.getSpec().getBaseSnapshot())))\n            .thenReturn(Mono.just(ContentWrapper.builder()\n                .snapshotName(post.getSpec().getHeadSnapshot())\n                .raw(null)\n                .content(null)\n                .rawType(\"markdown\")\n                .build()));\n\n        Snapshot snapshotV2 = TestPost.snapshotV2();\n        snapshotV2.getMetadata().setLabels(new HashMap<>());\n        snapshotV2.getSpec().setContributors(Set.of(\"guqing\", \"zhangsan\"));\n\n        Snapshot snapshotV1 = TestPost.snapshotV1();\n        snapshotV1.getSpec().setContributors(Set.of(\"guqing\"));\n\n        when(client.listAll(eq(Snapshot.class), any(), any()))\n            .thenReturn(List.of(snapshotV1, snapshotV2));\n\n        ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);\n        postReconciler.reconcile(new Reconciler.Request(name));\n\n        verify(client, times(1)).update(captor.capture());\n        Post value = captor.getValue();\n        assertThat(value.getStatus().getExcerpt()).isEqualTo(\"\");\n    }\n\n    @Test\n    void reconcileExcerpt() {\n        // https://github.com/halo-dev/halo/issues/2452\n        String name = \"post-A\";\n        Post post = TestPost.postV1();\n        post.getSpec().setPublish(true);\n        post.getSpec().setHeadSnapshot(\"post-A-head-snapshot\");\n        post.getSpec().setReleaseSnapshot(\"post-fake-released-snapshot\");\n        when(client.fetch(eq(Post.class), eq(name)))\n            .thenReturn(Optional.of(post));\n        when(postService.getContent(eq(post.getSpec().getReleaseSnapshot()),\n            eq(post.getSpec().getBaseSnapshot())))\n            .thenReturn(Mono.just(ContentWrapper.builder()\n                .snapshotName(post.getSpec().getHeadSnapshot())\n                .raw(\"hello world\")\n                .content(\"<p>hello world</p>\")\n                .rawType(\"markdown\")\n                .build()));\n\n        Snapshot snapshotV2 = TestPost.snapshotV2();\n        snapshotV2.getMetadata().setLabels(new HashMap<>());\n        snapshotV2.getSpec().setContributors(Set.of(\"guqing\", \"zhangsan\"));\n\n        Snapshot snapshotV1 = TestPost.snapshotV1();\n        snapshotV1.getSpec().setContributors(Set.of(\"guqing\"));\n\n        when(extensionGetter.getEnabledExtension(eq(ExcerptGenerator.class)))\n            .thenReturn(Mono.empty());\n\n        when(client.listAll(eq(Snapshot.class), any(), any()))\n            .thenReturn(List.of(snapshotV1, snapshotV2));\n\n        ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);\n        postReconciler.reconcile(new Reconciler.Request(name));\n\n        verify(client, times(1)).update(captor.capture());\n        Post value = captor.getValue();\n        assertThat(value.getStatus().getExcerpt()).isEqualTo(\"hello world\");\n    }\n\n    @Nested\n    class LastModifyTimeTest {\n        @Test\n        void reconcileLastModifyTimeWhenPostIsPublished() {\n            String name = \"post-A\";\n            Post post = TestPost.postV1();\n            post.getSpec().setPublish(true);\n            post.getSpec().setHeadSnapshot(\"post-A-head-snapshot\");\n            post.getSpec().setReleaseSnapshot(\"post-fake-released-snapshot\");\n            when(client.fetch(eq(Post.class), eq(name)))\n                .thenReturn(Optional.of(post));\n            when(postService.getContent(eq(post.getSpec().getReleaseSnapshot()),\n                eq(post.getSpec().getBaseSnapshot())))\n                .thenReturn(Mono.just(ContentWrapper.builder()\n                    .snapshotName(post.getSpec().getHeadSnapshot())\n                    .raw(\"hello world\")\n                    .content(\"<p>hello world</p>\")\n                    .rawType(\"markdown\")\n                    .build()));\n            Instant lastModifyTime = Instant.now();\n            Snapshot snapshotV2 = TestPost.snapshotV2();\n            snapshotV2.getSpec().setLastModifyTime(lastModifyTime);\n            when(client.fetch(eq(Snapshot.class), eq(post.getSpec().getReleaseSnapshot())))\n                .thenReturn(Optional.of(snapshotV2));\n\n            when(extensionGetter.getEnabledExtension(eq(ExcerptGenerator.class)))\n                .thenReturn(Mono.empty());\n\n            when(client.listAll(eq(Snapshot.class), any(), any()))\n                .thenReturn(List.of());\n\n            ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);\n            postReconciler.reconcile(new Reconciler.Request(name));\n\n            verify(client, times(1)).update(captor.capture());\n            Post value = captor.getValue();\n            assertThat(value.getStatus().getLastModifyTime()).isEqualTo(lastModifyTime);\n            verify(eventPublisher).publishEvent(any(PostPublishedEvent.class));\n        }\n\n        @Test\n        void reconcileLastModifyTimeWhenPostIsNotPublished() {\n            String name = \"post-A\";\n            Post post = TestPost.postV1();\n            post.getSpec().setPublish(false);\n            post.getSpec().setHeadSnapshot(\"post-A-head-snapshot\");\n            when(client.fetch(eq(Post.class), eq(name)))\n                .thenReturn(Optional.of(post));\n            when(postService.getContent(eq(post.getSpec().getReleaseSnapshot()),\n                eq(post.getSpec().getBaseSnapshot())))\n                .thenReturn(Mono.just(ContentWrapper.builder()\n                    .snapshotName(post.getSpec().getHeadSnapshot())\n                    .raw(\"hello world\")\n                    .content(\"<p>hello world</p>\")\n                    .rawType(\"markdown\")\n                    .build()));\n\n            when(extensionGetter.getEnabledExtension(eq(ExcerptGenerator.class)))\n                .thenReturn(Mono.empty());\n\n            when(client.listAll(eq(Snapshot.class), any(), any()))\n                .thenReturn(List.of());\n\n            ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);\n            postReconciler.reconcile(new Reconciler.Request(name));\n\n            verify(client, times(1)).update(captor.capture());\n            Post value = captor.getValue();\n            assertThat(value.getStatus().getLastModifyTime()).isNull();\n        }\n    }\n\n    @Test\n    void subscribeNewCommentNotificationTest() {\n        Post post = TestPost.postV1();\n\n        postReconciler.subscribeNewCommentNotification(post);\n\n        verify(notificationCenter).subscribe(\n            assertArg(subscriber -> assertThat(subscriber.getName())\n                .isEqualTo(post.getSpec().getOwner())),\n            assertArg(argReason -> {\n                var interestReason = new Subscription.InterestReason();\n                interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_POST);\n                interestReason.setExpression(\"props.postOwner == 'null'\");\n                assertThat(argReason).isEqualTo(interestReason);\n            }));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/ReverseProxyReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.core.reconciler.ReverseProxyReconciler;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.plugin.PluginConst;\nimport run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;\n\n/**\n * Tests for {@link ReverseProxyReconciler}.\n *\n * @author guqing\n * @since 2.0.1\n */\n@ExtendWith(MockitoExtension.class)\nclass ReverseProxyReconcilerTest {\n\n    @Mock\n    private ExtensionClient client;\n\n    @Mock\n    private ReverseProxyRouterFunctionRegistry routerFunctionRegistry;\n\n    @InjectMocks\n    private ReverseProxyReconciler reverseProxyReconciler;\n\n    @Test\n    void reconcileRemoval() {\n        // fix gh-2937\n        ReverseProxy reverseProxy = new ReverseProxy();\n        reverseProxy.setMetadata(new Metadata());\n        reverseProxy.getMetadata().setName(\"fake-reverse-proxy\");\n        reverseProxy.getMetadata().setDeletionTimestamp(Instant.now());\n        reverseProxy.getMetadata()\n            .setLabels(Map.of(PluginConst.PLUGIN_NAME_LABEL_NAME, \"fake-plugin\"));\n        reverseProxy.setRules(List.of());\n\n        doNothing().when(routerFunctionRegistry).remove(anyString(), anyString());\n        when(client.fetch(ReverseProxy.class, \"fake-reverse-proxy\"))\n            .thenReturn(Optional.of(reverseProxy));\n\n        reverseProxyReconciler.reconcile(new Reconciler.Request(\"fake-reverse-proxy\"));\n\n        verify(routerFunctionRegistry, never()).register(anyString(), any(ReverseProxy.class));\n\n        verify(routerFunctionRegistry, times(1))\n            .remove(eq(\"fake-plugin\"), eq(\"fake-reverse-proxy\"));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/SinglePageReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.content.TestPost.snapshotV1;\n\nimport java.net.URI;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationContext;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.ExcerptGenerator;\nimport run.halo.app.content.NotificationReasonConst;\nimport run.halo.app.content.SinglePageService;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.core.extension.content.Snapshot;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.notification.NotificationCenter;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Tests for {@link SinglePageReconciler}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SinglePageReconcilerTest {\n    @Mock\n    private ExtensionClient client;\n\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private CounterService counterService;\n\n    @Mock\n    private SinglePageService singlePageService;\n\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @Mock\n    NotificationCenter notificationCenter;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    @InjectMocks\n    private SinglePageReconciler singlePageReconciler;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(notificationCenter.subscribe(any(), any())).thenReturn(Mono.empty());\n    }\n\n    @Test\n    void reconcile() {\n        String name = \"page-A\";\n        SinglePage page = pageV1();\n        page.getSpec().setHeadSnapshot(\"page-A-head-snapshot\");\n        page.getSpec().setReleaseSnapshot(page.getSpec().getHeadSnapshot());\n        when(client.fetch(eq(SinglePage.class), eq(name)))\n            .thenReturn(Optional.of(page));\n        when(singlePageService.getContent(eq(page.getSpec().getReleaseSnapshot()),\n            eq(page.getSpec().getBaseSnapshot())))\n            .thenReturn(Mono.just(ContentWrapper.builder()\n                .snapshotName(page.getSpec().getHeadSnapshot())\n                .raw(\"hello world\")\n                .content(\"<p>hello world</p>\")\n                .rawType(\"markdown\")\n                .build())\n            );\n\n        Snapshot snapshotV1 = snapshotV1();\n        Snapshot snapshotV2 = TestPost.snapshotV2();\n        snapshotV1.getSpec().setContributors(Set.of(\"guqing\"));\n        snapshotV2.getSpec().setContributors(Set.of(\"guqing\", \"zhangsan\"));\n        when(client.listAll(eq(Snapshot.class), any(), any()))\n            .thenReturn(List.of(snapshotV1, snapshotV2));\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n\n        when(extensionGetter.getEnabledExtension(eq(ExcerptGenerator.class)))\n            .thenReturn(Mono.empty());\n\n        ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);\n        singlePageReconciler.reconcile(new Reconciler.Request(name));\n\n        verify(client, times(3)).update(captor.capture());\n\n        SinglePage value = captor.getValue();\n        assertThat(value.getStatus().getExcerpt()).isEqualTo(\"hello world\");\n        assertThat(value.getStatus().getContributors()).isEqualTo(List.of(\"guqing\", \"zhangsan\"));\n    }\n\n    @Test\n    void createPermalink() {\n        SinglePage page = pageV1();\n        page.getSpec().setSlug(\"page-slug\");\n\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n\n        String permalink = singlePageReconciler.createPermalink(page);\n        assertThat(permalink).isEqualTo(\"/page-slug\");\n\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"http://example.com\"));\n        permalink = singlePageReconciler.createPermalink(page);\n        assertThat(permalink).isEqualTo(\"http://example.com/page-slug\");\n\n        page.getSpec().setSlug(\"中文 slug\");\n        permalink = singlePageReconciler.createPermalink(page);\n        assertThat(permalink).isEqualTo(\"http://example.com/%E4%B8%AD%E6%96%87%20slug\");\n    }\n\n    @Nested\n    class LastModifyTimeTest {\n        @Test\n        void reconcileLastModifyTimeWhenPageIsPublished() {\n            String name = \"page-A\";\n            when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n\n            SinglePage page = pageV1();\n            page.getSpec().setPublish(true);\n            page.getSpec().setHeadSnapshot(\"page-A-head-snapshot\");\n            page.getSpec().setReleaseSnapshot(\"page-fake-released-snapshot\");\n            when(client.fetch(eq(SinglePage.class), eq(name)))\n                .thenReturn(Optional.of(page));\n            when(singlePageService.getContent(eq(page.getSpec().getReleaseSnapshot()),\n                eq(page.getSpec().getBaseSnapshot())))\n                .thenReturn(Mono.just(ContentWrapper.builder()\n                    .snapshotName(page.getSpec().getHeadSnapshot())\n                    .raw(\"hello world\")\n                    .content(\"<p>hello world</p>\")\n                    .rawType(\"markdown\")\n                    .build())\n                );\n            Instant lastModifyTime = Instant.now();\n            Snapshot snapshotV2 = TestPost.snapshotV2();\n            snapshotV2.getSpec().setLastModifyTime(lastModifyTime);\n            when(client.fetch(eq(Snapshot.class), eq(page.getSpec().getReleaseSnapshot())))\n                .thenReturn(Optional.of(snapshotV2));\n\n            when(extensionGetter.getEnabledExtension(eq(ExcerptGenerator.class)))\n                .thenReturn(Mono.empty());\n\n            when(client.listAll(eq(Snapshot.class), any(), any()))\n                .thenReturn(List.of());\n\n            ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);\n            singlePageReconciler.reconcile(new Reconciler.Request(name));\n\n            verify(client, times(4)).update(captor.capture());\n            SinglePage value = captor.getValue();\n            assertThat(value.getStatus().getLastModifyTime()).isEqualTo(lastModifyTime);\n        }\n\n        @Test\n        void reconcileLastModifyTimeWhenPageIsNotPublished() {\n            String name = \"page-A\";\n            when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n\n            SinglePage page = pageV1();\n            page.getSpec().setPublish(false);\n            when(client.fetch(eq(SinglePage.class), eq(name)))\n                .thenReturn(Optional.of(page));\n            when(singlePageService.getContent(eq(page.getSpec().getReleaseSnapshot()),\n                eq(page.getSpec().getBaseSnapshot())))\n                .thenReturn(Mono.just(ContentWrapper.builder()\n                    .snapshotName(page.getSpec().getHeadSnapshot())\n                    .raw(\"hello world\")\n                    .content(\"<p>hello world</p>\")\n                    .rawType(\"markdown\")\n                    .build())\n                );\n\n            when(extensionGetter.getEnabledExtension(eq(ExcerptGenerator.class)))\n                .thenReturn(Mono.empty());\n\n            when(client.listAll(eq(Snapshot.class), any(), any()))\n                .thenReturn(List.of());\n\n            ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);\n            singlePageReconciler.reconcile(new Reconciler.Request(name));\n\n            verify(client, times(3)).update(captor.capture());\n            SinglePage value = captor.getValue();\n            assertThat(value.getStatus().getLastModifyTime()).isNull();\n        }\n    }\n\n    public static SinglePage pageV1() {\n        SinglePage page = new SinglePage();\n        page.setKind(Post.KIND);\n\n        page.setApiVersion(\"content.halo.run/v1alpha1\");\n        Metadata metadata = new Metadata();\n        metadata.setName(\"page-A\");\n        page.setMetadata(metadata);\n\n        SinglePage.SinglePageSpec spec = new SinglePage.SinglePageSpec();\n        page.setSpec(spec);\n\n        spec.setTitle(\"page-A\");\n        spec.setSlug(\"page-slug\");\n        spec.setBaseSnapshot(snapshotV1().getMetadata().getName());\n        spec.setHeadSnapshot(\"base-snapshot\");\n        spec.setReleaseSnapshot(null);\n\n        return page;\n    }\n\n\n    @Test\n    void subscribeNewCommentNotificationTest() {\n        var page = pageV1();\n\n        singlePageReconciler.subscribeNewCommentNotification(page);\n\n        verify(notificationCenter).subscribe(\n            assertArg(subscriber -> assertThat(subscriber.getName())\n                .isEqualTo(page.getSpec().getOwner())),\n            assertArg(argReason -> {\n                var interestReason = new Subscription.InterestReason();\n                interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_PAGE);\n                interestReason.setExpression(\"props.pageOwner == 'null'\");\n                assertThat(argReason).isEqualTo(interestReason);\n            }));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/SystemConfigReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationEventPublisher;\nimport run.halo.app.core.extension.content.Constant;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.SystemConfigChangedEvent;\nimport run.halo.app.infra.SystemSetting;\n\n@ExtendWith(MockitoExtension.class)\nclass SystemConfigReconcilerTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    ApplicationEventPublisher eventPublisher;\n\n    @InjectMocks\n    SystemConfigReconciler reconciler;\n\n    ConfigMap systemConfigMap;\n    ConfigMap defaultConfigMap;\n\n    @BeforeEach\n    void setUp() {\n        systemConfigMap = createConfigMap(SystemSetting.SYSTEM_CONFIG);\n        defaultConfigMap = createConfigMap(SystemSetting.SYSTEM_CONFIG_DEFAULT);\n    }\n\n    @Test\n    void reconcileShouldDoNothingWhenConfigMapNotFound() {\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.empty());\n\n        reconciler.reconcile(request);\n\n        verify(client, never()).update(any(ConfigMap.class));\n        verify(eventPublisher, never()).publishEvent(any());\n    }\n\n    @Test\n    void reconcileShouldDoNothingWhenConfigMapIsDeleted() {\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        systemConfigMap.getMetadata().setDeletionTimestamp(Instant.now());\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n\n        reconciler.reconcile(request);\n\n        verify(client, never()).update(any(ConfigMap.class));\n        verify(eventPublisher, never()).publishEvent(any());\n    }\n\n    @Test\n    void reconcileShouldThrowExceptionForNonSystemConfig() {\n        var request = new Reconciler.Request(\"other-config\");\n\n        assertThrows(IllegalStateException.class, () -> reconciler.reconcile(request));\n    }\n\n    @Test\n    void reconcileShouldUpdateChecksumAndPublishEventWhenDataChanges() {\n        var data = Map.of(\"key1\", \"value1\", \"key2\", \"value2\");\n        systemConfigMap.setData(data);\n\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT))\n            .thenReturn(Optional.of(defaultConfigMap));\n\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        reconciler.reconcile(request);\n\n        // Verify checksum annotation was added\n        verify(client, times(1)).update(argThat(configMap -> {\n            var annotations = configMap.getMetadata().getAnnotations();\n            return annotations != null\n                && annotations.containsKey(Constant.CHECKSUM_CONFIG_ANNO)\n                && annotations.containsKey(\"halo.run/data-snapshot\");\n        }));\n\n        // Verify event was published\n        ArgumentCaptor<SystemConfigChangedEvent> eventCaptor =\n            ArgumentCaptor.forClass(SystemConfigChangedEvent.class);\n        verify(eventPublisher, times(1)).publishEvent(eventCaptor.capture());\n\n        var event = eventCaptor.getValue();\n        assertThat(event.getNewData()).isEqualTo(data);\n    }\n\n    @Test\n    void reconcileShouldNotUpdateWhenChecksumUnchanged() {\n        var data = Map.of(\"key1\", \"value1\");\n        systemConfigMap.setData(data);\n\n        // Pre-set checksum to match current data - sha256 of data.toString()\n        var existingChecksum = com.google.common.hash.Hashing.sha256()\n            .hashString(systemConfigMap.getData().toString(),\n                java.nio.charset.StandardCharsets.UTF_8)\n            .toString();\n        systemConfigMap.getMetadata().getAnnotations()\n            .put(Constant.CHECKSUM_CONFIG_ANNO, existingChecksum);\n\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        reconciler.reconcile(request);\n\n        verify(client, never()).update(any(ConfigMap.class));\n        verify(eventPublisher, never()).publishEvent(any());\n    }\n\n    @Test\n    void reconcileShouldHandleNullData() {\n        systemConfigMap.setData(null);\n\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT))\n            .thenReturn(Optional.of(defaultConfigMap));\n\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        reconciler.reconcile(request);\n\n        // Should still update with checksum\n        verify(client, times(1)).update(any(ConfigMap.class));\n        verify(eventPublisher, times(1)).publishEvent(any(SystemConfigChangedEvent.class));\n    }\n\n    @Test\n    void reconcileShouldMergeWithDefaultConfig() {\n        var userData = Map.of(\"user.key\", \"user-value\");\n        var defaultData = Map.of(\"default.key\", \"default-value\");\n\n        systemConfigMap.setData(userData);\n        defaultConfigMap.setData(defaultData);\n\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT))\n            .thenReturn(Optional.of(defaultConfigMap));\n\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        reconciler.reconcile(request);\n\n        ArgumentCaptor<SystemConfigChangedEvent> eventCaptor =\n            ArgumentCaptor.forClass(SystemConfigChangedEvent.class);\n        verify(eventPublisher, times(1)).publishEvent(eventCaptor.capture());\n\n        var event = eventCaptor.getValue();\n        // New data should contain user data\n        assertThat(event.getNewData()).containsEntry(\"user.key\", \"user-value\");\n    }\n\n    @Test\n    void reconcileShouldHandleEmptyData() {\n        systemConfigMap.setData(Map.of());\n\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT))\n            .thenReturn(Optional.of(defaultConfigMap));\n\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        reconciler.reconcile(request);\n\n        verify(client, times(1)).update(any(ConfigMap.class));\n        verify(eventPublisher, times(1)).publishEvent(any(SystemConfigChangedEvent.class));\n    }\n\n    @Test\n    void reconcileShouldPreserveDataSnapshotForComparison() {\n        var oldData = Map.of(\"key1\", \"old-value\");\n        var newData = Map.of(\"key1\", \"new-value\");\n\n        // Set initial data and snapshot\n        systemConfigMap.setData(oldData);\n        systemConfigMap.getMetadata().getAnnotations()\n            .put(\"halo.run/data-snapshot\", \"{\\\"key1\\\":\\\"old-value\\\"}\");\n\n        // Update to new data\n        systemConfigMap.setData(newData);\n\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT))\n            .thenReturn(Optional.of(defaultConfigMap));\n\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        reconciler.reconcile(request);\n\n        ArgumentCaptor<SystemConfigChangedEvent> eventCaptor =\n            ArgumentCaptor.forClass(SystemConfigChangedEvent.class);\n        verify(eventPublisher, times(1)).publishEvent(eventCaptor.capture());\n\n        var event = eventCaptor.getValue();\n        assertThat(event.getOldData()).containsEntry(\"key1\", \"old-value\");\n        assertThat(event.getNewData()).containsEntry(\"key1\", \"new-value\");\n    }\n\n    @Test\n    void reconcileShouldHandleNoDefaultConfig() {\n        var data = Map.of(\"key1\", \"value1\");\n        systemConfigMap.setData(data);\n\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Optional.of(systemConfigMap));\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT))\n            .thenReturn(Optional.empty());\n\n        var request = new Reconciler.Request(SystemSetting.SYSTEM_CONFIG);\n        reconciler.reconcile(request);\n\n        verify(client, times(1)).update(any(ConfigMap.class));\n\n        ArgumentCaptor<SystemConfigChangedEvent> eventCaptor =\n            ArgumentCaptor.forClass(SystemConfigChangedEvent.class);\n        verify(eventPublisher, times(1)).publishEvent(eventCaptor.capture());\n\n        var event = eventCaptor.getValue();\n        assertThat(event.getNewData()).isEqualTo(data);\n    }\n\n    private ConfigMap createConfigMap(String name) {\n        var configMap = new ConfigMap();\n        var metadata = new Metadata();\n        metadata.setName(name);\n        metadata.setAnnotations(new HashMap<>());\n        configMap.setMetadata(metadata);\n        configMap.setData(new HashMap<>());\n        return configMap;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/TagReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.content.permalinks.TagPermalinkPolicy;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.core.reconciler.TagReconciler;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\n\n/**\n * Tests for {@link TagReconciler}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass TagReconcilerTest {\n    @Mock\n    private ExtensionClient client;\n\n    @Mock\n    private TagPermalinkPolicy tagPermalinkPolicy;\n\n    @InjectMocks\n    private TagReconciler tagReconciler;\n\n    @Test\n    void reconcile() {\n        Tag tag = tag();\n        when(client.fetch(eq(Tag.class), eq(\"fake-tag\")))\n            .thenReturn(Optional.of(tag));\n        when(tagPermalinkPolicy.permalink(any()))\n            .thenAnswer(arg -> \"/tags/\" + tag.getSpec().getSlug());\n        ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);\n\n        tagReconciler.reconcile(new TagReconciler.Request(\"fake-tag\"));\n\n        verify(client).update(captor.capture());\n        Tag capture = captor.getValue();\n        assertThat(capture.getStatus().getPermalink()).isEqualTo(\"/tags/fake-slug\");\n\n        // change slug\n        tag.getSpec().setSlug(\"new-slug\");\n        tagReconciler.reconcile(new TagReconciler.Request(\"fake-tag\"));\n        verify(client, times(2)).update(captor.capture());\n        assertThat(capture.getStatus().getPermalink()).isEqualTo(\"/tags/new-slug\");\n    }\n\n    @Test\n    void reconcileDelete() {\n        Tag tag = tag();\n        tag.getMetadata().setDeletionTimestamp(Instant.now());\n        tag.getMetadata().setFinalizers(Set.of(TagReconciler.FINALIZER_NAME));\n        when(client.fetch(eq(Tag.class), eq(\"fake-tag\")))\n            .thenReturn(Optional.of(tag));\n        ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);\n\n        tagReconciler.reconcile(new TagReconciler.Request(\"fake-tag\"));\n        verify(client, times(1)).update(captor.capture());\n        verify(tagPermalinkPolicy, times(0)).permalink(any());\n    }\n\n    Tag tag() {\n        Tag tag = new Tag();\n        tag.setMetadata(new Metadata());\n        tag.getMetadata().setVersion(0L);\n        tag.getMetadata().setName(\"fake-tag\");\n\n        tag.setSpec(new Tag.TagSpec());\n        tag.getSpec().setSlug(\"fake-slug\");\n\n        tag.setStatus(new Tag.TagStatus());\n        return tag;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/ThemeReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.mockito.stubbing.Answer;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.core.retry.RetryException;\nimport org.springframework.core.retry.RetryPolicy;\nimport org.springframework.core.retry.RetryTemplate;\nimport org.springframework.util.FileSystemUtils;\nimport org.springframework.util.ResourceUtils;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.extension.controller.RequeueException;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.theme.TemplateEngineManager;\n\n/**\n * Tests for {@link ThemeReconciler}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ThemeReconcilerTest {\n\n    @Mock\n    private ExtensionClient extensionClient;\n\n    @Mock\n    private SystemVersionSupplier systemVersionSupplier;\n\n    @Mock\n    ThemeRootGetter themeRoot;\n\n    @Mock\n    private File defaultTheme;\n\n    @Mock\n    private TemplateEngineManager templateEngineManager;\n\n    @Spy\n    RetryTemplate retryTemplate = new RetryTemplate(RetryPolicy.builder()\n        .maxRetries(1)\n        .delay(Duration.ZERO)\n        .predicate(IllegalStateException.class::isInstance)\n        .build());\n\n    @InjectMocks\n    ThemeReconciler themeReconciler;\n\n    @TempDir\n    private Path tempDirectory;\n\n    @BeforeEach\n    void setUp() throws IOException {\n        themeReconciler.setRetryTemplate(retryTemplate);\n        defaultTheme = ResourceUtils.getFile(\"classpath:themes/default\");\n        lenient().when(systemVersionSupplier.get()).thenReturn(Version.parse(\"0.0.0\"));\n        lenient().when(templateEngineManager.clearCache(any())).thenReturn(Mono.empty());\n    }\n\n    @Test\n    void reconcileDelete() throws IOException, RetryException {\n        Path testWorkDir = tempDirectory.resolve(\"reconcile-delete\");\n        Files.createDirectory(testWorkDir);\n        when(themeRoot.get()).thenReturn(testWorkDir);\n\n        Theme theme = new Theme();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"theme-test\");\n        metadata.setFinalizers(new HashSet<>());\n        metadata.getFinalizers().add(\"theme-protection\");\n        metadata.setDeletionTimestamp(Instant.now());\n        theme.setMetadata(metadata);\n        theme.setKind(Theme.KIND);\n        theme.setApiVersion(\"theme.halo.run/v1alpha1\");\n        Theme.ThemeSpec themeSpec = new Theme.ThemeSpec();\n        themeSpec.setSettingName(\"theme-test-setting\");\n        theme.setSpec(themeSpec);\n\n        Path defaultThemePath = testWorkDir.resolve(\"theme-test\");\n\n        // copy to temp directory\n        FileSystemUtils.copyRecursively(defaultTheme.toPath(), defaultThemePath);\n\n        assertThat(testWorkDir).isNotEmptyDirectory();\n        assertThat(defaultThemePath).exists();\n\n        when(extensionClient.fetch(eq(Theme.class), eq(metadata.getName())))\n            .thenReturn(Optional.of(theme));\n        when(extensionClient.fetch(Setting.class, themeSpec.getSettingName()))\n            .thenReturn(Optional.empty());\n\n        themeReconciler.reconcile(new Reconciler.Request(metadata.getName()));\n\n        verify(extensionClient, times(1)).fetch(eq(Theme.class), eq(metadata.getName()));\n        verify(extensionClient, times(2)).fetch(eq(Setting.class), eq(themeSpec.getSettingName()));\n\n        verify(extensionClient, times(2)).list(eq(AnnotationSetting.class), any(), any());\n\n        assertThat(Files.exists(testWorkDir)).isTrue();\n        assertThat(Files.exists(defaultThemePath)).isFalse();\n    }\n\n    @Test\n    void reconcileDeleteRetry() {\n        var theme = fakeTheme();\n        var metadata = theme.getMetadata();\n        metadata.setDeletionTimestamp(Instant.now());\n        metadata.setFinalizers(new HashSet<>());\n        metadata.getFinalizers().add(\"theme-protection\");\n\n        when(extensionClient.fetch(Theme.class, \"theme-test\")).thenReturn(Optional.of(theme));\n\n        Path testWorkDir = tempDirectory.resolve(\"reconcile-delete\");\n        when(themeRoot.get()).thenReturn(testWorkDir);\n\n        final ThemeReconciler themeReconciler =\n            new ThemeReconciler(extensionClient, themeRoot, systemVersionSupplier,\n                templateEngineManager);\n\n        final int[] retryFlags = {0, 0};\n        when(extensionClient.fetch(eq(Setting.class), eq(\"theme-test-setting\")))\n            .thenAnswer((Answer<Optional<Setting>>) invocation -> {\n                retryFlags[0]++;\n                // retry 2 times\n                if (retryFlags[0] < 3) {\n                    return Optional.of(new Setting());\n                }\n                return Optional.empty();\n            });\n\n        when(extensionClient.list(eq(AnnotationSetting.class), any(), eq(null)))\n            .thenAnswer((Answer<List<AnnotationSetting>>) invocation -> {\n                retryFlags[1]++;\n                // retry 2 times\n                if (retryFlags[1] < 3) {\n                    return List.of(new AnnotationSetting());\n                }\n                return List.of();\n            });\n\n        themeReconciler.reconcile(new Reconciler.Request(metadata.getName()));\n\n        String settingName = theme.getSpec().getSettingName();\n        verify(extensionClient, times(1)).fetch(eq(Theme.class), eq(metadata.getName()));\n        verify(extensionClient, times(3)).fetch(eq(Setting.class), eq(settingName));\n        verify(extensionClient, times(3)).list(eq(AnnotationSetting.class), any(), eq(null));\n        verify(templateEngineManager).clearCache(eq(metadata.getName()));\n    }\n\n    @Test\n    void reconcileDeleteRetryWhenThrowException() {\n        var theme = fakeTheme();\n        theme.getMetadata().setDeletionTimestamp(Instant.now());\n        theme.getMetadata().setFinalizers(new HashSet<>());\n        theme.getMetadata().getFinalizers().add(\"theme-protection\");\n\n        when(extensionClient.fetch(Theme.class, \"theme-test\")).thenReturn(Optional.of(theme));\n\n        when(extensionClient.fetch(Setting.class, \"theme-test-setting\"))\n            .thenReturn(Optional.of(new Setting()));\n\n        String settingName = theme.getSpec().getSettingName();\n        assertThatThrownBy(\n            () -> themeReconciler.reconcile(new Reconciler.Request(theme.getMetadata().getName())))\n            .satisfies(t -> {\n                var e = Exceptions.unwrap(t);\n                assertThat(e).isInstanceOf(RetryException.class);\n            });\n        verify(extensionClient, times(3)).fetch(eq(Setting.class), eq(settingName));\n    }\n\n    @Test\n    void shouldBeFailedIfVersionNotSatisfied() {\n        when(systemVersionSupplier.get()).thenReturn(Version.parse(\"2.3.0\"));\n        var testWorkDir = tempDirectory.resolve(\"reconcile-delete\");\n        when(themeRoot.get()).thenReturn(testWorkDir);\n        var theme = fakeTheme();\n        theme.setStatus(null);\n        theme.getSpec().setRequires(\">2.3.0\");\n        theme.getSpec().setSettingName(null);\n        when(extensionClient.fetch(Theme.class, \"theme-test\"))\n            .thenReturn(Optional.of(theme));\n        var themeReconciler = new ThemeReconciler(\n            extensionClient, themeRoot, systemVersionSupplier, templateEngineManager\n        );\n\n        themeReconciler.reconcile(new Reconciler.Request(theme.getMetadata().getName()));\n\n        var themeUpdateCaptor = ArgumentCaptor.forClass(Theme.class);\n        verify(extensionClient).update(themeUpdateCaptor.capture());\n        Theme value = themeUpdateCaptor.getValue();\n        assertThat(value.getStatus()).isNotNull();\n        assertThat(value.getStatus().getConditions().peekFirst().getType())\n            .isEqualTo(Theme.ThemePhase.FAILED.name());\n        assertThat(value.getStatus().getPhase())\n            .isEqualTo(Theme.ThemePhase.FAILED);\n    }\n\n    @Test\n    void shouldBeReadyIfVersionSatisfied() {\n        when(systemVersionSupplier.get()).thenReturn(Version.parse(\"2.3.0\"));\n        var testWorkDir = tempDirectory.resolve(\"reconcile-delete\");\n        when(themeRoot.get()).thenReturn(testWorkDir);\n        var theme = fakeTheme();\n        theme.setStatus(null);\n        theme.getSpec().setRequires(\">=2.3.0\");\n        theme.getSpec().setSettingName(null);\n        when(extensionClient.fetch(Theme.class, \"theme-test\"))\n            .thenReturn(Optional.of(theme));\n        var themeReconciler = new ThemeReconciler(\n            extensionClient, themeRoot, systemVersionSupplier, templateEngineManager\n        );\n        var themeUpdateCaptor = ArgumentCaptor.forClass(Theme.class);\n        themeReconciler.reconcile(new Reconciler.Request(theme.getMetadata().getName()));\n        verify(extensionClient).update(themeUpdateCaptor.capture());\n        assertThat(themeUpdateCaptor.getValue().getStatus().getPhase())\n            .isEqualTo(Theme.ThemePhase.READY);\n    }\n\n    private Theme fakeTheme() {\n        Theme theme = new Theme();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"theme-test\");\n        theme.setMetadata(metadata);\n        theme.setKind(Theme.KIND);\n        theme.setApiVersion(\"theme.halo.run/v1alpha1\");\n        Theme.ThemeSpec themeSpec = new Theme.ThemeSpec();\n        themeSpec.setSettingName(\"theme-test-setting\");\n        theme.setSpec(themeSpec);\n        return theme;\n    }\n\n    @Test\n    void themeSettingDefaultValue() throws IOException, JSONException {\n        Path testWorkDir = tempDirectory.resolve(\"reconcile-setting-value\");\n        Files.createDirectory(testWorkDir);\n        when(themeRoot.get()).thenReturn(testWorkDir);\n\n        Theme theme = new Theme();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"theme-test\");\n        theme.setMetadata(metadata);\n        theme.setKind(Theme.KIND);\n        theme.setApiVersion(\"theme.halo.run/v1alpha1\");\n        Theme.ThemeSpec themeSpec = new Theme.ThemeSpec();\n        themeSpec.setSettingName(null);\n        theme.setSpec(themeSpec);\n\n        when(extensionClient.fetch(eq(Theme.class), eq(metadata.getName())))\n            .thenReturn(Optional.of(theme));\n        var reconcile = themeReconciler.reconcile(new Reconciler.Request(metadata.getName()));\n        assertThat(reconcile.reEnqueue()).isFalse();\n        verify(extensionClient, times(1)).fetch(eq(Theme.class), eq(metadata.getName()));\n\n        // setting exists\n        themeSpec.setSettingName(\"theme-test-setting\");\n        assertThat(theme.getSpec().getConfigMapName()).isNull();\n        ArgumentCaptor<Theme> captor = ArgumentCaptor.forClass(Theme.class);\n        Assertions.assertThrows(RequeueException.class,\n            () -> themeReconciler.reconcile(new Reconciler.Request(metadata.getName()))\n        );\n        verify(extensionClient, times(2)).fetch(eq(Theme.class), eq(metadata.getName()));\n        verify(extensionClient).update(captor.capture());\n        Theme value = captor.getValue();\n        assertThat(value.getSpec().getConfigMapName()).isNotNull();\n\n        // populate setting name and configMap name and configMap not exists\n        themeSpec.setSettingName(\"theme-test-setting\");\n        themeSpec.setConfigMapName(\"theme-test-configmap\");\n        when(extensionClient.fetch(eq(ConfigMap.class), any()))\n            .thenReturn(Optional.empty());\n        when(extensionClient.fetch(eq(Setting.class), eq(themeSpec.getSettingName())))\n            .thenReturn(Optional.of(getFakeSetting()));\n        themeReconciler.reconcile(new Reconciler.Request(metadata.getName()));\n        verify(extensionClient, times(2))\n            .fetch(eq(Setting.class), eq(themeSpec.getSettingName()));\n        ArgumentCaptor<ConfigMap> configMapCaptor = ArgumentCaptor.forClass(ConfigMap.class);\n        verify(extensionClient, times(1)).create(any(ConfigMap.class));\n        verify(extensionClient, times(1)).create(configMapCaptor.capture());\n        ConfigMap defaultValueConfigMap = configMapCaptor.getValue();\n        Map<String, String> data = defaultValueConfigMap.getData();\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"sns\": \"{\\\\\"email\\\\\":\\\\\"example@exmple.com\\\\\"}\"\n                }\n                \"\"\",\n            JsonUtils.objectToJson(data),\n            true);\n    }\n\n    private static Setting getFakeSetting() {\n        String settingJson = \"\"\"\n            {\n                \"apiVersion\": \"v1alpha1\",\n                \"kind\": \"Setting\",\n                \"metadata\": {\n                    \"name\": \"theme-default-setting\"\n                },\n                \"spec\": {\n                    \"forms\": [{\n                        \"formSchema\": [\n                            {\n                                \"$el\": \"h1\",\n                                \"children\": \"Register\"\n                            },\n                            {\n                                \"$formkit\": \"text\",\n                                \"label\": \"Email\",\n                                \"name\": \"email\",\n                                \"value\": \"example@exmple.com\"\n                            },\n                            {\n                                \"$formkit\": \"password\",\n                                \"label\": \"Password\",\n                                \"name\": \"password\",\n                                \"validation\": \"required|length:5,16\",\n                                \"value\": null\n                            }\n                        ],\n                        \"group\": \"sns\",\n                        \"label\": \"社交资料\"\n                    }]\n                }\n            }\n            \"\"\";\n        return JsonUtils.jsonToObject(settingJson, Setting.class);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/reconciler/UserReconcilerTest.java",
    "content": "package run.halo.app.core.reconciler;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.reconciler.UserReconciler;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.notification.NotificationCenter;\n\n/**\n * Tests for {@link UserReconciler}.\n *\n * @author guqing\n * @since 2.0.1\n */\n@ExtendWith(MockitoExtension.class)\nclass UserReconcilerTest {\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @Mock\n    private ExtensionClient client;\n\n    @Mock\n    private NotificationCenter notificationCenter;\n\n    @Mock\n    private RoleService roleService;\n\n    @InjectMocks\n    private UserReconciler userReconciler;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(notificationCenter.unsubscribe(any(), any())).thenReturn(Mono.empty());\n    }\n\n    @Test\n    void permalinkForFakeUser() throws URISyntaxException {\n        when(externalUrlSupplier.get()).thenReturn(new URI(\"http://localhost:8090\"));\n\n        when(roleService.getRolesByUsername(\"fake-user\"))\n            .thenReturn(Flux.empty());\n\n        when(client.fetch(eq(User.class), eq(\"fake-user\")))\n            .thenReturn(Optional.of(user(\"fake-user\")));\n        userReconciler.reconcile(new Reconciler.Request(\"fake-user\"));\n\n        verify(client).<User>update(assertArg(user ->\n            assertEquals(\n                \"http://localhost:8090/authors/fake-user\",\n                user.getStatus().getPermalink()\n            )\n        ));\n    }\n\n    @Test\n    void permalinkForAnonymousUser() {\n        when(client.fetch(eq(User.class), eq(AnonymousUserConst.PRINCIPAL)))\n            .thenReturn(Optional.of(user(AnonymousUserConst.PRINCIPAL)));\n        when(roleService.getRolesByUsername(AnonymousUserConst.PRINCIPAL)).thenReturn(Flux.empty());\n        userReconciler.reconcile(new Reconciler.Request(AnonymousUserConst.PRINCIPAL));\n        verify(client).update(any(User.class));\n    }\n\n    @Test\n    void ensureRoleNamesAnno() {\n        when(roleService.getRolesByUsername(\"fake-user\")).thenReturn(Flux.just(\"fake-role\"));\n        when(client.fetch(eq(User.class), eq(\"fake-user\")))\n            .thenReturn(Optional.of(user(\"fake-user\")));\n        when(externalUrlSupplier.get()).thenReturn(URI.create(\"/\"));\n\n        userReconciler.reconcile(new Reconciler.Request(\"fake-user\"));\n\n        verify(client).update(assertArg(user -> {\n            assertEquals(\"\"\"\n                    [\"fake-role\"]\\\n                    \"\"\",\n                user.getMetadata().getAnnotations().get(User.ROLE_NAMES_ANNO));\n        }));\n    }\n\n    User user(String name) {\n        User user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(name);\n        user.getMetadata().setFinalizers(Set.of(\"user-protection\"));\n        user.setSpec(new User.UserSpec());\n        return user;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/user/service/DefaultRoleServiceTest.java",
    "content": "package run.halo.app.core.user.service;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\nimport org.assertj.core.api.AssertionsForInterfaceTypes;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Flux;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.RoleBinding;\nimport run.halo.app.core.user.service.DefaultRoleService;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link DefaultRoleService}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultRoleServiceTest {\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private DefaultRoleService roleService;\n\n    @ParameterizedTest\n    @MethodSource(\"usernamesProvider\")\n    void shouldReturnEmptyMapIfNoUsernamesProvided(Collection<String> usernames) {\n        roleService.getRolesByUsernames(usernames)\n            .as(StepVerifier::create)\n            .expectNext(Map.of())\n            .verifyComplete();\n    }\n\n    static Stream<Collection<String>> usernamesProvider() {\n        return Stream.of(null, List.of(), Set.of());\n    }\n\n    @Nested\n    class ListDependenciesTest {\n        @Test\n        void listDependencies() {\n            // prepare test data\n            var role1 = createRole(\"role1\", \"role2\");\n            var role2 = createRole(\"role2\", \"role3\");\n            var role3 = createRole(\"role3\");\n\n            var roleNames = Set.of(\"role1\");\n\n\n            when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.just(role1))\n                .thenReturn(Flux.just(role2))\n                .thenReturn(Flux.just(role3))\n                .thenReturn(Flux.empty());\n\n            // call the method under test\n            var result = roleService.listDependenciesFlux(roleNames);\n\n            // verify the result\n            StepVerifier.create(result)\n                .expectNext(role1)\n                .expectNext(role2)\n                .expectNext(role3)\n                .verifyComplete();\n\n            // verify the mock invocations\n            verify(client, times(4)).listAll(\n                same(Role.class),\n                any(ListOptions.class),\n                any(Sort.class)\n            );\n        }\n\n        @Test\n        void listDependenciesWithCycle() {\n            // prepare test data\n            var role1 = createRole(\"role1\", \"role2\");\n            var role2 = createRole(\"role2\", \"role3\");\n            var role3 = createRole(\"role3\", \"role1\");\n\n            var roleNames = Set.of(\"role1\");\n\n            // setup mocks\n            when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.just(role1))\n                .thenReturn(Flux.just(role2))\n                .thenReturn(Flux.just(role3))\n                .thenReturn(Flux.empty());\n\n            // call the method under test\n            var result = roleService.listDependenciesFlux(roleNames);\n\n            // verify the result\n            StepVerifier.create(result)\n                .expectNext(role1)\n                .expectNext(role2)\n                .expectNext(role3)\n                .verifyComplete();\n\n            // verify the mock invocations\n            verify(client, times(4)).listAll(\n                same(Role.class),\n                any(ListOptions.class),\n                any(Sort.class)\n            );\n        }\n\n        @Test\n        void listDependenciesWithMiddleCycle() {\n            // prepare test data\n            // role1 -> role2 -> role3 -> role4\n            //             \\<-----|\n            var role1 = createRole(\"role1\", \"role2\");\n            var role2 = createRole(\"role2\", \"role3\");\n            var role3 = createRole(\"role3\", \"role2\", \"role4\");\n            var role4 = createRole(\"role4\");\n\n            var roleNames = Set.of(\"role1\");\n\n            when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.just(role1))\n                .thenReturn(Flux.just(role2))\n                .thenReturn(Flux.just(role3))\n                .thenReturn(Flux.just(role4))\n                .thenReturn(Flux.empty());\n\n            // call the method under test\n            var result = roleService.listDependenciesFlux(roleNames);\n\n            // verify the result\n            StepVerifier.create(result)\n                .expectNext(role1)\n                .expectNext(role2)\n                .expectNext(role3)\n                .expectNext(role4)\n                .verifyComplete();\n\n            // verify the mock invocations\n            verify(client, times(5)).listAll(\n                same(Role.class),\n                any(ListOptions.class),\n                any(Sort.class)\n            );\n        }\n\n        @Test\n        void listDependenciesWithCycleAndSequence() {\n            // prepare test data\n            // role1 -> role2 -> role3\n            //   \\->role4  \\<-----|\n            Role role1 = createRole(\"role1\", \"role4\", \"role2\");\n            Role role2 = createRole(\"role2\", \"role3\");\n            Role role3 = createRole(\"role3\", \"role2\");\n            Role role4 = createRole(\"role4\");\n\n            Set<String> roleNames = Set.of(\"role1\");\n\n            when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.just(role1))\n                .thenReturn(Flux.just(role4, role2))\n                .thenReturn(Flux.just(role3))\n                .thenReturn(Flux.empty());\n\n            // call the method under test\n            var result = roleService.listDependenciesFlux(roleNames);\n\n            // verify the result\n            StepVerifier.create(result)\n                .expectNext(role1)\n                .expectNext(role4)\n                .expectNext(role2)\n                .expectNext(role3)\n                .verifyComplete();\n\n            // verify the mock invocations\n            verify(client, times(4)).listAll(same(Role.class), any(), any());\n        }\n\n        @Test\n        void listDependenciesAfterCycle() {\n            // prepare test data\n            // role1 -> role2 -> role3\n            //   \\->role4  \\<-----|\n            Role role1 = createRole(\"role1\", \"role4\", \"role2\");\n            Role role2 = createRole(\"role2\", \"role3\");\n            Role role3 = createRole(\"role3\", \"role2\");\n            Role role4 = createRole(\"role4\");\n\n            Set<String> roleNames = Set.of(\"role2\");\n\n            when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.just(role2))\n                .thenReturn(Flux.just(role3))\n                .thenReturn(Flux.empty());\n\n            // call the method under test\n            var result = roleService.listDependenciesFlux(roleNames);\n\n            // verify the result\n            StepVerifier.create(result)\n                .expectNext(role2)\n                .expectNext(role3)\n                .verifyComplete();\n\n            // verify the mock invocations\n            verify(client, times(3)).listAll(\n                same(Role.class),\n                any(ListOptions.class),\n                any(Sort.class)\n            );\n        }\n\n        @Test\n        void listDependenciesWithNullParam() {\n            var result = roleService.listDependenciesFlux(null);\n\n            // verify the result\n            StepVerifier.create(result)\n                .verifyComplete();\n\n            result = roleService.listDependenciesFlux(Set.of());\n            StepVerifier.create(result)\n                .verifyComplete();\n\n            // verify the mock invocations\n            verify(client, never()).listAll(\n                eq(Role.class),\n                any(ListOptions.class),\n                any(Sort.class)\n            );\n        }\n\n        @Test\n        void listDependenciesAndSomeOneNotFound() {\n            var role1 = createRole(\"role1\", \"role2\");\n            var role2 = createRole(\"role2\", \"role3\", \"role4\");\n            var role4 = createRole(\"role4\");\n\n            var roleNames = Set.of(\"role1\");\n\n            when(client.listAll(same(Role.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.just(role1))\n                .thenReturn(Flux.just(role2))\n                .thenReturn(Flux.just(role4))\n                .thenReturn(Flux.empty())\n            ;\n\n            var result = roleService.listDependenciesFlux(roleNames);\n            // verify the result\n            StepVerifier.create(result)\n                .expectNext(role1)\n                .expectNext(role2)\n                .expectNext(role4)\n                .verifyComplete();\n\n            // verify the mock invocations\n            verify(client, times(4)).listAll(\n                same(Role.class),\n                any(ListOptions.class),\n                any(Sort.class)\n            );\n        }\n\n        @Test\n        void testSubjectMatch() {\n            RoleBinding fakeAuthenticatedBinding =\n                createRoleBinding(\"authenticated-fake-binding\", \"fake\", \"authenticated\");\n            RoleBinding fakeEditorBinding =\n                createRoleBinding(\"editor-fake-binding\", \"fake\", \"editor\");\n            RoleBinding fakeAnonymousBinding =\n                createRoleBinding(\"test-anonymous-binding\", \"test\", \"anonymous\");\n\n            RoleBinding.Subject subject = new RoleBinding.Subject();\n            subject.setName(\"authenticated\");\n            subject.setKind(Role.KIND);\n            subject.setApiGroup(Role.GROUP);\n\n            Predicate<RoleBinding> predicate = roleService.getRoleBindingPredicate(subject);\n            List<RoleBinding> result =\n                Stream.of(fakeAuthenticatedBinding, fakeEditorBinding, fakeAnonymousBinding)\n                    .filter(predicate)\n                    .toList();\n            AssertionsForInterfaceTypes.assertThat(result)\n                .containsExactly(fakeAuthenticatedBinding);\n\n            subject.setName(\"editor\");\n            predicate = roleService.getRoleBindingPredicate(subject);\n            result =\n                Stream.of(fakeAuthenticatedBinding, fakeEditorBinding, fakeAnonymousBinding)\n                    .filter(predicate)\n                    .toList();\n            AssertionsForInterfaceTypes.assertThat(result).containsExactly(fakeEditorBinding);\n        }\n\n        RoleBinding createRoleBinding(String name, String refName, String subjectName) {\n            RoleBinding roleBinding = new RoleBinding();\n            roleBinding.setMetadata(new Metadata());\n            roleBinding.getMetadata().setName(name);\n            roleBinding.setRoleRef(new RoleBinding.RoleRef());\n            roleBinding.getRoleRef().setKind(Role.KIND);\n            roleBinding.getRoleRef().setApiGroup(Role.GROUP);\n            roleBinding.getRoleRef().setName(refName);\n            roleBinding.setSubjects(List.of(new RoleBinding.Subject()));\n            roleBinding.getSubjects().get(0).setKind(Role.KIND);\n            roleBinding.getSubjects().get(0).setName(subjectName);\n            roleBinding.getSubjects().get(0).setApiGroup(Role.GROUP);\n            return roleBinding;\n        }\n\n        private Role createRole(String name, String... dependencies) {\n            Role role = new Role();\n            role.setMetadata(new Metadata());\n            role.getMetadata().setName(name);\n\n            Map<String, String> annotations = new HashMap<>();\n            annotations.put(Role.ROLE_DEPENDENCIES_ANNO, JsonUtils.objectToJson(dependencies));\n            role.getMetadata().setAnnotations(annotations);\n            return role;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/user/service/impl/EmailPasswordRecoveryServiceImplTest.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n/**\n * Tests for {@link EmailPasswordRecoveryServiceImpl}.\n *\n * @author guqing\n * @since 2.11.0\n */\n@ExtendWith(MockitoExtension.class)\nclass EmailPasswordRecoveryServiceImplTest {\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/user/service/impl/EmailVerificationServiceImplTest.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static run.halo.app.core.user.service.impl.EmailVerificationServiceImpl.MAX_ATTEMPTS;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.core.user.service.impl.EmailVerificationServiceImpl.EmailVerificationManager.UsernameEmail;\nimport run.halo.app.infra.exception.EmailVerificationFailed;\n\n/**\n * Tests for {@link EmailVerificationServiceImpl}.\n *\n * @author guqing\n * @since 2.11.0\n */\n@ExtendWith(MockitoExtension.class)\nclass EmailVerificationServiceImplTest {\n\n    @Nested\n    class EmailVerificationManagerTest {\n\n        @Test\n        public void generateCodeTest() {\n            var emailVerificationManager =\n                new EmailVerificationServiceImpl.EmailVerificationManager();\n            emailVerificationManager.generateCode(\"fake-user\", \"fake-email\");\n            var result = emailVerificationManager.contains(\"fake-user\", \"fake-email\");\n            assertThat(result).isTrue();\n\n            emailVerificationManager.generateCode(\"guqing\", \"hi@halo.run\");\n            result = emailVerificationManager.contains(\"guqing\", \"hi@halo.run\");\n            assertThat(result).isTrue();\n\n            result = emailVerificationManager.contains(\"123\", \"123\");\n            assertThat(result).isFalse();\n        }\n\n        @Test\n        public void removeTest() {\n            var emailVerificationManager =\n                new EmailVerificationServiceImpl.EmailVerificationManager();\n            emailVerificationManager.generateCode(\"fake-user\", \"fake-email\");\n            var result = emailVerificationManager.contains(\"fake-user\", \"fake-email\");\n            emailVerificationManager.removeCode(\"fake-user\", \"fake-email\");\n            result = emailVerificationManager.contains(\"fake-user\", \"fake-email\");\n            assertThat(result).isFalse();\n        }\n\n        @Test\n        void verifyCodeTestNormal() {\n            String username = \"guqing\";\n            String email = \"hi@halo.run\";\n            var emailVerificationManager =\n                new EmailVerificationServiceImpl.EmailVerificationManager();\n            var result = emailVerificationManager.verifyCode(username, email, \"fake-code\");\n            assertThat(result).isFalse();\n\n            var code = emailVerificationManager.generateCode(username, email);\n            result = emailVerificationManager.verifyCode(username, email, \"fake-code\");\n            assertThat(result).isFalse();\n\n            result = emailVerificationManager.verifyCode(username, email, code);\n            assertThat(result).isTrue();\n        }\n\n        @Test\n        void verifyCodeFailedAfterMaxAttempts() {\n            String username = \"guqing\";\n            String email = \"example@example.com\";\n            var emailVerificationManager =\n                new EmailVerificationServiceImpl.EmailVerificationManager();\n            var code = emailVerificationManager.generateCode(username, email);\n            for (int i = 0; i <= MAX_ATTEMPTS; i++) {\n                var result = emailVerificationManager.verifyCode(username, email, \"fake-code\");\n                assertThat(result).isFalse();\n            }\n\n            assertThatThrownBy(() -> emailVerificationManager.verifyCode(username, email, code))\n                .isInstanceOf(EmailVerificationFailed.class)\n                .hasMessage(\"400 BAD_REQUEST \\\"Too many attempts. Please try again later.\\\"\");\n        }\n    }\n\n    @Test\n    void shouldBeEqualUsernameEmailWithDifferentCase() {\n        var expected = new UsernameEmail(\"faker\", \"a@b.com\");\n        var got = new UsernameEmail(\"faker\", \"A@B.com\");\n        assertEquals(expected, got);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/core/user/service/impl/UserServiceImplTest.java",
    "content": "package run.halo.app.core.user.service.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anySet;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.extension.GroupVersionKind.fromExtension;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.security.core.session.ReactiveSessionInformation;\nimport org.springframework.security.core.session.ReactiveSessionRegistry;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport org.springframework.transaction.ReactiveTransaction;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.RoleBinding;\nimport run.halo.app.core.extension.RoleBinding.Subject;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.user.service.EmailVerificationService;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.SignUpData;\nimport run.halo.app.core.user.service.UserPostCreatingHandler;\nimport run.halo.app.core.user.service.UserPreCreatingHandler;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.event.user.PasswordChangedEvent;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.exception.DuplicateNameException;\nimport run.halo.app.infra.exception.EmailAlreadyTakenException;\nimport run.halo.app.infra.exception.UnsatisfiedAttributeValueException;\nimport run.halo.app.infra.exception.UserNotFoundException;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n@ExtendWith(MockitoExtension.class)\nclass UserServiceImplTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    SystemConfigFetcher environmentFetcher;\n\n    @Mock\n    PasswordEncoder passwordEncoder;\n\n    @Mock\n    ApplicationEventPublisher eventPublisher;\n\n    @Mock\n    RoleService roleService;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    @Mock\n    EmailVerificationService emailVerificationService;\n\n    @Mock\n    ReactiveTransactionManager txManager;\n\n    @Mock\n    ReactiveSessionRegistry sessionRegistry;\n\n    @InjectMocks\n    UserServiceImpl userService;\n\n    @Test\n    void shouldThrowExceptionIfUserNotFoundInExtension() {\n        when(client.get(eq(User.class), eq(\"faker\"))).thenReturn(\n            Mono.error(new ExtensionNotFoundException(fromExtension(User.class), \"faker\")));\n        StepVerifier.create(userService.getUser(\"faker\"))\n            .verifyError(UserNotFoundException.class);\n\n        verify(client, times(1)).get(eq(User.class), eq(\"faker\"));\n    }\n\n    @Test\n    void shouldGetUserIfUserFoundInExtension() {\n        User fakeUser = new User();\n        when(client.get(User.class, \"faker\")).thenReturn(Mono.just(fakeUser));\n\n        StepVerifier.create(userService.getUser(\"faker\"))\n            .assertNext(user -> assertEquals(fakeUser, user))\n            .verifyComplete();\n\n        verify(client, times(1)).get(eq(User.class), eq(\"faker\"));\n    }\n\n    @Test\n    void shouldFindUserByVerifiedEmail() {\n        var fakeUser = createUser(\"fake-user\", \"fake-password\");\n        when(client.listAll(eq(User.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.just(fakeUser));\n        userService.findUserByVerifiedEmail(\"faker@halo.run\")\n            .as(StepVerifier::create)\n            .expectNext(fakeUser)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnEmptyIfNoUserWithVerifiedEmail() {\n        when(client.listAll(eq(User.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.empty());\n        userService.findUserByVerifiedEmail(\"faker@halo.run\")\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetGhostsIfUsersContainDeleted() {\n        var fakeUser1 = createUser(\"fake-user1\", \"fake-password\");\n        var fakeUser2 = createUser(\"fake-user2\", \"fake-password\");\n        var ghost = createUser(UserService.GHOST_USER_NAME, \"fake-password\");\n        when(client.listAll(eq(User.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.just(fakeUser1, fakeUser2, ghost));\n        userService.getUsersOrGhosts(List.of(\"fake-user1\", \"deleted-user\", \"fake-user2\"))\n            .as(StepVerifier::create)\n            .expectNext(fakeUser1, ghost, fakeUser2)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldUpdatePasswordIfUserFoundInExtension() {\n        var fakeUser = new User();\n        fakeUser.setSpec(new User.UserSpec());\n\n        when(client.get(User.class, \"faker\")).thenReturn(Mono.just(fakeUser));\n        when(client.update(eq(fakeUser))).thenReturn(Mono.just(fakeUser));\n\n        StepVerifier.create(userService.updatePassword(\"faker\", \"new-fake-password\"))\n            .expectNext(fakeUser)\n            .verifyComplete();\n\n        verify(client, times(1)).get(eq(User.class), eq(\"faker\"));\n        verify(client, times(1)).update(argThat(extension -> {\n            var user = (User) extension;\n            return \"new-fake-password\".equals(user.getSpec().getPassword());\n        }));\n\n        verify(eventPublisher).publishEvent(any(PasswordChangedEvent.class));\n    }\n\n    @Nested\n    @DisplayName(\"UpdateWithRawPassword\")\n    class UpdateWithRawPasswordTest {\n\n        @Test\n        void shouldUpdatePasswordWithDifferentPassword() {\n            var oldUser = createUser(\"fake@password\");\n            var newUser = createUser(\"new@password\");\n\n            when(client.get(User.class, \"fake-user\")).thenReturn(\n                Mono.just(oldUser));\n            when(client.update(eq(oldUser))).thenReturn(Mono.just(newUser));\n            when(passwordEncoder.matches(\"new@password\", \"fake@password\")).thenReturn(false);\n            when(passwordEncoder.encode(\"new@password\")).thenReturn(\"encoded@new@password\");\n\n            StepVerifier.create(userService.updateWithRawPassword(\"fake-user\", \"new@password\"))\n                .expectNext(newUser)\n                .verifyComplete();\n\n            verify(passwordEncoder).matches(\"new@password\", \"fake@password\");\n            verify(passwordEncoder).encode(\"new@password\");\n            verify(client).get(User.class, \"fake-user\");\n            verify(client).update(argThat(extension -> {\n                var user = (User) extension;\n                return \"encoded@new@password\".equals(user.getSpec().getPassword());\n            }));\n            verify(eventPublisher).publishEvent(any(PasswordChangedEvent.class));\n        }\n\n        @Test\n        void shouldUpdatePasswordIfNoPasswordBefore() {\n            var oldUser = createUser(null);\n            var newUser = createUser(\"new@password\");\n\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(oldUser));\n            when(client.update(oldUser)).thenReturn(Mono.just(newUser));\n            when(passwordEncoder.encode(\"new@password\")).thenReturn(\"encoded@new@password\");\n\n            StepVerifier.create(userService.updateWithRawPassword(\"fake-user\", \"new@password\"))\n                .expectNext(newUser)\n                .verifyComplete();\n\n            verify(passwordEncoder, never()).matches(\"new@password\", null);\n            verify(passwordEncoder).encode(\"new@password\");\n            verify(client).update(argThat(extension -> {\n                var user = (User) extension;\n                return \"encoded@new@password\".equals(user.getSpec().getPassword());\n            }));\n            verify(client).get(User.class, \"fake-user\");\n            verify(eventPublisher).publishEvent(any(PasswordChangedEvent.class));\n        }\n\n        @Test\n        void shouldDoNothingIfPasswordNotChanged() {\n            userService = spy(userService);\n\n            var oldUser = createUser(\"fake@password\");\n            var newUser = createUser(\"new@password\");\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(oldUser));\n            when(passwordEncoder.matches(\"fake@password\", \"fake@password\")).thenReturn(true);\n\n            StepVerifier.create(userService.updateWithRawPassword(\"fake-user\", \"fake@password\"))\n                .expectNextCount(0)\n                .verifyComplete();\n\n            verify(passwordEncoder, times(1)).matches(\"fake@password\", \"fake@password\");\n            verify(passwordEncoder, never()).encode(any());\n            verify(client, never()).update(any());\n            verify(client).get(User.class, \"fake-user\");\n            verify(eventPublisher, times(0)).publishEvent(any(PasswordChangedEvent.class));\n        }\n\n        @Test\n        void shouldThrowExceptionIfUserNotFound() {\n            when(client.get(eq(User.class), eq(\"fake-user\")))\n                .thenReturn(Mono.error(\n                    new ExtensionNotFoundException(fromExtension(User.class), \"fake-user\")));\n\n            StepVerifier.create(userService.updateWithRawPassword(\"fake-user\", \"new@password\"))\n                .verifyError(UserNotFoundException.class);\n\n            verify(passwordEncoder, never()).matches(anyString(), anyString());\n            verify(passwordEncoder, never()).encode(anyString());\n            verify(client, never()).update(any());\n            verify(client).get(User.class, \"fake-user\");\n        }\n\n        @Test\n        void shouldThrowWhenPwdContainsInvalidChars() {\n            StepVerifier.create(userService.updateWithRawPassword(\"fake-user\", \"new-password\"))\n                .expectError(UnsatisfiedAttributeValueException.class)\n                .verify();\n\n            verify(passwordEncoder, never()).encode(anyString());\n            verify(client, never()).update(any());\n        }\n\n    }\n\n    User createUser(String username, String password) {\n        var user = new User();\n        Metadata metadata = new Metadata();\n        metadata.setName(username);\n        user.setMetadata(metadata);\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setPassword(password);\n        return user;\n    }\n\n    User createUser(String password) {\n        return createUser(\"fake-user\", password);\n    }\n\n    @Nested\n    class GrantRolesTest {\n\n        @BeforeEach\n        void setUp() {\n            var tx = mock(ReactiveTransaction.class);\n\n            when(txManager.getReactiveTransaction(any()))\n                .thenReturn(Mono.just(tx));\n            when(txManager.commit(tx)).thenReturn(Mono.empty());\n        }\n\n        @Test\n        void shouldGetNotFoundIfUserNotFound() {\n            when(client.get(User.class, \"invalid-user\"))\n                .thenReturn(Mono.error(\n                    new ExtensionNotFoundException(fromExtension(User.class), \"invalid-user\"))\n                );\n\n            when(roleService.listRoleBindings(any())).thenReturn(Flux.empty());\n            when(client.create(isA(RoleBinding.class)))\n                .thenReturn(Mono.just(mock(RoleBinding.class)));\n            when(sessionRegistry.getAllSessions(\"invalid-user\")).thenReturn(Flux.empty());\n\n            var grantRolesMono = userService.grantRoles(\"invalid-user\", Set.of(\"fake-role\"));\n            StepVerifier.create(grantRolesMono)\n                .expectError(ExtensionNotFoundException.class)\n                .verify();\n\n            verify(client).get(User.class, \"invalid-user\");\n        }\n\n        @Test\n        void shouldCreateRoleBindingIfNotExist() {\n            var user = createUser(\"fake-password\");\n            when(client.get(User.class, \"fake-user\"))\n                .thenReturn(Mono.just(user));\n            when(roleService.listRoleBindings(any(Subject.class))).thenReturn(Flux.empty());\n            when(client.create(isA(RoleBinding.class))).thenReturn(\n                Mono.just(mock(RoleBinding.class)));\n            when(client.update(user)).thenReturn(Mono.just(user));\n            var session = mock(ReactiveSessionInformation.class);\n            when(session.invalidate()).thenReturn(Mono.empty());\n            when(sessionRegistry.getAllSessions(\"fake-user\")).thenReturn(Flux.just(session));\n\n            var grantRolesMono = userService.grantRoles(\"fake-user\", Set.of(\"fake-role\"));\n            StepVerifier.create(grantRolesMono)\n                .expectNextCount(1)\n                .verifyComplete();\n\n            verify(client).create(isA(RoleBinding.class));\n        }\n\n        @Test\n        void shouldDeleteRoleBindingIfNotProvided() {\n            var notProvidedRoleBinding = RoleBinding.create(\"fake-user\", \"non-provided-fake-role\");\n            var existingRoleBinding = RoleBinding.create(\"fake-user\", \"fake-role\");\n            when(roleService.listRoleBindings(any(Subject.class)))\n                .thenReturn(Flux.just(notProvidedRoleBinding, existingRoleBinding));\n            when(client.delete(isA(RoleBinding.class)))\n                .thenReturn(Mono.just(mock(RoleBinding.class)));\n            when(sessionRegistry.getAllSessions(\"fake-user\")).thenReturn(Flux.empty());\n            var user = createUser(\"fake-password\");\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(user));\n            when(client.update(user)).thenReturn(Mono.just(user));\n\n            StepVerifier.create(userService.grantRoles(\"fake-user\", Set.of(\"fake-role\")))\n                .expectNext(user)\n                .verifyComplete();\n        }\n\n        @Test\n        void shouldUpdateRoleBindingIfExists() {\n            // add another subject\n            var anotherSubject = new Subject();\n            anotherSubject.setName(\"another-fake-user\");\n            anotherSubject.setKind(User.KIND);\n            anotherSubject.setApiGroup(User.GROUP);\n            var notProvidedRoleBinding = RoleBinding.create(\"fake-user\", \"non-provided-fake-role\");\n            notProvidedRoleBinding.getSubjects().add(anotherSubject);\n\n            var existingRoleBinding = RoleBinding.create(\"fake-user\", \"fake-role\");\n\n            when(roleService.listRoleBindings(any(Subject.class)))\n                .thenReturn(Flux.just(notProvidedRoleBinding, existingRoleBinding));\n            when(client.update(isA(RoleBinding.class)))\n                .thenReturn(Mono.just(mock(RoleBinding.class)));\n\n            when(sessionRegistry.getAllSessions(\"fake-user\")).thenReturn(Flux.empty());\n\n            var user = createUser(\"fake-password\");\n            when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(user));\n            when(client.update(user)).thenReturn(Mono.just(user));\n\n            StepVerifier.create(userService.grantRoles(\"fake-user\", Set.of(\"fake-role\")))\n                // Because the roles are the same, so no need to update the existingRoleBinding\n                .expectNext(user)\n                .verifyComplete();\n\n            verify(client).update(notProvidedRoleBinding);\n        }\n    }\n\n\n    @Nested\n    class SignUpTest {\n\n        @Test\n        void signUpWhenRegistrationNotAllowed() {\n            SystemSetting.User userSetting = new SystemSetting.User();\n            userSetting.setAllowRegistration(false);\n            when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP),\n                eq(SystemSetting.User.class)))\n                .thenReturn(Mono.just(userSetting));\n\n            var signUpData = createSignUpData(\"fake-user\", \"fake-password\");\n\n            userService.signUp(signUpData)\n                .as(StepVerifier::create)\n                .consumeErrorWith(e -> {\n                    assertInstanceOf(ServerWebInputException.class, e);\n                    assertTrue(e.getMessage().contains(\"registration is not allowed\"));\n                })\n                .verify();\n        }\n\n        @Test\n        void signUpWhenRegistrationDefaultRoleNotConfigured() {\n            SystemSetting.User userSetting = new SystemSetting.User();\n            userSetting.setAllowRegistration(true);\n            when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP),\n                eq(SystemSetting.User.class)))\n                .thenReturn(Mono.just(userSetting));\n\n            var signUpData = createSignUpData(\"fake-user\", \"fake-password\");\n\n            userService.signUp(signUpData)\n                .as(StepVerifier::create)\n                .consumeErrorWith(e -> {\n                    assertInstanceOf(ServerWebInputException.class, e);\n                    assertTrue(e.getMessage().contains(\"default role is not configured\"));\n                })\n                .verify();\n        }\n\n        @Test\n        void signUpWhenRegistrationUsernameExists() {\n            SystemSetting.User userSetting = new SystemSetting.User();\n            userSetting.setAllowRegistration(true);\n            userSetting.setDefaultRole(\"fake-role\");\n            when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP),\n                eq(SystemSetting.User.class)))\n                .thenReturn(Mono.just(userSetting));\n            when(passwordEncoder.encode(eq(\"fake-password\"))).thenReturn(\"fake-password\");\n            when(client.fetch(eq(User.class), eq(\"fake-user\")))\n                .thenReturn(Mono.just(createFakeUser(\"test\", \"test\")));\n            when(extensionGetter.getExtensions(UserPreCreatingHandler.class))\n                .thenReturn(Flux.empty());\n\n            var signUpData = createSignUpData(\"fake-user\", \"fake-password\");\n            userService.signUp(signUpData)\n                .as(StepVerifier::create)\n                .expectError(DuplicateNameException.class)\n                .verify();\n        }\n\n        @Test\n        void signUpWhenEmailAlreadyTaken() {\n            SystemSetting.User userSetting = new SystemSetting.User();\n            userSetting.setAllowRegistration(true);\n            userSetting.setMustVerifyEmailOnRegistration(true);\n            userSetting.setDefaultRole(\"fake-role\");\n            when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP),\n                eq(SystemSetting.User.class)))\n                .thenReturn(Mono.just(userSetting));\n            when(passwordEncoder.encode(eq(\"fake-password\"))).thenReturn(\"fake-password\");\n            when(emailVerificationService.verifyRegisterVerificationCode(\"fake@example.com\",\n                \"fakeCode\"))\n                .thenReturn(Mono.just(true));\n            when(client.listAll(same(User.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.from(Mono.fromSupplier(() -> {\n                    var user = new User();\n                    user.setSpec(new User.UserSpec());\n                    user.getSpec().setEmailVerified(true);\n                    return user;\n                })));\n\n            var signUpData = createSignUpData(\"fake-user\", \"fake-password\");\n            signUpData.setEmail(\"fake@example.com\");\n            signUpData.setEmailCode(\"fakeCode\");\n            userService.signUp(signUpData)\n                .as(StepVerifier::create)\n                .expectError(EmailAlreadyTakenException.class)\n                .verify();\n        }\n\n        @Test\n        void signUpWhenRegistrationSuccessfully() {\n            SystemSetting.User userSetting = new SystemSetting.User();\n            userSetting.setAllowRegistration(true);\n            userSetting.setDefaultRole(\"fake-role\");\n            when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP),\n                eq(SystemSetting.User.class)))\n                .thenReturn(Mono.just(userSetting));\n            when(passwordEncoder.encode(eq(\"fake-password\"))).thenReturn(\"fake-password\");\n            when(client.fetch(eq(User.class), eq(\"fake-user\")))\n                .thenReturn(Mono.empty());\n\n            User fakeUser = createFakeUser(\"fake-user\", \"fake-password\");\n            var signUpData = createSignUpData(\"fake-user\", \"fake-password\");\n\n            when(client.fetch(eq(Role.class), anyString())).thenReturn(Mono.just(new Role()));\n            when(client.create(any(User.class))).thenReturn(Mono.just(fakeUser));\n            UserServiceImpl spyUserService = spy(userService);\n            doReturn(Mono.just(fakeUser)).when(spyUserService).grantRoles(eq(\"fake-user\"),\n                anySet());\n            when(extensionGetter.getExtensions(UserPreCreatingHandler.class))\n                .thenReturn(Flux.just(user -> {\n                    if (user.getMetadata().getAnnotations() == null) {\n                        user.getMetadata().setAnnotations(new HashMap<>());\n                    }\n                    user.getMetadata().getAnnotations()\n                        .put(\"pre.creating.handler.handled\", \"true\");\n                    return Mono.empty();\n                }));\n            when(extensionGetter.getExtensions(UserPostCreatingHandler.class))\n                .thenReturn(Flux.just(user -> {\n                    assertEquals(fakeUser, user);\n                    return Mono.empty();\n                }));\n\n            spyUserService.signUp(signUpData)\n                .as(StepVerifier::create)\n                .consumeNextWith(user -> {\n                    assertThat(user.getMetadata().getName()).isEqualTo(\"fake-user\");\n                    assertThat(user.getSpec().getPassword()).isEqualTo(\"fake-password\");\n                })\n                .verifyComplete();\n\n            verify(client).create(assertArg(u -> {\n                var handled = u.getMetadata().getAnnotations().get(\"pre.creating.handler.handled\");\n                assertEquals(\"true\", handled);\n            }));\n            verify(spyUserService).grantRoles(eq(\"fake-user\"), anySet());\n        }\n\n        User createFakeUser(String name, String password) {\n            User user = new User();\n            user.setMetadata(new Metadata());\n            user.getMetadata().setName(name);\n            user.setSpec(new User.UserSpec());\n            user.getSpec().setPassword(password);\n            return user;\n        }\n\n        SignUpData createSignUpData(String name, String password) {\n            SignUpData signUpData = new SignUpData();\n            signUpData.setUsername(name);\n            signUpData.setPassword(password);\n            signUpData.setDisplayName(name);\n            return signUpData;\n        }\n    }\n\n    @Test\n    void confirmPasswordWhenPasswordNotSet() {\n        var user = new User();\n        user.setSpec(new User.UserSpec());\n        when(client.get(User.class, \"fake-user\")).thenReturn(Mono.just(user));\n        userService.confirmPassword(\"fake-user\", \"fake-password\")\n            .as(StepVerifier::create)\n            .expectNext(true)\n            .verifyComplete();\n\n        user.getSpec().setPassword(\"\");\n        userService.confirmPassword(\"fake-user\", \"fake-password\")\n            .as(StepVerifier::create)\n            .expectNext(true)\n            .verifyComplete();\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/AbstractExtensionTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.Test;\n\nclass AbstractExtensionTest {\n\n    @Test\n    void groupVersionKind() {\n        var extension = new AbstractExtension() {\n        };\n        extension.setApiVersion(\"fake.halo.run/v1alpha1\");\n        extension.setKind(\"Fake\");\n        var gvk = extension.groupVersionKind();\n\n        assertEquals(new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"), gvk);\n    }\n\n    @Test\n    void testGroupVersionKind() {\n        var extension = new AbstractExtension() {\n        };\n        extension.groupVersionKind(new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"));\n\n        assertEquals(\"fake.halo.run/v1alpha1\", extension.getApiVersion());\n        assertEquals(\"Fake\", extension.getKind());\n    }\n\n    @Test\n    void metadata() {\n        var extension = new AbstractExtension() {\n        };\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake\");\n        extension.setMetadata(metadata);\n\n        assertEquals(metadata, extension.getMetadata());\n    }\n\n    @Test\n    void testMetadata() {\n        var extension = new AbstractExtension() {\n        };\n\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake\");\n        extension.setMetadata(metadata);\n\n        assertEquals(metadata, extension.getMetadata());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/ComparatorsTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\nclass ComparatorsTest {\n\n    @Nested\n    class CompareCreationTimestamp {\n\n        FakeExtension createFake(String name, Instant creationTimestamp) {\n            var metadata = new Metadata();\n            metadata.setName(name);\n            metadata.setCreationTimestamp(creationTimestamp);\n            var fake = new FakeExtension();\n            fake.setMetadata(metadata);\n            return fake;\n        }\n\n        @Test\n        void desc() {\n            var comparator = Comparators.compareCreationTimestamp(false);\n            var now = Instant.now();\n            var before = now.minusMillis(1);\n            var after = now.plusMillis(1);\n\n            var fakeNow = createFake(\"now\", now);\n            var fakeBefore = createFake(\"before\", before);\n            var fakeAfter = createFake(\"after\", after);\n\n            var sortedFakes = new ArrayList<>(List.of(fakeNow, fakeAfter, fakeBefore));\n            sortedFakes.sort(comparator);\n\n            assertEquals(List.of(fakeAfter, fakeNow, fakeBefore), sortedFakes);\n        }\n\n        @Test\n        void asc() {\n            var comparator = Comparators.compareCreationTimestamp(true);\n            var now = Instant.now();\n            var before = now.minusMillis(1);\n            var after = now.plusMillis(1);\n\n            var fakeNow = createFake(\"now\", now);\n            var fakeBefore = createFake(\"before\", before);\n            var fakeAfter = createFake(\"after\", after);\n\n            var sortedFakes = new ArrayList<>(List.of(fakeNow, fakeAfter, fakeBefore));\n            sortedFakes.sort(comparator);\n\n            assertEquals(List.of(fakeBefore, fakeNow, fakeAfter), sortedFakes);\n        }\n    }\n\n    @Nested\n    class CompareName {\n\n        FakeExtension createFake(String name) {\n            var metadata = new Metadata();\n            metadata.setName(name);\n            var fake = new FakeExtension();\n            fake.setMetadata(metadata);\n            return fake;\n        }\n\n        @Test\n        void desc() {\n            var comparator = Comparators.compareName(false);\n            var fake01 = createFake(\"fake01\");\n            var fake02 = createFake(\"fake02\");\n            var fake03 = createFake(\"fake03\");\n\n            var sortedFakes = new ArrayList<>(List.of(fake02, fake01, fake03));\n            sortedFakes.sort(comparator);\n\n            assertEquals(List.of(fake03, fake02, fake01), sortedFakes);\n        }\n\n        @Test\n        void asc() {\n            var comparator = Comparators.compareName(true);\n            var fake01 = createFake(\"fake01\");\n            var fake02 = createFake(\"fake02\");\n            var fake03 = createFake(\"fake03\");\n\n            var sortedFakes = new ArrayList<>(List.of(fake02, fake03, fake01));\n            sortedFakes.sort(comparator);\n\n            assertEquals(List.of(fake01, fake02, fake03), sortedFakes);\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/ConfigMapTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.doNothing;\n\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.security.util.InMemoryResource;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\n\n/**\n * Tests for {@link ConfigMap}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ConfigMapTest {\n\n    @Mock\n    ExtensionClient extensionClient;\n\n    @Test\n    void configMapTest() {\n        ArgumentCaptor<ConfigMap> argumentCaptor = ArgumentCaptor.forClass(ConfigMap.class);\n        doNothing().when(extensionClient).create(argumentCaptor.capture());\n\n        ConfigMap configMap = new ConfigMap();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"test-configmap\");\n        configMap.setMetadata(metadata);\n        Map<String, String> data = Map.of(\"k1\", \"v1\", \"k2\", \"v2\", \"k3\", \"v3\");\n        configMap.setData(data);\n        extensionClient.create(configMap);\n\n        ConfigMap value = argumentCaptor.getValue();\n        assertThat(value).isNotNull();\n        assertThat(value.getData()).isEqualTo(data);\n    }\n\n    @Test\n    void putDataItem() {\n        ConfigMap configMap = new ConfigMap();\n        configMap.putDataItem(\"k1\", \"v1\")\n            .putDataItem(\"k2\", \"v2\")\n            .putDataItem(\"k3\", \"v3\");\n\n        assertThat(configMap.getData()).isNotNull();\n        assertThat(configMap.getData()).hasSize(3);\n        assertThat(configMap.getData()).isEqualTo(\n            Map.of(\"k1\", \"v1\", \"k2\", \"v2\", \"k3\", \"v3\"));\n    }\n\n    @Test\n    void equalsTest() {\n        ConfigMap configMapA = new ConfigMap();\n        Metadata metadataA = new Metadata();\n        metadataA.setName(\"test-configmap\");\n        configMapA.setMetadata(metadataA);\n        configMapA.putDataItem(\"k1\", \"v1\");\n\n        ConfigMap configMapB = new ConfigMap();\n        Metadata metadataB = new Metadata();\n        metadataB.setName(\"test-configmap\");\n        configMapB.setMetadata(metadataB);\n        configMapB.putDataItem(\"k1\", \"v1\");\n\n        assertThat(configMapA).isEqualTo(configMapB);\n\n        configMapB.getMetadata().setName(\"test-configmap-2\");\n        assertThat(configMapA).isNotEqualTo(configMapB);\n    }\n\n    @Test\n    void yamlTest() {\n        String configMapYaml = \"\"\"\n                apiVersion: v1alpha1\n                kind: ConfigMap\n                metadata:\n                  name: test-configmap\n                data:\n                  k1: v1\n                  k2: v2\n                  k3: v3\n            \"\"\";\n        List<Unstructured> unstructureds =\n            new YamlUnstructuredLoader(new InMemoryResource(configMapYaml)).load();\n        assertThat(unstructureds).hasSize(1);\n        Unstructured unstructured = unstructureds.get(0);\n        ConfigMap configMap =\n            Unstructured.OBJECT_MAPPER.convertValue(unstructured, ConfigMap.class);\n\n        assertThat(configMap.getData()).isEqualTo(Map.of(\"k1\", \"v1\", \"k2\", \"v2\", \"k3\", \"v3\"));\n        assertThat(configMap.getMetadata().getName()).isEqualTo(\"test-configmap\");\n        assertThat(configMap.getApiVersion()).isEqualTo(\"v1alpha1\");\n        assertThat(configMap.getKind()).isEqualTo(\"ConfigMap\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/DefaultSchemeManagerTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.verify;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationEventPublisher;\nimport run.halo.app.extension.event.SchemeAddedEvent;\nimport run.halo.app.extension.event.SchemeRemovedEvent;\nimport run.halo.app.extension.exception.SchemeNotFoundException;\nimport run.halo.app.extension.index.IndexEngine;\nimport run.halo.app.extension.index.IndicesManager;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultSchemeManagerTest {\n\n    @Mock\n    IndicesManager indicesManager;\n\n    @Mock\n    IndexEngine indexEngine;\n\n    @Mock\n    ApplicationEventPublisher eventPublisher;\n\n    @InjectMocks\n    DefaultSchemeManager schemeManager;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(indexEngine.getIndicesManager()).thenReturn(indicesManager);\n    }\n\n    @Test\n    void shouldThrowExceptionWhenNoGvkAnnotation() {\n        class WithoutGvkExtension extends AbstractExtension {\n        }\n\n        assertThrows(IllegalArgumentException.class,\n            () -> schemeManager.register(WithoutGvkExtension.class));\n    }\n\n    @Test\n    void shouldGetNothingWhenUnregistered() {\n        final var gvk = new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\");\n        var scheme = schemeManager.fetch(gvk);\n        assertFalse(scheme.isPresent());\n\n        assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(gvk));\n        assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(FakeExtension.class));\n        assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(new FakeExtension()));\n    }\n\n    @Test\n    void shouldGetSchemeWhenRegistered() {\n        schemeManager.register(FakeExtension.class);\n        final var gvk = new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\");\n        var scheme = schemeManager.fetch(gvk);\n        assertTrue(scheme.isPresent());\n\n        assertEquals(gvk, schemeManager.get(gvk).groupVersionKind());\n        assertEquals(gvk, schemeManager.get(FakeExtension.class).groupVersionKind());\n        assertEquals(gvk, schemeManager.get(new FakeExtension()).groupVersionKind());\n    }\n\n    @Test\n    void shouldUnregisterSuccessfully() {\n        schemeManager.register(FakeExtension.class);\n        Scheme scheme = schemeManager.get(FakeExtension.class);\n        assertNotNull(scheme);\n\n        schemeManager.unregister(scheme);\n        assertThrows(SchemeNotFoundException.class, () -> schemeManager.get(FakeExtension.class));\n    }\n\n    @Test\n    void shouldTriggerOnChangeOnlyOnceWhenRegisterTwice() {\n        schemeManager.register(FakeExtension.class);\n        schemeManager.register(FakeExtension.class);\n\n        verify(eventPublisher).publishEvent(isA(SchemeAddedEvent.class));\n        verify(indicesManager).add(same(FakeExtension.class), any());\n    }\n\n    @Test\n    void shouldTriggerOnChangeOnlyOnceWhenUnregisterTwice() {\n        schemeManager.register(FakeExtension.class);\n        var scheme = schemeManager.get(FakeExtension.class);\n        schemeManager.unregister(scheme);\n        schemeManager.unregister(scheme);\n\n        verify(eventPublisher).publishEvent(isA(SchemeAddedEvent.class));\n        verify(eventPublisher).publishEvent(isA(SchemeRemovedEvent.class));\n        verify(indicesManager).add(same(FakeExtension.class), any());\n    }\n\n    @Test\n    void getSizeOfSchemes() {\n        assertEquals(0, schemeManager.size());\n        schemeManager.register(FakeExtension.class);\n        assertEquals(1, schemeManager.size());\n        schemeManager.unregister(schemeManager.get(FakeExtension.class));\n        assertEquals(0, schemeManager.size());\n    }\n\n    @Test\n    void shouldReturnCopyOnWriteList() {\n        schemeManager.register(FakeExtension.class);\n        var schemes = schemeManager.schemes();\n        schemes.forEach(scheme -> {\n            // make sure concurrent modification won't happen\n            schemeManager.register(FooExtension.class);\n        });\n    }\n\n    @GVK(group = \"fake.halo.run\", version = \"v1alpha1\", kind = \"Foo\",\n        plural = \"foos\", singular = \"foo\")\n    static class FooExtension extends AbstractExtension {\n    }\n}\n\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/ExtensionOperatorTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport org.junit.jupiter.api.Test;\n\nclass ExtensionOperatorTest {\n\n    @Test\n    void testIsNotDeleted() {\n        var ext = mock(ExtensionOperator.class);\n        var metadata = mock(Metadata.class);\n        when(metadata.getDeletionTimestamp()).thenReturn(null);\n        when(ext.getMetadata()).thenReturn(metadata);\n\n        assertTrue(ExtensionOperator.isNotDeleted().test(ext));\n\n        when(metadata.getDeletionTimestamp()).thenReturn(Instant.now());\n        assertFalse(ExtensionOperator.isNotDeleted().test(ext));\n    }\n\n    @Test\n    void testIsDeleted() {\n        var ext = mock(ExtensionOperator.class);\n\n        when(ext.getMetadata()).thenReturn(null);\n        assertFalse(ExtensionOperator.isDeleted(ext));\n\n        var metadata = mock(Metadata.class);\n        when(ext.getMetadata()).thenReturn(metadata);\n        when(metadata.getDeletionTimestamp()).thenReturn(null);\n        assertFalse(ExtensionOperator.isDeleted(ext));\n\n        when(metadata.getDeletionTimestamp()).thenReturn(Instant.now());\n        assertTrue(ExtensionOperator.isDeleted(ext));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/ExtensionStoreUtilTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass ExtensionStoreUtilTest {\n\n    Scheme scheme;\n\n    Scheme grouplessScheme;\n\n    @BeforeEach\n    void setUp() {\n        scheme = new Scheme(FakeExtension.class,\n            new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"),\n            \"fakes\",\n            \"fake\",\n            new ObjectNode(null));\n        grouplessScheme = new Scheme(FakeExtension.class,\n            new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"),\n            \"fakes\",\n            \"fake\",\n            new ObjectNode(null));\n    }\n\n    @Test\n    void buildStoreNamePrefix() {\n        var prefix = ExtensionStoreUtil.buildStoreNamePrefix(scheme);\n        assertEquals(\"/registry/fake.halo.run/fakes\", prefix);\n\n        prefix = ExtensionStoreUtil.buildStoreNamePrefix(grouplessScheme);\n        assertEquals(\"/registry/fakes\", prefix);\n    }\n\n    @Test\n    void buildStoreName() {\n        var storeName = ExtensionStoreUtil.buildStoreName(scheme, \"fake-name\");\n        assertEquals(\"/registry/fake.halo.run/fakes/fake-name\", storeName);\n\n        storeName = ExtensionStoreUtil.buildStoreName(grouplessScheme, \"fake-name\");\n        assertEquals(\"/registry/fakes/fake-name\", storeName);\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/FakeExtension.java",
    "content": "package run.halo.app.extension;\n\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.ToString;\n\n@GVK(group = \"fake.halo.run\",\n    version = \"v1alpha1\",\n    kind = \"Fake\",\n    plural = \"fakes\",\n    singular = \"fake\")\n@Data\n@ToString(callSuper = true)\n@EqualsAndHashCode(callSuper = true)\npublic class FakeExtension extends AbstractExtension {\n\n    private FakeStatus status = new FakeStatus();\n\n    public static FakeExtension createFake(String name) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        var fake = new FakeExtension();\n        fake.setMetadata(metadata);\n        return fake;\n    }\n\n    @Data\n    public static class FakeStatus {\n        private String state;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/GroupVersionKindTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static run.halo.app.extension.GroupVersionKind.fromAPIVersionAndKind;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nclass GroupVersionKindTest {\n\n    @Test\n    void testFromApiVersionAndKind() {\n        record TestCase(String apiVersion, String kind, GroupVersionKind expected,\n                        Class<? extends Throwable> exception) {\n        }\n\n        List.of(\n            new TestCase(\"v1alpha1\", \"Fake\", new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"), null),\n            new TestCase(\"fake.halo.run/v1alpha1\", \"Fake\",\n                new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"), null),\n            new TestCase(\"\", \"\", null, IllegalArgumentException.class),\n            new TestCase(\"\", \"Fake\", null, IllegalArgumentException.class),\n            new TestCase(\"v1alpha1\", \"\", null, IllegalArgumentException.class),\n            new TestCase(\"fake.halo.run/v1alpha1/v1alpha2\", \"Fake\", null,\n                IllegalArgumentException.class)\n        ).forEach(testCase -> {\n            if (testCase.exception != null) {\n                assertThrows(testCase.exception, () -> {\n                    fromAPIVersionAndKind(testCase.apiVersion, testCase.kind);\n                });\n            } else {\n                var got = fromAPIVersionAndKind(testCase.apiVersion, testCase.kind);\n                assertEquals(testCase.expected, got);\n            }\n        });\n    }\n\n    @Test\n    void testHasGroup() {\n        record TestCase(GroupVersionKind gvk, boolean hasGroup) {\n        }\n\n        List.of(\n            new TestCase(new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"), false),\n            new TestCase(new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"), true)\n        ).forEach(testCase -> assertEquals(testCase.hasGroup, testCase.gvk.hasGroup()));\n    }\n\n    @Test\n    void testGroupKind() {\n        record TestCase(GroupVersionKind gvk, GroupKind gk) {\n        }\n\n        List.of(\n            new TestCase(new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"), new GroupKind(\"\", \"Fake\")),\n            new TestCase(new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"),\n                new GroupKind(\"fake.halo.run\", \"Fake\"))\n        ).forEach(testCase -> {\n            assertEquals(testCase.gk, testCase.gvk.groupKind());\n        });\n    }\n\n    @Test\n    void testGroupVersion() {\n        record TestCase(GroupVersionKind gvk, GroupVersion gv) {\n        }\n\n        List.of(\n            new TestCase(new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"),\n                new GroupVersion(\"\", \"v1alpha1\")),\n            new TestCase(new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"),\n                new GroupVersion(\"fake.halo.run\", \"v1alpha1\"))\n        ).forEach(testCase -> {\n            assertEquals(testCase.gv, testCase.gvk.groupVersion());\n        });\n    }\n\n    @Test\n    void fromExtension() {\n        GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(FakeExtension.class);\n        assertEquals(\"fake.halo.run\", groupVersionKind.group());\n        assertEquals(\"v1alpha1\", groupVersionKind.version());\n        assertEquals(\"Fake\", groupVersionKind.kind());\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/GroupVersionTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport org.junit.jupiter.api.Test;\n\nclass GroupVersionTest {\n\n    @Test\n    void shouldThrowIllegalArgumentExceptionWhenAPIVersionIsIllegal() {\n        assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion(null),\n            \"apiVersion is null\");\n        assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion(\"\"),\n            \"apiVersion is empty\");\n        assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion(\"    \"),\n            \"apiVersion is blank\");\n        assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion(\"a/b/c\"),\n            \"apiVersion contains more than 1 '/'\");\n    }\n\n    @Test\n    void shouldReturnGroupVersionCorrectly() {\n        assertEquals(new GroupVersion(\"\", \"v1\"), GroupVersion.parseAPIVersion(\"v1\"),\n            \"only contains version\");\n        assertEquals(new GroupVersion(\"core.halo.run\", \"v1\"),\n            GroupVersion.parseAPIVersion(\"core.halo.run/v1\"), \"only contains version\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/JsonExtensionConverterTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.lenient;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.IOException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.exception.ExtensionConvertException;\nimport run.halo.app.extension.exception.SchemaViolationException;\nimport run.halo.app.extension.store.ExtensionStore;\n\n@ExtendWith(MockitoExtension.class)\nclass JsonExtensionConverterTest {\n\n    @InjectMocks\n    JSONExtensionConverter converter;\n\n    @Mock\n    SchemeManager schemeManager;\n\n    ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER;\n\n    @BeforeEach\n    void setUp() {\n        converter.setObjectMapper(objectMapper);\n\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        lenient().when(schemeManager.get(scheme.groupVersionKind())).thenReturn(scheme);\n    }\n\n    @Test\n    void convertTo() throws IOException {\n        var fake = createFakeExtension(\"fake\", 10L);\n\n        var extensionStore = converter.convertTo(fake);\n\n        assertEquals(\"/registry/fake.halo.run/fakes/fake\", extensionStore.getName());\n        assertEquals(10L, extensionStore.getVersion());\n        assertEquals(fake, objectMapper.readValue(extensionStore.getData(), FakeExtension.class));\n    }\n\n    @Test\n    void convertFrom() throws JsonProcessingException {\n        var fake = createFakeExtension(\"fake\", 20L);\n\n        var store = new ExtensionStore();\n        store.setName(\"/registry/fake.halo.run/fakes/fake\");\n        store.setVersion(20L);\n        store.setData(objectMapper.writeValueAsBytes(fake));\n\n        FakeExtension gotFake = converter.convertFrom(FakeExtension.class, store);\n        assertEquals(fake, gotFake);\n    }\n\n    @Test\n    void shouldThrowConvertExceptionWhenDataIsInvalid() {\n        var store = new ExtensionStore();\n        store.setName(\"/registry/fake.halo.run/fakes/fake\");\n        store.setVersion(20L);\n        store.setData(\"{\".getBytes());\n\n        assertThrows(ExtensionConvertException.class,\n            () -> converter.convertFrom(FakeExtension.class, store));\n    }\n\n    @Test\n    void shouldThrowSchemaViolationExceptionWhenNameNotSet() {\n        var fake = new FakeExtension();\n        Metadata metadata = new Metadata();\n        fake.setMetadata(metadata);\n        fake.setApiVersion(\"fake.halo.run/v1alpha1\");\n        fake.setKind(\"Fake\");\n        var error = assertThrows(SchemaViolationException.class, () -> converter.convertTo(fake));\n        assertEquals(1, error.getErrors().size());\n        var result = error.getErrors().items().get(0);\n        assertEquals(1026, result.code());\n        assertEquals(\"Field 'name' is required.\", result.message());\n    }\n\n    FakeExtension createFakeExtension(String name, Long version) {\n        var fake = new FakeExtension();\n        fake.groupVersionKind(new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"));\n        Metadata metadata = new Metadata();\n        metadata.setName(name);\n        metadata.setVersion(version);\n        fake.setMetadata(metadata);\n\n        return fake;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/JsonExtensionTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.databind.node.TextNode;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\n\nclass JsonExtensionTest {\n\n    ObjectMapper objectMapper;\n\n    @BeforeEach\n    void setUp() {\n        objectMapper = JsonMapper.builder().build();\n    }\n\n    @Test\n    void serializeEmptyExt() throws JsonProcessingException, JSONException {\n        var ext = new JsonExtension(objectMapper);\n        var json = objectMapper.writeValueAsString(ext);\n        JSONAssert.assertEquals(\"{}\", json, true);\n    }\n\n    @Test\n    void serializeExt() throws JsonProcessingException, JSONException {\n        var ext = new JsonExtension(objectMapper);\n        ext.setApiVersion(\"fake.halo.run/v1alpha\");\n        ext.setKind(\"Fake\");\n        var metadata = ext.getMetadataOrCreate();\n        metadata.setName(\"fake-name\");\n\n        ext.getInternal().set(\"data\", TextNode.valueOf(\"halo\"));\n\n        JSONAssert.assertEquals(\"\"\"\n            {\n              \"apiVersion\": \"fake.halo.run/v1alpha\",\n              \"kind\": \"Fake\",\n              \"metadata\": {\n                \"name\": \"fake-name\"\n              },\n              \"data\": \"halo\"\n            }\"\"\", objectMapper.writeValueAsString(ext), true);\n    }\n\n    @Test\n    void deserialize() throws JsonProcessingException {\n        var json = \"\"\"\n            {\n              \"apiVersion\": \"fake.halo.run/v1alpha1\",\n              \"kind\": \"Fake\",\n              \"metadata\": {\n                \"name\": \"faker\"\n              },\n              \"otherProperty\": \"otherPropertyValue\"\n            }\"\"\";\n\n        var ext = objectMapper.readValue(json, JsonExtension.class);\n\n        assertEquals(\"fake.halo.run/v1alpha1\", ext.getApiVersion());\n        assertEquals(\"Fake\", ext.getKind());\n        assertNotNull(ext.getMetadata());\n        assertEquals(\"faker\", ext.getMetadata().getName());\n        assertNull(ext.getMetadata().getVersion());\n        assertNull(ext.getMetadata().getFinalizers());\n        assertNull(ext.getMetadata().getAnnotations());\n        assertNull(ext.getMetadata().getLabels());\n        assertNull(ext.getMetadata().getGenerateName());\n        assertNull(ext.getMetadata().getCreationTimestamp());\n        assertNull(ext.getMetadata().getDeletionTimestamp());\n        assertEquals(\"otherPropertyValue\", ext.getInternal().get(\"otherProperty\").asText());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/ListResultTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport java.lang.reflect.ParameterizedType;\nimport java.util.List;\nimport java.util.Optional;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\n\nclass ListResultTest {\n\n    @Test\n    void generateGenericClass() {\n        var fakeListClass =\n            ListResult.generateGenericClass(Scheme.buildFromType(FakeExtension.class));\n        assertTrue(ListResult.class.isAssignableFrom(fakeListClass));\n        assertSame(FakeExtension.class, ((ParameterizedType) fakeListClass.getGenericSuperclass())\n            .getActualTypeArguments()[0]);\n        assertEquals(\"FakeList\", fakeListClass.getSimpleName());\n    }\n\n    @Test\n    void generateGenericClassForClassParam() {\n        var fakeListClass = ListResult.generateGenericClass(FakeExtension.class);\n        assertTrue(ListResult.class.isAssignableFrom(fakeListClass));\n        assertSame(FakeExtension.class, ((ParameterizedType) fakeListClass.getGenericSuperclass())\n            .getActualTypeArguments()[0]);\n        assertEquals(\"FakeExtensionList\", fakeListClass.getSimpleName());\n    }\n\n    @Test\n    void totalPages() {\n        var listResult = new ListResult<>(1, 10, 100, List.of());\n        assertEquals(10, listResult.getTotalPages());\n\n        listResult = new ListResult<>(1, 10, 1, List.of());\n        assertEquals(1, listResult.getTotalPages());\n\n        listResult = new ListResult<>(1, 10, 9, List.of());\n        assertEquals(1, listResult.getTotalPages());\n\n        listResult = new ListResult<>(1, 0, 100, List.of());\n        assertEquals(1, listResult.getTotalPages());\n    }\n\n    @Test\n    void subListWhenSizeIsZero() {\n        var list = List.of(1, 2, 3, 4, 5);\n        assertSubList(list);\n\n        list = List.of(1);\n        assertSubList(list);\n    }\n\n    @Test\n    void firstTest() {\n        var listResult = new ListResult<>(List.of());\n        assertEquals(Optional.empty(), ListResult.first(listResult));\n\n        listResult = new ListResult<>(1, 10, 1, List.of(\"A\"));\n        assertEquals(Optional.of(\"A\"), ListResult.first(listResult));\n    }\n\n    @Test\n    void serializationTest() throws JsonProcessingException {\n        var result = new ListResult<>(1, 10, 100, List.of(\"a\", \"b\", \"c\"));\n        var json = JsonMapper.builder()\n            .build()\n            .writeValueAsString(result);\n        JSONAssert.assertEquals(\"\"\"\n            {\n              \"page\": 1,\n              \"size\": 10,\n              \"total\": 100,\n              \"items\": [\n                \"a\",\n                \"b\",\n                \"c\"\n              ],\n              \"first\": true,\n              \"last\": false,\n              \"hasNext\": true,\n              \"hasPrevious\": false,\n              \"totalPages\": 10\n            }\n            \"\"\", json, true);\n    }\n\n    @Test\n    void deserializationTest() throws JsonProcessingException {\n        var json = \"\"\"\n            {\n              \"page\": 2,\n              \"size\": 10,\n              \"total\": 100,\n              \"items\": [\n                \"a\",\n                \"b\",\n                \"c\"\n              ],\n              \"first\": false,\n              \"last\": false,\n              \"hasNext\": true,\n              \"hasPrevious\": true,\n              \"totalPages\": 10\n            }\n            \"\"\";\n        var result = JsonMapper.builder()\n            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n            .build()\n            .readValue(json, new TypeReference<ListResult<String>>() {\n            });\n        assertEquals(2, result.getPage());\n        assertEquals(100, result.getTotal());\n        assertEquals(10, result.getTotalPages());\n        assertEquals(10, result.getSize());\n        assertFalse(result.isFirst());\n        assertFalse(result.isLast());\n        assertTrue(result.hasNext());\n        assertTrue(result.hasPrevious());\n        assertEquals(List.of(\"a\", \"b\", \"c\"), result.getItems());\n    }\n\n    private void assertSubList(List<Integer> list) {\n        var result = ListResult.subList(list, 0, 0);\n        assertEquals(list, result);\n\n        result = ListResult.subList(list, 0, 1);\n        assertEquals(list.subList(0, 1), result);\n\n        result = ListResult.subList(list, 1, 0);\n        assertEquals(list, result);\n\n        assertEquals(list.subList(0, 1), ListResult.subList(list, -1, 1));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/MetadataOperatorTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\n\nclass MetadataOperatorTest {\n\n    Instant now = Instant.now();\n\n    @Test\n    void testEqualsWithSameType() {\n        assertTrue(MetadataOperator.equals(null, null));\n\n        var left = createFullMetadata();\n        var right = createFullMetadata();\n        assertFalse(MetadataOperator.equals(left, null));\n        assertFalse(MetadataOperator.equals(null, right));\n        assertTrue(MetadataOperator.equals(left, right));\n\n        left.setDeletionTimestamp(null);\n        assertFalse(MetadataOperator.equals(left, right));\n        right.setDeletionTimestamp(null);\n        assertTrue(MetadataOperator.equals(left, right));\n\n        left.setCreationTimestamp(null);\n        assertFalse(MetadataOperator.equals(left, right));\n        right.setCreationTimestamp(null);\n        assertTrue(MetadataOperator.equals(left, right));\n\n        left.setVersion(null);\n        assertFalse(MetadataOperator.equals(left, right));\n        right.setVersion(null);\n        assertTrue(MetadataOperator.equals(left, right));\n\n        left.setAnnotations(null);\n        assertFalse(MetadataOperator.equals(left, right));\n        right.setAnnotations(null);\n        assertTrue(MetadataOperator.equals(left, right));\n\n        left.setLabels(null);\n        assertFalse(MetadataOperator.equals(left, right));\n        right.setLabels(null);\n        assertTrue(MetadataOperator.equals(left, right));\n\n        left.setName(null);\n        assertFalse(MetadataOperator.equals(left, right));\n        right.setName(null);\n        assertTrue(MetadataOperator.equals(left, right));\n\n        left.setFinalizers(null);\n        assertFalse(MetadataOperator.equals(left, right));\n        right.setFinalizers(null);\n        assertTrue(MetadataOperator.equals(left, right));\n    }\n\n    @Test\n    void testEqualsWithDifferentType() {\n        var mockMetadata = mock(MetadataOperator.class);\n        when(mockMetadata.getName()).thenReturn(\"fake-name\");\n        when(mockMetadata.getLabels()).thenReturn(Map.of(\"fake-label-key\", \"fake-label-value\"));\n        when(mockMetadata.getAnnotations()).thenReturn(Map.of(\"fake-anno-key\", \"fake-anno-value\"));\n        when(mockMetadata.getVersion()).thenReturn(123L);\n        when(mockMetadata.getCreationTimestamp()).thenReturn(now);\n        when(mockMetadata.getDeletionTimestamp()).thenReturn(now);\n        when(mockMetadata.getFinalizers())\n            .thenReturn(Set.of(\"fake-finalizer-1\", \"fake-finalizer-2\"));\n\n        var metadata = createFullMetadata();\n        assertTrue(MetadataOperator.equals(metadata, mockMetadata));\n    }\n\n    Metadata createFullMetadata() {\n        var metadata = new Metadata();\n        metadata.setName(\"fake-name\");\n        metadata.setLabels(Map.of(\"fake-label-key\", \"fake-label-value\"));\n        metadata.setAnnotations(Map.of(\"fake-anno-key\", \"fake-anno-value\"));\n        metadata.setVersion(123L);\n        metadata.setCreationTimestamp(now);\n        metadata.setDeletionTimestamp(now);\n        metadata.setFinalizers(Set.of(\"fake-finalizer-2\", \"fake-finalizer-1\"));\n        return metadata;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/ReactiveExtensionClientTest.java",
    "content": "package run.halo.app.extension;\n\nimport static java.util.Collections.emptyList;\nimport static java.util.Collections.reverseOrder;\nimport static java.util.Comparator.comparing;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.extension.GroupVersionKind.fromAPIVersionAndKind;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.dao.DataIntegrityViolationException;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport org.springframework.transaction.reactive.TransactionalOperator;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.exception.SchemeNotFoundException;\nimport run.halo.app.extension.index.IndexEngine;\nimport run.halo.app.extension.store.ExtensionStore;\nimport run.halo.app.extension.store.ReactiveExtensionStoreClient;\n\n@ExtendWith(MockitoExtension.class)\nclass ReactiveExtensionClientTest {\n\n    static final Scheme fakeScheme = Scheme.buildFromType(FakeExtension.class);\n\n    @Mock\n    ReactiveExtensionStoreClient storeClient;\n\n    @Mock\n    ExtensionConverter converter;\n\n    @Mock\n    SchemeManager schemeManager;\n\n    @Mock\n    ReactiveTransactionManager reactiveTransactionManager;\n\n    @Spy\n    ObjectMapper objectMapper = JsonMapper.builder()\n        .addModule(new JavaTimeModule())\n        .build();\n\n    @Mock\n    IndexEngine indexEngine;\n\n    @InjectMocks\n    ReactiveExtensionClientImpl client;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(schemeManager.get(eq(FakeExtension.class)))\n            .thenReturn(fakeScheme);\n        lenient().when(schemeManager.get(eq(fakeScheme.groupVersionKind()))).thenReturn(fakeScheme);\n        var transactionalOperator = mock(TransactionalOperator.class);\n        client.setTransactionalOperator(transactionalOperator);\n        lenient().when(transactionalOperator.transactional(any(Mono.class)))\n            .thenAnswer(invocation -> invocation.getArgument(0));\n    }\n\n    FakeExtension createFakeExtension(String name, Long version) {\n        var fake = new FakeExtension();\n        var metadata = new Metadata();\n        metadata.setName(name);\n        metadata.setVersion(version);\n\n        fake.setMetadata(metadata);\n        fake.setApiVersion(\"fake.halo.run/v1alpha1\");\n        fake.setKind(\"Fake\");\n\n        return fake;\n    }\n\n    ExtensionStore createExtensionStore(String name) {\n        return createExtensionStore(name, null);\n    }\n\n    ExtensionStore createExtensionStore(String name, Long version) {\n        var extensionStore = new ExtensionStore();\n        extensionStore.setName(name);\n        extensionStore.setVersion(version);\n        extensionStore.setData(\"fake data\".getBytes());\n        return extensionStore;\n    }\n\n    Unstructured createUnstructured() throws JsonProcessingException {\n        String extensionJson = \"\"\"\n            {\n                \"apiVersion\": \"fake.halo.run/v1alpha1\",\n                \"kind\": \"Fake\",\n                \"metadata\": {\n                    \"labels\": {\n                        \"category\": \"fake\",\n                        \"default\": \"true\"\n                    },\n                    \"name\": \"fake\",\n                    \"creationTimestamp\": \"2011-12-03T10:15:30Z\",\n                    \"version\": 12345\n                }\n            }\n            \"\"\";\n        return Unstructured.OBJECT_MAPPER.readValue(extensionJson, Unstructured.class);\n    }\n\n    @Test\n    void shouldThrowSchemeNotFoundExceptionWhenSchemeNotRegistered() {\n        class UnRegisteredExtension extends AbstractExtension {\n        }\n\n        when(schemeManager.get(eq(UnRegisteredExtension.class)))\n            .thenThrow(SchemeNotFoundException.class);\n        when(schemeManager.get(isA(GroupVersionKind.class)))\n            .thenThrow(SchemeNotFoundException.class);\n\n        assertThrows(SchemeNotFoundException.class,\n            () -> client.list(UnRegisteredExtension.class, null,\n                null));\n        assertThrows(SchemeNotFoundException.class,\n            () -> client.list(UnRegisteredExtension.class, null, null, 0, 10));\n        assertThrows(SchemeNotFoundException.class,\n            () -> client.fetch(UnRegisteredExtension.class, \"fake\"));\n        assertThrows(SchemeNotFoundException.class, () ->\n            client.fetch(fromAPIVersionAndKind(\"fake.halo.run/v1alpha1\", \"UnRegistered\"), \"fake\"));\n\n        when(converter.convertTo(any())).thenThrow(SchemeNotFoundException.class);\n        StepVerifier.create(client.create(createFakeExtension(\"fake\", null)))\n            .verifyError(SchemeNotFoundException.class);\n\n        assertThrows(SchemeNotFoundException.class, () -> {\n            when(converter.convertTo(any())).thenThrow(SchemeNotFoundException.class);\n            client.update(createFakeExtension(\"fake\", 1L));\n        });\n        assertThrows(SchemeNotFoundException.class, () -> {\n            when(converter.convertTo(any())).thenThrow(SchemeNotFoundException.class);\n            client.delete(createFakeExtension(\"fake\", 1L));\n        });\n    }\n\n    @Test\n    void shouldReturnEmptyExtensions() {\n        when(storeClient.listByNamePrefix(anyString())).thenReturn(Flux.empty());\n        var fakes = client.list(FakeExtension.class, null, null);\n        StepVerifier.create(fakes)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldReturnExtensionsWithFilterAndSorter() {\n        var fake1 = createFakeExtension(\"fake-01\", 1L);\n        var fake2 = createFakeExtension(\"fake-02\", 1L);\n\n        when(\n            converter.convertFrom(FakeExtension.class, createExtensionStore(\"fake-01\"))).thenReturn(\n            fake1);\n        when(\n            converter.convertFrom(FakeExtension.class, createExtensionStore(\"fake-02\"))).thenReturn(\n            fake2);\n        when(storeClient.listByNamePrefix(anyString())).thenReturn(\n            Flux.fromIterable(\n                List.of(createExtensionStore(\"fake-01\"), createExtensionStore(\"fake-02\"))));\n\n        // without filter and sorter\n        Flux<FakeExtension> fakes =\n            client.list(FakeExtension.class, null, null);\n        StepVerifier.create(fakes)\n            .expectNext(fake1)\n            .expectNext(fake2)\n            .verifyComplete();\n\n        // with filter\n        fakes = client.list(FakeExtension.class, fake -> {\n            String name = fake.getMetadata().getName();\n            return \"fake-01\".equals(name);\n        }, null);\n        StepVerifier.create(fakes)\n            .expectNext(fake1)\n            .verifyComplete();\n\n        // with sorter\n        fakes = client.list(FakeExtension.class, null,\n            reverseOrder(comparing(fake -> fake.getMetadata().getName())));\n        StepVerifier.create(fakes)\n            .expectNext(fake2)\n            .expectNext(fake1)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldQueryPageableAndCorrectly() {\n        var fake1 = createFakeExtension(\"fake-01\", 1L);\n        var fake2 = createFakeExtension(\"fake-02\", 1L);\n        var fake3 = createFakeExtension(\"fake-03\", 1L);\n\n        when(\n            converter.convertFrom(FakeExtension.class, createExtensionStore(\"fake-01\"))).thenReturn(\n            fake1);\n        when(\n            converter.convertFrom(FakeExtension.class, createExtensionStore(\"fake-02\"))).thenReturn(\n            fake2);\n        when(\n            converter.convertFrom(FakeExtension.class, createExtensionStore(\"fake-03\"))).thenReturn(\n            fake3);\n\n        when(storeClient.listByNamePrefix(anyString())).thenReturn(Flux.fromIterable(\n            List.of(createExtensionStore(\"fake-01\"), createExtensionStore(\"fake-02\"),\n                createExtensionStore(\"fake-03\"))));\n\n        // without filter and sorter.\n        var fakes = client.list(FakeExtension.class, null, null, 1, 10);\n        StepVerifier.create(fakes)\n            .expectNext(new ListResult<>(1, 10, 3, List.of(fake1, fake2, fake3)))\n            .verifyComplete();\n\n        // out of page range\n        fakes = client.list(FakeExtension.class, null, null, 100, 10);\n        StepVerifier.create(fakes)\n            .expectNext(new ListResult<>(100, 10, 3, emptyList()))\n            .verifyComplete();\n\n        // with filter only\n        fakes =\n            client.list(FakeExtension.class, fake -> \"fake-03\".equals(fake.getMetadata().getName()),\n                null, 1, 10);\n        StepVerifier.create(fakes)\n            .expectNext(new ListResult<>(1, 10, 1, List.of(fake3)))\n            .verifyComplete();\n\n        // with sorter only\n        fakes = client.list(FakeExtension.class, null,\n            reverseOrder(comparing(fake -> fake.getMetadata().getName())), 1, 10);\n        StepVerifier.create(fakes)\n            .expectNext(new ListResult<>(1, 10, 3, List.of(fake3, fake2, fake1)))\n            .verifyComplete();\n\n        // without page\n        fakes = client.list(FakeExtension.class, null, null, 0, 0);\n        StepVerifier.create(fakes)\n            .expectNext(new ListResult<>(0, 0, 3, List.of(fake1, fake2, fake3)))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFetchNothing() {\n        when(storeClient.fetchByName(any())).thenReturn(Mono.empty());\n\n        var fake = client.fetch(FakeExtension.class, \"fake\");\n\n        StepVerifier.create(fake)\n            .verifyComplete();\n\n        verify(converter, times(0)).convertFrom(any(), any());\n        verify(storeClient, times(1)).fetchByName(any());\n    }\n\n    @Test\n    void shouldNotFetchUnstructured() {\n        when(schemeManager.get(isA(GroupVersionKind.class)))\n            .thenReturn(fakeScheme);\n        when(storeClient.fetchByName(any())).thenReturn(Mono.empty());\n        var unstructuredFake = client.fetch(fakeScheme.groupVersionKind(), \"fake\");\n        StepVerifier.create(unstructuredFake)\n            .verifyComplete();\n\n        verify(converter, times(0)).convertFrom(any(), any());\n        verify(schemeManager, times(1)).get(isA(GroupVersionKind.class));\n        verify(storeClient, times(1)).fetchByName(any());\n    }\n\n    @Test\n    void shouldFetchAnExtension() {\n        var storeName = \"/registry/fake.halo.run/fakes/fake\";\n        when(storeClient.fetchByName(storeName)).thenReturn(\n            Mono.just(createExtensionStore(storeName)));\n\n        when(\n            converter.convertFrom(FakeExtension.class, createExtensionStore(storeName))).thenReturn(\n            createFakeExtension(\"fake\", 1L));\n\n        var fake = client.fetch(FakeExtension.class, \"fake\");\n        StepVerifier.create(fake)\n            .expectNext(createFakeExtension(\"fake\", 1L))\n            .verifyComplete();\n\n        verify(storeClient, times(1)).fetchByName(eq(storeName));\n        verify(converter, times(1)).convertFrom(eq(FakeExtension.class),\n            eq(createExtensionStore(storeName)));\n    }\n\n    @Test\n    void shouldFetchUnstructuredExtension() throws JsonProcessingException {\n        var storeName = \"/registry/fake.halo.run/fakes/fake\";\n        when(storeClient.fetchByName(storeName)).thenReturn(\n            Mono.just(createExtensionStore(storeName)));\n        when(schemeManager.get(isA(GroupVersionKind.class)))\n            .thenReturn(fakeScheme);\n        when(converter.convertFrom(Unstructured.class, createExtensionStore(storeName)))\n            .thenReturn(createUnstructured());\n\n        var fake = client.fetch(fakeScheme.groupVersionKind(), \"fake\");\n        StepVerifier.create(fake)\n            .expectNext(createUnstructured())\n            .verifyComplete();\n\n        verify(storeClient, times(1)).fetchByName(eq(storeName));\n        verify(schemeManager, times(1)).get(isA(GroupVersionKind.class));\n        verify(converter, times(1)).convertFrom(eq(Unstructured.class),\n            eq(createExtensionStore(storeName)));\n    }\n\n    @Test\n    void shouldCreateSuccessfully() {\n        var fake = createFakeExtension(\"fake\", null);\n        when(converter.convertTo(any())).thenReturn(\n            createExtensionStore(\"/registry/fake.halo.run/fakes/fake\"));\n        when(storeClient.create(any(), any())).thenReturn(\n            Mono.just(createExtensionStore(\"/registry/fake.halo.run/fakes/fake\")));\n        when(converter.convertFrom(same(FakeExtension.class), any())).thenReturn(fake);\n        doNothing().when(indexEngine).insert(any());\n\n        StepVerifier.create(client.create(fake))\n            .expectNext(fake)\n            .verifyComplete();\n\n        verify(converter, times(1)).convertTo(eq(fake));\n        verify(storeClient, times(1)).create(eq(\"/registry/fake.halo.run/fakes/fake\"), any());\n        assertNotNull(fake.getMetadata().getCreationTimestamp());\n    }\n\n    @Test\n    void shouldCreateWithGenerateNameSuccessfully() {\n        var fake = createFakeExtension(\"fake\", null);\n        fake.getMetadata().setName(\"\");\n        fake.getMetadata().setGenerateName(\"fake-\");\n        when(converter.convertTo(any())).thenReturn(\n            createExtensionStore(\"/registry/fake.halo.run/fakes/fake\"));\n        when(storeClient.create(any(), any())).thenReturn(\n            Mono.just(createExtensionStore(\"/registry/fake.halo.run/fakes/fake\")));\n        when(converter.convertFrom(same(FakeExtension.class), any())).thenReturn(fake);\n        doNothing().when(indexEngine).insert(any());\n\n        StepVerifier.create(client.create(fake))\n            .expectNext(fake)\n            .verifyComplete();\n\n        verify(converter, times(1)).convertTo(argThat(ext -> {\n            var name = ext.getMetadata().getName();\n            return name.startsWith(ext.getMetadata().getGenerateName());\n        }));\n        verify(storeClient, times(1)).create(eq(\"/registry/fake.halo.run/fakes/fake\"), any());\n        assertNotNull(fake.getMetadata().getCreationTimestamp());\n    }\n\n    @Test\n    void shouldThrowExceptionIfCreatingWithoutGenerateName() {\n        var fake = createFakeExtension(\"fake\", null);\n        fake.getMetadata().setName(\"\");\n        fake.getMetadata().setGenerateName(\"\");\n\n        StepVerifier.create(client.create(fake))\n            .verifyError(IllegalArgumentException.class);\n    }\n\n    @Test\n    void shouldThrowExceptionIfPrimaryKeyDuplicated() {\n        var fake = createFakeExtension(\"fake\", null);\n        fake.getMetadata().setName(\"\");\n        fake.getMetadata().setGenerateName(\"fake-\");\n        when(converter.convertTo(any())).thenReturn(\n            createExtensionStore(\"/registry/fake.halo.run/fakes/fake\"));\n        when(storeClient.create(any(), any())).thenThrow(DataIntegrityViolationException.class);\n\n        StepVerifier.create(client.create(fake))\n            .expectErrorMatches(Exceptions::isRetryExhausted)\n            .verify();\n    }\n\n    @Test\n    void shouldCreateUsingUnstructuredSuccessfully() throws JsonProcessingException {\n        var fake = createUnstructured();\n\n        when(converter.convertTo(any())).thenReturn(\n            createExtensionStore(\"/registry/fake.halo.run/fakes/fake\"));\n        when(storeClient.create(any(), any())).thenReturn(\n            Mono.just(createExtensionStore(\"/registry/fake.halo.run/fakes/fake\")));\n        when(converter.convertFrom(same(Unstructured.class), any())).thenReturn(fake);\n        doNothing().when(indexEngine).insert(any());\n\n        StepVerifier.create(client.create(fake))\n            .expectNext(fake)\n            .verifyComplete();\n\n        verify(converter, times(1)).convertTo(eq(fake));\n        verify(storeClient, times(1)).create(eq(\"/registry/fake.halo.run/fakes/fake\"), any());\n        assertNotNull(fake.getMetadata().getCreationTimestamp());\n    }\n\n    @Test\n    void shouldUpdateSuccessfully() {\n        var fake = createFakeExtension(\"fake\", 2L);\n        fake.getMetadata().setLabels(Map.of(\"new\", \"true\"));\n        var storeName = \"/registry/fake.halo.run/fakes/fake\";\n        when(converter.convertTo(any())).thenReturn(\n            createExtensionStore(storeName, 2L));\n        when(storeClient.update(any(), any(), any())).thenReturn(\n            Mono.just(createExtensionStore(storeName, 2L)));\n        when(storeClient.fetchByName(storeName)).thenReturn(\n            Mono.just(createExtensionStore(storeName, 1L)));\n        doNothing().when(indexEngine).update(any());\n\n        var oldFake = createFakeExtension(\"fake\", 2L);\n        oldFake.getMetadata().setLabels(Map.of(\"old\", \"true\"));\n\n        var updatedFake = createFakeExtension(\"fake\", 3L);\n        updatedFake.getMetadata().setLabels(Map.of(\"updated\", \"true\"));\n        when(converter.convertFrom(same(FakeExtension.class), any()))\n            .thenReturn(oldFake)\n            .thenReturn(updatedFake);\n\n        StepVerifier.create(client.update(fake))\n            .expectNext(updatedFake)\n            .verifyComplete();\n\n        verify(storeClient).fetchByName(storeName);\n        verify(converter).convertTo(isA(JsonExtension.class));\n        verify(converter, times(2)).convertFrom(same(FakeExtension.class), any());\n        verify(storeClient)\n            .update(eq(\"/registry/fake.halo.run/fakes/fake\"), eq(2L), any());\n    }\n\n    @Test\n    void shouldNotUpdateIfExtensionNotChange() {\n        var fake = createFakeExtension(\"fake\", 2L);\n        var storeName = \"/registry/fake.halo.run/fakes/fake\";\n        when(storeClient.fetchByName(storeName)).thenReturn(\n            Mono.just(createExtensionStore(storeName, 1L)));\n\n        var oldFake = createFakeExtension(\"fake\", 2L);\n\n        when(converter.convertFrom(same(FakeExtension.class), any())).thenReturn(oldFake);\n\n        StepVerifier.create(client.update(fake))\n            .expectNext(fake)\n            .verifyComplete();\n\n        verify(storeClient).fetchByName(storeName);\n        verify(converter).convertFrom(same(FakeExtension.class), any());\n        verify(converter, never()).convertTo(any());\n        verify(storeClient, never()).update(any(), any(), any());\n    }\n\n    @Test\n    void shouldNotUpdateIfUnstructuredNotChange() throws JsonProcessingException {\n        var storeName = \"/registry/fake.halo.run/fakes/fake\";\n        var extensionStore = createExtensionStore(storeName, 2L);\n        when(storeClient.fetchByName(storeName)).thenReturn(\n            Mono.just(extensionStore));\n\n        var fakeJson = objectMapper.writeValueAsString(createFakeExtension(\"fake\", 2L));\n        var oldFakeJson = objectMapper.writeValueAsString(createFakeExtension(\"fake\", 2L));\n\n        var fake = objectMapper.readValue(fakeJson, Unstructured.class);\n        var oldFake = objectMapper.readValue(oldFakeJson, Unstructured.class);\n        oldFake.getMetadata().setVersion(2L);\n\n        when(converter.convertFrom(Unstructured.class, extensionStore)).thenReturn(oldFake);\n\n        StepVerifier.create(client.update(fake))\n            .expectNext(fake)\n            .verifyComplete();\n\n        verify(storeClient).fetchByName(storeName);\n        verify(converter).convertFrom(Unstructured.class, extensionStore);\n        verify(converter, never()).convertTo(any());\n        verify(storeClient, never()).update(any(), any(), any());\n    }\n\n    @Test\n    void shouldUpdateIfExtensionStatusChangedOnly() {\n        var fake = createFakeExtension(\"fake\", 2L);\n        fake.getStatus().setState(\"new-state\");\n        var storeName = \"/registry/fake.halo.run/fakes/fake\";\n        when(converter.convertTo(any())).thenReturn(\n            createExtensionStore(storeName, 2L));\n        when(storeClient.update(any(), any(), any())).thenReturn(\n            Mono.just(createExtensionStore(storeName, 2L)));\n        when(storeClient.fetchByName(storeName)).thenReturn(\n            Mono.just(createExtensionStore(storeName, 1L)));\n        doNothing().when(indexEngine).update(any());\n\n        var oldFake = createFakeExtension(\"fake\", 2L);\n        oldFake.getStatus().setState(\"old-state\");\n\n        var updatedFake = createFakeExtension(\"fake\", 3L);\n        when(converter.convertFrom(same(FakeExtension.class), any()))\n            .thenReturn(oldFake)\n            .thenReturn(updatedFake);\n\n        StepVerifier.create(client.update(fake))\n            .expectNext(updatedFake)\n            .verifyComplete();\n\n        verify(storeClient).fetchByName(storeName);\n        verify(converter).convertTo(isA(JsonExtension.class));\n        verify(converter, times(2)).convertFrom(same(FakeExtension.class), any());\n        verify(storeClient)\n            .update(eq(\"/registry/fake.halo.run/fakes/fake\"), eq(2L), any());\n    }\n\n    @Test\n    void shouldUpdateUnstructuredSuccessfully() throws JsonProcessingException {\n        var fake = createUnstructured();\n        var name = \"/registry/fake.halo.run/fakes/fake\";\n        when(converter.convertTo(any()))\n            .thenReturn(createExtensionStore(name, 12345L));\n        when(storeClient.update(any(), any(), any()))\n            .thenReturn(Mono.just(createExtensionStore(name, 12345L)));\n        when(storeClient.fetchByName(name))\n            .thenReturn(Mono.just(createExtensionStore(name, 12346L)));\n        doNothing().when(indexEngine).update(any());\n\n        var oldFake = createUnstructured();\n        oldFake.getMetadata().setLabels(Map.of(\"old\", \"true\"));\n\n        var updatedFake = createUnstructured();\n        updatedFake.getMetadata().setLabels(Map.of(\"updated\", \"true\"));\n        when(converter.convertFrom(same(Unstructured.class), any()))\n            .thenReturn(oldFake)\n            .thenReturn(updatedFake);\n\n        StepVerifier.create(client.update(fake))\n            .expectNext(updatedFake)\n            .verifyComplete();\n\n        verify(storeClient).fetchByName(name);\n        verify(converter).convertTo(isA(JsonExtension.class));\n        verify(converter, times(2)).convertFrom(same(Unstructured.class), any());\n        verify(storeClient)\n            .update(eq(\"/registry/fake.halo.run/fakes/fake\"), eq(12345L), any());\n    }\n\n    @Test\n    void shouldDeleteSuccessfully() {\n        var fake = createFakeExtension(\"fake\", 2L);\n        when(converter.convertTo(any())).thenReturn(\n            createExtensionStore(\"/registry/fake.halo.run/fakes/fake\"));\n        when(storeClient.update(any(), any(), any())).thenReturn(\n            Mono.just(createExtensionStore(\"/registry/fake.halo.run/fakes/fake\")));\n        when(converter.convertFrom(same(FakeExtension.class), any())).thenReturn(fake);\n        doNothing().when(indexEngine).update(any());\n\n        StepVerifier.create(client.delete(fake))\n            .expectNext(fake)\n            .verifyComplete();\n\n        verify(converter, times(1)).convertTo(any());\n        verify(storeClient, times(1)).update(any(), any(), any());\n        verify(storeClient, never()).delete(any(), any());\n    }\n\n    @Test\n    void shouldGetJsonExtension() {\n        var storeName = \"/registry/fake.halo.run/fakes/fake\";\n        when(storeClient.fetchByName(storeName)).thenReturn(\n            Mono.just(createExtensionStore(storeName)));\n\n        var fake = createFakeExtension(\"fake\", 1L);\n        var expectedJsonExt = objectMapper.convertValue(fake, JsonExtension.class);\n\n        when(converter.convertFrom(JsonExtension.class, createExtensionStore(storeName)))\n            .thenReturn(expectedJsonExt);\n\n        var gvk = Scheme.buildFromType(FakeExtension.class).groupVersionKind();\n        StepVerifier.create(client.getJsonExtension(gvk, \"fake\"))\n            .expectNext(expectedJsonExt)\n            .verifyComplete();\n\n        verify(storeClient, times(1)).fetchByName(eq(storeName));\n        verify(converter, times(1)).convertFrom(eq(JsonExtension.class),\n            eq(createExtensionStore(storeName)));\n    }\n\n\n    @Nested\n    @DisplayName(\"Extension watcher test\")\n    class WatcherTest {\n\n        @Mock\n        Watcher watcher;\n\n        @BeforeEach\n        void setUp() {\n            client.watch(watcher);\n        }\n\n        @Test\n        void shouldWatchOnAddSuccessfully() {\n            doNothing().when(watcher).onAdd(isA(Extension.class));\n            shouldCreateSuccessfully();\n\n            verify(watcher, times(1)).onAdd(isA(Extension.class));\n        }\n\n        @Test\n        void shouldWatchOnUpdateSuccessfully() {\n            doNothing().when(watcher).onUpdate(any(), any());\n            shouldUpdateSuccessfully();\n\n            verify(watcher, times(1)).onUpdate(any(), any());\n        }\n\n        @Test\n        void shouldNotWatchOnUpdateIfExtensionNotChange() {\n            shouldNotUpdateIfExtensionNotChange();\n\n            verify(watcher, never()).onUpdate(any(), any());\n        }\n\n        @Test\n        void shouldNotWatchOnUpdateIfExtensionStatusChangeOnly() {\n            shouldUpdateIfExtensionStatusChangedOnly();\n\n            verify(watcher, never()).onUpdate(any(), any());\n        }\n\n        @Test\n        void shouldWatchOnDeleteSuccessfully() {\n            doNothing().when(watcher).onDelete(any());\n            shouldDeleteSuccessfully();\n\n            verify(watcher, times(1)).onDelete(any());\n        }\n\n        @Test\n        void shouldWatchRealTypeOnAdd() {\n            var name = \"/registry/fake.halo.run/fakes/fake\";\n            var extensionStore = createExtensionStore(name);\n            var fake = createFakeExtension(\"fake\", 1L);\n            var unstructured = Unstructured.OBJECT_MAPPER.convertValue(fake, Unstructured.class);\n\n            when(converter.convertTo(unstructured)).thenReturn(extensionStore);\n            when(converter.convertFrom(Unstructured.class, extensionStore))\n                .thenReturn(unstructured);\n            when(storeClient.create(eq(name), any(byte[].class)))\n                .thenReturn(Mono.just(extensionStore));\n            doNothing().when(watcher).onAdd(isA(FakeExtension.class));\n\n            client.create(unstructured)\n                .as(StepVerifier::create)\n                .expectNext(unstructured)\n                .verifyComplete();\n        }\n\n        @Test\n        void shouldWatchRealTypeOnUpdate() {\n            var name = \"/registry/fake.halo.run/fakes/fake\";\n\n            var oldExtensionStore = createExtensionStore(name, 1L);\n            var extensionStore = createExtensionStore(name, 2L);\n            var oldFake = createFakeExtension(\"fake\", 1L);\n            var fake = createFakeExtension(\"fake\", 2L);\n            var oldUnstructured =\n                Unstructured.OBJECT_MAPPER.convertValue(oldFake, Unstructured.class);\n            var unstructured = Unstructured.OBJECT_MAPPER.convertValue(fake, Unstructured.class);\n\n            when(storeClient.fetchByName(name))\n                .thenReturn(Mono.just(oldExtensionStore));\n            when(converter.convertFrom(Unstructured.class, oldExtensionStore))\n                .thenReturn(oldUnstructured);\n            when(converter.convertFrom(Unstructured.class, extensionStore))\n                .thenReturn(unstructured);\n            when(converter.convertTo(isA(JsonExtension.class))).thenReturn(extensionStore);\n            when(storeClient.update(eq(name), eq(2L), any(byte[].class)))\n                .thenReturn(Mono.just(extensionStore));\n            doNothing().when(watcher).onUpdate(isA(FakeExtension.class), isA(FakeExtension.class));\n\n            client.update(unstructured)\n                .as(StepVerifier::create)\n                .expectNext(unstructured)\n                .verifyComplete();\n        }\n\n        @Test\n        void shouldWatchRealTypeOnDelete() {\n            var name = \"/registry/fake.halo.run/fakes/fake\";\n\n            var extensionStore = createExtensionStore(name, 1L);\n            var fake = createFakeExtension(\"fake\", 1L);\n            var unstructured = Unstructured.OBJECT_MAPPER.convertValue(fake, Unstructured.class);\n\n            when(converter.convertFrom(Unstructured.class, extensionStore))\n                .thenReturn(unstructured);\n            when(converter.convertTo(unstructured)).thenReturn(extensionStore);\n            when(storeClient.update(eq(name), eq(1L), any(byte[].class)))\n                .thenReturn(Mono.just(extensionStore));\n            doNothing().when(watcher).onDelete(isA(FakeExtension.class));\n\n            client.delete(unstructured)\n                .as(StepVerifier::create)\n                .expectNext(unstructured)\n                .verifyComplete();\n        }\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/RefTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static run.halo.app.extension.GroupVersionKind.fromAPIVersionAndKind;\nimport static run.halo.app.extension.GroupVersionKind.fromExtension;\n\nimport org.junit.jupiter.api.Test;\n\nclass RefTest {\n\n    @Test\n    void shouldHasSameGroupAndKind() {\n        FakeExtension fake = new FakeExtension();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake\");\n        fake.setMetadata(metadata);\n        assertTrue(Ref.groupKindEquals(Ref.of(fake), fromExtension(fake.getClass())));\n        // has different version\n        assertTrue(Ref.groupKindEquals(Ref.of(fake),\n            fromAPIVersionAndKind(\"fake.halo.run/v11111111111\", \"Fake\")));\n        assertFalse(Ref.groupKindEquals(Ref.of(fake),\n            fromAPIVersionAndKind(\"fake.halo.run/v1alpha1\", \"NotFake\")));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/SchemeTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport org.junit.jupiter.api.Test;\n\nclass SchemeTest {\n\n    @Test\n    void requiredFieldTest() {\n        assertThrows(IllegalArgumentException.class,\n            () -> new Scheme(null, new GroupVersionKind(\"\", \"v1alpha1\", \"\"), \"\", \"\", null));\n        assertThrows(IllegalArgumentException.class,\n            () -> new Scheme(FakeExtension.class, new GroupVersionKind(\"\", \"\", \"\"), \"\", \"\",\n                null));\n        assertThrows(IllegalArgumentException.class,\n            () -> new Scheme(FakeExtension.class, new GroupVersionKind(\"\", \"v1alpha1\", \"\"), \"\",\n                \"\", null));\n        assertThrows(IllegalArgumentException.class,\n            () -> new Scheme(FakeExtension.class, new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"), \"\",\n                \"\", null));\n        assertThrows(IllegalArgumentException.class,\n            () -> new Scheme(FakeExtension.class, new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"),\n                \"fakes\", \"\", null));\n        assertThrows(IllegalArgumentException.class, () -> {\n            new Scheme(FakeExtension.class, new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"), \"fakes\",\n                \"fake\", null);\n        });\n        new Scheme(FakeExtension.class, new GroupVersionKind(\"\", \"v1alpha1\", \"Fake\"), \"fakes\",\n            \"fake\", new ObjectNode(null));\n    }\n\n\n    @Test\n    void shouldThrowExceptionWhenTypeHasNoGvkAnno() {\n        class NoGvkExtension extends AbstractExtension {\n        }\n\n        assertThrows(IllegalArgumentException.class,\n            () -> Scheme.getGvkFromType(NoGvkExtension.class));\n        assertThrows(IllegalArgumentException.class,\n            () -> Scheme.buildFromType(NoGvkExtension.class));\n    }\n\n    @Test\n    void shouldGetGvkFromTypeWithGvkAnno() {\n        var gvk = Scheme.getGvkFromType(FakeExtension.class);\n        assertEquals(\"fake.halo.run\", gvk.group());\n        assertEquals(\"v1alpha1\", gvk.version());\n        assertEquals(\"Fake\", gvk.kind());\n        assertEquals(\"fake\", gvk.singular());\n        assertEquals(\"fakes\", gvk.plural());\n    }\n\n    @Test\n    void shouldCreateSchemeSuccessfully() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        assertEquals(new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Fake\"),\n            scheme.groupVersionKind());\n        assertEquals(\"fake\", scheme.singular());\n        assertEquals(\"fakes\", scheme.plural());\n        assertNotNull(scheme.openApiSchema());\n        assertEquals(FakeExtension.class, scheme.type());\n    }\n\n    @Test\n    void equalsAndHashCodeTest() {\n        var scheme1 = Scheme.buildFromType(FakeExtension.class);\n        var scheme2 = Scheme.buildFromType(FakeExtension.class);\n        assertEquals(scheme1, scheme2);\n        assertEquals(scheme1.hashCode(), scheme2.hashCode());\n\n        // openApiSchema is not included in equals and hashCode.\n        var scheme3 = new Scheme(FakeExtension.class, scheme1.groupVersionKind(),\n            scheme1.plural(), scheme1.singular(), JsonNodeFactory.instance.objectNode());\n        assertEquals(scheme1, scheme3);\n\n        // singular and plural are not included in equals and hashCode.\n        var scheme4 = new Scheme(FakeExtension.class, scheme1.groupVersionKind(),\n            scheme1.plural(), \"other\", scheme1.openApiSchema());\n        assertEquals(scheme1, scheme4);\n\n        // plural is not included in equals and hashCode.\n        var scheme5 = new Scheme(FakeExtension.class, scheme1.groupVersionKind(),\n            \"other\", scheme1.singular(), scheme1.openApiSchema());\n        assertEquals(scheme1, scheme5);\n\n        // type is not included in equals and hashCode.\n        var scheme6 = new Scheme(FakeExtension.class, scheme1.groupVersionKind(),\n            scheme1.plural(), scheme1.singular(), scheme1.openApiSchema());\n        assertEquals(scheme1, scheme6);\n\n        // groupVersionKind is included in equals and hashCode.\n        var scheme7 = new Scheme(FakeExtension.class,\n            new GroupVersionKind(\"other.halo.run\", \"v1alpha1\", \"Fake\"),\n            scheme1.plural(), scheme1.singular(), scheme1.openApiSchema());\n        assertNotEquals(scheme1, scheme7);\n\n        scheme7 = new Scheme(FakeExtension.class,\n            new GroupVersionKind(\"fake.halo.run\", \"v1alpha2\", \"Fake\"),\n            scheme1.plural(), scheme1.singular(), scheme1.openApiSchema());\n        assertNotEquals(scheme1, scheme7);\n\n        scheme7 = new Scheme(FakeExtension.class,\n            new GroupVersionKind(\"fake.halo.run\", \"v1alpha1\", \"Other\"),\n            scheme1.plural(), scheme1.singular(), scheme1.openApiSchema());\n        assertNotEquals(scheme1, scheme7);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/UnstructuredTest.java",
    "content": "package run.halo.app.extension;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.test.json.JsonAssert;\nimport org.springframework.test.json.JsonCompareMode;\nimport tools.jackson.databind.json.JsonMapper;\n\nclass UnstructuredTest {\n\n    ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER;\n\n    String extensionJson = \"\"\"\n        {\n            \"apiVersion\": \"fake.halo.run/v1alpha1\",\n            \"kind\": \"Fake\",\n            \"metadata\": {\n                \"labels\": {\n                    \"category\": \"fake\",\n                    \"default\": \"true\"\n                },\n                \"name\": \"fake-extension\",\n                \"creationTimestamp\": \"2011-12-03T10:15:30Z\",\n                \"version\": 12345,\n                \"finalizers\": [\"finalizer.1\", \"finalizer.2\"]\n            }\n        }\n        \"\"\";\n\n    @Test\n    void shouldSerializeCorrectly() throws JsonProcessingException {\n        Map extensionMap = objectMapper.readValue(extensionJson, Map.class);\n        var extension = new Unstructured(extensionMap);\n\n        var gotNode = objectMapper.valueToTree(extension);\n        assertEquals(objectMapper.readTree(extensionJson), gotNode);\n    }\n\n    @Test\n    void shouldSetCreationTimestamp() throws JsonProcessingException, JSONException {\n        Map extensionMap = objectMapper.readValue(extensionJson, Map.class);\n        var extension = new Unstructured(extensionMap);\n\n        var beforeChange = objectMapper.writeValueAsString(extension);\n\n        var metadata = extension.getMetadata();\n        metadata.setCreationTimestamp(metadata.getCreationTimestamp());\n\n        var afterChange = objectMapper.writeValueAsString(extension);\n\n        JSONAssert.assertEquals(beforeChange, afterChange, true);\n    }\n\n    @Test\n    void shouldDeserializeCorrectly() throws JsonProcessingException, JSONException {\n        var extension = objectMapper.readValue(extensionJson, Unstructured.class);\n        var gotJson = objectMapper.writeValueAsString(extension);\n        JSONAssert.assertEquals(extensionJson, gotJson, true);\n    }\n\n    @Test\n    void shouldGetExtensionCorrectly() throws JsonProcessingException {\n        var extension = objectMapper.readValue(extensionJson, Unstructured.class);\n\n        assertEquals(\"fake.halo.run/v1alpha1\", extension.getApiVersion());\n        assertEquals(\"Fake\", extension.getKind());\n        MetadataOperator.equals(createMetadata(), extension.getMetadata());\n    }\n\n    @Test\n    void shouldSetExtensionCorrectly() {\n        var extension = createUnstructured();\n\n        assertEquals(\"fake.halo.run/v1alpha1\", extension.getApiVersion());\n        assertEquals(\"Fake\", extension.getKind());\n        assertTrue(MetadataOperator.equals(createMetadata(), extension.getMetadata()));\n    }\n\n    @Test\n    void shouldBeEqual() {\n        assertEquals(new Unstructured(), new Unstructured());\n        assertEquals(createUnstructured(), createUnstructured());\n    }\n\n    @Test\n    void shouldNotBeEqual() {\n        var another = createUnstructured();\n        another.getMetadata().setName(\"fake-extension-2\");\n        assertNotEquals(createUnstructured(), another);\n    }\n\n    @Test\n    void shouldGetFinalizersCorrectly() throws JsonProcessingException {\n        var extension = objectMapper.readValue(extensionJson, Unstructured.class);\n\n        assertEquals(Set.of(\"finalizer.1\", \"finalizer.2\"), extension.getMetadata().getFinalizers());\n\n        extension.getMetadata().setFinalizers(Set.of(\"finalizer.3\", \"finalizer.4\"));\n        assertEquals(Set.of(\"finalizer.3\", \"finalizer.4\"), extension.getMetadata().getFinalizers());\n    }\n\n    @Test\n    void shouldSetLabelsCorrectly() throws JsonProcessingException {\n        var extension = objectMapper.readValue(extensionJson, Unstructured.class);\n\n        assertEquals(Map.of(\"category\", \"fake\", \"default\", \"true\"),\n            extension.getMetadata().getLabels());\n\n        extension.getMetadata().setLabels(Map.of(\"category\", \"fake\", \"default\", \"false\"));\n        assertEquals(Map.of(\"category\", \"fake\", \"default\", \"false\"),\n            extension.getMetadata().getLabels());\n    }\n\n    @Test\n    void shouldSetAnnotationsCorrectly() throws JsonProcessingException {\n        var extension = objectMapper.readValue(extensionJson, Unstructured.class);\n\n        assertNull(extension.getMetadata().getAnnotations());\n\n        extension.getMetadata()\n            .setAnnotations(Map.of(\"annotation1\", \"value1\", \"annotation2\", \"value2\"));\n        assertEquals(Map.of(\"annotation1\", \"value1\", \"annotation2\", \"value2\"),\n            extension.getMetadata().getAnnotations());\n    }\n\n    @Nested\n    class JacksonJson3Test {\n\n        JsonMapper jsonMapper = JsonMapper.shared();\n\n        @Test\n        void shouldSerializeCorrectly() {\n            var json = \"\"\"\n                {\n                    \"apiVersion\": \"fake.halo.run/v1alpha1\",\n                    \"kind\": \"Fake\",\n                    \"metadata\": {\n                        \"labels\": {\n                            \"category\": \"fake\",\n                            \"default\": \"true\"\n                        },\n                        \"name\": \"fake-extension\",\n                        \"creationTimestamp\": \"2011-12-03T10:15:30Z\",\n                        \"version\": 12345\n                    },\n                    \"spec\": {\n                        \"field1\": \"value1\",\n                        \"field2\": 2\n                    }\n                }\n                \"\"\";\n            var unstructured = jsonMapper.readValue(json, Unstructured.class);\n            assertEquals(\"fake-extension\", unstructured.getMetadata().getName());\n            assertEquals(\"fake.halo.run/v1alpha1\", unstructured.getApiVersion());\n            assertEquals(\"Fake\", unstructured.getKind());\n            assertEquals(\"fake\", unstructured.getMetadata().getLabels().get(\"category\"));\n            assertEquals(\"true\", unstructured.getMetadata().getLabels().get(\"default\"));\n            assertEquals(Instant.parse(\"2011-12-03T10:15:30Z\"),\n                unstructured.getMetadata().getCreationTimestamp());\n            assertEquals(12345L, unstructured.getMetadata().getVersion());\n\n            var field1 =\n                Unstructured.getNestedValue(unstructured.getData(), \"spec\", \"field1\").orElse(null);\n            var field2 =\n                Unstructured.getNestedValue(unstructured.getData(), \"spec\", \"field2\").orElse(null);\n            assertEquals(\"value1\", field1);\n            assertEquals(2, field2);\n        }\n\n        @Test\n        void shouldDeserializeCorrectly() {\n            var u = new Unstructured();\n            u.setApiVersion(\"fake.halo.run/v1alpha1\");\n            u.setKind(\"Fake\");\n            var metadata = new Metadata();\n            metadata.setName(\"fake-extension\");\n            u.setMetadata(metadata);\n            Unstructured.setNestedValue(u.getData(), new HashMap<>(), \"spec\");\n            Unstructured.setNestedValue(u.getData(), \"value1\", \"spec\", \"field1\");\n\n            var json = jsonMapper.writeValueAsString(u);\n            JsonAssert.comparator(JsonCompareMode.STRICT).assertIsMatch(\"\"\"\n                {\n                    \"apiVersion\": \"fake.halo.run/v1alpha1\",\n                    \"kind\": \"Fake\",\n                    \"metadata\": {\n                        \"name\": \"fake-extension\"\n                    },\n                    \"spec\": {\n                        \"field1\": \"value1\"\n                    }\n                }\n                \"\"\", json);\n        }\n    }\n\n    Unstructured createUnstructured() {\n        var unstructured = new Unstructured();\n        unstructured.setApiVersion(\"fake.halo.run/v1alpha1\");\n        unstructured.setKind(\"Fake\");\n        unstructured.setMetadata(createMetadata());\n        return unstructured;\n    }\n\n    private Metadata createMetadata() {\n        var metadata = new Metadata();\n        metadata.setName(\"fake-extension\");\n        metadata.setLabels(Map.of(\"category\", \"fake\", \"default\", \"true\"));\n        metadata.setCreationTimestamp(Instant.parse(\"2011-12-03T10:15:30Z\"));\n        metadata.setVersion(12345L);\n        return metadata;\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/gc/GcReconcilerTest.java",
    "content": "package run.halo.app.extension.gc;\n\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.transaction.ReactiveTransaction;\nimport org.springframework.transaction.ReactiveTransactionManager;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.ExtensionConverter;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.index.IndexEngine;\nimport run.halo.app.extension.store.ExtensionStore;\nimport run.halo.app.extension.store.ReactiveExtensionStoreClient;\n\n@ExtendWith(MockitoExtension.class)\nclass GcReconcilerTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    ReactiveExtensionStoreClient storeClient;\n\n    @Mock\n    ExtensionConverter converter;\n\n    @Mock\n    SchemeManager schemeManager;\n\n    @Mock\n    IndexEngine indexEngine;\n\n    @Mock\n    ReactiveTransactionManager txManager;\n\n    @InjectMocks\n    GcReconciler reconciler;\n\n    @BeforeEach\n    void setUp() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        when(schemeManager.get(scheme.groupVersionKind())).thenReturn(scheme);\n    }\n\n    @Test\n    void shouldDoNothingIfExtensionNotFound() {\n        var fake = createExtension();\n        when(client.fetch(FakeExtension.class, fake.getMetadata().getName()))\n            .thenReturn(Optional.empty());\n\n        var result = reconciler.reconcile(createGcRequest());\n        assertNull(result);\n        verify(converter, never()).convertTo(any());\n        verify(storeClient, never()).delete(any(), any());\n    }\n\n    @Test\n    void shouldDoNothingIfFinalizersPresent() {\n        var fake = createExtension();\n        fake.getMetadata().setFinalizers(Set.of(\"fake-finalizer\"));\n        fake.getMetadata().setDeletionTimestamp(null);\n        when(client.fetch(FakeExtension.class, fake.getMetadata().getName()))\n            .thenReturn(Optional.of(fake));\n\n        var result = reconciler.reconcile(createGcRequest());\n        assertNull(result);\n        verify(converter, never()).convertTo(any());\n        verify(storeClient, never()).delete(any(), any());\n    }\n\n    @Test\n    void shouldDoNothingIfDeletionTimestampIsNull() {\n        var fake = createExtension();\n        fake.getMetadata().setDeletionTimestamp(null);\n        fake.getMetadata().setFinalizers(null);\n        when(client.fetch(FakeExtension.class, fake.getMetadata().getName()))\n            .thenReturn(Optional.of(fake));\n\n        var result = reconciler.reconcile(createGcRequest());\n        assertNull(result);\n        verify(converter, never()).convertTo(any());\n        verify(storeClient, never()).delete(any(), any());\n    }\n\n    @Test\n    void shouldDeleteCorrectly() {\n        var fake = createExtension();\n        fake.getMetadata().setDeletionTimestamp(Instant.now());\n        fake.getMetadata().setFinalizers(null);\n        when(client.fetch(FakeExtension.class, fake.getMetadata().getName()))\n            .thenReturn(Optional.of(fake));\n\n        ExtensionStore store = new ExtensionStore();\n        store.setName(\"fake-store-name\");\n        store.setVersion(1L);\n\n        when(converter.convertTo(any())).thenReturn(store);\n        doNothing().when(indexEngine).delete(any());\n        var tx = mock(ReactiveTransaction.class);\n        when(txManager.getReactiveTransaction(any())).thenReturn(Mono.just(tx));\n        when(txManager.commit(tx)).thenReturn(Mono.empty());\n        when(storeClient.delete(\"fake-store-name\", 1L)).thenReturn(Mono.just(store));\n\n        var result = reconciler.reconcile(createGcRequest());\n        assertNull(result);\n        verify(converter).convertTo(any());\n        verify(storeClient).delete(\"fake-store-name\", 1L);\n    }\n\n    GcRequest createGcRequest() {\n        var fake = createExtension();\n        return new GcRequest(fake.groupVersionKind(), fake.getMetadata().getName());\n    }\n\n    FakeExtension createExtension() {\n        var fake = new FakeExtension();\n        var metadata = new Metadata();\n        metadata.setName(\"fake\");\n        fake.setMetadata(metadata);\n        return fake;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/gc/GcSynchronizerTest.java",
    "content": "package run.halo.app.extension.gc;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.verify;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.SchemeManager;\n\n@ExtendWith(MockitoExtension.class)\nclass GcSynchronizerTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    SchemeManager schemeManager;\n\n    @InjectMocks\n    GcSynchronizer synchronizer;\n\n    @Test\n    void shouldStartNormally() {\n        synchronizer.start();\n\n        assertFalse(synchronizer.isDisposed());\n        verify(client).watch(isA(GcWatcher.class));\n        verify(schemeManager).schemes();\n    }\n\n    @Test\n    void shouldDisposeSuccessfully() {\n        assertFalse(synchronizer.isDisposed());\n\n        synchronizer.dispose();\n\n        assertTrue(synchronizer.isDisposed());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/gc/GcWatcherTest.java",
    "content": "package run.halo.app.extension.gc;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nimport java.time.Instant;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.RequestQueue;\n\n@ExtendWith(MockitoExtension.class)\nclass GcWatcherTest {\n\n    @Mock\n    RequestQueue<GcRequest> queue;\n\n    @InjectMocks\n    GcWatcher watcher;\n\n    @Test\n    void shouldAddIntoQueueWhenDeletionTimestampSet() {\n        var fake = createExtension();\n        fake.getMetadata().setDeletionTimestamp(Instant.now());\n\n        watcher.onAdd(fake);\n        verify(queue).addImmediately(any(GcRequest.class));\n\n        watcher.onUpdate(fake, fake);\n        verify(queue, times(2)).addImmediately(any(GcRequest.class));\n\n        watcher.onDelete(fake);\n        verify(queue, times(3)).addImmediately(any(GcRequest.class));\n    }\n\n    @Test\n    void shouldNotAddIntoQueueWhenDeletionTimestampNotSet() {\n        var fake = createExtension();\n        watcher.onAdd(fake);\n        verify(queue, never()).addImmediately(any(GcRequest.class));\n\n        watcher.onUpdate(fake, fake);\n        verify(queue, never()).addImmediately(any(GcRequest.class));\n\n        watcher.onDelete(fake);\n        verify(queue, never()).addImmediately(any(GcRequest.class));\n    }\n\n    @Test\n    void shouldNotAddIntoQueueWhenDisposed() {\n        var fake = createExtension();\n        fake.getMetadata().setDeletionTimestamp(Instant.now());\n        watcher.dispose();\n\n        watcher.onAdd(fake);\n        verify(queue, never()).addImmediately(any(GcRequest.class));\n\n        watcher.onUpdate(fake, fake);\n        verify(queue, never()).addImmediately(any(GcRequest.class));\n\n        watcher.onDelete(fake);\n        verify(queue, never()).addImmediately(any(GcRequest.class));\n    }\n\n    @Test\n    void shouldDisposeHookCorrectly() {\n        var run = mock(Runnable.class);\n        watcher.registerDisposeHook(run);\n        assertFalse(watcher.isDisposed());\n        watcher.dispose();\n        assertTrue(watcher.isDisposed());\n        verify(run).run();\n    }\n\n\n    FakeExtension createExtension() {\n        var fake = new FakeExtension();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake\");\n        fake.setMetadata(metadata);\n        return fake;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/DefaultIndexEngineTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.data.domain.Sort.Direction.ASC;\nimport static org.springframework.data.domain.Sort.Direction.DESC;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport java.util.stream.StreamSupport;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.core.convert.ConversionService;\nimport org.springframework.data.domain.Sort;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequestImpl;\nimport run.halo.app.extension.index.query.Queries;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultIndexEngineTest {\n\n    @Mock\n    ConversionService conversionService;\n\n    @Mock\n    IndicesManager indicesManager;\n\n    @Mock\n    Indices<Fake> indices;\n\n    @Mock\n    SingleValueIndex<Fake, String> singleValueIndex;\n\n    @InjectMocks\n    DefaultIndexEngine engine;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(indicesManager.get(Fake.class)).thenReturn(indices);\n        engine.setIndicesManager(indicesManager);\n        assertEquals(indicesManager, engine.getIndicesManager());\n    }\n\n    @Test\n    void shouldDestroyIndicesOnClose() throws Exception {\n        engine.destroy();\n        verify(indicesManager).close();\n    }\n\n    @Test\n    void shouldInsertExtensions() {\n        var fake = createFake(\"fake\");\n\n        engine.insert(List.of(fake));\n\n        verify(indices).insert(fake);\n    }\n\n    @Test\n    void shouldUpdateExtensions() {\n        var fake = createFake(\"fake\");\n\n        engine.update(List.of(fake));\n\n        verify(indices).update(fake);\n    }\n\n    @Test\n    void shouldDeleteExtensions() {\n        var fake = createFake(\"fake\");\n\n        engine.delete(List.of(fake));\n\n        verify(indices).delete(fake);\n    }\n\n    @Test\n    void shouldRetrieveWithConditionsAndPage() {\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        var page = PageRequestImpl.of(2, 2);\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(Set.of(\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"));\n        var result = engine.retrieve(Fake.class, options, page);\n        assertEquals(6, result.getTotal());\n        assertEquals(List.of(\"3\", \"4\"), result.getItems());\n    }\n\n    @Test\n    void shouldRetrieveWithConditionsAndFirstPage() {\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(Set.of(\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"));\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        var page = PageRequestImpl.of(1, 4);\n        var result = engine.retrieve(Fake.class, options, page);\n        assertEquals(6, result.getTotal());\n        assertEquals(List.of(\"1\", \"2\", \"3\", \"4\"), result.getItems());\n    }\n\n    @Test\n    void shouldRetrieveWithConditionsAndLastPage() {\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(Set.of(\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"));\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        var page = PageRequestImpl.of(2, 4);\n        var result = engine.retrieve(Fake.class, options, page);\n        assertEquals(6, result.getTotal());\n        assertEquals(List.of(\"5\", \"6\"), result.getItems());\n    }\n\n    @Test\n    void shouldRetrieveWithConditionsAndExceededPage() {\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(Set.of(\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"));\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        var page = PageRequestImpl.of(4, 2);\n        var result = engine.retrieve(Fake.class, options, page);\n        assertEquals(6, result.getTotal());\n        assertEquals(List.of(), result.getItems());\n    }\n\n    @Test\n    void shouldRetrieveAllWithConditionsAndNonPositiveSize() {\n        var allResult = IntStream.rangeClosed(1, 1001)\n            .boxed()\n            .map(o -> String.format(\"%04d\", o))\n            .collect(Collectors.toSet());\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(allResult);\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        var page = PageRequestImpl.of(1, 0);\n        var result = engine.retrieve(Fake.class, options, page);\n        assertEquals(1001, result.getTotal());\n        assertEquals(1000, result.getItems().size());\n        assertEquals(\"0001\", result.getItems().getFirst());\n        assertEquals(\"1000\", result.getItems().getLast());\n    }\n\n    @Test\n    void shouldRetrieveAllWithConditions() {\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(Set.of(\"1\", \"2\", \"3\"));\n        when(singleValueIndex.getKey(\"1\")).thenReturn(\"1\");\n        when(singleValueIndex.getKey(\"2\")).thenReturn(\"2\");\n        when(singleValueIndex.getKey(\"3\")).thenReturn(\"3\");\n        var result = engine.retrieveAll(Fake.class, options, Sort.by(DESC, \"metadata.name\"));\n        assertEquals(List.of(\"3\", \"2\", \"1\"),\n            StreamSupport.stream(result.spliterator(), false).toList()\n        );\n    }\n\n    @Test\n    void shouldRetrieveTopNWithConditions() {\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(Set.of(\"1\", \"2\", \"3\", \"4\", \"5\"));\n        when(singleValueIndex.getKey(\"1\")).thenReturn(\"1\");\n        when(singleValueIndex.getKey(\"2\")).thenReturn(\"2\");\n        when(singleValueIndex.getKey(\"3\")).thenReturn(\"3\");\n        when(singleValueIndex.getKey(\"4\")).thenReturn(\"4\");\n        when(singleValueIndex.getKey(\"5\")).thenReturn(\"5\");\n        var result = engine.retrieveTopN(Fake.class, options, Sort.by(DESC, \"metadata.name\"), 3);\n        assertEquals(List.of(\"5\", \"4\", \"3\"),\n            StreamSupport.stream(result.spliterator(), false).toList()\n        );\n\n        result = engine.retrieveTopN(Fake.class, options, Sort.by(ASC, \"metadata.name\"), 2);\n        assertEquals(List.of(\"1\", \"2\"), StreamSupport.stream(result.spliterator(), false).toList());\n    }\n\n    @Test\n    void shouldCountWithConditions() {\n        var options = ListOptions.builder()\n            .andQuery(Queries.all(\"metadata.name\"))\n            .build();\n        when(indices.<String>getIndex(\"metadata.name\")).thenReturn(singleValueIndex);\n        when(singleValueIndex.all()).thenReturn(Set.of(\"1\", \"2\", \"3\", \"4\"));\n        var count = engine.count(Fake.class, options);\n        assertEquals(4L, count);\n    }\n\n    Fake createFake(String name) {\n        var fake = new Fake();\n        fake.setMetadata(new Metadata());\n        fake.getMetadata().setName(name);\n        return fake;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/DefaultIndicesManagerTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertSame;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultIndicesManagerTest {\n\n    @Mock\n    SingleValueIndexSpec<Fake, String> singleValueIndexSpec;\n\n    @Mock\n    MultiValueIndexSpec<Fake, String> multiValueIndexSpec;\n\n    @Mock\n    SingleValueIndexSpec<Fake, Long> duplicateNameIndexSpec;\n\n    @InjectMocks\n    DefaultIndicesManager indicesManager;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(singleValueIndexSpec.getName()).thenReturn(\"singleValueIndex\");\n        lenient().when(multiValueIndexSpec.getName()).thenReturn(\"multiValueIndex\");\n    }\n\n    @Test\n    void shouldAddDefaultIndexSpecs() {\n        indicesManager.add(Fake.class, List.of(singleValueIndexSpec, multiValueIndexSpec));\n        var indices = indicesManager.get(Fake.class);\n        assertNotNull(indices.getIndex(\"metadata.name\"));\n        assertNotNull(indices.getIndex(\"metadata.creationTimestamp\"));\n        assertNotNull(indices.getIndex(\"metadata.deletionTimestamp\"));\n        assertNotNull(indices.getIndex(\"singleValueIndex\"));\n        assertNotNull(indices.getIndex(\"multiValueIndex\"));\n    }\n\n    @Test\n    void shouldThrowExceptionForUnknownType() {\n        assertThrows(IllegalArgumentException.class, () -> indicesManager.get(Fake.class));\n    }\n\n    @Test\n    void shouldCloseIndicesManager() throws Exception {\n        indicesManager.add(Fake.class, List.of(singleValueIndexSpec, multiValueIndexSpec));\n        indicesManager.close();\n        assertThrows(IllegalArgumentException.class, () -> indicesManager.get(Fake.class));\n    }\n\n    @Test\n    void shouldNotOverwriteDefaultIndices() {\n        when(duplicateNameIndexSpec.getName()).thenReturn(\"metadata.name\");\n        indicesManager.add(Fake.class, List.of(duplicateNameIndexSpec));\n        var indices = indicesManager.get(Fake.class);\n        var nameIndex = indices.getIndex(\"metadata.name\");\n        assertSame(String.class, nameIndex.getKeyType());\n    }\n\n    @Test\n    void shouldRemoveIndices() {\n        indicesManager.add(Fake.class, List.of(singleValueIndexSpec, multiValueIndexSpec));\n        indicesManager.remove(Fake.class);\n        assertThrows(IllegalArgumentException.class, () -> indicesManager.get(Fake.class));\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/DefaultIndicesTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.Metadata;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultIndicesTest {\n\n    @Mock\n    Index<Fake, String> index1;\n\n    @Mock\n    Index<Fake, String> index2;\n\n    @Mock\n    Index<Fake, String> index3;\n\n    @Mock\n    Index<Fake, Long> longIndex;\n\n    DefaultIndices<Fake> indices;\n\n    @BeforeEach\n    void setUp() {\n        when(index1.getName()).thenReturn(\"index1\");\n        when(index2.getName()).thenReturn(\"index2\");\n        when(index3.getName()).thenReturn(\"index3\");\n        indices = new DefaultIndices<>(List.of(index1, index2, index3));\n    }\n\n    @Test\n    void shouldNotReplaceWhenDuplicateIndexNames() {\n        when(longIndex.getName()).thenReturn(\"index1\");\n        indices = new DefaultIndices<>(List.of(index1, longIndex));\n        // The first index should be retained\n        assertEquals(index1, indices.getIndex(\"index1\"));\n    }\n\n    @Test\n    void shouldInsertCorrectly() {\n        var fake = createFake(\"fake\");\n\n        var to = mock(TransactionalOperation.class);\n        doNothing().when(to).prepare();\n        doNothing().when(to).commit();\n\n        when(index1.prepareInsert(fake)).thenReturn(to);\n        when(index2.prepareInsert(fake)).thenReturn(to);\n        when(index3.prepareInsert(fake)).thenReturn(to);\n\n        indices.insert(fake);\n\n        verify(index1).prepareInsert(fake);\n        verify(index2).prepareInsert(fake);\n        verify(index3).prepareInsert(fake);\n\n        verify(to, times(3)).prepare();\n        verify(to, times(3)).commit();\n        verify(to, never()).rollback();\n    }\n\n    @Test\n    void shouldUpdateCorrectly() {\n        var fake = createFake(\"fake\");\n\n        var to = mock(TransactionalOperation.class);\n        doNothing().when(to).prepare();\n        doNothing().when(to).commit();\n\n        when(index1.prepareUpdate(fake)).thenReturn(to);\n        when(index2.prepareUpdate(fake)).thenReturn(to);\n        when(index3.prepareUpdate(fake)).thenReturn(to);\n\n        indices.update(fake);\n\n        verify(index1).prepareUpdate(fake);\n        verify(index2).prepareUpdate(fake);\n        verify(index3).prepareUpdate(fake);\n\n        verify(to, times(3)).prepare();\n        verify(to, times(3)).commit();\n        verify(to, never()).rollback();\n    }\n\n    @Test\n    void shouldDeleteCorrectly() {\n        var to = mock(TransactionalOperation.class);\n        doNothing().when(to).prepare();\n        doNothing().when(to).commit();\n\n        var name = \"fake\";\n        when(index1.prepareDelete(name)).thenReturn(to);\n        when(index2.prepareDelete(name)).thenReturn(to);\n        when(index3.prepareDelete(name)).thenReturn(to);\n\n        var fake = createFake(name);\n        indices.delete(fake);\n\n        verify(index1).prepareDelete(name);\n        verify(index2).prepareDelete(name);\n        verify(index3).prepareDelete(name);\n\n        verify(to, times(3)).prepare();\n        verify(to, times(3)).commit();\n        verify(to, never()).rollback();\n    }\n\n    @Test\n    void shouldRollbackOnInsertFailure() {\n        var fake = createFake(\"fake\");\n\n        var to1 = mock(TransactionalOperation.class);\n        var to2 = mock(TransactionalOperation.class);\n        var to3 = mock(TransactionalOperation.class);\n\n        doNothing().when(to1).prepare();\n        doNothing().when(to1).commit();\n\n        doNothing().when(to2).prepare();\n        // Simulate failure on second index\n        doThrow(new RuntimeException(\"Insert failed\")).when(to2).commit();\n\n        doNothing().when(to3).prepare();\n\n        when(index1.prepareInsert(fake)).thenReturn(to1);\n        when(index2.prepareInsert(fake)).thenReturn(to2);\n        when(index3.prepareInsert(fake)).thenReturn(to3);\n\n        assertThrows(RuntimeException.class, () -> indices.insert(fake));\n\n        verify(to1).prepare();\n        verify(to1).commit();\n        verify(to1).rollback();\n\n        verify(to2).prepare();\n        verify(to2).commit();\n        verify(to2).rollback();\n\n        verify(to3).prepare();\n        // to3 not committed, so no rollback\n        verify(to3, never()).commit();\n        verify(to3).rollback();\n    }\n\n    @Test\n    void shouldRollbackOnUpdateFailure() {\n        var fake = createFake(\"fake\");\n\n        var to1 = mock(TransactionalOperation.class);\n        var to2 = mock(TransactionalOperation.class);\n        var to3 = mock(TransactionalOperation.class);\n\n        doNothing().when(to1).prepare();\n        doNothing().when(to1).commit();\n\n        doNothing().when(to2).prepare();\n        // Simulate failure on second index\n        doThrow(new RuntimeException(\"Update failed\")).when(to2).commit();\n\n        doNothing().when(to3).prepare();\n\n        when(index1.prepareUpdate(fake)).thenReturn(to1);\n        when(index2.prepareUpdate(fake)).thenReturn(to2);\n        when(index3.prepareUpdate(fake)).thenReturn(to3);\n\n        assertThrows(RuntimeException.class, () -> indices.update(fake));\n\n        verify(to1).prepare();\n        verify(to1).commit();\n        verify(to1).rollback();\n\n        verify(to2).prepare();\n        verify(to2).commit();\n        verify(to2).rollback();\n\n        verify(to3).prepare();\n        // to3 not committed, so no rollback\n        verify(to3, never()).commit();\n        verify(to3).rollback();\n    }\n\n    @Test\n    void shouldRollbackOnDeleteFailure() {\n        var to1 = mock(TransactionalOperation.class);\n        var to2 = mock(TransactionalOperation.class);\n        var to3 = mock(TransactionalOperation.class);\n\n        doNothing().when(to1).prepare();\n        doNothing().when(to1).commit();\n\n        doNothing().when(to2).prepare();\n        // Simulate failure on second index\n        doThrow(new RuntimeException(\"Delete failed\")).when(to2).commit();\n\n        doNothing().when(to3).prepare();\n\n        var name = \"fake\";\n        when(index1.prepareDelete(name)).thenReturn(to1);\n        when(index2.prepareDelete(name)).thenReturn(to2);\n        when(index3.prepareDelete(name)).thenReturn(to3);\n\n        var fake = createFake(name);\n        assertThrows(RuntimeException.class, () -> indices.delete(fake));\n\n        verify(to1).prepare();\n        verify(to1).commit();\n        verify(to1).rollback();\n\n        verify(to2).prepare();\n        verify(to2).commit();\n        verify(to2).rollback();\n\n        verify(to3).prepare();\n        // to3 not committed, so no rollback\n        verify(to3, never()).commit();\n        verify(to3).rollback();\n    }\n\n    @Test\n    void shouldGetIndexCorrectly() {\n        assertEquals(index1, indices.getIndex(\"index1\"));\n        assertEquals(index2, indices.getIndex(\"index2\"));\n        assertEquals(index3, indices.getIndex(\"index3\"));\n\n        assertThrows(IllegalArgumentException.class, () -> indices.getIndex(\"non-existent\"));\n    }\n\n    @Test\n    void shouldCloseIndicesCorrectly() throws Exception {\n        doNothing().when(index1).close();\n        doNothing().when(index2).close();\n        doNothing().when(index3).close();\n\n        indices.close();\n\n        verify(index1).close();\n        verify(index2).close();\n        verify(index3).close();\n\n        var fake = createFake(\"fake\");\n        assertThrows(IllegalStateException.class, () -> indices.insert(fake));\n        assertThrows(IllegalStateException.class, () -> indices.update(fake));\n        assertThrows(IllegalStateException.class, () -> indices.delete(fake));\n        assertThrows(IllegalStateException.class, () -> indices.getIndex(\"index1\"));\n    }\n\n    Fake createFake(String name) {\n        var fake = new Fake();\n        fake.setMetadata(new Metadata());\n        fake.getMetadata().setName(name);\n        return fake;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/Fake.java",
    "content": "package run.halo.app.extension.index;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\n\n@GVK(\n    group = \"fake.halo.app\",\n    version = \"v1\",\n    kind = \"Fake\",\n    singular = \"fake\",\n    plural = \"fakes\"\n)\n@Data\n@EqualsAndHashCode(callSuper = true)\nclass Fake extends AbstractExtension {\n\n    private Set<String> stringValues = new HashSet<>();\n\n    private String stringValue;\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/LabelIndexTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Metadata;\n\n@ExtendWith(MockitoExtension.class)\nclass LabelIndexTest {\n\n    @InjectMocks\n    LabelIndex<Fake> index;\n\n    @Nested\n    class IndexOperationTests {\n\n        @Test\n        void shouldInsertCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.getMetadata().setLabels(Map.of(\"k1\", \"v1\", \"k2\", \"v2\"));\n\n            var op = index.prepareInsert(fake);\n            op.prepare();\n            op.commit();\n\n            assertEquals(Set.of(\"fake\"), index.equal(\"k1\", \"v1\"));\n            assertEquals(Set.of(\"fake\"), index.exists(\"k1\"));\n            assertTrue(index.equal(\"k1\", \"non-existent\").isEmpty());\n        }\n\n        @Test\n        void shouldUpdateAndRollbackCorrectly() throws Exception {\n            var fake = createFake(\"fake\");\n            fake.getMetadata().setLabels(Map.of(\"k1\", \"v1\"));\n            // insert\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            // update to v2\n            fake.getMetadata().setLabels(Map.of(\"k1\", \"v2\"));\n            var update = index.prepareUpdate(fake);\n            update.prepare();\n            update.commit();\n\n            assertTrue(index.equal(\"k1\", \"v1\").isEmpty());\n            assertEquals(Set.of(\"fake\"), index.equal(\"k1\", \"v2\"));\n\n            // rollback should restore previous state (v1)\n            update.rollback();\n            assertEquals(Set.of(\"fake\"), index.equal(\"k1\", \"v1\"));\n            assertTrue(index.equal(\"k1\", \"v2\").isEmpty());\n        }\n\n        @Test\n        void shouldDeleteAndRollbackCorrectly() throws Exception {\n            var fake = createFake(\"fake\");\n            fake.getMetadata().setLabels(Map.of(\"k1\", \"v1\"));\n            insert(fake);\n\n            var delete = index.prepareDelete(\"fake\");\n            delete.prepare();\n            delete.commit();\n\n            assertTrue(index.equal(\"k1\", \"v1\").isEmpty());\n            // rollback restores it\n            delete.rollback();\n            assertEquals(Set.of(\"fake\"), index.equal(\"k1\", \"v1\"));\n        }\n\n        @Test\n        void shouldTreatEmptyLabelsAsEmptySet() throws Exception {\n            var fake = createFake(\"empty\");\n            fake.getMetadata().setLabels(Map.of());\n            var op = index.prepareInsert(fake);\n            op.prepare();\n            op.commit();\n\n            // no entries for any label key\n            assertTrue(index.exists(\"any\").isEmpty());\n            assertTrue(index.equal(\"any\", \"any\").isEmpty());\n        }\n\n        @Test\n        void shouldCloseIndexCorrectly() throws IOException {\n            var fake = createFake(\"f\");\n            fake.getMetadata().setLabels(Map.of(\"k\", \"v\"));\n            insert(fake);\n\n            index.close();\n\n            assertTrue(index.exists(\"k\").isEmpty());\n            assertTrue(index.equal(\"k\", \"v\").isEmpty());\n        }\n    }\n\n    @Nested\n    class QueryTests {\n\n        @BeforeEach\n        void setUp() {\n            index = new LabelIndex<>(); // fresh\n            var f1 = createFake(\"f1\");\n            f1.getMetadata().setLabels(Map.of(\"k1\", \"v1\"));\n\n            var f2 = createFake(\"f2\");\n            f2.getMetadata().setLabels(Map.of(\"k1\", \"v2\"));\n\n            var f3 = createFake(\"f3\");\n            f3.getMetadata().setLabels(Map.of(\"k1\", \"v3\"));\n\n            insert(f1);\n            insert(f2);\n            insert(f3);\n        }\n\n        @Test\n        void existsQuery() {\n            assertEquals(Set.of(\"f1\", \"f2\", \"f3\"), index.exists(\"k1\"));\n            assertTrue(index.exists(\"non-existent\").isEmpty());\n        }\n\n        @Test\n        void equalAndNotEqualQuery() {\n            assertEquals(Set.of(\"f1\"), index.equal(\"k1\", \"v1\"));\n            assertEquals(Set.of(\"f2\", \"f3\"), index.notEqual(\"k1\", \"v1\"));\n        }\n\n        @Test\n        void inAndNotInQuery() {\n            var inResult = index.in(\"k1\", List.of(\"v1\", \"v3\", \"non-existent\"));\n            assertEquals(Set.of(\"f1\", \"f3\"), inResult);\n\n            var notInResult = index.notIn(\"k1\", List.of(\"v1\", \"non-existent\"));\n            assertEquals(Set.of(\"f2\", \"f3\"), notInResult);\n        }\n    }\n\n    void insert(Fake fake) {\n        var op = index.prepareInsert(fake);\n        op.prepare();\n        op.commit();\n    }\n\n    Fake createFake(String name) {\n        var fake = new Fake();\n        fake.setMetadata(new Metadata());\n        fake.getMetadata().setName(name);\n        return fake;\n    }\n\n    @GVK(\n        group = \"fake.halo.app\",\n        version = \"v1\",\n        kind = \"Fake\",\n        singular = \"fake\",\n        plural = \"fakes\"\n    )\n    @Data\n    @EqualsAndHashCode(callSuper = true)\n    static class Fake extends AbstractExtension {\n        // ...existing code...\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/MultiValueIndexTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.lenient;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.dao.DuplicateKeyException;\nimport run.halo.app.extension.Metadata;\n\n@ExtendWith(MockitoExtension.class)\nclass MultiValueIndexTest {\n\n    @Mock\n    MultiValueIndexSpec<Fake, String> spec;\n\n    @InjectMocks\n    MultiValueIndex<Fake, String> index;\n\n    @Nested\n    class UniqueMultiValueTests {\n\n        @BeforeEach\n        void setUp() {\n            lenient().when(spec.getName()).thenReturn(\"metadata.name\");\n            lenient().when(spec.getKeyType()).thenReturn(String.class);\n            lenient().when(spec.isUnique()).thenReturn(true);\n            lenient().when(spec.getValues(any(Fake.class))).thenAnswer(invocation -> {\n                Fake fake = invocation.getArgument(0);\n                return fake.getStringValues();\n            });\n        }\n\n        @Test\n        void shouldCloseIndexCorrectly() throws IOException {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"a\", \"b\"));\n            insert(fake);\n\n            index.close();\n\n            assertTrue(index.all().isEmpty());\n        }\n\n        @Nested\n        class IndexOperationTest {\n\n            @Test\n            void shouldGetKeyType() {\n                assertEquals(String.class, index.getKeyType());\n            }\n\n            @Test\n            void shouldInsertCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\", \"s2\"));\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n                assertEquals(Set.of(\"fake\"), index.equal(\"s2\"));\n                assertEquals(Set.of(\"s1\", \"s2\"), index.getKeys(\"fake\"));\n                assertTrue(index.isNull().isEmpty());\n            }\n\n            @Test\n            void shouldRollbackInsertCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\", \"s2\"));\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n                insert.rollback();\n\n                assertTrue(index.all().isEmpty());\n            }\n\n            @Test\n            void shouldUpdateCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\"));\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                fake.setStringValues(Set.of(\"s2\", \"s3\"));\n                var update = index.prepareUpdate(fake);\n                update.prepare();\n                update.commit();\n\n                assertTrue(index.equal(\"s1\").isEmpty());\n                assertEquals(Set.of(\"fake\"), index.equal(\"s2\"));\n                assertEquals(Set.of(\"fake\"), index.equal(\"s3\"));\n                assertEquals(Set.of(\"s2\", \"s3\"), index.getKeys(\"fake\"));\n            }\n\n            @Test\n            void shouldRollbackUpdateCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\"));\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                fake.setStringValues(Set.of(\"s2\"));\n                var update = index.prepareUpdate(fake);\n                update.prepare();\n                update.commit();\n                update.rollback();\n\n                assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n                assertTrue(index.equal(\"s2\").isEmpty());\n                assertEquals(Set.of(\"s1\"), index.getKeys(\"fake\"));\n            }\n\n            @Test\n            void shouldDeleteCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\", \"s2\"));\n                insert(fake);\n\n                var delete = index.prepareDelete(\"fake\");\n                delete.prepare();\n                delete.commit();\n\n                assertTrue(index.all().isEmpty());\n            }\n\n            @Test\n            void shouldHandleDeleteIfPrimaryKeyNotExist() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\"));\n                insert(fake);\n\n                var delete = index.prepareDelete(\"non-existent-fake\");\n                delete.prepare();\n                delete.commit();\n\n                assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n            }\n\n            @Test\n            void shouldRollbackDeleteCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\", \"s2\"));\n                insert(fake);\n\n                var delete = index.prepareDelete(\"fake\");\n                delete.prepare();\n                delete.commit();\n                delete.rollback();\n\n                // rollback restores previous keys\n                assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n                assertEquals(Set.of(\"fake\"), index.equal(\"s2\"));\n                assertEquals(Set.of(\"s1\", \"s2\"), index.getKeys(\"fake\"));\n            }\n\n            @Test\n            void shouldHandleDuplicateKeysOnInsert() {\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValues(Set.of(\"common\"));\n\n                // first insert\n                var insert1 = index.prepareInsert(fake1);\n                insert1.prepare();\n                insert1.commit();\n\n                // second insert with the same key -> duplicate occurs on commit\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValues(Set.of(\"common\"));\n                var insert2 = index.prepareInsert(fake2);\n                insert2.prepare();\n                assertThrows(DuplicateKeyException.class, insert2::commit);\n            }\n\n            @Test\n            void shouldHandleDuplicateKeysOnUpdate() {\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValues(Set.of(\"s1\"));\n                var insert1 = index.prepareInsert(fake1);\n                insert1.prepare();\n                insert1.commit();\n\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValues(Set.of(\"s2\"));\n                var insert2 = index.prepareInsert(fake2);\n                insert2.prepare();\n                insert2.commit();\n\n                // Attempt to update fake2's values to contain \"s1\", which should cause a duplicate\n                fake2.setStringValues(Set.of(\"s1\"));\n                var update = index.prepareUpdate(fake2);\n                update.prepare();\n                assertThrows(DuplicateKeyException.class, update::commit);\n            }\n\n            @Test\n            void shouldHandleEmptyValuesAsNullOnInsert() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Collections.emptySet());\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                assertEquals(Set.of(\"fake\"), index.isNull());\n                assertTrue(index.isNotNull().isEmpty());\n                assertEquals(Collections.emptySet(), index.getKeys(\"fake\"));\n            }\n\n            @Test\n            void shouldHandleEmptyValuesAsNullOnUpdate() {\n                var fake = createFake(\"fake\");\n                fake.setStringValues(Set.of(\"s1\"));\n                insert(fake);\n\n                // update to empty values\n                fake.setStringValues(Collections.emptySet());\n                var update = index.prepareUpdate(fake);\n                update.prepare();\n                update.commit();\n\n                assertEquals(Set.of(\"fake\"), index.isNull());\n                assertTrue(index.equal(\"s1\").isEmpty());\n                assertEquals(Collections.emptySet(), index.getKeys(\"fake\"));\n            }\n        }\n\n        @Nested\n        class IndexQueryTest {\n\n            @BeforeEach\n            void setUp() {\n                // insert some data for query tests (single-value sets)\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValues(Set.of(\"string1\"));\n\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValues(Set.of(\"string2\"));\n\n                var fake3 = createFake(\"fake3\");\n                fake3.setStringValues(Set.of(\"string3\"));\n\n                var fakenull = createFake(\"fakenull\");\n                fakenull.setStringValues(Set.of());\n\n                insert(fake1);\n                insert(fake2);\n                insert(fake3);\n                insert(fakenull);\n            }\n\n            @Test\n            void equalQuery() {\n                var result1 = index.equal(\"string1\");\n                assertEquals(Set.of(\"fake1\"), result1);\n\n                var result2 = index.equal(\"string2\");\n                assertEquals(Set.of(\"fake2\"), result2);\n\n                var result3 = index.equal(\"non-existent-string\");\n                assertTrue(result3.isEmpty());\n            }\n\n            @Test\n            void notEqualQuery() {\n                var result1 = index.notEqual(\"string1\");\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result1);\n\n                var result2 = index.notEqual(\"string2\");\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result2);\n\n                var result3 = index.notEqual(\"non-existent-string\");\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), result3);\n            }\n\n            @Test\n            void allQuery() {\n                var result = index.all();\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\", \"fakenull\"), result);\n            }\n\n            @Test\n            void inQuery() {\n                var result = index.in(Set.of(\"string1\", \"string3\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result);\n            }\n\n            @Test\n            void notInQuery() {\n                var result = index.notIn(Set.of(\"string1\", \"string3\"));\n                assertEquals(Set.of(\"fake2\"), result);\n            }\n\n            @Test\n            void isNullQuery() {\n                assertEquals(Set.of(\"fakenull\"), index.isNull());\n            }\n\n            @Test\n            void isNotNullQuery() {\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), index.isNotNull());\n            }\n\n            @Test\n            void betweenQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.between(\"string1\", true, \"string3\", false)\n                );\n            }\n\n            @Test\n            void notBetweenQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.notBetween(\"string1\", true, \"string2\", false)\n                );\n            }\n\n            @Test\n            void lessThanQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.lessThan(\"string3\", false)\n                );\n            }\n\n            @Test\n            void greaterThanQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.greaterThan(\"string1\", false)\n                );\n            }\n\n            @Test\n            void stringContainsQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringContains(\"ing\")\n                );\n            }\n\n            @Test\n            void stringNotContainsQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringNotContains(\"ing\")\n                );\n            }\n\n            @Test\n            void stringStartsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringStartsWith(\"string\")\n                );\n            }\n\n            @Test\n            void stringNotStartsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringNotStartsWith(\"string\")\n                );\n            }\n\n            @Test\n            void stringEndsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringEndsWith(\"ing\")\n                );\n            }\n\n            @Test\n            void stringNotEndsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringNotEndsWith(\"ing\")\n                );\n            }\n\n        }\n    }\n\n    @Nested\n    class NonUniqueMultiValueTests {\n\n        @BeforeEach\n        void setUp() {\n            lenient().when(spec.getName()).thenReturn(\"metadata.name\");\n            lenient().when(spec.getKeyType()).thenReturn(String.class);\n            lenient().when(spec.isUnique()).thenReturn(false);\n            lenient().when(spec.getValues(any(Fake.class))).thenAnswer(invocation -> {\n                Fake fake = invocation.getArgument(0);\n                return fake.getStringValues();\n            });\n        }\n\n        @Test\n        void shouldInsertCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\", \"s2\"));\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n            assertEquals(Set.of(\"fake\"), index.equal(\"s2\"));\n            assertEquals(Set.of(\"s1\", \"s2\"), index.getKeys(\"fake\"));\n            assertTrue(index.isNull().isEmpty());\n        }\n\n        @Test\n        void shouldRollbackInsertCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\", \"s2\"));\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n            insert.rollback();\n\n            assertTrue(index.all().isEmpty());\n        }\n\n        @Test\n        void shouldUpdateCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\"));\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            fake.setStringValues(Set.of(\"s2\", \"s3\"));\n            var update = index.prepareUpdate(fake);\n            update.prepare();\n            update.commit();\n\n            assertTrue(index.equal(\"s1\").isEmpty());\n            assertEquals(Set.of(\"fake\"), index.equal(\"s2\"));\n            assertEquals(Set.of(\"fake\"), index.equal(\"s3\"));\n            assertEquals(Set.of(\"s2\", \"s3\"), index.getKeys(\"fake\"));\n        }\n\n        @Test\n        void shouldRollbackUpdateCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\"));\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            fake.setStringValues(Set.of(\"s2\"));\n            var update = index.prepareUpdate(fake);\n            update.prepare();\n            update.commit();\n            update.rollback();\n\n            assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n            assertTrue(index.equal(\"s2\").isEmpty());\n            assertEquals(Set.of(\"s1\"), index.getKeys(\"fake\"));\n        }\n\n        @Test\n        void shouldDeleteCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\", \"s2\"));\n            insert(fake);\n\n            var delete = index.prepareDelete(\"fake\");\n            delete.prepare();\n            delete.commit();\n\n            assertTrue(index.all().isEmpty());\n        }\n\n        @Test\n        void shouldHandleDeleteIfPrimaryKeyNotExist() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\"));\n            insert(fake);\n\n            var delete = index.prepareDelete(\"non-existent-fake\");\n            delete.prepare();\n            delete.commit();\n\n            assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n        }\n\n        @Test\n        void shouldRollbackDeleteCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\", \"s2\"));\n            insert(fake);\n\n            var delete = index.prepareDelete(\"fake\");\n            delete.prepare();\n            delete.commit();\n            delete.rollback();\n\n            // rollback restores previous keys\n            assertEquals(Set.of(\"fake\"), index.equal(\"s1\"));\n            assertEquals(Set.of(\"fake\"), index.equal(\"s2\"));\n            assertEquals(Set.of(\"s1\", \"s2\"), index.getKeys(\"fake\"));\n        }\n\n        @Test\n        void shouldAllowDuplicateKeysOnInsert() {\n            var fake1 = createFake(\"fake1\");\n            fake1.setStringValues(Set.of(\"common\"));\n\n            // first insert\n            var insert1 = index.prepareInsert(fake1);\n            insert1.prepare();\n            insert1.commit();\n\n            // second insert with the same key -> should succeed for non-unique index\n            var fake2 = createFake(\"fake2\");\n            fake2.setStringValues(Set.of(\"common\"));\n            var insert2 = index.prepareInsert(fake2);\n            insert2.prepare();\n            insert2.commit();\n\n            assertEquals(Set.of(\"fake1\", \"fake2\"), index.equal(\"common\"));\n        }\n\n        @Test\n        void shouldAllowDuplicateKeysOnUpdate() {\n            var fake1 = createFake(\"fake1\");\n            fake1.setStringValues(Set.of(\"s1\"));\n            var insert1 = index.prepareInsert(fake1);\n            insert1.prepare();\n            insert1.commit();\n\n            var fake2 = createFake(\"fake2\");\n            fake2.setStringValues(Set.of(\"s2\"));\n            var insert2 = index.prepareInsert(fake2);\n            insert2.prepare();\n            insert2.commit();\n\n            // Update fake2's values to contain \"s1\" — should be allowed for non-unique index\n            fake2.setStringValues(Set.of(\"s1\"));\n            var update = index.prepareUpdate(fake2);\n            update.prepare();\n            update.commit();\n\n            assertEquals(Set.of(\"fake1\", \"fake2\"), index.equal(\"s1\"));\n        }\n\n        @Test\n        void shouldHandleEmptyValuesAsNullOnInsert() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Collections.emptySet());\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            assertEquals(Set.of(\"fake\"), index.isNull());\n            assertTrue(index.isNotNull().isEmpty());\n            assertEquals(Collections.emptySet(), index.getKeys(\"fake\"));\n        }\n\n        @Test\n        void shouldHandleEmptyValuesAsNullOnUpdate() {\n            var fake = createFake(\"fake\");\n            fake.setStringValues(Set.of(\"s1\"));\n            insert(fake);\n\n            // update to empty values\n            fake.setStringValues(Collections.emptySet());\n            var update = index.prepareUpdate(fake);\n            update.prepare();\n            update.commit();\n\n            assertEquals(Set.of(\"fake\"), index.isNull());\n            assertTrue(index.equal(\"s1\").isEmpty());\n            assertEquals(Collections.emptySet(), index.getKeys(\"fake\"));\n        }\n\n        @Nested\n        class IndexQueryTest {\n\n            @BeforeEach\n            void setUp() {\n                // insert some data for query tests (single-value sets)\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValues(Set.of(\"string1\", \"string2\"));\n\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValues(Set.of(\"string2\"));\n\n                var fake3 = createFake(\"fake3\");\n                fake3.setStringValues(Set.of(\"string3\"));\n\n                var fakenull = createFake(\"fakenull\");\n                fakenull.setStringValues(Set.of());\n\n                insert(fake1);\n                insert(fake2);\n                insert(fake3);\n                insert(fakenull);\n            }\n\n            @Test\n            void equalQuery() {\n                var result1 = index.equal(\"string1\");\n                assertEquals(Set.of(\"fake1\"), result1);\n\n                var result2 = index.equal(\"string2\");\n                assertEquals(Set.of(\"fake1\", \"fake2\"), result2);\n\n                var result3 = index.equal(\"non-existent-string\");\n                assertTrue(result3.isEmpty());\n            }\n\n            @Test\n            void notEqualQuery() {\n                var result1 = index.notEqual(\"string1\");\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result1);\n\n                var result2 = index.notEqual(\"string2\");\n                assertEquals(Set.of(\"fake3\"), result2);\n\n                var result3 = index.notEqual(\"non-existent-string\");\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), result3);\n            }\n\n            @Test\n            void inQuery() {\n                var result = index.in(Set.of(\"string1\", \"string2\"));\n                assertEquals(Set.of(\"fake1\", \"fake2\"), result);\n            }\n\n            @Test\n            void notInQuery() {\n                var result = index.notIn(Set.of(\"string1\", \"string3\"));\n                assertEquals(Set.of(\"fake2\"), result);\n\n                result = index.notIn(Set.of(\"string2\"));\n                assertEquals(Set.of(\"fake3\"), result);\n\n                result = index.notIn(Set.of(\"non-existent-string\"));\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), result);\n            }\n\n            @Test\n            void allQuery() {\n                var result = index.all();\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\", \"fakenull\"), result);\n            }\n\n            @Test\n            void isNullQuery() {\n                assertEquals(Set.of(\"fakenull\"), index.isNull());\n            }\n\n            @Test\n            void isNotNullQuery() {\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), index.isNotNull());\n            }\n\n            @Test\n            void betweenQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.between(\"string1\", true, \"string3\", false)\n                );\n            }\n\n            @Test\n            void notBetweenQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.notBetween(\"string1\", true, \"string2\", false)\n                );\n            }\n\n            @Test\n            void lessThanQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.lessThan(\"string3\", false)\n                );\n            }\n\n            @Test\n            void greaterThanQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.greaterThan(\"string1\", false)\n                );\n            }\n\n            @Test\n            void stringContainsQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringContains(\"ing\")\n                );\n            }\n\n            @Test\n            void stringNotContainsQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringNotContains(\"ing\")\n                );\n            }\n\n            @Test\n            void stringStartsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringStartsWith(\"string\")\n                );\n            }\n\n            @Test\n            void stringNotStartsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringNotStartsWith(\"string\")\n                );\n            }\n\n            @Test\n            void stringEndsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringEndsWith(\"ing\")\n                );\n            }\n\n            @Test\n            void stringNotEndsWithQuery() {\n                assertThrows(UnsupportedOperationException.class,\n                    () -> index.stringNotEndsWith(\"ing\")\n                );\n            }\n\n        }\n\n    }\n\n    void insert(Fake fake) {\n        var insert = index.prepareInsert(fake);\n        insert.prepare();\n        insert.commit();\n    }\n\n    Fake createFake(String name) {\n        var fake = new Fake();\n        fake.setMetadata(new Metadata());\n        fake.getMetadata().setName(name);\n        return fake;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/SingleValueIndexTest.java",
    "content": "package run.halo.app.extension.index;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.lenient;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.dao.DuplicateKeyException;\nimport run.halo.app.extension.Metadata;\n\n@ExtendWith(MockitoExtension.class)\nclass SingleValueIndexTest {\n\n    @Mock\n    SingleValueIndexSpec<Fake, String> spec;\n\n    @InjectMocks\n    SingleValueIndex<Fake, String> index;\n\n\n    @Nested\n    class NonNullAndUniqueTest {\n\n        @BeforeEach\n        void setUp() {\n            lenient().when(spec.getName()).thenReturn(\"metadata.name\");\n            lenient().when(spec.getKeyType()).thenReturn(String.class);\n            lenient().when(spec.isNullable()).thenReturn(false);\n            lenient().when(spec.isUnique()).thenReturn(true);\n            lenient().when(spec.getValue(any(Fake.class))).thenAnswer(invocation -> {\n                Fake fake = invocation.getArgument(0);\n                return fake.getStringValue();\n            });\n        }\n\n        @Test\n        void shouldCloseIndexCorrectly() throws IOException {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(\"string\");\n            insert(fake);\n\n            index.close();\n\n            assertEquals(Set.of(), index.equal(\"string\"));\n        }\n\n        @Nested\n        class IndexOperationTest {\n\n            @Test\n            void shouldGetKeyType() {\n                assertEquals(String.class, index.getKeyType());\n            }\n\n            @Test\n            void shouldInsertCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n                assertEquals(Set.of(\"fake\"), index.equal(\"string\"));\n            }\n\n            @Test\n            void shouldRollbackInsertCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n                insert.rollback();\n\n                assertEquals(Set.of(), index.equal(\"string\"));\n            }\n\n            @Test\n            void shouldUpdateCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                fake.setStringValue(\"new-string\");\n                var update = index.prepareUpdate(fake);\n                update.prepare();\n                update.commit();\n\n                assertEquals(Set.of(\"fake\"), index.equal(\"new-string\"));\n            }\n\n\n            @Test\n            void shouldRollbackUpdateCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                fake.setStringValue(\"new-string\");\n                var update = index.prepareUpdate(fake);\n                update.prepare();\n                update.commit();\n                update.rollback();\n\n                assertEquals(Set.of(\"fake\"), index.equal(\"string\"));\n            }\n\n            @Test\n            void shouldDeleteCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                var delete = index.prepareDelete(\"fake\");\n                delete.prepare();\n                delete.commit();\n\n                assertEquals(Set.of(), index.equal(\"string\"));\n            }\n\n            @Test\n            void shouldHandleDeleteIfPrimaryKeyNotExist() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                var delete = index.prepareDelete(\"non-existent-fake\");\n                delete.prepare();\n                delete.commit();\n\n                assertEquals(Set.of(\"fake\"), index.equal(\"string\"));\n            }\n\n            @Test\n            void shouldRollbackDeleteCorrectly() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                var delete = index.prepareDelete(\"fake\");\n                delete.prepare();\n                delete.commit();\n                delete.rollback();\n\n                assertEquals(Set.of(\"fake\"), index.equal(\"string\"));\n            }\n\n            @Test\n            void shouldHandleDuplicateKeysOnInsert() {\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValue(\"string\");\n\n                // first insert\n                var insert = index.prepareInsert(fake1);\n                insert.prepare();\n                insert.commit();\n\n                // second insert with the same string value\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValue(\"string\");\n                insert = index.prepareInsert(fake2);\n                assertThrows(DuplicateKeyException.class, insert::prepare);\n            }\n\n            @Test\n            void shouldHandleDuplicateKeysOnUpdate() {\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValue(\"string1\");\n\n                var insert1 = index.prepareInsert(fake1);\n                insert1.prepare();\n                insert1.commit();\n\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValue(\"string2\");\n\n                var insert2 = index.prepareInsert(fake2);\n                insert2.prepare();\n                insert2.commit();\n\n                // Attempt to update fake2's stringValue to \"string1\", which should cause a\n                // duplicate\n                // key\n                fake2.setStringValue(\"string1\");\n                var update = index.prepareUpdate(fake2);\n                assertThrows(DuplicateKeyException.class, update::prepare);\n            }\n\n            @Test\n            void shouldHandleNullValueOnInsert() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(null);\n\n                var insert = index.prepareInsert(fake);\n                assertThrows(IllegalArgumentException.class, insert::prepare);\n            }\n\n            @Test\n            void shouldHandleNullValueOnUpdate() {\n                var fake = createFake(\"fake\");\n                fake.setStringValue(\"string\");\n\n                var insert = index.prepareInsert(fake);\n                insert.prepare();\n                insert.commit();\n\n                // Now update to null value\n                fake.setStringValue(null);\n                var update = index.prepareUpdate(fake);\n                assertThrows(IllegalArgumentException.class, update::prepare);\n            }\n        }\n\n        @Nested\n        class IndexQueryTest {\n\n            @BeforeEach\n            void setUp() {\n                // insert some data for query tests\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValue(\"string1\");\n\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValue(\"string2\");\n\n                var fake3 = createFake(\"fake3\");\n                fake3.setStringValue(\"string3\");\n\n                insert(fake1);\n                insert(fake2);\n                insert(fake3);\n            }\n\n            @Test\n            void equalQuery() {\n                var result1 = index.equal(\"string1\");\n                assertEquals(Set.of(\"fake1\"), result1);\n\n                var result2 = index.equal(\"string2\");\n                assertEquals(Set.of(\"fake2\"), result2);\n\n                var result3 = index.equal(\"non-existent-string\");\n                assertTrue(result3.isEmpty());\n            }\n\n            @Test\n            void notEqualQuery() {\n                var result1 = index.notEqual(\"string1\");\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result1);\n\n                var result2 = index.notEqual(\"string2\");\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result2);\n\n                var result3 = index.notEqual(\"non-existent-string\");\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), result3);\n            }\n\n            @Test\n            void allQuery() {\n                var result = index.all();\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), result);\n            }\n\n            @Test\n            void betweenQuery() {\n                var result = index.between(\"string1\", true, \"string3\", false);\n                assertEquals(Set.of(\"fake1\", \"fake2\"), result);\n            }\n\n            @Test\n            void notBetweenQuery() {\n                var result = index.notBetween(\"string1\", true, \"string2\", false);\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result);\n            }\n\n            @Test\n            void inQuery() {\n                var result = index.in(Set.of(\"string1\", \"string3\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result);\n            }\n\n            @Test\n            void notInQueryForSet() {\n                var result = index.notIn(Set.of(\"string1\", \"string3\"));\n                assertEquals(Set.of(\"fake2\"), result);\n            }\n\n            @Test\n            void notInQueryForList() {\n                var result = index.notIn(List.of(\"string2\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result);\n            }\n\n            @Test\n            void lessThanQuery() {\n                var result = index.lessThan(\"string3\", false);\n                assertEquals(Set.of(\"fake1\", \"fake2\"), result);\n            }\n\n            @Test\n            void greaterThanQuery() {\n                var result = index.greaterThan(\"string1\", false);\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result);\n            }\n\n            @Test\n            void isNullQuery() {\n                assertThrows(IllegalArgumentException.class, () -> index.isNull());\n            }\n\n            @Test\n            void isNotNullQuery() {\n                assertThrows(IllegalArgumentException.class, () -> index.isNotNull());\n            }\n\n            @Test\n            void stringContainsQuery() {\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), index.stringContains(\"ing\"));\n                assertEquals(Set.of(\"fake2\"), index.stringContains(\"ing2\"));\n            }\n\n            @Test\n            void stringNotContainsQuery() {\n                assertEquals(Set.of(), index.stringNotContains(\"ing\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), index.stringNotContains(\"ing2\"));\n            }\n\n            @Test\n            void stringStartsWithQuery() {\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), index.stringStartsWith(\"string\"));\n                assertEquals(Set.of(\"fake2\"), index.stringStartsWith(\"string2\"));\n            }\n\n            @Test\n            void stringNotStartsWithQuery() {\n                assertEquals(Set.of(), index.stringNotStartsWith(\"string\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), index.stringNotStartsWith(\"string2\"));\n            }\n\n            @Test\n            void stringEndsWithQuery() {\n                assertEquals(Set.of(\"fake1\"), index.stringEndsWith(\"ing1\"));\n                assertEquals(Set.of(\"fake3\"), index.stringEndsWith(\"ing3\"));\n            }\n\n            @Test\n            void stringNotEndsWithQuery() {\n                assertEquals(Set.of(\"fake2\", \"fake3\"), index.stringNotEndsWith(\"ing1\"));\n                assertEquals(Set.of(\"fake1\", \"fake2\"), index.stringNotEndsWith(\"ing3\"));\n            }\n\n        }\n\n    }\n\n    @Nested\n    class NullableAndNonUniqueTest {\n\n        @BeforeEach\n        void setUp() {\n            lenient().when(spec.getName()).thenReturn(\"metadata.name\");\n            lenient().when(spec.getKeyType()).thenReturn(String.class);\n            lenient().when(spec.isNullable()).thenReturn(true);\n            lenient().when(spec.isUnique()).thenReturn(false);\n            lenient().when(spec.getValue(any(Fake.class))).thenAnswer(invocation -> {\n                Fake fake = invocation.getArgument(0);\n                return fake.getStringValue();\n            });\n        }\n\n        @Test\n        void shouldGetKeyType() {\n            assertEquals(String.class, index.getKeyType());\n        }\n\n        @Test\n        void shouldInsertAllowDuplicate() {\n            var fake1 = createFake(\"fake1\");\n            fake1.setStringValue(\"string\");\n\n            var fake2 = createFake(\"fake2\");\n            fake2.setStringValue(\"string\");\n\n            var insert1 = index.prepareInsert(fake1);\n            insert1.prepare();\n            insert1.commit();\n\n            var insert2 = index.prepareInsert(fake2);\n            insert2.prepare();\n            insert2.commit();\n\n            assertEquals(Set.of(\"fake1\", \"fake2\"), index.equal(\"string\"));\n        }\n\n        @Test\n        void shouldUpdateAllowDuplicate() {\n            var fake1 = createFake(\"fake1\");\n            fake1.setStringValue(\"string1\");\n\n            var insert1 = index.prepareInsert(fake1);\n            insert1.prepare();\n            insert1.commit();\n\n            var fake2 = createFake(\"fake2\");\n            fake2.setStringValue(\"string2\");\n\n            var insert2 = index.prepareInsert(fake2);\n            insert2.prepare();\n            insert2.commit();\n\n            // update fake2 to string1 should be allowed when not unique\n            fake2.setStringValue(\"string1\");\n            var update = index.prepareUpdate(fake2);\n            update.prepare();\n            update.commit();\n\n            assertEquals(Set.of(\"fake1\", \"fake2\"), index.equal(\"string1\"));\n        }\n\n        @Test\n        void shouldHandleNullValueOnInsert() {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(null);\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            assertEquals(Set.of(\"fake\"), index.isNull());\n            // no non-null entries exist\n            assertTrue(index.isNotNull().isEmpty());\n        }\n\n        @Test\n        void shouldHandleNullValueOnUpdate() {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(\"string\");\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            // update to null should be allowed\n            fake.setStringValue(null);\n            var update = index.prepareUpdate(fake);\n            update.prepare();\n            update.commit();\n\n            assertEquals(Set.of(\"fake\"), index.isNull());\n            assertTrue(index.equal(\"string\").isEmpty());\n        }\n\n        @Test\n        void shouldRollbackInsertCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(\"string\");\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n            insert.rollback();\n\n            assertEquals(Set.of(), index.equal(\"string\"));\n        }\n\n        @Test\n        void shouldRollbackUpdateCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(\"string\");\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            fake.setStringValue(\"new-string\");\n            var update = index.prepareUpdate(fake);\n            update.prepare();\n            update.commit();\n            update.rollback();\n\n            assertEquals(Set.of(\"fake\"), index.equal(\"string\"));\n        }\n\n        @Test\n        void shouldDeleteCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(\"string\");\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            var delete = index.prepareDelete(\"fake\");\n            delete.prepare();\n            delete.commit();\n\n            assertEquals(Set.of(), index.equal(\"string\"));\n        }\n\n        @Test\n        void shouldHandleDeleteIfPrimaryKeyNotExist() {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(\"string\");\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            var delete = index.prepareDelete(\"non-existent-fake\");\n            delete.prepare();\n            delete.commit();\n\n            assertEquals(Set.of(\"fake\"), index.equal(\"string\"));\n        }\n\n        @Test\n        void shouldRollbackDeleteCorrectly() {\n            var fake = createFake(\"fake\");\n            fake.setStringValue(\"string\");\n\n            var insert = index.prepareInsert(fake);\n            insert.prepare();\n            insert.commit();\n\n            var delete = index.prepareDelete(\"fake\");\n            delete.prepare();\n            delete.commit();\n            delete.rollback();\n\n            assertEquals(Set.of(\"fake\"), index.equal(\"string\"));\n        }\n\n        @Nested\n        class IndexQueryTest {\n\n            @BeforeEach\n            void setUp() {\n                // insert some data for query tests\n                var fake1 = createFake(\"fake1\");\n                fake1.setStringValue(\"string1\");\n\n                var fake2 = createFake(\"fake2\");\n                fake2.setStringValue(\"string2\");\n\n                var fake3 = createFake(\"fake3\");\n                fake3.setStringValue(\"string3\");\n\n                var fakenull = createFake(\"fakenull\");\n                fakenull.setStringValue(null);\n\n                insert(fake1);\n                insert(fake2);\n                insert(fake3);\n                insert(fakenull);\n            }\n\n            @Test\n            void equalQuery() {\n                var result1 = index.equal(\"string1\");\n                assertEquals(Set.of(\"fake1\"), result1);\n\n                var result2 = index.equal(\"string2\");\n                assertEquals(Set.of(\"fake2\"), result2);\n\n                var result3 = index.equal(\"non-existent-string\");\n                assertTrue(result3.isEmpty());\n            }\n\n            @Test\n            void notEqualQuery() {\n                var result1 = index.notEqual(\"string1\");\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result1);\n\n                var result2 = index.notEqual(\"string2\");\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result2);\n\n                var result3 = index.notEqual(\"non-existent-string\");\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), result3);\n            }\n\n            @Test\n            void allQuery() {\n                var result = index.all();\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), result);\n            }\n\n            @Test\n            void betweenQuery() {\n                var result = index.between(\"string1\", true, \"string3\", false);\n                assertEquals(Set.of(\"fake1\", \"fake2\"), result);\n            }\n\n            @Test\n            void notBetweenQuery() {\n                var result = index.notBetween(\"string1\", true, \"string2\", false);\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result);\n            }\n\n            @Test\n            void inQuery() {\n                var result = index.in(Set.of(\"string1\", \"string3\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result);\n            }\n\n            @Test\n            void notInQueryForSet() {\n                var result = index.notIn(Set.of(\"string1\", \"string3\"));\n                assertEquals(Set.of(\"fake2\"), result);\n            }\n\n            @Test\n            void notInQueryForList() {\n                var result = index.notIn(List.of(\"string2\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), result);\n            }\n\n            @Test\n            void lessThanQuery() {\n                var result = index.lessThan(\"string3\", false);\n                assertEquals(Set.of(\"fake1\", \"fake2\"), result);\n            }\n\n            @Test\n            void greaterThanQuery() {\n                var result = index.greaterThan(\"string1\", false);\n                assertEquals(Set.of(\"fake2\", \"fake3\"), result);\n            }\n\n            @Test\n            void isNullQuery() {\n                assertEquals(Set.of(\"fakenull\"), index.isNull());\n            }\n\n            @Test\n            void isNotNullQuery() {\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), index.isNotNull());\n            }\n\n            @Test\n            void stringContainsQuery() {\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), index.stringContains(\"ing\"));\n                assertEquals(Set.of(\"fake2\"), index.stringContains(\"ing2\"));\n            }\n\n            @Test\n            void stringNotContainsQuery() {\n                assertEquals(Set.of(), index.stringNotContains(\"ing\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), index.stringNotContains(\"ing2\"));\n            }\n\n            @Test\n            void stringStartsWithQuery() {\n                assertEquals(Set.of(\"fake1\", \"fake2\", \"fake3\"), index.stringStartsWith(\"string\"));\n                assertEquals(Set.of(\"fake2\"), index.stringStartsWith(\"string2\"));\n            }\n\n            @Test\n            void stringNotStartsWithQuery() {\n                assertEquals(Set.of(), index.stringNotStartsWith(\"string\"));\n                assertEquals(Set.of(\"fake1\", \"fake3\"), index.stringNotStartsWith(\"string2\"));\n            }\n\n            @Test\n            void stringEndsWithQuery() {\n                assertEquals(Set.of(\"fake1\"), index.stringEndsWith(\"ing1\"));\n                assertEquals(Set.of(\"fake3\"), index.stringEndsWith(\"ing3\"));\n            }\n\n            @Test\n            void stringNotEndsWithQuery() {\n                assertEquals(Set.of(\"fake2\", \"fake3\"), index.stringNotEndsWith(\"ing1\"));\n                assertEquals(Set.of(\"fake1\", \"fake2\"), index.stringNotEndsWith(\"ing3\"));\n            }\n\n        }\n    }\n\n    void insert(Fake fake) {\n        var insert = index.prepareInsert(fake);\n        insert.prepare();\n        insert.commit();\n    }\n\n    Fake createFake(String name) {\n        var fake = new Fake();\n        fake.setMetadata(new Metadata());\n        fake.getMetadata().setName(name);\n        return fake;\n    }\n\n}\n\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/index/query/QueryVisitorTest.java",
    "content": "package run.halo.app.extension.index.query;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.Mockito.withSettings;\n\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.boot.convert.ApplicationConversionService;\nimport org.springframework.core.convert.ConversionFailedException;\nimport org.springframework.core.convert.ConversionService;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.index.Index;\nimport run.halo.app.extension.index.Indices;\nimport run.halo.app.extension.index.LabelIndexQuery;\nimport run.halo.app.extension.index.ValueIndexQuery;\n\n@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n@ExtendWith(MockitoExtension.class)\nclass QueryVisitorTest {\n\n    @Mock\n    Indices<FakeExtension> indices;\n\n    @Spy\n    ConversionService conversionService = ApplicationConversionService.getSharedInstance();\n\n    @InjectMocks\n    QueryVisitor<FakeExtension> visitor;\n\n    @Test\n    void shouldVisitAllCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.all()).thenReturn(Set.of(\"all\", \"data\"));\n        var condition = Queries.all(\"metadata.name\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisitNoneCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        var condition = Queries.all(\"metadata.name\").not();\n        condition.visit(visitor);\n        assertEquals(Set.of(), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisitEmptyCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.all()).thenReturn(Set.of(\"all\", \"data\"));\n        var condition = Condition.empty();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisitEqualsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.equal(\"test-name\")).thenReturn(Set.of(\"equal\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Queries.equal(\"metadata.name\", \"test-name\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"equal\", \"data\"), visitor.getResult());\n\n        verify(conversionService).canConvert(String.class, String.class);\n        verify(conversionService).convert(\"test-name\", String.class);\n    }\n\n    @Test\n    void shouldVisitNotEqualsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.notEqual(\"test-name\")).thenReturn(Set.of(\"not-equal\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Queries.notEqual(\"metadata.name\", \"test-name\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"not-equal\", \"data\"), visitor.getResult());\n\n        verify(conversionService).canConvert(String.class, String.class);\n        verify(conversionService).convert(\"test-name\", String.class);\n    }\n\n    @Test\n    void shouldVisitInCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.in(argThat(c -> c.containsAll(List.of(\"name1\", \"name1\")))))\n            .thenReturn(Set.of(\"in\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Queries.in(\"metadata.name\", Set.<String>of(\"name1\", \"name2\"));\n        condition.visit(visitor);\n        assertEquals(Set.of(\"in\", \"data\"), visitor.getResult());\n        verify(conversionService, times(2)).canConvert(String.class, String.class);\n        verify(conversionService).convert(\"name1\", String.class);\n        verify(conversionService).convert(\"name2\", String.class);\n    }\n\n    @Test\n    void shouldVisitNotInCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.notIn(argThat(c -> c.containsAll(List.of(\"name1\", \"name2\")))))\n            .thenReturn(Set.of(\"not-in\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Queries.in(\"metadata.name\", Set.<String>of(\"name1\", \"name2\")).not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"not-in\", \"data\"), visitor.getResult());\n        verify(conversionService, times(2)).canConvert(String.class, String.class);\n        verify(conversionService).convert(\"name1\", String.class);\n        verify(conversionService).convert(\"name2\", String.class);\n    }\n\n    @Test\n    void shouldVisitGreaterThanCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.age\")).thenReturn(index);\n        when(query.greaterThan(18, false)).thenReturn(Set.of(\"gt\", \"data\"));\n        when(index.getKeyType()).thenReturn(Integer.class);\n        var condition = Queries.greaterThan(\"metadata.age\", 18);\n        condition.visit(visitor);\n        assertEquals(Set.of(\"gt\", \"data\"), visitor.getResult());\n\n        verify(conversionService).canConvert(Integer.class, Integer.class);\n        verify(conversionService).convert(18, Integer.class);\n    }\n\n    @Test\n    void shouldVisitLessThanCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.age\")).thenReturn(index);\n        when(query.lessThan(65, false)).thenReturn(Set.of(\"lt\", \"data\"));\n        when(index.getKeyType()).thenReturn(Integer.class);\n        var condition = Queries.lessThan(\"metadata.age\", 65);\n        condition.visit(visitor);\n        assertEquals(Set.of(\"lt\", \"data\"), visitor.getResult());\n\n        verify(conversionService).canConvert(Integer.class, Integer.class);\n        verify(conversionService).convert(65, Integer.class);\n    }\n\n    @Test\n    void shouldVisitBetweenCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.age\")).thenReturn(index);\n        when(query.between(18, true, 65, false)).thenReturn(Set.of(\"between\", \"data\"));\n        when(index.getKeyType()).thenReturn(Integer.class);\n        var condition = Queries.between(\"metadata.age\", 18, true, 65, false);\n        condition.visit(visitor);\n        assertEquals(Set.of(\"between\", \"data\"), visitor.getResult());\n\n        verify(conversionService, times(2)).canConvert(Integer.class, Integer.class);\n        verify(conversionService).convert(18, Integer.class);\n        verify(conversionService).convert(65, Integer.class);\n    }\n\n    @Test\n    void shouldVisitNotBetweenCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.age\")).thenReturn(index);\n        when(query.notBetween(18, false, 65, true)).thenReturn(Set.of(\"not-between\", \"data\"));\n        when(index.getKeyType()).thenReturn(Integer.class);\n        var condition = Queries.between(\"metadata.age\", 18, true, 65, false).not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"not-between\", \"data\"), visitor.getResult());\n\n        verify(conversionService, times(2)).canConvert(Integer.class, Integer.class);\n        verify(conversionService).convert(18, Integer.class);\n        verify(conversionService).convert(65, Integer.class);\n    }\n\n    @Test\n    void shouldVisitStringContainsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.description\")).thenReturn(index);\n        when(query.stringContains(\"keyword\")).thenReturn(Set.of(\"contains\", \"data\"));\n        var condition = Queries.contains(\"metadata.description\", \"keyword\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"contains\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"keyword\", String.class);\n    }\n\n    @Test\n    void shouldVisitStringNotContainsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.description\")).thenReturn(index);\n        when(query.stringNotContains(\"keyword\")).thenReturn(Set.of(\"not-contains\", \"data\"));\n        var condition = Queries.contains(\"metadata.description\", \"keyword\").not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"not-contains\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"keyword\", String.class);\n    }\n\n    @Test\n    void shouldVisitStringStartsWithCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.title\")).thenReturn(index);\n        when(query.stringStartsWith(\"prefix\")).thenReturn(Set.of(\"starts-with\", \"data\"));\n        var condition = Queries.startsWith(\"metadata.title\", \"prefix\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"starts-with\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"prefix\", String.class);\n    }\n\n    @Test\n    void shouldVisitStringNotStartsWithCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.title\")).thenReturn(index);\n        when(query.stringNotStartsWith(\"prefix\")).thenReturn(Set.of(\"not-starts-with\", \"data\"));\n        var condition = Queries.startsWith(\"metadata.title\", \"prefix\").not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"not-starts-with\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"prefix\", String.class);\n    }\n\n    @Test\n    void shouldVisitStringEndsWithCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.title\")).thenReturn(index);\n        when(query.stringEndsWith(\"suffix\")).thenReturn(Set.of(\"ends-with\", \"data\"));\n        var condition = Queries.endsWith(\"metadata.title\", \"suffix\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"ends-with\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"suffix\", String.class);\n    }\n\n    @Test\n    void shouldVisitStringNotEndsWithCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.title\")).thenReturn(index);\n        when(query.stringNotEndsWith(\"suffix\")).thenReturn(Set.of(\"not-ends-with\", \"data\"));\n        var condition = Queries.endsWith(\"metadata.title\", \"suffix\").not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"not-ends-with\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"suffix\", String.class);\n    }\n\n    @Test\n    void shouldVisitIsNullCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.optionalField\")).thenReturn(index);\n        when(query.isNull()).thenReturn(Set.of(\"is-null\", \"data\"));\n        var condition = Queries.isNull(\"metadata.optionalField\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"is-null\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisitIsNotNullCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.optionalField\")).thenReturn(index);\n        when(query.isNotNull()).thenReturn(Set.of(\"is-not-null\", \"data\"));\n        var condition = Queries.isNull(\"metadata.optionalField\").not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"is-not-null\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisitLabelEqualsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(LabelIndexQuery.class));\n        var query = (LabelIndexQuery) index;\n        when(indices.getIndex(\"metadata.labels\")).thenReturn(index);\n        when(query.equal(\"env\", \"production\")).thenReturn(Set.of(\"label-equal\", \"data\"));\n        var condition = Queries.labelEqual(\"env\", \"production\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"label-equal\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"production\", String.class);\n    }\n\n    @Test\n    void shouldVisitLabelNotEqualsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(LabelIndexQuery.class));\n        var primaryIndex = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (LabelIndexQuery) index;\n        var primaryQuery = (ValueIndexQuery) primaryIndex;\n        when(indices.getIndex(\"metadata.labels\")).thenReturn(index);\n        when(indices.getIndex(\"metadata.name\")).thenReturn(primaryIndex);\n        when(query.equal(\"env\", \"production\")).thenReturn(Set.of(\"equal\", \"data\"));\n        when(primaryQuery.all()).thenReturn(Set.of(\"all\", \"data\"));\n        var condition = Queries.labelEqual(\"env\", \"production\").not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisitLabelInCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(LabelIndexQuery.class));\n        var query = (LabelIndexQuery) index;\n        when(indices.getIndex(\"metadata.labels\")).thenReturn(index);\n        when(query.in(eq(\"env\"), eq(Set.of(\"production\", \"staging\"))))\n            .thenReturn(Set.of(\"label-in\", \"data\"));\n        var condition = Queries.labelIn(\"env\", Set.of(\"production\", \"staging\"));\n        condition.visit(visitor);\n        assertEquals(Set.of(\"label-in\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"production\", String.class);\n        verify(conversionService, never()).convert(\"staging\", String.class);\n    }\n\n    @Test\n    void shouldVisitLabelNotInCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(LabelIndexQuery.class));\n        var query = (LabelIndexQuery) index;\n        when(indices.getIndex(\"metadata.labels\")).thenReturn(index);\n        when(query.notIn(eq(\"env\"), eq(Set.of(\"production\", \"staging\"))))\n            .thenReturn(Set.of(\"label-not-in\", \"data\"));\n        var condition = Queries.labelIn(\"env\", Set.of(\"production\", \"staging\")).not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"label-not-in\", \"data\"), visitor.getResult());\n\n        verify(conversionService, never()).canConvert(String.class, String.class);\n        verify(conversionService, never()).convert(\"production\", String.class);\n        verify(conversionService, never()).convert(\"staging\", String.class);\n    }\n\n    @Test\n    void shouldVisitLabelExistsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(LabelIndexQuery.class));\n        var query = (LabelIndexQuery) index;\n        when(indices.getIndex(\"metadata.labels\")).thenReturn(index);\n        when(query.exists(\"env\")).thenReturn(Set.of(\"label-exists\", \"data\"));\n        var condition = Queries.labelExists(\"env\");\n        condition.visit(visitor);\n        assertEquals(Set.of(\"label-exists\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisitLabelNotExistsCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(LabelIndexQuery.class));\n        var primaryIndex = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (LabelIndexQuery) index;\n        var primaryQuery = (ValueIndexQuery) primaryIndex;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(primaryIndex);\n        when(indices.getIndex(\"metadata.labels\")).thenReturn(index);\n        when(query.exists(\"env\")).thenReturn(Set.of(\"label-exists\", \"data\"));\n        when(primaryQuery.all()).thenReturn(Set.of(\"all\", \"data\"));\n        var condition = Queries.labelExists(\"env\").not();\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisiteAndCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.equal(\"name1\")).thenReturn(Set.of(\"name1\", \"data\"));\n        when(query.equal(\"name2\")).thenReturn(Set.of(\"name2\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Queries.equal(\"metadata.name\", \"name1\")\n            .and(Queries.equal(\"metadata.name\", \"name2\"));\n        condition.visit(visitor);\n        assertEquals(Set.of(\"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisiteOrCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.equal(\"name1\")).thenReturn(Set.of(\"name1\", \"data\"));\n        when(query.equal(\"name2\")).thenReturn(Set.of(\"name2\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Queries.equal(\"metadata.name\", \"name1\")\n            .or(Queries.equal(\"metadata.name\", \"name2\"));\n        condition.visit(visitor);\n        assertEquals(Set.of(\"name1\", \"name2\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldVisiteNotCondition() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.notEqual(\"name1\")).thenReturn(Set.of(\"not-equal\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = new NotCondition(Queries.equal(\"metadata.name\", \"name1\"));\n        condition.visit(visitor);\n        assertEquals(Set.of(\"not-equal\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldRefineAndConditionWithLeftEmpty() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.equal(\"name1\")).thenReturn(Set.of(\"name1\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Condition.empty()\n            .and(Queries.equal(\"metadata.name\", \"name1\"));\n        condition.visit(visitor);\n        assertEquals(Set.of(\"name1\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldRefineAndConditionWithRightEmpty() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.equal(\"name1\")).thenReturn(Set.of(\"name1\", \"data\"));\n        when(index.getKeyType()).thenReturn(String.class);\n        var condition = Queries.equal(\"metadata.name\", \"name1\")\n            .and(Condition.empty());\n        condition.visit(visitor);\n        assertEquals(Set.of(\"name1\", \"data\"), visitor.getResult());\n    }\n\n    @Test\n    void shouldRefineOrConditionWithLeftEmpty() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.all()).thenReturn(Set.of(\"all\"));\n        var condition = Condition.empty()\n            .or(Queries.equal(\"metadata.name\", \"name1\"));\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\"), visitor.getResult());\n\n        verify(query, never()).equal(\"name1\");\n    }\n\n    @Test\n    void shouldRefineOrConditionWithRightEmpty() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.all()).thenReturn(Set.of(\"all\"));\n        var condition = Queries.equal(\"metadata.name\", \"name1\")\n            .or(Condition.empty());\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\"), visitor.getResult());\n\n        verify(query, never()).equal(\"name1\");\n    }\n\n    @Test\n    void shouldRefineAndConditionWithBothEmpty() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.all()).thenReturn(Set.of(\"all\"));\n        var condition = Condition.empty().and(Condition.empty());\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\"), visitor.getResult());\n\n        verify(query).all();\n    }\n\n    @Test\n    void shouldRefineOrConditionWithBothEmpty() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        var query = (ValueIndexQuery) index;\n        when(indices.getIndex(\"metadata.name\")).thenReturn(index);\n        when(query.all()).thenReturn(Set.of(\"all\"));\n        var condition = Condition.empty().or(Condition.empty());\n        condition.visit(visitor);\n        assertEquals(Set.of(\"all\"), visitor.getResult());\n\n        verify(query).all();\n    }\n\n    @Test\n    void throwErrorIfIndexNotFound() {\n        var condition = Queries.equal(\"metadata.unknownField\", \"value\");\n        when(indices.getIndex(\"metadata.unknownField\")).thenThrow(IllegalArgumentException.class);\n        assertThrows(IllegalArgumentException.class, (() -> {\n            condition.visit(visitor);\n        }));\n    }\n\n    @Test\n    void throwErrorIfIndexTypeMismatchForValueIndexQuery() {\n        var labelIndex = mock(Index.class, withSettings().extraInterfaces(LabelIndexQuery.class));\n        when(indices.getIndex(\"metadata.name\")).thenReturn(labelIndex);\n        var condition = Queries.equal(\"metadata.name\", \"name1\");\n        assertThrows(IllegalArgumentException.class, (() -> {\n            condition.visit(visitor);\n        }));\n    }\n\n    @Test\n    void throwErrorIfIndexTypeMismatchForLabelIndexQuery() {\n        var valueIndex = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        when(indices.getIndex(\"metadata.labels\")).thenReturn(valueIndex);\n        var condition = Queries.labelEqual(\"env\", \"production\");\n        assertThrows(IllegalArgumentException.class, (() -> {\n            condition.visit(visitor);\n        }));\n    }\n\n    @Test\n    void throwErrorIfValueConversionNotSupported() {\n        var index = mock(Index.class, withSettings().extraInterfaces(ValueIndexQuery.class));\n        when(indices.getIndex(\"metadata.age\")).thenReturn(index);\n        when(index.getKeyType()).thenReturn(Integer.class);\n        var condition = Queries.equal(\"metadata.age\", \"not-an-integer\");\n        assertThrows(ConversionFailedException.class, (() -> {\n            condition.visit(visitor);\n        }));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/ExtensionCompositeRouterFunctionTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.web.reactive.function.server.HandlerStrategies;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.event.SchemeAddedEvent;\nimport run.halo.app.extension.event.SchemeRemovedEvent;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionCompositeRouterFunctionTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @InjectMocks\n    ExtensionCompositeRouterFunction extensionRouterFunc;\n\n    @Test\n    void shouldRouteWhenSchemeRegistered() {\n        var exchange = MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"/apis/fake.halo.run/v1alpha1/fakes\").build());\n\n        var messageReaders = HandlerStrategies.withDefaults().messageReaders();\n        ServerRequest request = ServerRequest.create(exchange, messageReaders);\n\n        var handlerFunc = extensionRouterFunc.route(request).block();\n        assertNull(handlerFunc);\n\n        // trigger registering scheme\n        extensionRouterFunc.onSchemeAddedEvent(\n            new SchemeAddedEvent(this, Scheme.buildFromType(FakeExtension.class))\n        );\n\n        handlerFunc = extensionRouterFunc.route(request).block();\n        assertNotNull(handlerFunc);\n    }\n\n    @Test\n    void shouldNotRouteWhenSchemeUnregistered() {\n        var exchange = MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"/apis/fake.halo.run/v1alpha1/fakes\").build());\n\n        var messageReaders = HandlerStrategies.withDefaults().messageReaders();\n\n        // trigger registering scheme\n        extensionRouterFunc.onSchemeAddedEvent(\n            new SchemeAddedEvent(this, Scheme.buildFromType(FakeExtension.class))\n        );\n\n        ServerRequest request = ServerRequest.create(exchange, messageReaders);\n        var handlerFunc = extensionRouterFunc.route(request).block();\n        assertNotNull(handlerFunc);\n\n        // trigger registering scheme\n        extensionRouterFunc.onSchemeRemovedEvent(\n            new SchemeRemovedEvent(this, Scheme.buildFromType(FakeExtension.class))\n        );\n        handlerFunc = extensionRouterFunc.route(request).block();\n        assertNull(handlerFunc);\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/ExtensionCreateHandlerTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Objects;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.web.reactive.function.server.EntityResponse;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.extension.exception.ExtensionConvertException;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionCreateHandlerTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Test\n    void shouldBuildPathPatternCorrectly() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var createHandler = new ExtensionCreateHandler(scheme, client);\n        var pathPattern = createHandler.pathPattern();\n        assertEquals(\"/apis/fake.halo.run/v1alpha1/fakes\", pathPattern);\n    }\n\n    @Test\n    void shouldHandleCorrectly() {\n        final var fake = new FakeExtension();\n        var metadata = new Metadata();\n        metadata.setName(\"my-fake\");\n        fake.setMetadata(metadata);\n\n        var unstructured = new Unstructured();\n        unstructured.setMetadata(metadata);\n        unstructured.setApiVersion(\"fake.halo.run/v1alpha1\");\n        unstructured.setKind(\"Fake\");\n\n        var serverRequest = MockServerRequest.builder()\n            .body(Mono.just(unstructured));\n        when(client.create(any(Unstructured.class))).thenReturn(Mono.just(unstructured));\n\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var getHandler = new ExtensionCreateHandler(scheme, client);\n        var responseMono = getHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .consumeNextWith(response -> {\n                assertEquals(HttpStatus.CREATED, response.statusCode());\n                assertEquals(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\",\n                    Objects.requireNonNull(response.headers().getLocation()).toString());\n                assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());\n                assertTrue(response instanceof EntityResponse<?>);\n                assertEquals(unstructured, ((EntityResponse<?>) response).entity());\n            })\n            .verifyComplete();\n        verify(client, times(1)).create(eq(unstructured));\n    }\n\n    @Test\n    void shouldReturnErrorWhenNoBodyProvided() {\n        var serverRequest = MockServerRequest.builder()\n            .body(Mono.empty());\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var getHandler = new ExtensionCreateHandler(scheme, client);\n        var responseMono = getHandler.handle(serverRequest);\n        StepVerifier.create(responseMono)\n            .verifyError(ExtensionConvertException.class);\n    }\n\n    @Test\n    void shouldReturnErrorWhenExtensionNotFound() {\n        final var unstructured = new Unstructured();\n        var metadata = new Metadata();\n        metadata.setName(\"my-fake\");\n        unstructured.setMetadata(metadata);\n        unstructured.setApiVersion(\"fake.halo.run/v1alpha1\");\n        unstructured.setKind(\"Fake\");\n\n        var serverRequest = MockServerRequest.builder()\n            .body(Mono.just(unstructured));\n        doThrow(ExtensionNotFoundException.class).when(client).create(any());\n\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var createHandler = new ExtensionCreateHandler(scheme, client);\n        var responseMono = createHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .verifyError(ExtensionNotFoundException.class);\n        verify(client, times(1)).create(\n            argThat(extension -> Objects.equals(\"my-fake\", extension.getMetadata().getName())));\n        verify(client, times(0)).fetch(same(FakeExtension.class), anyString());\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/ExtensionDeleteHandlerTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.extension.GroupVersionKind.fromExtension;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.web.reactive.function.server.EntityResponse;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionDeleteHandlerTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Test\n    void shouldBuildPathPatternCorrectly() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var deleteHandler = new ExtensionDeleteHandler(scheme, client);\n        var pathPattern = deleteHandler.pathPattern();\n        assertEquals(\"/apis/fake.halo.run/v1alpha1/fakes/{name}\", pathPattern);\n    }\n\n    @Test\n    void shouldHandleCorrectly() {\n        final var fake = new FakeExtension();\n        var metadata = new Metadata();\n        metadata.setName(\"my-fake\");\n        fake.setMetadata(metadata);\n\n        var unstructured = new Unstructured();\n        unstructured.setMetadata(metadata);\n        unstructured.setApiVersion(\"fake.halo.run/v1alpha1\");\n        unstructured.setKind(\"Fake\");\n\n        var serverRequest = MockServerRequest.builder()\n            .pathVariable(\"name\", \"my-fake\")\n            .body(Mono.just(unstructured));\n        when(client.get(eq(FakeExtension.class), eq(\"my-fake\"))).thenReturn(Mono.just(fake));\n        when(client.delete(eq(fake))).thenReturn(Mono.just(fake));\n\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var deleteHandler = new ExtensionDeleteHandler(scheme, client);\n        var responseMono = deleteHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .assertNext(response -> {\n                assertEquals(HttpStatus.OK, response.statusCode());\n                assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());\n                assertTrue(response instanceof EntityResponse<?>);\n                assertEquals(fake, ((EntityResponse<?>) response).entity());\n            })\n            .verifyComplete();\n        verify(client, times(1)).get(eq(FakeExtension.class), eq(\"my-fake\"));\n        verify(client, times(1)).delete(any());\n        verify(client, times(0)).update(any());\n    }\n\n    @Test\n    void shouldReturnErrorWhenNoNameProvided() {\n        var serverRequest = MockServerRequest.builder()\n            .body(Mono.empty());\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var deleteHandler = new ExtensionDeleteHandler(scheme, client);\n        assertThrows(IllegalArgumentException.class, () -> deleteHandler.handle(serverRequest));\n    }\n\n    @Test\n    void shouldReturnErrorWhenExtensionNotFound() {\n        var serverRequest = MockServerRequest.builder()\n            .pathVariable(\"name\", \"my-fake\")\n            .build();\n        when(client.get(FakeExtension.class, \"my-fake\")).thenReturn(\n            Mono.error(\n                new ExtensionNotFoundException(fromExtension(FakeExtension.class), \"my-fake\")));\n\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var deleteHandler = new ExtensionDeleteHandler(scheme, client);\n        var responseMono = deleteHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .verifyError(ExtensionNotFoundException.class);\n\n        verify(client, times(1)).get(same(FakeExtension.class), anyString());\n        verify(client, times(0)).update(any());\n        verify(client, times(0)).delete(any());\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/ExtensionGetHandlerTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.extension.GroupVersionKind.fromExtension;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.web.reactive.function.server.EntityResponse;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionGetHandlerTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Test\n    void shouldBuildPathPatternCorrectly() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var getHandler = new ExtensionGetHandler(scheme, client);\n        var pathPattern = getHandler.pathPattern();\n        assertEquals(\"/apis/fake.halo.run/v1alpha1/fakes/{name}\", pathPattern);\n    }\n\n    @Test\n    void shouldHandleCorrectly() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var getHandler = new ExtensionGetHandler(scheme, client);\n        var serverRequest = MockServerRequest.builder()\n            .pathVariable(\"name\", \"my-fake\")\n            .build();\n        final var fake = new FakeExtension();\n        when(client.get(eq(FakeExtension.class), eq(\"my-fake\"))).thenReturn(Mono.just(fake));\n\n        var responseMono = getHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .consumeNextWith(response -> {\n                assertEquals(HttpStatus.OK, response.statusCode());\n                assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());\n                assertTrue(response instanceof EntityResponse<?>);\n                assertEquals(fake, ((EntityResponse<?>) response).entity());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldThrowExceptionWhenExtensionNotFound() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var getHandler = new ExtensionGetHandler(scheme, client);\n        var serverRequest = MockServerRequest.builder()\n            .pathVariable(\"name\", \"my-fake\")\n            .build();\n        when(client.get(eq(FakeExtension.class), eq(\"my-fake\"))).thenReturn(Mono.error(\n            new ExtensionNotFoundException(fromExtension(FakeExtension.class), \"my-fake\")));\n\n        Mono<ServerResponse> responseMono = getHandler.handle(serverRequest);\n        StepVerifier.create(responseMono)\n            .expectError(ExtensionNotFoundException.class)\n            .verify();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/ExtensionListHandlerTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.web.reactive.function.server.EntityResponse;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionListHandlerTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Test\n    void shouldBuildPathPatternCorrectly() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var listHandler = new ExtensionListHandler(scheme, client);\n        var pathPattern = listHandler.pathPattern();\n        assertEquals(\"/apis/fake.halo.run/v1alpha1/fakes\", pathPattern);\n    }\n\n    @Test\n    void shouldHandleCorrectly() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var listHandler = new ExtensionListHandler(scheme, client);\n        var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/fake\")\n            .queryParam(\"sort\", \"metadata.name,desc\"));\n        var serverRequest = MockServerRequest.builder().exchange(exchange).build();\n        final var fake01 = FakeExtension.createFake(\"fake01\");\n        final var fake02 = FakeExtension.createFake(\"fake02\");\n        var fakeListResult = new ListResult<>(0, 0, 2, List.of(fake01, fake02));\n        when(client.listBy(same(FakeExtension.class), any(ListOptions.class), any()))\n            .thenReturn(Mono.just(fakeListResult));\n\n        var responseMono = listHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .consumeNextWith(response -> {\n                assertEquals(HttpStatus.OK, response.statusCode());\n                assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());\n                assertTrue(response instanceof EntityResponse<?>);\n                assertEquals(fakeListResult, ((EntityResponse<?>) response).entity());\n            })\n            .verifyComplete();\n        verify(client).listBy(same(FakeExtension.class), any(ListOptions.class), any());\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/ExtensionRouterFunctionFactoryTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.github.fge.jackson.jsonpointer.JsonPointer;\nimport com.github.fge.jsonpatch.AddOperation;\nimport com.github.fge.jsonpatch.JsonPatch;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.codec.HttpMessageReader;\nimport org.springframework.http.codec.json.JacksonJsonDecoder;\nimport org.springframework.http.codec.json.JacksonJsonEncoder;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.HandlerStrategies;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.JsonExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.CreateHandler;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.GetHandler;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.ListHandler;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.UpdateHandler;\nimport run.halo.app.infra.config.JacksonAdapterModule;\nimport tools.jackson.databind.json.JsonMapper;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionRouterFunctionFactoryTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Spy\n    Scheme scheme = Scheme.buildFromType(FakeExtension.class);\n\n    @InjectMocks\n    ExtensionRouterFunctionFactory factory;\n\n    WebTestClient webClient;\n\n    @BeforeEach\n    void setUp() {\n        var mapper = JsonMapper.builder()\n            .addModule(new JacksonAdapterModule(\n                () -> com.fasterxml.jackson.databind.json.JsonMapper.builder().build())\n            )\n            .build();\n        webClient = WebTestClient.bindToRouterFunction(factory.create())\n            .handlerStrategies(HandlerStrategies.builder()\n                .codecs(c -> {\n                    c.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper));\n                    c.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper));\n                })\n                .build()\n            )\n            .configureClient()\n            .codecs(c -> {\n                c.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper));\n                c.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper));\n            })\n            .build();\n    }\n\n    @Nested\n    class PatchTest {\n\n        @Test\n        void shouldResponse404IfMethodNotPatch() {\n            webClient.method(HttpMethod.POST)\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n                .exchange()\n                .expectStatus().isNotFound();\n        }\n\n        @Test\n        void shouldResponse415IfMediaTypeIsInsufficient() {\n            webClient.method(HttpMethod.PATCH)\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n                .exchange()\n                .expectStatus().isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE);\n\n            webClient.method(HttpMethod.PATCH)\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString())\n                .exchange()\n                .expectStatus().isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE);\n        }\n\n        @Test\n        void shouldResponseBadRequestIfNoPatchBody() {\n            webClient.method(HttpMethod.PATCH)\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n                .header(HttpHeaders.CONTENT_TYPE, \"application/json-patch+json\")\n                .exchange()\n                .expectStatus().isBadRequest();\n        }\n\n        @Test\n        void shouldPatchCorrectly() {\n            var fake = new FakeExtension();\n            var metadata = new Metadata();\n            metadata.setName(\"my-fake\");\n            fake.setMetadata(metadata);\n            var mapper = com.fasterxml.jackson.databind.json.JsonMapper.builder().build();\n            var jsonExt = mapper.convertValue(fake, JsonExtension.class);\n\n            when(client.getJsonExtension(scheme.groupVersionKind(), \"my-fake\"))\n                .thenReturn(Mono.just(jsonExt));\n\n            var status = new FakeExtension.FakeStatus();\n            status.setState(\"running\");\n            fake.setStatus(status);\n            var updatedExt = mapper.convertValue(fake, JsonExtension.class);\n            when(client.update(any(JsonExtension.class))).thenReturn(Mono.just(updatedExt));\n\n            var stateNode = JsonNodeFactory.instance.textNode(\"running\");\n            var jsonPatch = new JsonPatch(List.of(\n                new AddOperation(JsonPointer.of(\"status\", \"state\"), stateNode)\n            ));\n            webClient.method(HttpMethod.PATCH)\n                .uri(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\")\n                .header(HttpHeaders.CONTENT_TYPE, \"application/json-patch+json\")\n                .bodyValue(jsonPatch)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(JsonExtension.class).isEqualTo(updatedExt);\n\n            verify(client).<JsonExtension>update(assertArg(ext -> {\n                var state = ext.getInternal().get(\"status\").get(\"state\")\n                    .asText();\n                assertEquals(\"running\", state);\n            }));\n        }\n    }\n\n    @Test\n    void shouldCreateSuccessfully() {\n        var routerFunction = factory.create();\n\n        testCases().forEach(testCase -> {\n            List<HttpMessageReader<?>> messageReaders =\n                HandlerStrategies.withDefaults().messageReaders();\n            var request = ServerRequest.create(testCase.webExchange, messageReaders);\n            var handlerFunc = routerFunction.route(request).block();\n            assertInstanceOf(testCase.expectHandlerType, handlerFunc);\n        });\n    }\n\n    List<TestCase> testCases() {\n        var listWebExchange = MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"/apis/fake.halo.run/v1alpha1/fakes\").build());\n\n        var getWebExchange = MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\").build()\n        );\n\n        var createWebExchange = MockServerWebExchange.from(\n            MockServerHttpRequest.post(\"/apis/fake.halo.run/v1alpha1/fakes\").body(\"{}\")\n        );\n\n        var updateWebExchange = MockServerWebExchange.from(\n            MockServerHttpRequest.put(\"/apis/fake.halo.run/v1alpha1/fakes/my-fake\").body(\"{}\")\n        );\n\n        return List.of(\n            new TestCase(listWebExchange, ListHandler.class),\n            new TestCase(getWebExchange, GetHandler.class),\n            new TestCase(createWebExchange, CreateHandler.class),\n            new TestCase(updateWebExchange, UpdateHandler.class)\n        );\n    }\n\n    record TestCase(ServerWebExchange webExchange,\n                    Class<? extends HandlerFunction<ServerResponse>> expectHandlerType) {\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/ExtensionUpdateHandlerTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Objects;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.web.reactive.function.server.EntityResponse;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.extension.exception.ExtensionNotFoundException;\n\n@ExtendWith(MockitoExtension.class)\nclass ExtensionUpdateHandlerTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Test\n    void shouldBuildPathPatternCorrectly() {\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var updateHandler = new ExtensionUpdateHandler(scheme, client);\n        var pathPattern = updateHandler.pathPattern();\n        assertEquals(\"/apis/fake.halo.run/v1alpha1/fakes/{name}\", pathPattern);\n    }\n\n    @Test\n    void shouldHandleCorrectly() {\n        final var fake = new FakeExtension();\n        var metadata = new Metadata();\n        metadata.setName(\"my-fake\");\n        fake.setMetadata(metadata);\n\n        var unstructured = new Unstructured();\n        unstructured.setMetadata(metadata);\n        unstructured.setApiVersion(\"fake.halo.run/v1alpha1\");\n        unstructured.setKind(\"Fake\");\n\n        var serverRequest = MockServerRequest.builder()\n            .pathVariable(\"name\", \"my-fake\")\n            .body(Mono.just(unstructured));\n        // when(client.fetch(eq(FakeExtension.class), eq(\"my-fake\"))).thenReturn(Mono.just(fake));\n        when(client.update(eq(unstructured))).thenReturn(Mono.just(unstructured));\n\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var updateHandler = new ExtensionUpdateHandler(scheme, client);\n        var responseMono = updateHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .assertNext(response -> {\n                assertEquals(HttpStatus.OK, response.statusCode());\n                assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());\n                assertTrue(response instanceof EntityResponse<?>);\n                assertEquals(unstructured, ((EntityResponse<?>) response).entity());\n            })\n            .verifyComplete();\n        // verify(client, times(1)).fetch(eq(FakeExtension.class), eq(\"my-fake\"));\n        verify(client, times(1)).update(eq(unstructured));\n    }\n\n    @Test\n    void shouldReturnErrorWhenNoBodyProvided() {\n        var serverRequest = MockServerRequest.builder()\n            .pathVariable(\"name\", \"my-fake\")\n            .body(Mono.empty());\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var updateHandler = new ExtensionUpdateHandler(scheme, client);\n        var responseMono = updateHandler.handle(serverRequest);\n        StepVerifier.create(responseMono)\n            .verifyError(ServerWebInputException.class);\n    }\n\n    @Test\n    void shouldReturnErrorWhenNoNameProvided() {\n        var serverRequest = MockServerRequest.builder()\n            .body(Mono.empty());\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var updateHandler = new ExtensionUpdateHandler(scheme, client);\n        assertThrows(IllegalArgumentException.class, () -> updateHandler.handle(serverRequest));\n    }\n\n    @Test\n    void shouldReturnErrorWhenExtensionNotFound() {\n        final var unstructured = new Unstructured();\n        var metadata = new Metadata();\n        metadata.setName(\"my-fake\");\n        unstructured.setMetadata(metadata);\n        unstructured.setApiVersion(\"fake.halo.run/v1alpha1\");\n        unstructured.setKind(\"Fake\");\n\n        var serverRequest = MockServerRequest.builder()\n            .pathVariable(\"name\", \"my-fake\")\n            .body(Mono.just(unstructured));\n        doThrow(ExtensionNotFoundException.class).when(client).update(any());\n\n        var scheme = Scheme.buildFromType(FakeExtension.class);\n        var updateHandler = new ExtensionUpdateHandler(scheme, client);\n        var responseMono = updateHandler.handle(serverRequest);\n\n        StepVerifier.create(responseMono)\n            .verifyError(ExtensionNotFoundException.class);\n\n        verify(client, times(1)).update(\n            argThat(extension -> Objects.equals(\"my-fake\", extension.getMetadata().getName())));\n        verify(client, times(0)).fetch(same(FakeExtension.class), anyString());\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/router/PathPatternGeneratorTest.java",
    "content": "package run.halo.app.extension.router;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.AbstractExtension;\nimport run.halo.app.extension.GVK;\nimport run.halo.app.extension.Scheme;\nimport run.halo.app.extension.router.ExtensionRouterFunctionFactory.PathPatternGenerator;\n\nclass PathPatternGeneratorTest {\n\n    @GVK(group = \"fake.halo.run\", version = \"v1alpha1\", kind = \"Fake\",\n        singular = \"fake\", plural = \"fakes\")\n    private static class GroupExtension extends AbstractExtension {\n    }\n\n    @GVK(group = \"\", version = \"v1alpha1\", kind = \"Fake\",\n        singular = \"fake\", plural = \"fakes\")\n    private static class GrouplessExtension extends AbstractExtension {\n    }\n\n    @Test\n    void buildGroupedExtensionPathPattern() {\n        var scheme = Scheme.buildFromType(GroupExtension.class);\n        var pathPattern = PathPatternGenerator.buildExtensionPathPattern(scheme);\n        assertEquals(\"/apis/fake.halo.run/v1alpha1/fakes\", pathPattern);\n    }\n\n    @Test\n    void buildGrouplessExtensionPathPattern() {\n        var scheme = Scheme.buildFromType(GrouplessExtension.class);\n        var pathPattern = PathPatternGenerator.buildExtensionPathPattern(scheme);\n        assertEquals(\"/api/v1alpha1/fakes\", pathPattern);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/extension/store/ReactiveExtensionStoreClientImplTest.java",
    "content": "package run.halo.app.extension.store;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.r2dbc.core.R2dbcEntityOperations;\nimport org.springframework.data.r2dbc.core.ReactiveSelectOperation;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\n@ExtendWith(MockitoExtension.class)\nclass ReactiveExtensionStoreClientImplTest {\n\n    @Mock\n    ExtensionStoreRepository repository;\n\n    @Mock\n    R2dbcEntityOperations entityOperations;\n\n    @InjectMocks\n    ReactiveExtensionStoreClientImpl client;\n\n    @Test\n    void listByNamePrefix() {\n        var expectedExtensions = List.of(\n            new ExtensionStore(\"/registry/posts/hello-world\", \"this is post\".getBytes(), 1L),\n            new ExtensionStore(\"/registry/posts/hello-halo\", \"this is post\".getBytes(), 1L)\n        );\n\n        var select = mock(ReactiveSelectOperation.ReactiveSelect.class);\n        var selectWithQuery = mock(ReactiveSelectOperation.SelectWithQuery.class);\n        var terminatingSelect = mock(ReactiveSelectOperation.TerminatingSelect.class);\n        when(terminatingSelect.all()).thenReturn(Flux.fromIterable(expectedExtensions));\n        when(selectWithQuery.matching(any())).thenReturn(terminatingSelect);\n        when(select.withFetchSize(100)).thenReturn(selectWithQuery);\n        when(entityOperations.select(ExtensionStore.class)).thenReturn(select);\n\n        client.listByNamePrefix(\"/registry/posts\").collectList()\n            .as(StepVerifier::create)\n            .expectNext(expectedExtensions)\n            .verifyComplete();\n    }\n\n    @Test\n    void fetchByName() {\n        var expectedExtension =\n            new ExtensionStore(\"/registry/posts/hello-world\", \"this is post\".getBytes(), 1L);\n\n        when(repository.findById(\"/registry/posts/hello-halo\"))\n            .thenReturn(Mono.just(expectedExtension));\n\n        var gotExtension = client.fetchByName(\"/registry/posts/hello-halo\").blockOptional();\n        assertTrue(gotExtension.isPresent());\n        assertEquals(expectedExtension, gotExtension.get());\n    }\n\n    @Test\n    void create() {\n        var expectedExtension =\n            new ExtensionStore(\"/registry/posts/hello-halo\", \"hello halo\".getBytes(), 2L);\n\n        when(repository.save(any()))\n            .thenReturn(Mono.just(expectedExtension));\n\n        var createdExtension =\n            client.create(\"/registry/posts/hello-halo\", \"hello halo\".getBytes())\n                .block();\n\n        assertEquals(expectedExtension, createdExtension);\n    }\n\n    @Test\n    void update() {\n        var expectedExtension =\n            new ExtensionStore(\"/registry/posts/hello-halo\", \"hello halo\".getBytes(), 2L);\n\n        when(repository.save(any())).thenReturn(Mono.just(expectedExtension));\n\n        var updatedExtension =\n            client.update(\"/registry/posts/hello-halo\", 1L, \"hello halo\".getBytes())\n                .block();\n\n        assertEquals(expectedExtension, updatedExtension);\n    }\n\n    @Test\n    void shouldDoNotThrowExceptionWhenDeletingNonExistExt() {\n        when(repository.findById(anyString())).thenReturn(Mono.empty());\n\n        client.delete(\"/registry/posts/hello-halo\", 1L).block();\n    }\n\n    @Test\n    void shouldDeleteSuccessfully() {\n        var expectedExtension =\n            new ExtensionStore(\"/registry/posts/hello-halo\", \"hello halo\".getBytes(), 2L);\n\n        when(repository.findById(anyString())).thenReturn(Mono.just(expectedExtension));\n        when(repository.delete(any())).thenReturn(Mono.empty());\n\n        var deletedExtension = client.delete(\"/registry/posts/hello-halo\", 2L).block();\n\n        assertEquals(expectedExtension, deletedExtension);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/ConditionListTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Iterator;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link ConditionList}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass ConditionListTest {\n\n    @Test\n    void add() {\n        ConditionList conditionList = new ConditionList();\n        conditionList.add(condition(\"type\", \"message\", \"reason\", ConditionStatus.FALSE));\n        conditionList.add(condition(\"type\", \"message\", \"reason\", ConditionStatus.FALSE));\n\n        assertThat(conditionList.size()).isEqualTo(1);\n        conditionList.add(condition(\"type\", \"message\", \"reason\", ConditionStatus.TRUE));\n        assertThat(conditionList.size()).isEqualTo(2);\n    }\n\n    @Test\n    void addAndEvictFIFO() throws JSONException {\n        ConditionList conditionList = new ConditionList();\n        conditionList.addFirst(condition(\"type\", \"message\", \"reason\", ConditionStatus.FALSE));\n        conditionList.addFirst(condition(\"type2\", \"message2\", \"reason2\", ConditionStatus.FALSE));\n        conditionList.addFirst(condition(\"type3\", \"message3\", \"reason3\", ConditionStatus.FALSE));\n\n        JSONAssert.assertEquals(\"\"\"\n                [\n                    {\n                        \"type\": \"type3\",\n                        \"status\": \"FALSE\",\n                        \"message\": \"message3\",\n                        \"reason\": \"reason3\"\n                    },\n                    {\n                        \"type\": \"type2\",\n                        \"status\": \"FALSE\",\n                        \"message\": \"message2\",\n                        \"reason\": \"reason2\"\n                    },\n                    {\n                        \"type\": \"type\",\n                        \"status\": \"FALSE\",\n                        \"message\": \"message\",\n                        \"reason\": \"reason\"\n                    }\n                ]\n                \"\"\",\n            JsonUtils.objectToJson(conditionList),\n            true);\n        assertThat(conditionList.size()).isEqualTo(3);\n\n        conditionList.addAndEvictFIFO(\n            condition(\"type4\", \"message4\", \"reason4\", ConditionStatus.FALSE), 1);\n\n        assertThat(conditionList.size()).isEqualTo(1);\n\n        // json serialize test.\n        JSONAssert.assertEquals(\"\"\"\n                [\n                    {\n                        \"type\": \"type4\",\n                        \"status\": \"FALSE\",\n                        \"message\": \"message4\",\n                        \"reason\": \"reason4\"\n                    }\n                ]\n                \"\"\",\n            JsonUtils.objectToJson(conditionList), true);\n    }\n\n    @Test\n    void peek() {\n        ConditionList conditionList = new ConditionList();\n        conditionList.addFirst(condition(\"type\", \"message\", \"reason\", ConditionStatus.FALSE));\n        Condition condition = condition(\"type2\", \"message2\", \"reason2\", ConditionStatus.FALSE);\n        conditionList.addFirst(condition);\n\n        Condition peek = conditionList.peek();\n        assertThat(peek).isEqualTo(condition);\n    }\n\n    @Test\n    void removeLast() {\n        ConditionList conditionList = new ConditionList();\n        Condition condition = condition(\"type\", \"message\", \"reason\", ConditionStatus.FALSE);\n        conditionList.addFirst(condition);\n\n        conditionList.addFirst(condition(\"type2\", \"message2\", \"reason2\", ConditionStatus.FALSE));\n\n        assertThat(conditionList.size()).isEqualTo(2);\n        assertThat(conditionList.removeLast()).isEqualTo(condition);\n        assertThat(conditionList.size()).isEqualTo(1);\n    }\n\n    @Test\n    void test() {\n        ConditionList conditionList = new ConditionList();\n        conditionList.addAndEvictFIFO(\n            condition(\"type\", \"message\", \"reason\", ConditionStatus.FALSE));\n        conditionList.addAndEvictFIFO(\n            condition(\"type2\", \"message2\", \"reason2\", ConditionStatus.FALSE));\n\n        Iterator<Condition> iterator = conditionList.iterator();\n        assertThat(iterator.next().getType()).isEqualTo(\"type2\");\n        assertThat(iterator.next().getType()).isEqualTo(\"type\");\n    }\n\n    @Test\n    void deserialization() {\n        String s = \"\"\"\n            [{\n                \"type\": \"type3\",\n                \"status\": \"FALSE\",\n                \"message\": \"message3\",\n                \"reason\": \"reason3\"\n            },\n            {\n                \"type\": \"type2\",\n                \"status\": \"FALSE\",\n                \"message\": \"message2\",\n                \"reason\": \"reason2\"\n            },\n            {\n                \"type\": \"type\",\n                \"status\": \"FALSE\",\n                \"message\": \"message\",\n                \"reason\": \"reason\"\n            }]\n            \"\"\";\n        ConditionList conditions = JsonUtils.jsonToObject(s, ConditionList.class);\n        assertThat(conditions.peek().getType()).isEqualTo(\"type3\");\n    }\n\n    @Test\n    void shouldNotAddIfTypeIsSame() {\n        var conditions = new ConditionList();\n        var condition = Condition.builder()\n            .type(\"type\")\n            .status(ConditionStatus.TRUE)\n            .reason(\"reason\")\n            .message(\"message\")\n            .build();\n\n        var anotherCondition = Condition.builder()\n            .type(\"type\")\n            .status(ConditionStatus.FALSE)\n            .reason(\"another reason\")\n            .message(\"another message\")\n            .build();\n\n        conditions.addAndEvictFIFO(condition);\n        conditions.addAndEvictFIFO(anotherCondition);\n\n        assertEquals(1, conditions.size());\n    }\n\n    @Test\n    void shouldNotUpdateLastTransitionTimeIfStatusNotChanged() {\n        var now = Instant.now();\n        var conditions = new ConditionList();\n        conditions.addAndEvictFIFO(\n            Condition.builder()\n                .type(\"type\")\n                .status(ConditionStatus.TRUE)\n                .reason(\"reason\")\n                .message(\"message\")\n                .lastTransitionTime(now)\n                .build()\n        );\n\n        conditions.addAndEvictFIFO(\n            Condition.builder()\n                .type(\"type\")\n                .status(ConditionStatus.TRUE)\n                .reason(\"reason\")\n                .message(\"message\")\n                .lastTransitionTime(now.plus(Duration.ofSeconds(1)))\n                .build()\n        );\n\n        assertEquals(1, conditions.size());\n        // make sure the last transition time was not modified.\n        assertEquals(now, conditions.peek().getLastTransitionTime());\n    }\n\n    @Test\n    void shouldUpdateLastTransitionTimeIfStatusChanged() {\n        var now = Instant.now();\n        var conditions = new ConditionList();\n        conditions.addAndEvictFIFO(\n            Condition.builder()\n                .type(\"type\")\n                .status(ConditionStatus.TRUE)\n                .reason(\"reason\")\n                .message(\"message\")\n                .lastTransitionTime(now)\n                .build()\n        );\n\n        conditions.addAndEvictFIFO(\n            Condition.builder()\n                .type(\"type\")\n                .status(ConditionStatus.FALSE)\n                .reason(\"reason\")\n                .message(\"message\")\n                .lastTransitionTime(now.plus(Duration.ofSeconds(1)))\n                .build()\n        );\n\n        assertEquals(1, conditions.size());\n        assertEquals(now.plus(Duration.ofSeconds(1)), conditions.peek().getLastTransitionTime());\n    }\n\n    private Condition condition(String type, String message, String reason,\n        ConditionStatus status) {\n        Condition condition = new Condition();\n        condition.setType(type);\n        condition.setMessage(message);\n        condition.setReason(reason);\n        condition.setStatus(status);\n        return condition;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/DefaultBackupRootGetterTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultBackupRootGetterTest {\n\n    @Mock\n    HaloProperties haloProperties;\n\n    @InjectMocks\n    DefaultBackupRootGetter backupRootGetter;\n\n    @Test\n    void shouldGetBackupRootFromWorkDir() {\n        when(haloProperties.getWorkDir()).thenReturn(Path.of(\"workdir\"));\n        var backupRoot = this.backupRootGetter.get();\n        assertEquals(Path.of(\"workdir\", \"backups\"), backupRoot);\n        verify(haloProperties).getWorkDir();\n    }\n\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/DefaultExternalLinkProcessorTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.web.filter.reactive.ServerWebExchangeContextFilter.EXCHANGE_CONTEXT_ATTRIBUTE;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.test.StepVerifier;\n\n/**\n * Tests for {@link DefaultExternalLinkProcessor}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultExternalLinkProcessorTest {\n\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @InjectMocks\n    DefaultExternalLinkProcessor externalLinkProcessor;\n\n\n    @Test\n    void processWhenLinkIsEmpty() {\n        assertThat(externalLinkProcessor.processLink((String) null)).isNull();\n        assertThat(externalLinkProcessor.processLink(\"\")).isEmpty();\n    }\n\n    @Test\n    void process() throws MalformedURLException {\n        when(externalUrlSupplier.getRaw()).thenReturn(null);\n        assertThat(externalLinkProcessor.processLink(\"/test\")).isEqualTo(\"/test\");\n\n        when(externalUrlSupplier.getRaw()).thenReturn(URI.create(\"https://halo.run\").toURL());\n        assertThat(externalLinkProcessor.processLink(\"/test\")).isEqualTo(\"https://halo.run/test\");\n\n        assertThat(externalLinkProcessor.processLink(\"https://guqing.xyz/test\"))\n            .isEqualTo(\"https://guqing.xyz/test\");\n\n        when(externalUrlSupplier.getRaw()).thenReturn(URI.create(\"https://halo.run/\").toURL());\n        assertThat(externalLinkProcessor.processLink(\"/test\")).isEqualTo(\"https://halo.run/test\");\n        assertThat(externalLinkProcessor.processLink(\"https://halo.run/test\"))\n            .isEqualTo(\"https://halo.run/test\");\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"processUriTestWithoutServerWebExchangeArguments\")\n    void processUriWithoutServerWebExchange(String link, String expectedLink)\n        throws MalformedURLException {\n        lenient().when(externalUrlSupplier.getRaw())\n            .thenReturn(new URL(\"https://www.halo.run/context-path\"));\n        externalLinkProcessor.processLink(URI.create(link))\n            .as(StepVerifier::create)\n            .expectNext(URI.create(expectedLink))\n            .verifyComplete();\n    }\n\n    static Stream<Arguments> processUriTestWithoutServerWebExchangeArguments() {\n        return Stream.of(\n            Arguments.of(\"http://localhost:8090/halo\", \"http://localhost:8090/halo\"),\n            Arguments.of(\"/halo\", \"https://www.halo.run/context-path/halo\"),\n            Arguments.of(\"halo\", \"https://www.halo.run/context-path/halo\"),\n            Arguments.of(\"/halo?query\", \"https://www.halo.run/context-path/halo?query\"),\n            Arguments.of(\n                \"/halo?query#fragment\", \"https://www.halo.run/context-path/halo?query#fragment\"\n            ),\n            Arguments.of(\"/halo/subpath\", \"https://www.halo.run/context-path/halo/subpath\"),\n            Arguments.of(\"/halo/中文\", \"https://www.halo.run/context-path/halo/%E4%B8%AD%E6%96%87\"),\n            Arguments.of(\"/halo/ooo%2Fooo\", \"https://www.halo.run/context-path/halo/ooo%2Fooo\")\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"processUriTestWithServerWebExchangeArguments\")\n    void processUriWithServerWebExchange(String link, String expectLink)\n        throws MalformedURLException {\n        lenient().when(externalUrlSupplier.getRaw())\n            .thenReturn(URI.create(\"https://www.halo.run\").toURL());\n        var request = mock(ServerHttpRequest.class);\n        var exchange = mock(ServerWebExchange.class);\n        lenient().when(exchange.getRequest()).thenReturn(request);\n        lenient().when(externalUrlSupplier.getURL(request)).thenReturn(\n            new URL(\"https://antoher.halo.run/context-path\"));\n        externalLinkProcessor.processLink(URI.create(link))\n            .contextWrite(context -> context.put(EXCHANGE_CONTEXT_ATTRIBUTE, exchange))\n            .as(StepVerifier::create)\n            .expectNext(URI.create(expectLink))\n            .verifyComplete();\n    }\n\n    static Stream<Arguments> processUriTestWithServerWebExchangeArguments() {\n        return Stream.of(\n            Arguments.of(\"http://localhost:8090/halo?query#fragment\",\n                \"http://localhost:8090/halo?query#fragment\"),\n            Arguments.of(\"/halo\", \"https://antoher.halo.run/context-path/halo\"),\n            Arguments.of(\"halo\", \"https://antoher.halo.run/context-path/halo\"),\n            Arguments.of(\"/halo?query\", \"https://antoher.halo.run/context-path/halo?query\"),\n            Arguments.of(\"/halo?query#fragment\",\n                \"https://antoher.halo.run/context-path/halo?query#fragment\"),\n            Arguments.of(\"/halo/subpath\", \"https://antoher.halo.run/context-path/halo/subpath\"),\n            Arguments.of(\"/halo/中文\",\n                \"https://antoher.halo.run/context-path/halo/%E4%B8%AD%E6%96%87\"),\n            Arguments.of(\"/halo/ooo%2Fooo\", \"https://antoher.halo.run/context-path/halo/ooo%2Fooo\")\n        );\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/DefaultSystemConfigFetcherTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.core.convert.ConversionService;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport tools.jackson.databind.json.JsonMapper;\n\n/**\n * Tests for {@link DefaultSystemConfigFetcher}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultSystemConfigFetcherTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    ConversionService conversionService;\n\n    @Spy\n    JsonMapper mapper = JsonMapper.shared();\n\n    @InjectMocks\n    DefaultSystemConfigFetcher systemConfigFetcher;\n\n    private ConfigMap mockConfigMap;\n\n    @BeforeEach\n    void setUp() {\n        mockConfigMap = new ConfigMap();\n        mockConfigMap.setData(Map.of(\n            \"basic\", \"\"\"\n                {\n                  \"title\": \"Test Blog\",\n                  \"subtitle\": \"Test\"\n                }\"\"\",\n            \"post\", \"\"\"\n                {\n                  \"postPageSize\": 10\n                }\"\"\"\n        ));\n    }\n\n    @Test\n    void testFetchWithConvertibleType() {\n        // Arrange\n        when(conversionService.canConvert(String.class, String.class))\n            .thenReturn(true);\n        when(conversionService.convert(\"testValue\", String.class))\n            .thenReturn(\"testValue\");\n\n        var configMapWithString = new ConfigMap();\n        var data = new HashMap<String, String>();\n        data.put(\"testKey\", \"testValue\");\n        configMapWithString.setData(data);\n\n        systemConfigFetcher.getConfigMapCache().set(configMapWithString.getData());\n\n        // Act & Assert\n        systemConfigFetcher.fetch(\"testKey\", String.class)\n            .as(StepVerifier::create)\n            .expectNext(\"testValue\")\n            .verifyComplete();\n    }\n\n    @Test\n    void testFetchWithJsonConversion() {\n        // Arrange\n        var configMap = new ConfigMap();\n        configMap.setData(Map.of(\n            \"basic\", \"\"\"\n                {\n                  \"title\": \"Test Blog\",\n                  \"subtitle\": \"Test Subtitle\"\n                }\"\"\"\n        ));\n        systemConfigFetcher.getConfigMapCache().set(configMap.getData());\n\n        when(conversionService.canConvert(String.class, SystemSetting.Basic.class))\n            .thenReturn(false);\n\n        // Act & Assert\n        systemConfigFetcher.fetch(\"basic\", SystemSetting.Basic.class)\n            .as(StepVerifier::create)\n            .assertNext(basic -> {\n                assertThat(basic.getTitle()).isEqualTo(\"Test Blog\");\n                assertThat(basic.getSubtitle()).isEqualTo(\"Test Subtitle\");\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void testFetchWhenKeyDoesNotExist() {\n        // Arrange\n        systemConfigFetcher.getConfigMapCache().set(mockConfigMap.getData());\n\n        // Act & Assert\n        systemConfigFetcher.fetch(\"nonExistentKey\", String.class)\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void testGetBasicWithValidData() {\n        // Arrange\n        var configMap = new ConfigMap();\n        configMap.setData(Map.of(\n            \"basic\", \"\"\"\n                {\n                  \"title\": \"My Blog\",\n                  \"subtitle\": \"My Subtitle\",\n                  \"logo\": \"logo.png\"\n                }\"\"\"\n        ));\n        systemConfigFetcher.getConfigMapCache().set(configMap.getData());\n\n        when(conversionService.canConvert(String.class, SystemSetting.Basic.class))\n            .thenReturn(false);\n\n        // Act & Assert\n        systemConfigFetcher.getBasic()\n            .as(StepVerifier::create)\n            .assertNext(basic -> {\n                assertThat(basic.getTitle()).isEqualTo(\"My Blog\");\n                assertThat(basic.getSubtitle()).isEqualTo(\"My Subtitle\");\n                assertThat(basic.getLogo()).isEqualTo(\"logo.png\");\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void testGetBasicWhenKeyDoesNotExist() {\n        // Arrange\n        systemConfigFetcher.getConfigMapCache().set(Map.of());\n\n        // Act & Assert - should return a new instance\n        systemConfigFetcher.getBasic()\n            .as(StepVerifier::create)\n            .assertNext(basic -> assertThat(basic).isNotNull())\n            .verifyComplete();\n    }\n\n    @Test\n    void testFetchComment() {\n        // Arrange\n        var configMap = new ConfigMap();\n        configMap.setData(Map.of(\n            \"comment\", \"\"\"\n                {\n                  \"enable\": true\n                }\"\"\"\n        ));\n        systemConfigFetcher.getConfigMapCache().set(configMap.getData());\n\n        when(conversionService.canConvert(String.class, SystemSetting.Comment.class))\n            .thenReturn(false);\n\n        // Act & Assert\n        systemConfigFetcher.fetchComment()\n            .as(StepVerifier::create)\n            .expectNextMatches(java.util.Objects::nonNull)\n            .verifyComplete();\n    }\n\n    @Test\n    void testFetchPost() {\n        // Arrange\n        var configMap = new ConfigMap();\n        configMap.setData(Map.of(\n            \"post\", \"\"\"\n                {\n                  \"postPageSize\": 10,\n                  \"archivePageSize\": 20\n                }\"\"\"\n        ));\n        systemConfigFetcher.getConfigMapCache().set(configMap.getData());\n\n        when(conversionService.canConvert(String.class, SystemSetting.Post.class))\n            .thenReturn(false);\n\n        // Act & Assert\n        systemConfigFetcher.fetchPost()\n            .as(StepVerifier::create)\n            .assertNext(post -> {\n                assertThat(post.getPostPageSize()).isEqualTo(10);\n                assertThat(post.getArchivePageSize()).isEqualTo(20);\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void testFetchRouteRules() {\n        // Arrange\n        var configMap = new ConfigMap();\n        configMap.setData(Map.of(\n            \"routeRules\", \"\"\"\n                {\n                  \"post\": \"/articles/{slug}\",\n                  \"tags\": \"/labels\"\n                }\"\"\"\n        ));\n        systemConfigFetcher.getConfigMapCache().set(configMap.getData());\n\n        when(conversionService.canConvert(String.class, SystemSetting.ThemeRouteRules.class))\n            .thenReturn(false);\n\n        // Act & Assert\n        systemConfigFetcher.fetchRouteRules()\n            .as(StepVerifier::create)\n            .assertNext(rules -> {\n                assertThat(rules.getPost()).isEqualTo(\"/articles/{slug}\");\n                assertThat(rules.getTags()).isEqualTo(\"/labels\");\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void testGetConfig() {\n        // Arrange\n        var configData = mockConfigMap.getData();\n        systemConfigFetcher.getConfigMapCache().set(configData);\n\n        // Act & Assert\n        systemConfigFetcher.getConfig()\n            .as(StepVerifier::create)\n            .expectNext(java.util.Objects.requireNonNull(configData))\n            .verifyComplete();\n    }\n\n    @Test\n    void testGetConfigMap() {\n        // Arrange\n        when(client.fetch(ConfigMap.class, \"system\"))\n            .thenReturn(Mono.just(mockConfigMap));\n\n        // Act & Assert\n        systemConfigFetcher.getConfigMap()\n            .as(StepVerifier::create)\n            .expectNext(mockConfigMap)\n            .verifyComplete();\n    }\n\n    @Test\n    void testGetConfigMapBlocking() {\n        // Arrange\n        when(client.fetch(ConfigMap.class, \"system\"))\n            .thenReturn(Mono.just(mockConfigMap));\n\n        // Act\n        Optional<ConfigMap> result = systemConfigFetcher.getConfigMapBlocking();\n\n        // Assert\n        assertThat(result).isPresent();\n        assertThat(result.get()).isEqualTo(mockConfigMap);\n    }\n\n    @Test\n    void testGetConfigMapBlockingWhenNotFound() {\n        // Arrange\n        when(client.fetch(ConfigMap.class, \"system\"))\n            .thenReturn(Mono.empty());\n\n        // Act\n        Optional<ConfigMap> result = systemConfigFetcher.getConfigMapBlocking();\n\n        // Assert\n        assertThat(result).isEmpty();\n    }\n\n    @Test\n    void testOnApplicationEvent() {\n        // Arrange\n        var oldData = new HashMap<String, String>();\n        var newDataMap = new HashMap<String, String>();\n        newDataMap.put(\"key1\", \"value1\");\n        newDataMap.put(\"key2\", \"value2\");\n        var event = new SystemConfigChangedEvent(this, oldData, newDataMap);\n\n        // Act\n        systemConfigFetcher.onApplicationEvent(event);\n\n        // Assert\n        assertThat(systemConfigFetcher.getConfigMapCache().get()).isEqualTo(newDataMap);\n    }\n\n    @Test\n    void testCacheInvalidation() {\n        // Arrange\n        var initialDataMap = new HashMap<String, String>();\n        initialDataMap.put(\"key1\", \"value1\");\n        systemConfigFetcher.getConfigMapCache().set(initialDataMap);\n\n        // Act & Assert - Verify cache is updated when new data differs\n        var newDataMap = new HashMap<String, String>();\n        newDataMap.put(\"key1\", \"newValue\");\n        var event = new SystemConfigChangedEvent(this, initialDataMap, newDataMap);\n        systemConfigFetcher.onApplicationEvent(event);\n\n        assertThat(systemConfigFetcher.getConfigMapCache().get()).isEqualTo(newDataMap);\n    }\n\n    @Test\n    void shouldGetConfigMapFromDatabaseIfNoCache() {\n        // Arrange\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))\n            .thenReturn(Mono.just(mockConfigMap));\n        when(client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT))\n            .thenReturn(Mono.empty());\n\n        assertNotNull(mockConfigMap.getData());\n        systemConfigFetcher.getConfigMapMono()\n            .as(StepVerifier::create)\n            .expectNext(mockConfigMap.getData())\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetConfigMapFromCacheIfPresent() {\n        // Arrange\n        var cachedData = Map.of(\n            \"cachedKey\", \"\"\"\n                {\n                  \"key1\": \"value1\"\n                }\"\"\"\n        );\n        systemConfigFetcher.getConfigMapCache().set(cachedData);\n\n        // Act & Assert\n        systemConfigFetcher.getConfigMapMono()\n            .as(StepVerifier::create)\n            .expectNext(cachedData)\n            .verifyComplete();\n\n        verify(client, never()).fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.util.Properties;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.boot.info.BuildProperties;\n\n/**\n * Tests for {@link DefaultSystemVersionSupplier}.\n *\n * @author guqing\n * @since 2.0.0\n */\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultSystemVersionSupplierTest {\n\n    @InjectMocks\n    private DefaultSystemVersionSupplier systemVersionSupplier;\n\n    @Mock\n    ObjectProvider<BuildProperties> buildPropertiesProvider;\n\n    @Test\n    void getWhenBuildPropertiesNotSet() {\n        Version version = systemVersionSupplier.get();\n        assertThat(version.toString()).isEqualTo(\"0.0.0\");\n    }\n\n    @Test\n    void getWhenBuildPropertiesButVersionIsNull() {\n        Properties properties = new Properties();\n        BuildProperties buildProperties = new BuildProperties(properties);\n        when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);\n\n        Version version = systemVersionSupplier.get();\n        assertThat(version.toString()).isEqualTo(\"0.0.0\");\n    }\n\n    @Test\n    void getWhenBuildPropertiesAndVersionNotEmpty() {\n        Properties properties = new Properties();\n        properties.put(\"version\", \"2.0.0\");\n        BuildProperties buildProperties = new BuildProperties(properties);\n        when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);\n\n        Version version = systemVersionSupplier.get();\n        assertThat(version.toString()).isEqualTo(\"2.0.0\");\n\n        properties.put(\"version\", \"2.0.0-SNAPSHOT\");\n        buildProperties = new BuildProperties(properties);\n        when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);\n        version = systemVersionSupplier.get();\n        assertThat(version.toString()).isEqualTo(\"2.0.0-SNAPSHOT\");\n        assertThat(version.preReleaseVersion().orElseThrow()).isEqualTo(\"SNAPSHOT\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/ExtensionResourceInitializerTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.boot.context.event.ApplicationStartedEvent;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.util.FileSystemUtils;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link ExtensionResourceInitializer}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ExtensionResourceInitializerTest {\n\n    @Mock\n    ReactiveExtensionClient extensionClient;\n    @Mock\n    HaloProperties haloProperties;\n    @Mock\n    ApplicationStartedEvent applicationStartedEvent;\n\n    @Mock\n    ApplicationEventPublisher eventPublisher;\n\n    @InjectMocks\n    ExtensionResourceInitializer extensionResourceInitializer;\n\n    List<Path> dirsToClean;\n\n    @BeforeEach\n    void setUp() throws IOException {\n        dirsToClean = new ArrayList<>(2);\n\n        Path tempDirectory = Files.createTempDirectory(\"extension-resource-initializer-test\");\n        dirsToClean.add(tempDirectory);\n        Path multiDirectory =\n            Files.createDirectories(tempDirectory.resolve(\"a\").resolve(\"b\").resolve(\"c\"));\n        Files.writeString(tempDirectory.resolve(\"hello.yml\"), \"\"\"\n                kind: FakeExtension\n                apiVersion: v1\n                metadata:\n                  name: fake-extension\n                spec:\n                  hello: world\n                \"\"\",\n            StandardCharsets.UTF_8);\n\n        Files.writeString(multiDirectory.getParent().resolve(\"fake-1.txt\"), \"\"\"\n                kind: FakeExtension\n                name: fake-extension\n                \"\"\",\n            StandardCharsets.UTF_8);\n        Files.writeString(multiDirectory.resolve(\"fake.yaml\"), \"\"\"\n                kind: FakeExtension\n                apiVersion: v1\n                metadata:\n                  name: fake-extension\n                spec:\n                  hello: world\n                \"\"\",\n            StandardCharsets.UTF_8);\n\n        // test file in directory\n        Path secondTempDir = Files.createTempDirectory(\"extension-resource-file-test\");\n        dirsToClean.add(secondTempDir);\n        Path filePath = secondTempDir.resolve(\"good.yml\");\n        Files.writeString(filePath, \"\"\"\n                kind: FakeExtension\n                apiVersion: v1\n                metadata:\n                  name: config-file-is-ok\n                spec:\n                  key: value\n                \"\"\",\n            StandardCharsets.UTF_8);\n\n        when(haloProperties.getInitialExtensionLocations())\n            .thenReturn(Set.of(\"file:\" + tempDirectory + \"/**/*.yaml\",\n                \"file:\" + tempDirectory + \"/**/*.yml\",\n                \"file:\" + filePath));\n    }\n\n    @AfterEach\n    void cleanUp() throws IOException {\n        if (dirsToClean != null) {\n            for (var dir : dirsToClean) {\n                FileSystemUtils.deleteRecursively(dir);\n            }\n        }\n    }\n\n    @Test\n    void shouldStartCorrectly() throws Exception {\n        when(haloProperties.isRequiredExtensionDisabled()).thenReturn(true);\n        var argumentCaptor = ArgumentCaptor.forClass(Unstructured.class);\n\n        when(extensionClient.fetch(any(GroupVersionKind.class), any()))\n            .thenReturn(Mono.empty());\n        when(extensionClient.create(any())).thenReturn(Mono.empty());\n\n        extensionResourceInitializer.start();\n\n        verify(extensionClient, times(3)).create(argumentCaptor.capture());\n\n        List<Unstructured> values = argumentCaptor.getAllValues();\n        assertThat(values).isNotNull();\n        assertThat(values).hasSize(3);\n        JSONAssert.assertEquals(\"\"\"\n            [\n                 {\n                     \"kind\": \"FakeExtension\",\n                     \"apiVersion\": \"v1\",\n                     \"metadata\": {\n                         \"name\": \"config-file-is-ok\"\n                     },\n                     \"spec\": {\n                         \"key\": \"value\"\n                     }\n                 },\n                 {\n                     \"kind\": \"FakeExtension\",\n                     \"apiVersion\": \"v1\",\n                     \"metadata\": {\n                         \"name\": \"fake-extension\"\n                     },\n                     \"spec\": {\n                         \"hello\": \"world\"\n                     }\n                 },\n                 {\n                     \"kind\": \"FakeExtension\",\n                     \"apiVersion\": \"v1\",\n                     \"metadata\": {\n                         \"name\": \"fake-extension\"\n                     },\n                     \"spec\": {\n                         \"hello\": \"world\"\n                     }\n                 }\n            ]\n            \"\"\", JsonUtils.objectToJson(values), false);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/InitializationStateGetterTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Tests for {@link InitializationStateGetter}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass InitializationStateGetterTest {\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private DefaultInitializationStateGetter initializationStateGetter;\n\n    @Test\n    void userInitialized() {\n        when(client.listBy(eq(User.class), any(), any(PageRequest.class)))\n            .thenReturn(Mono.empty());\n        initializationStateGetter.userInitialized()\n            .as(StepVerifier::create)\n            .expectNext(false)\n            .verifyComplete();\n\n        User user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(\"fake-hidden-user\");\n        user.getMetadata().setLabels(Map.of(\"halo.run/hidden-user\", \"true\"));\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setDisplayName(\"fake-hidden-user\");\n        ListResult<User> listResult = new ListResult<>(List.of(user));\n\n        when(client.listBy(eq(User.class), any(), any(PageRequest.class)))\n            .thenReturn(Mono.just(listResult));\n        initializationStateGetter.userInitialized()\n            .as(StepVerifier::create)\n            .expectNext(true)\n            .verifyComplete();\n    }\n\n    @Test\n    void dataInitialized() {\n        ConfigMap configMap = new ConfigMap();\n        configMap.setMetadata(new Metadata());\n        configMap.getMetadata().setName(SystemState.SYSTEM_STATES_CONFIGMAP);\n        configMap.setData(Map.of(\"states\", \"{\\\"isSetup\\\":true}\"));\n        when(client.fetch(eq(ConfigMap.class), eq(SystemState.SYSTEM_STATES_CONFIGMAP)))\n            .thenReturn(Mono.just(configMap));\n        initializationStateGetter.dataInitialized()\n            .as(StepVerifier::create)\n            .expectNext(true)\n            .verifyComplete();\n\n        // call again\n        initializationStateGetter.dataInitialized()\n            .as(StepVerifier::create)\n            .expectNext(true)\n            .verifyComplete();\n        // execute only once\n        verify(client).fetch(eq(ConfigMap.class), eq(SystemState.SYSTEM_STATES_CONFIGMAP));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/ReactiveExtensionPaginatedOperatorImplTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.FakeExtension;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n@ExtendWith(MockitoExtension.class)\nclass ReactiveExtensionPaginatedOperatorImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private ReactiveExtensionPaginatedOperatorImpl service;\n\n    @Nested\n    class ListTest {\n\n        @BeforeEach\n        void setUp() {\n            Instant now = Instant.now();\n            var items = new ArrayList<>();\n            // Generate 900 items\n            for (int j = 0; j < 9; j++) {\n                items.addAll(generateItems(100, now));\n            }\n            // mock new items during the process\n            Instant otherNow = now.plusSeconds(1000);\n            items.addAll(generateItems(90, otherNow));\n\n            when(client.listBy(any(), any(), any())).thenAnswer(invocation -> {\n                PageRequest pageRequest = invocation.getArgument(2);\n                int pageNumber = pageRequest.getPageNumber();\n                var list = ListResult.subList(items, pageNumber, pageRequest.getPageSize());\n                var result = new ListResult<>(pageNumber, pageRequest.getPageSize(),\n                    items.size(), list);\n                return Mono.just(result);\n            });\n        }\n\n        @Test\n        public void listTest() {\n            StepVerifier.create(service.list(FakeExtension.class, new ListOptions()))\n                .expectNextCount(900)\n                .verifyComplete();\n        }\n    }\n\n    @Test\n    void nextPageTest() {\n        var result = new ListResult<FakeExtension>(1, 10, 30, List.of());\n        var sort = Sort.by(\"metadata.creationTimestamp\");\n        var nextPage = ReactiveExtensionPaginatedOperatorImpl.nextPage(result, sort);\n        assertThat(nextPage.getPageNumber()).isEqualTo(2);\n        assertThat(nextPage.getPageSize()).isEqualTo(10);\n        assertThat(nextPage.getSort()).isEqualTo(sort);\n    }\n\n    @Test\n    void shouldTakeNextTest() {\n        var now = Instant.now();\n        var item = new FakeExtension();\n        item.setMetadata(new Metadata());\n        item.getMetadata().setCreationTimestamp(now);\n        var result = ReactiveExtensionPaginatedOperatorImpl.shouldTakeNext(item, now);\n        assertThat(result).isTrue();\n\n        item.getMetadata().setCreationTimestamp(now.minusSeconds(1));\n        result = ReactiveExtensionPaginatedOperatorImpl.shouldTakeNext(item, now);\n        assertThat(result).isTrue();\n\n        item.getMetadata().setCreationTimestamp(now.plusSeconds(1));\n        result = ReactiveExtensionPaginatedOperatorImpl.shouldTakeNext(item, now);\n        assertThat(result).isFalse();\n    }\n\n    private List<FakeExtension> generateItems(int count, Instant creationTimestamp) {\n        List<FakeExtension> items = new ArrayList<>();\n        for (int i = 0; i < count; i++) {\n            var item = new FakeExtension();\n            item.setMetadata(new Metadata());\n            item.getMetadata().setCreationTimestamp(creationTimestamp);\n            items.add(item);\n        }\n        return items;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplierTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.boot.webflux.autoconfigure.WebFluxProperties;\nimport org.springframework.http.HttpRequest;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@ExtendWith(MockitoExtension.class)\nclass SystemConfigFirstExternalUrlSupplierTest {\n\n    @Mock\n    HaloProperties haloProperties;\n\n    @Mock\n    WebFluxProperties webFluxProperties;\n\n    @Mock\n    SystemConfigFetcher systemConfigFetcher;\n\n    @InjectMocks\n    SystemConfigFirstExternalUrlSupplier externalUrl;\n\n    @Nested\n    class HaloPropertiesSupplier {\n\n        @BeforeEach\n        void setUp() {\n            when(systemConfigFetcher.getBasic()).thenReturn(Mono.empty());\n            externalUrl.onExtensionInitialized(null);\n        }\n\n        @Test\n        void getURIWhenUsingAbsolutePermalink() throws MalformedURLException {\n            var fakeUri = URI.create(\"https://halo.run/fake\");\n            when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());\n            when(haloProperties.isUseAbsolutePermalink()).thenReturn(true);\n\n            assertEquals(fakeUri, externalUrl.get());\n        }\n\n        @Test\n        void getURIWhenBasePathSetAndNotUsingAbsolutePermalink() {\n            when(webFluxProperties.getBasePath()).thenReturn(\"/blog\");\n            when(haloProperties.isUseAbsolutePermalink()).thenReturn(false);\n\n            assertEquals(URI.create(\"/blog\"), externalUrl.get());\n        }\n\n        @Test\n        void getURIWhenBasePathSetAndUsingAbsolutePermalink() throws MalformedURLException {\n            var fakeUri = URI.create(\"https://halo.run/fake\");\n            when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());\n            lenient().when(webFluxProperties.getBasePath()).thenReturn(\"/blog\");\n            when(haloProperties.isUseAbsolutePermalink()).thenReturn(true);\n\n            assertEquals(URI.create(\"https://halo.run/fake\"), externalUrl.get());\n        }\n\n\n        @Test\n        void getURIWhenUsingRelativePermalink() throws MalformedURLException {\n            when(haloProperties.isUseAbsolutePermalink()).thenReturn(false);\n\n            assertEquals(URI.create(\"/\"), externalUrl.get());\n        }\n\n        @Test\n        void getURLWhenExternalURLProvided() throws MalformedURLException {\n            var fakeUri = URI.create(\"https://halo.run/fake\");\n            when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());\n            var mockRequest = mock(HttpRequest.class);\n            var url = externalUrl.getURL(mockRequest);\n            assertEquals(fakeUri.toURL(), url);\n        }\n\n        @Test\n        void getURLWhenExternalURLAbsent() throws MalformedURLException {\n            var fakeUri = URI.create(\"https://localhost/fake\");\n            when(haloProperties.getExternalUrl()).thenReturn(null);\n            var mockRequest = mock(HttpRequest.class);\n            when(mockRequest.getURI()).thenReturn(fakeUri);\n            var url = externalUrl.getURL(mockRequest);\n            assertEquals(new URL(\"https://localhost/\"), url);\n        }\n\n        @Test\n        void getURLWhenBasePathSetAndExternalURLProvided() throws MalformedURLException {\n            var fakeUri = URI.create(\"https://localhost/fake\");\n            when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());\n            lenient().when(webFluxProperties.getBasePath()).thenReturn(\"/blog\");\n            var mockRequest = mock(HttpRequest.class);\n            lenient().when(mockRequest.getURI()).thenReturn(fakeUri);\n            var url = externalUrl.getURL(mockRequest);\n            assertEquals(new URL(\"https://localhost/fake\"), url);\n        }\n\n        @Test\n        void getURLWhenBasePathSetAndExternalURLAbsent() throws MalformedURLException {\n            var fakeUri = URI.create(\"https://localhost/fake\");\n            when(haloProperties.getExternalUrl()).thenReturn(null);\n            when(webFluxProperties.getBasePath()).thenReturn(\"/blog\");\n            var mockRequest = mock(HttpRequest.class);\n            when(mockRequest.getURI()).thenReturn(fakeUri);\n            var url = externalUrl.getURL(mockRequest);\n            assertEquals(new URL(\"https://localhost/blog\"), url);\n        }\n\n        @Test\n        void getRaw() throws MalformedURLException {\n            var fakeUri = URI.create(\"http://localhost/fake\");\n            when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL());\n            assertEquals(fakeUri.toURL(), externalUrl.getRaw());\n\n            when(haloProperties.getExternalUrl()).thenReturn(null);\n            assertNull(externalUrl.getRaw());\n        }\n\n    }\n\n    @Nested\n    class SystemConfigSupplier {\n\n        @Test\n        void shouldGetUrlWhenUseAbsolutePermalink() throws Exception {\n            var basic = new SystemSetting.Basic();\n            basic.setExternalUrl(\"https://www.halo.run\");\n            when(systemConfigFetcher.getBasic()).thenReturn(Mono.just(basic));\n            when(haloProperties.isUseAbsolutePermalink()).thenReturn(true);\n            externalUrl.onExtensionInitialized(null);\n            assertEquals(URI.create(\"https://www.halo.run\").toURL(), externalUrl.getRaw());\n            assertEquals(URI.create(\"https://www.halo.run\"), externalUrl.get());\n\n            var mockRequest = mock(HttpRequest.class);\n            assertEquals(URI.create(\"https://www.halo.run\").toURL(),\n                externalUrl.getURL(mockRequest));\n        }\n\n        @Test\n        void shouldGetUrlWhenNotUsingAbsolutePermalink() throws MalformedURLException {\n            var basic = new SystemSetting.Basic();\n            basic.setExternalUrl(\"https://www.halo.run\");\n            when(systemConfigFetcher.getBasic()).thenReturn(Mono.just(basic));\n            when(haloProperties.isUseAbsolutePermalink()).thenReturn(false);\n            when(webFluxProperties.getBasePath()).thenReturn(\"/fake\");\n            externalUrl.onExtensionInitialized(null);\n\n            assertEquals(URI.create(\"https://www.halo.run\").toURL(), externalUrl.getRaw());\n            assertEquals(URI.create(\"/fake\"), externalUrl.get());\n            var mockRequest = mock(HttpRequest.class);\n            assertEquals(URI.create(\"https://www.halo.run\").toURL(),\n                externalUrl.getURL(mockRequest));\n        }\n\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/SystemSettingTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.HashMap;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.infra.SystemSetting.Comment;\nimport run.halo.app.infra.SystemSetting.ExtensionPointEnabled;\nimport run.halo.app.infra.utils.JsonUtils;\n\nclass SystemSettingTest {\n\n    @Nested\n    class ExtensionPointEnabledTest {\n\n        @Test\n        void deserializeTest() {\n            var json = \"\"\"\n                    {\n                      \"run.halo.app.search.post.PostSearchService\": [\n                        \"run.halo.app.search.post.LucenePostSearchService\"\n                      ]\n                    }\n                \"\"\";\n\n            var enabled = JsonUtils.jsonToObject(json, ExtensionPointEnabled.class);\n            assertTrue(enabled.containsKey(\"run.halo.app.search.post.PostSearchService\"));\n        }\n    }\n\n    @Test\n    void shouldGetConfigFromJson() {\n        var configMap = new ConfigMap();\n        configMap.putDataItem(\"comment\", \"\"\"\n            {\"enable\": true}\n            \"\"\");\n        var comment = SystemSetting.get(configMap.getData(), Comment.GROUP, Comment.class);\n        assertTrue(comment.getEnable());\n    }\n\n    @Test\n    void shouldGetNullIfKeyNotExist() {\n        var configMap = new ConfigMap();\n        configMap.setData(new HashMap<>());\n        String fake = SystemSetting.get(configMap.getData(), \"fake-key\", String.class);\n        assertNull(fake);\n    }\n\n    @Test\n    void shouldGetConfigViaConversionService() {\n        var configMap = new ConfigMap();\n        configMap.putDataItem(\"int\", \"100\");\n        var integer = SystemSetting.get(configMap.getData(), \"int\", Integer.class);\n        assertEquals(100, integer);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/SystemStateTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.ConfigMap;\n\n/**\n * Tests for {@link SystemState}.\n *\n * @author guqing\n * @since 2.8.0\n */\nclass SystemStateTest {\n\n    @Test\n    void deserialize() {\n        ConfigMap configMap = new ConfigMap();\n        SystemState systemState = SystemState.deserialize(configMap);\n        assertThat(systemState).isNotNull();\n\n        configMap.setData(Map.of(SystemState.GROUP, \"{\\\"isSetup\\\":true}\"));\n        systemState = SystemState.deserialize(configMap);\n        assertThat(systemState.getIsSetup()).isTrue();\n    }\n\n    @Test\n    void update() {\n        SystemState newSystemState = new SystemState();\n        newSystemState.setIsSetup(true);\n\n        ConfigMap configMap = new ConfigMap();\n        SystemState.update(newSystemState, configMap);\n        assertThat(configMap.getData().get(SystemState.GROUP)).isEqualTo(\"{\\\"isSetup\\\":true}\");\n\n        var data = new LinkedHashMap<String, String>();\n        configMap.setData(data);\n        data.put(SystemState.GROUP, \"{\\\"isSetup\\\":false}\");\n        SystemState.update(newSystemState, configMap);\n        assertThat(configMap.getData().get(SystemState.GROUP)).isEqualTo(\"{\\\"isSetup\\\":true}\");\n\n        data.clear();\n        data.put(SystemState.GROUP, \"{\\\"isSetup\\\":true, \\\"foo\\\":\\\"bar\\\"}\");\n        newSystemState.setIsSetup(false);\n        SystemState.update(newSystemState, configMap);\n        assertThat(configMap.getData().get(SystemState.GROUP))\n            .isEqualTo(\"{\\\"isSetup\\\":false,\\\"foo\\\":\\\"bar\\\"}\");\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/ValidationUtilsTest.java",
    "content": "package run.halo.app.infra;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link ValidationUtils}.\n *\n * @author guqing\n * @since 2.5.0\n */\nclass ValidationUtilsTest {\n\n    @Nested\n    class NameValidationTest {\n        @Test\n        void nullName() {\n            assertThat(validateName(null)).isFalse();\n        }\n\n        @Test\n        void emptyUsername() {\n            assertThat(validateName(\"\")).isFalse();\n        }\n\n        @Test\n        void startWithIllegalCharacter() {\n            assertThat(validateName(\"-abc\")).isFalse();\n        }\n\n        @Test\n        void endWithIllegalCharacter() {\n            assertThat(validateName(\"abc-\")).isFalse();\n            assertThat(validateName(\"abcD\")).isFalse();\n        }\n\n        @Test\n        void middleWithIllegalCharacter() {\n            assertThat(validateName(\"ab?c\")).isFalse();\n        }\n\n        @Test\n        void moreThan63Characters() {\n            assertThat(validateName(StringUtils.repeat('a', 64))).isFalse();\n        }\n\n        @Test\n        void correctUsername() {\n            assertThat(validateName(\"abc\")).isTrue();\n            assertThat(validateName(\"ab-c\")).isTrue();\n            assertThat(validateName(\"1st\")).isTrue();\n            assertThat(validateName(\"ast1\")).isTrue();\n            assertThat(validateName(\"ast-1\")).isTrue();\n        }\n\n        static boolean validateName(String name) {\n            if (StringUtils.isBlank(name)) {\n                return false;\n            }\n            boolean matches = ValidationUtils.NAME_PATTERN.matcher(name).matches();\n            return matches && name.length() <= 63;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/config/SessionConfigurationTest.java",
    "content": "package run.halo.app.infra.config;\n\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.UnsatisfiedDependencyException;\nimport org.springframework.boot.session.autoconfigure.SessionProperties;\nimport org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;\nimport org.springframework.boot.web.server.autoconfigure.ServerProperties;\nimport org.springframework.session.ReactiveFindByIndexNameSessionRepository;\nimport run.halo.app.security.session.ReactiveIndexedSessionRepository;\n\nclass SessionConfigurationTest {\n\n    @Test\n    void shouldLoadContextIfNoStoreTypeProvided() {\n        var contextRunner = new ReactiveWebApplicationContextRunner()\n            .withUserConfiguration(SessionConfiguration.class)\n            .withBean(SessionProperties.class)\n            .withBean(ServerProperties.class);\n        contextRunner.run(context -> {\n            assertNull(context.getStartupFailure());\n            assertTrue(context.isActive());\n            assertInstanceOf(\n                ReactiveIndexedSessionRepository.class,\n                context.getBean(ReactiveIndexedSessionRepository.class)\n            );\n            assertInstanceOf(\n                ReactiveIndexedSessionRepository.class,\n                context.getBean(ReactiveFindByIndexNameSessionRepository.class)\n            );\n        });\n    }\n\n    @Test\n    void shouldLoadContextIfStoreTypeIsInMemory() {\n        var contextRunner = new ReactiveWebApplicationContextRunner()\n            .withUserConfiguration(SessionConfiguration.class)\n            .withBean(SessionProperties.class)\n            .withBean(ServerProperties.class)\n            .withPropertyValues(\"halo.session.store-type=in-memory\");\n        contextRunner.run(context -> {\n            assertNull(context.getStartupFailure());\n            assertTrue(context.isActive());\n            assertInstanceOf(\n                ReactiveIndexedSessionRepository.class,\n                context.getBean(ReactiveIndexedSessionRepository.class)\n            );\n            assertInstanceOf(\n                ReactiveIndexedSessionRepository.class,\n                context.getBean(ReactiveFindByIndexNameSessionRepository.class)\n            );\n        });\n    }\n\n    @Test\n    void shouldFailToLoadContextIfStoreTypeIsInvalid() {\n        var contextRunner = new ReactiveWebApplicationContextRunner()\n            .withUserConfiguration(SessionConfiguration.class)\n            .withBean(SessionProperties.class)\n            .withBean(ServerProperties.class)\n            .withPropertyValues(\"halo.session.store-type=invalid-type\");\n        contextRunner.run(context ->\n            assertInstanceOf(UnsatisfiedDependencyException.class, context.getStartupFailure())\n        );\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/exception/handlers/I18nExceptionTest.java",
    "content": "package run.halo.app.infra.exception.handlers;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;\n\nimport java.util.Locale;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.TestConfiguration;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.dao.ConcurrencyFailureException;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ProblemDetail;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PutMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.ResponseStatus;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ResponseStatusException;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass I18nExceptionTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    Locale currentLocale;\n\n    @BeforeEach\n    void setUp() {\n        currentLocale = Locale.getDefault();\n        Locale.setDefault(Locale.ENGLISH);\n    }\n\n    @AfterEach\n    void tearDown() {\n        Locale.setDefault(currentLocale);\n    }\n\n    @Test\n    void shouldBeOkForGreetingEndpoint() {\n        webClient.get().uri(\"/response-entity/greet\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(String.class).isEqualTo(\"Hello Halo\");\n    }\n\n    @Test\n    void shouldGetErrorIfErrorResponseThrow() {\n        webClient.get().uri(\"/response-entity/error-response\")\n            .exchange()\n            .expectStatus().isBadRequest()\n            .expectBody(ProblemDetail.class)\n            .value(problemDetail -> {\n                assertEquals(\"Error Response\", problemDetail.getTitle());\n                assertEquals(\"Message argument is {0}.\", problemDetail.getDetail());\n            });\n    }\n\n\n    @Test\n    void shouldGetErrorIfErrorResponseThrowWithMessageCode() {\n        webClient.get().uri(\"/response-entity/error-response/with-message-code\")\n            .exchange()\n            .expectStatus().isBadRequest()\n            .expectBody(ProblemDetail.class)\n            .value(problemDetail -> {\n                assertEquals(\"Error Response\", problemDetail.getTitle());\n                assertEquals(\"Something went wrong, argument is fake-arg.\",\n                    problemDetail.getDetail());\n            });\n    }\n\n    @Test\n    void shouldGetErrorIfErrorResponseThrowWithMessageCodeAndLocaleIsChinese() {\n        webClient.get().uri(\"/response-entity/error-response/with-message-code\")\n            .header(HttpHeaders.ACCEPT_LANGUAGE, \"zh-CN,zh\")\n            .exchange()\n            .expectStatus().isBadRequest()\n            .expectBody(ProblemDetail.class)\n            .value(problemDetail -> {\n                assertEquals(\"发生错误\", problemDetail.getTitle());\n                assertEquals(\"发生了一些错误，参数：fake-arg。\",\n                    problemDetail.getDetail());\n            });\n\n    }\n\n    @Test\n    void shouldGetErrorIfThrowingResponseStatusException() {\n        webClient.get().uri(\"/response-entity/with-response-status-error\")\n            .exchange()\n            .expectStatus().isEqualTo(HttpStatus.GONE)\n            .expectBody(ProblemDetail.class)\n            .value(problemDetail -> {\n                assertEquals(\"Gone\", problemDetail.getTitle());\n                assertEquals(\"Something went wrong\",\n                    problemDetail.getDetail());\n            });\n    }\n\n    @Test\n    void shouldGetErrorIfThrowingGeneralException() {\n        // problem reason will be a fixed prompt when internal server error occurred.\n        webClient.get().uri(\"/response-entity/general-error\")\n            .exchange()\n            .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)\n            .expectBody(ProblemDetail.class)\n            .value(problemDetail -> {\n                assertEquals(\"Internal Server Error\", problemDetail.getTitle());\n                assertEquals(\"Something went wrong, please try again later.\",\n                    problemDetail.getDetail());\n            });\n    }\n\n    @Test\n    void shouldGetConflictError() {\n        webClient.mutate().apply(csrf()).build()\n            .put().uri(\"/response-entity/conflict-error\")\n            .exchange()\n            .expectStatus().isEqualTo(HttpStatus.CONFLICT)\n            .expectBody(ProblemDetail.class)\n            .value(problemDetail -> {\n                assertEquals(\"Conflict\", problemDetail.getTitle());\n                assertEquals(\"Conflict detected.\",\n                    problemDetail.getDetail());\n            });\n    }\n\n    @TestConfiguration\n    static class TestConfig {\n\n        @RestController\n        @RequestMapping(\"/response-entity\")\n        static class ResponseEntityController {\n\n            @GetMapping(\"/greet\")\n            ResponseEntity<String> greet() {\n                return ResponseEntity.ok(\"Hello Halo\");\n            }\n\n            @GetMapping(\"/error-response\")\n            ResponseEntity<String> throwErrorResponseException() {\n                throw new ErrorResponseException();\n            }\n\n            @GetMapping(\"/error-response/with-message-args\")\n            ResponseEntity<String> throwErrorResponseExceptionWithMessageArgs() {\n                throw new ErrorResponseException(\"Something went wrong.\",\n                    null, new Object[] {\"fake-arg\"});\n            }\n\n            @GetMapping(\"/error-response/with-message-code\")\n            ResponseEntity<String> throwErrorResponseExceptionWithMessageCode() {\n                throw new ErrorResponseException(\"Something went wrong.\",\n                    \"error.somethingWentWrong\", new Object[] {\"fake-arg\"});\n            }\n\n            @GetMapping(\"/with-response-status-error\")\n            ResponseEntity<String> throwWithResponseStatusException() {\n                throw new WithResponseStatusException();\n            }\n\n            @GetMapping(\"/general-error\")\n            ResponseEntity<String> throwGeneralException() {\n                throw new GeneralException(\"Something went wrong\");\n            }\n\n            @PutMapping(\"/conflict-error\")\n            ResponseEntity<String> throwConflictException() {\n                throw new ConcurrencyFailureException(\"Conflict detected\");\n            }\n        }\n    }\n\n    static class ErrorResponseException extends ResponseStatusException {\n\n        public ErrorResponseException() {\n            this(\"Something went wrong.\");\n        }\n\n        public ErrorResponseException(String reason) {\n            this(reason, null, null);\n        }\n\n        public ErrorResponseException(String reason, String detailCode, Object[] detailArgs) {\n            super(HttpStatus.BAD_REQUEST, reason, null, detailCode, detailArgs);\n        }\n    }\n\n    @ResponseStatus(value = HttpStatus.GONE, reason = \"Something went wrong\")\n    static class WithResponseStatusException extends RuntimeException {\n\n    }\n\n    static class GeneralException extends RuntimeException {\n\n        public GeneralException(String message) {\n            super(message);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/properties/HaloPropertiesTest.java",
    "content": "package run.halo.app.infra.properties;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.springframework.validation.SimpleErrors;\n\nclass HaloPropertiesTest {\n\n    static Stream<Arguments> validateTest() throws MalformedURLException {\n        return Stream.of(\n            Arguments.of(true, new URL(\"http://localhost:8080\"), true),\n            Arguments.of(false, new URL(\"http://localhost:8080\"), true),\n            Arguments.of(true, new URL(\"https://localhost:8080\"), true),\n            Arguments.of(false, new URL(\"https://localhost:8080\"), true),\n            Arguments.of(true, new URL(\"ftp://localhost:8080\"), false),\n            Arguments.of(false, new URL(\"ftp://localhost:8080\"), false),\n            Arguments.of(true, new URL(\"http:www/halo/run\"), false),\n            Arguments.of(false, new URL(\"http:www/halo.run\"), false),\n            Arguments.of(true, new URL(\"https:www/halo/run\"), false),\n            Arguments.of(false, new URL(\"https:www/halo/run\"), false),\n            Arguments.of(true, new URL(\"https:///path\"), false),\n            Arguments.of(false, new URL(\"https:///path\"), false),\n            Arguments.of(true, new URL(\"http:///path\"), false),\n            Arguments.of(false, new URL(\"http:///path\"), false),\n            Arguments.of(true, null, false),\n            Arguments.of(false, null, true)\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource\n    void validateTest(boolean useAbsolutePermalink, URL externalUrl, boolean valid) {\n        var properties = new HaloProperties();\n        properties.setUseAbsolutePermalink(useAbsolutePermalink);\n        properties.setExternalUrl(externalUrl);\n        var errors = new SimpleErrors(properties);\n        properties.validate(properties, errors);\n        Assertions.assertEquals(valid, !errors.hasErrors());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/Base62UtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport io.seruco.encoding.base62.Base62;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link Base62}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass Base62UtilsTest {\n\n    @Test\n    void encode() {\n        getNaiveTestSet().forEach(\n            (str, encoded) -> assertThat(Base62Utils.encode(str)).isEqualTo(encoded));\n    }\n\n    @Test\n    void decodeToString() {\n        getNaiveTestSet().forEach(\n            (str, encoded) -> assertThat(Base62Utils.decodeToString(encoded)).isEqualTo(str));\n    }\n\n    public static Map<String, String> getNaiveTestSet() {\n        Map<String, String> testSet = new HashMap<>();\n\n        testSet.put(\"\", \"\");\n        testSet.put(\"a\", \"1Z\");\n        testSet.put(\"Hello\", \"5TP3P3v\");\n        testSet.put(\"Hello world!\", \"T8dgcjRGuYUueWht\");\n        testSet.put(\"Just a test\", \"7G0iTmJjQFG2t6K\");\n        testSet.put(\"!!!!!!!!!!!!!!!!!\", \"4A7f43EVXQoS6Am897ZKbAn\");\n        testSet.put(\"0123456789\", \"18XU2xYejWO9d3\");\n        testSet.put(\"The quick brown fox jumps over the lazy dog\",\n            \"83UM8dOjD4xrzASgmqLOXTgTagvV1jPegUJ39mcYnwHwTlzpdfKXvpp4RL\");\n        testSet.put(\"Sphinx of black quartz, judge my vow\",\n            \"1Ul5yQGNM8YFBp3sz19dYj1kTp95OW7jI8pTcTP5JhYjIaFmx\");\n\n        return testSet;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/FileNameUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static run.halo.app.infra.utils.FileNameUtils.randomFileName;\nimport static run.halo.app.infra.utils.FileNameUtils.removeFileExtension;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\nclass FileNameUtilsTest {\n\n    @Nested\n    class RemoveFileExtensionTest {\n\n        @Test\n        public void shouldNotRemoveExtIfNoExt() {\n            assertEquals(\"halo\", removeFileExtension(\"halo\", true));\n            assertEquals(\"halo\", removeFileExtension(\"halo\", false));\n        }\n\n        @Test\n        public void shouldRemoveExtIfHasOnlyOneExt() {\n            assertEquals(\"halo\", removeFileExtension(\"halo.run\", true));\n            assertEquals(\"halo\", removeFileExtension(\"halo.run\", false));\n        }\n\n        @Test\n        public void shouldNotRemoveExtIfDotfile() {\n            assertEquals(\".halo\", removeFileExtension(\".halo\", true));\n            assertEquals(\".halo\", removeFileExtension(\".halo\", false));\n        }\n\n        @Test\n        public void shouldRemoveExtIfDotfileHasOneExt() {\n            assertEquals(\".halo\", removeFileExtension(\".halo.run\", true));\n            assertEquals(\".halo\", removeFileExtension(\".halo.run\", false));\n        }\n\n        @Test\n        public void shouldRemoveExtIfHasTwoExt() {\n            assertEquals(\"halo\", removeFileExtension(\"halo.tar.gz\", true));\n            assertEquals(\"halo.tar\", removeFileExtension(\"halo.tar.gz\", false));\n        }\n\n        @Test\n        public void shouldRemoveExtIfDotfileHasTwoExt() {\n            assertEquals(\".halo\", removeFileExtension(\".halo.tar.gz\", true));\n            assertEquals(\".halo.tar\", removeFileExtension(\".halo.tar.gz\", false));\n        }\n\n        @Test\n        void shouldReturnNullIfFilenameIsNull() {\n            assertNull(removeFileExtension(null, true));\n            assertNull(removeFileExtension(null, false));\n        }\n    }\n\n    @Nested\n    class AppendRandomFileNameTest {\n        @Test\n        void normalFileName() {\n            String randomFileName = randomFileName(\"halo.run\", 3);\n            assertEquals(12, randomFileName.length());\n            assertTrue(randomFileName.startsWith(\"halo-\"));\n            assertTrue(randomFileName.endsWith(\".run\"));\n\n            randomFileName = randomFileName(\".run\", 3);\n            assertEquals(7, randomFileName.length());\n            assertTrue(randomFileName.endsWith(\".run\"));\n\n            randomFileName = randomFileName(\"halo\", 3);\n            assertEquals(8, randomFileName.length());\n            assertTrue(randomFileName.startsWith(\"halo-\"));\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/FileTypeDetectUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport org.apache.tika.mime.MimeTypeException;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.util.ResourceUtils;\n\n/**\n * Test for {@link FileTypeDetectUtils}.\n *\n * @author guqing\n * @since 2.18.0\n */\nclass FileTypeDetectUtilsTest {\n\n    @Test\n    void detectMimeTypeTest() throws IOException {\n        var file = ResourceUtils.getFile(\"classpath:app.key\");\n        String mimeType = FileTypeDetectUtils.detectMimeType(Files.newInputStream(file.toPath()));\n        assertThat(mimeType).isEqualTo(\"application/x-x509-key; format=pem\");\n\n        file = ResourceUtils.getFile(\"classpath:console/index.html\");\n        mimeType = FileTypeDetectUtils.detectMimeType(Files.newInputStream(file.toPath()));\n        assertThat(mimeType).isEqualTo(\"text/plain\");\n\n        file = ResourceUtils.getFile(\"classpath:themes/test-theme.zip\");\n        mimeType = FileTypeDetectUtils.detectMimeType(Files.newInputStream(file.toPath()));\n        assertThat(mimeType).isEqualTo(\"application/zip\");\n    }\n\n    @Test\n    void detectMimeTypeWithNameTest() throws IOException {\n        var stream = getFileInputStream(\"classpath:file-type-detect/index.js\");\n        String mimeType = FileTypeDetectUtils.detectMimeType(stream, \"index.js\");\n        assertThat(mimeType).isEqualTo(\"text/javascript\");\n\n        stream = getFileInputStream(\"classpath:file-type-detect/index.html\");\n        mimeType =\n            FileTypeDetectUtils.detectMimeType(stream, \"index.html\");\n        assertThat(mimeType).isEqualTo(\"text/html\");\n\n        stream = getFileInputStream(\"classpath:file-type-detect/test.json\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream, \"test.json\");\n        assertThat(mimeType).isEqualTo(\"application/json\");\n\n        stream = getFileInputStream(\"classpath:file-type-detect/other.xlsx\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream, \"other.xlsx\");\n        assertThat(mimeType).isEqualTo(\n            \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\");\n\n        // other.xlsx detect without name\n        stream = getFileInputStream(\"classpath:file-type-detect/other.xlsx\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream);\n        assertThat(mimeType).isEqualTo(\"application/zip\");\n\n        // other.xlsx detect with wrong name\n        stream = getFileInputStream(\"classpath:file-type-detect/other.xlsx\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream, \"other.txt\");\n        assertThat(mimeType).isEqualTo(\"application/zip\");\n\n        stream = getFileInputStream(\"classpath:file-type-detect/test.docx\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream, \"test.docx\");\n        assertThat(mimeType).isEqualTo(\n            \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\");\n\n        // docx detect without file name\n        stream = getFileInputStream(\"classpath:file-type-detect/test.docx\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream);\n        assertThat(mimeType).isEqualTo(\"application/zip\");\n\n        stream = getFileInputStream(\"classpath:file-type-detect/test.svg\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream, \"test.svg\");\n        assertThat(mimeType).isEqualTo(\"image/svg+xml\");\n\n        stream = getFileInputStream(\"classpath:file-type-detect/test.png\");\n        mimeType = FileTypeDetectUtils.detectMimeType(stream, \"test.png\");\n        assertThat(mimeType).isEqualTo(\"image/png\");\n    }\n\n    private static InputStream getFileInputStream(String location) throws IOException {\n        var file = ResourceUtils.getFile(location);\n        return Files.newInputStream(file.toPath());\n    }\n\n    @Test\n    void detectFileExtensionTest() throws MimeTypeException {\n        var ext = FileTypeDetectUtils.detectFileExtension(\"application/x-x509-key; format=pem\");\n        assertThat(ext).isEqualTo(\"\");\n\n        ext = FileTypeDetectUtils.detectFileExtension(\"text/plain\");\n        assertThat(ext).isEqualTo(\".txt\");\n\n        ext = FileTypeDetectUtils.detectFileExtension(\"application/zip\");\n        assertThat(ext).isEqualTo(\".zip\");\n\n        ext = FileTypeDetectUtils.detectFileExtension(\"image/bmp\");\n        assertThat(ext).isEqualTo(\".bmp\");\n    }\n\n    @Test\n    void detectFileExtensionsTest() throws MimeTypeException {\n        var extensions = FileTypeDetectUtils.detectFileExtensions(\n            \"application/x-x509-key; format=pem\"\n        );\n        assertThat(extensions).isEmpty();\n\n        extensions = FileTypeDetectUtils.detectFileExtensions(\"text/plain\");\n        assertThat(extensions).contains(\".text\");\n\n        extensions = FileTypeDetectUtils.detectFileExtensions(\"application/zip\");\n        assertThat(extensions).contains(\".zipx\");\n\n        extensions = FileTypeDetectUtils.detectFileExtensions(\"image/bmp\");\n        assertThat(extensions).contains(\".dib\");\n\n        extensions = FileTypeDetectUtils.detectFileExtensions(\"image/jpeg\");\n        assertThat(extensions).contains(\".jpeg\");\n    }\n\n    @Test\n    void getFileExtensionTest() {\n        var ext = FileTypeDetectUtils.getFileExtension(\"BMP+HTML+JAR.html\");\n        assertThat(ext).isEqualTo(\".html\");\n\n        ext = FileTypeDetectUtils.getFileExtension(\"test.jpg\");\n        assertThat(ext).isEqualTo(\".jpg\");\n\n        ext = FileTypeDetectUtils.getFileExtension(\"hello\");\n        assertThat(ext).isEqualTo(\"\");\n    }\n\n    @Test\n    void isValidExtensionForMimeTest() {\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"image/bmp\", \"hello.html\"))\n            .isFalse();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"image/bmp\", \"hello.bmp\"))\n            .isTrue();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"image/jpeg\", \"hello.html\"))\n            .isFalse();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"image/jpeg\", \"hello.jpeg\"))\n            .isTrue();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"image/jpeg\", \"hello.JPEG\"))\n            .isTrue();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"image/jpeg\", \"hello.jpg\"))\n            .isTrue();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"image/jpeg\", \"hello.jpe\"))\n            .isTrue();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"audio/mpeg\", \"hello.html\"))\n            .isFalse();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"audio/mpeg\", \"hello.mp3\"))\n            .isTrue();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"audio/mpeg\", \"hello.MPGA\"))\n            .isTrue();\n\n        assertThat(FileTypeDetectUtils.isValidExtensionForMime(\"audio/mpeg\", \"hello.m3a\"))\n            .isTrue();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/FileUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;\nimport static run.halo.app.infra.utils.FileUtils.deleteFileSilently;\nimport static run.halo.app.infra.utils.FileUtils.jar;\nimport static run.halo.app.infra.utils.FileUtils.unzip;\nimport static run.halo.app.infra.utils.FileUtils.zip;\n\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.NoSuchFileException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.zip.ZipInputStream;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport reactor.test.StepVerifier;\nimport run.halo.app.infra.exception.AccessDeniedException;\n\nclass FileUtilsTest {\n\n    @TempDir\n    Path tempDirectory;\n\n    @Nested\n    class DirectoryTraversalTest {\n\n        @Test\n        void traversalTestWhenSuccess() {\n            checkDirectoryTraversal(\"/etc/\", \"/etc/halo/halo/../test\");\n            checkDirectoryTraversal(\"/etc/\", \"/etc/halo/../test\");\n            checkDirectoryTraversal(\"/etc/\", \"/etc/test\");\n        }\n\n        @Test\n        void traversalTestWhenFailure() {\n            assertThrows(AccessDeniedException.class,\n                () -> checkDirectoryTraversal(\"/etc/\", \"/etc/../tmp\"));\n            assertThrows(AccessDeniedException.class,\n                () -> checkDirectoryTraversal(\"/etc/\", \"/../tmp\"));\n            assertThrows(AccessDeniedException.class,\n                () -> checkDirectoryTraversal(\"/etc/\", \"/tmp\"));\n        }\n\n    }\n\n    @Nested\n    class ZipTest {\n\n        @Test\n        void zipFolderAndUnzip() throws IOException, URISyntaxException {\n            var uri = requireNonNull(getClass().getClassLoader().getResource(\"folder-to-zip\"))\n                .toURI();\n            var zipPath = tempDirectory.resolve(\"example.zip\");\n            zip(Paths.get(uri), zipPath);\n\n            var unzipTarget = tempDirectory.resolve(\"example-folder\");\n            try (var zis = new ZipInputStream(Files.newInputStream(zipPath))) {\n                unzip(zis, unzipTarget);\n            }\n\n            var lines = Files.readAllLines(unzipTarget.resolve(\"examplefile\"));\n            assertEquals(1, lines.size());\n            assertEquals(\"Here is an example file.\", lines.get(0));\n        }\n\n        @Test\n        void jarFolderAndUnzip() throws IOException, URISyntaxException {\n            var uri = requireNonNull(getClass().getClassLoader().getResource(\"folder-to-zip\"))\n                .toURI();\n            var zipPath = tempDirectory.resolve(\"example.zip\");\n            jar(Paths.get(uri), zipPath);\n\n            var unzipTarget = tempDirectory.resolve(\"example-folder\");\n            try (var zis = new ZipInputStream(Files.newInputStream(zipPath))) {\n                unzip(zis, unzipTarget);\n            }\n            var lines = Files.readAllLines(unzipTarget.resolve(\"examplefile\"));\n            assertEquals(1, lines.size());\n            assertEquals(\"Here is an example file.\", lines.get(0));\n        }\n\n        @Test\n        void zipFolderIfNoSuchFolder() {\n            assertThrows(NoSuchFileException.class, () ->\n                zip(Paths.get(\"no-such-folder\"), tempDirectory.resolve(\"example.zip\")));\n        }\n\n        @Test\n        void jarFolderIfNoSuchFolder() {\n            assertThrows(NoSuchFileException.class, () ->\n                jar(Paths.get(\"no-such-folder\"), tempDirectory.resolve(\"example.zip\")));\n        }\n\n    }\n\n    @Test\n    void deleteFileSilentlyTest() throws IOException {\n        StepVerifier.create(deleteFileSilently(null))\n            .expectNext(false)\n            .verifyComplete();\n\n        StepVerifier.create(deleteFileSilently(tempDirectory))\n            .expectNext(false)\n            .verifyComplete();\n\n        StepVerifier.create(\n                deleteFileSilently(Files.createFile(tempDirectory.resolve(\"for-deleting\"))))\n            .expectNext(true)\n            .verifyComplete();\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/HaloUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport run.halo.app.theme.router.ModelConst;\n\nclass HaloUtilsTest {\n\n    @Test\n    void checkNoCache() {\n        var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/\").build());\n        var request = MockServerRequest.builder()\n            .exchange(exchange)\n            .build();\n        var applied = HaloUtils.noCache().apply(request);\n        assertEquals(applied, request);\n        assertTrue(() -> exchange.getRequiredAttribute(ModelConst.NO_CACHE));\n    }\n\n    @ParameterizedTest\n    @CsvSource({\n        \"http://example.com/path with spaces, http://example.com/path%20with%20spaces\",\n        \"https://example.com/üñîçødé, https://example.com/%C3%BC%C3%B1%C3%AE%C3%A7%C3%B8d%C3%A9\",\n        \"ftp://example.com/special?param=äöü, ftp://example.com/special?param=%C3%A4%C3%B6%C3%BC\",\n        \"http://example.com/normal-path, http://example.com/normal-path\",\n        \"http://example.com/路径, http://example.com/%E8%B7%AF%E5%BE%84\",\n        \"http://example.com/space%20space, http://example.com/space%20space\",\n        \"http://example.com/100%100, http://example.com/100%100\",\n        \"http://example.com/mixed chars%20and 中文, http://example.com/mixed%20chars%20and%20%E4%B8%AD%E6%96%87\",\n        \"http://中文.com/path/to/中文, http://xn--fiq228c.com/path/to/%E4%B8%AD%E6%96%87\"\n    })\n    void shouldConvertUriSafely(String uri, String expected) {\n        assertEquals(expected, HaloUtils.safeToUri(uri).toString());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/IpAddressUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.net.InetSocketAddress;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\n\nclass IpAddressUtilsTest {\n\n    @Test\n    void testGetIPAddressFromCloudflareProxy() {\n        var request = MockServerHttpRequest.get(\"/\")\n            .header(\"CF-Connecting-IP\", \"127.0.0.1\")\n            .build();\n        var expected = \"127.0.0.1\";\n        var actual = IpAddressUtils.getClientIp(request);\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetIPAddressFromXRealIpHeader() {\n        var request = MockServerHttpRequest.get(\"/\")\n            .header(\"X-Real-IP\", \"127.0.0.1\")\n            .build();\n        var expected = \"127.0.0.1\";\n        var actual = IpAddressUtils.getClientIp(request);\n        assertEquals(expected, actual);\n    }\n\n    @Test\n    void testGetUnknownIPAddressWhenRemoteAddressIsNull() {\n        var request = MockServerHttpRequest.get(\"/\").build();\n        var actual = IpAddressUtils.getClientIp(request);\n        assertEquals(IpAddressUtils.UNKNOWN, actual);\n    }\n\n    @Test\n    void testGetUnknownIPAddressWhenRemoteAddressIsUnresolved() {\n        var request = MockServerHttpRequest.get(\"/\")\n            .remoteAddress(InetSocketAddress.createUnresolved(\"localhost\", 8090))\n            .build();\n        var actual = IpAddressUtils.getClientIp(request);\n        assertEquals(IpAddressUtils.UNKNOWN, actual);\n    }\n\n    @Test\n    void testGetIPAddressWithMultipleHeaders() {\n        var headers = new HttpHeaders();\n        headers.add(\"X-Forwarded-For\", \"127.0.0.1, 127.0.1.1\");\n        headers.add(\"Proxy-Client-IP\", \"127.0.0.2\");\n        headers.add(\"CF-Connecting-IP\", \"127.0.0.2\");\n        headers.add(\"WL-Proxy-Client-IP\", \"127.0.0.3\");\n        headers.add(\"HTTP_CLIENT_IP\", \"127.0.0.4\");\n        headers.add(\"HTTP_X_FORWARDED_FOR\", \"127.0.0.5\");\n        var request = MockServerHttpRequest.get(\"/\")\n            .headers(headers)\n            .build();\n        var expected = \"127.0.0.1\";\n        var actual = IpAddressUtils.getClientIp(request);\n        assertEquals(expected, actual);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/SettingUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport java.util.Map;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.core.extension.Setting;\n\n/**\n * Tests for {@link SettingUtils}.\n *\n * @author guqing\n * @since 2.0.1\n */\nclass SettingUtilsTest {\n\n    @Test\n    void settingDefinedDefaultValueMap() throws JSONException {\n        Setting setting = getFakeSetting();\n        var map = SettingUtils.settingDefinedDefaultValueMap(setting);\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"sns\": \"{\\\\\"email\\\\\":\\\\\"example@exmple.com\\\\\"}\"\n                }\n                \"\"\",\n            JsonUtils.objectToJson(map),\n            true);\n    }\n\n    @Test\n    void mergePatch() throws JSONException {\n        Map<String, String> defaultValue =\n            Map.of(\"comment\", \"{\\\"enable\\\":true,\\\"requireReviewForNew\\\":true}\",\n                \"basic\", \"{\\\"title\\\":\\\"guqing's blog\\\"}\",\n                \"authProvider\", \"{\\\"github\\\":{\\\"clientId\\\":\\\"fake-client-id\\\"}}\");\n        Map<String, String> modified = Map.of(\"comment\",\n            \"{\\\"enable\\\":true,\\\"requireReviewForNew\\\":true,\\\"systemUserOnly\\\":false}\",\n            \"basic\", \"{\\\"title\\\":\\\"guqing's blog\\\", \\\"subtitle\\\": \\\"fake-sub-title\\\"}\");\n\n        Map<String, String> result = SettingUtils.mergePatch(modified, defaultValue);\n        Map<String, String> excepted = Map.of(\"comment\",\n            \"{\\\"enable\\\":true,\\\"requireReviewForNew\\\":true,\\\"systemUserOnly\\\":false}\",\n            \"basic\", \"{\\\"title\\\":\\\"guqing's blog\\\",\\\"subtitle\\\":\\\"fake-sub-title\\\"}\",\n            \"authProvider\", \"{\\\"github\\\":{\\\"clientId\\\":\\\"fake-client-id\\\"}}\");\n        JSONAssert.assertEquals(JsonUtils.objectToJson(excepted), JsonUtils.objectToJson(result),\n            true);\n    }\n\n    @Test\n    void mergePatchWithMoreType() throws JSONException {\n        Map<String, String> defaultValue = Map.of(\n            \"array\", \"[1,2,3]\",\n            \"number\", \"1\",\n            \"boolean\", \"false\",\n            \"string\", \"new-default-string-value\",\n            \"object\", \"{\\\"name\\\":\\\"guqing\\\"}\"\n        );\n        Map<String, String> modified = Map.of(\n            \"stringArray\", \"[\\\"hello\\\", \\\"world\\\"]\",\n            \"boolean\", \"true\",\n            \"string\", \"hello\",\n            \"object\", \"{\\\"name\\\":\\\"guqing\\\", \\\"age\\\": 18}\"\n        );\n        Map<String, String> result = SettingUtils.mergePatch(modified, defaultValue);\n        Map<String, String> excepted = Map.of(\n            \"array\", \"[1,2,3]\",\n            \"number\", \"1\",\n            \"boolean\", \"true\",\n            \"string\", \"hello\",\n            \"object\", \"{\\\"name\\\":\\\"guqing\\\",\\\"age\\\":18}\",\n            \"stringArray\", \"[\\\"hello\\\",\\\"world\\\"]\"\n        );\n        JSONAssert.assertEquals(JsonUtils.objectToJson(excepted), JsonUtils.objectToJson(result),\n            true);\n    }\n\n    private static Setting getFakeSetting() {\n        String settingJson = \"\"\"\n            {\n                \"apiVersion\": \"v1alpha1\",\n                \"kind\": \"Setting\",\n                \"metadata\": {\n                    \"name\": \"theme-default-setting\"\n                },\n                \"spec\": {\n                    \"forms\": [{\n                        \"formSchema\": [\n                            {\n                                \"$el\": \"h1\",\n                                \"children\": \"Register\"\n                            },\n                            {\n                                \"$formkit\": \"text\",\n                                \"label\": \"Email\",\n                                \"name\": \"email\",\n                                \"value\": \"example@exmple.com\"\n                            },\n                            {\n                                \"$formkit\": \"password\",\n                                \"label\": \"Password\",\n                                \"name\": \"password\",\n                                \"validation\": \"required|length:5,16\",\n                                \"value\": null\n                            }\n                        ],\n                        \"group\": \"sns\",\n                        \"label\": \"社交资料\"\n                    }]\n                }\n            }\n            \"\"\";\n        return JsonUtils.jsonToObject(settingJson, Setting.class);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/SortUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link SortUtils}.\n *\n * @author guqing\n * @since 2.19.0\n */\nclass SortUtilsTest {\n\n    @Test\n    void resolve() {\n        // null case\n        assertThat(SortUtils.resolve(null).isUnsorted()).isTrue();\n\n        // multiple sort and directions\n        var str = List.of(\"name,asc\", \"age,desc\");\n        var sort = SortUtils.resolve(str);\n        assertThat(sort.toString()).isEqualTo(\"name: ASC,age: DESC\");\n\n        // missing direction\n        str = List.of(\"name\");\n        sort = SortUtils.resolve(str);\n        assertThat(sort.toString()).isEqualTo(\"name: ASC\");\n\n        // whitespace in direction\n        assertThatThrownBy(() -> SortUtils.resolve(List.of(\"name, desc\")))\n            .isInstanceOf(IllegalArgumentException.class)\n            .hasMessage(\"Direction must not contain whitespace\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/SystemConfigUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\n\nclass SystemConfigUtilsTest {\n\n    private final ObjectMapper mapper = JsonUtils.mapper();\n\n    @Test\n    void mergeMapShouldMergeEmptyMaps() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        Map<String, String> overrideMap = new HashMap<>();\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        assertNotNull(result);\n        assertTrue(result.isEmpty());\n    }\n\n    @Test\n    void mergeMapShouldReturnDefaultMapWhenOverrideIsEmpty() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\"}\");\n        Map<String, String> overrideMap = new HashMap<>();\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        assertEquals(1, result.size());\n        assertEquals(\"{\\\"key1\\\":\\\"value1\\\"}\", result.get(\"group1\"));\n    }\n\n    @Test\n    void mergeMapShouldAddNewKeysFromOverrideMap() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\"}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group2\", \"{\\\"key2\\\":\\\"value2\\\"}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        assertEquals(2, result.size());\n        assertEquals(\"{\\\"key1\\\":\\\"value1\\\"}\", result.get(\"group1\"));\n        assertEquals(\"{\\\"key2\\\":\\\"value2\\\"}\", result.get(\"group2\"));\n    }\n\n    @Test\n    void mergeMapShouldDeepMergeJsonObjects() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\",\\\"nested\\\":{\\\"a\\\":\\\"1\\\"}}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\", \"{\\\"key2\\\":\\\"value2\\\",\\\"nested\\\":{\\\"b\\\":\\\"2\\\"}}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        assertEquals(1, result.size());\n        JsonNode resultNode = mapper.readTree(result.get(\"group1\"));\n        assertEquals(\"value1\", resultNode.get(\"key1\").asText());\n        assertEquals(\"value2\", resultNode.get(\"key2\").asText());\n        assertEquals(\"1\", resultNode.get(\"nested\").get(\"a\").asText());\n        assertEquals(\"2\", resultNode.get(\"nested\").get(\"b\").asText());\n    }\n\n    @Test\n    void mergeMapShouldOverrideExistingValues() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{\\\"key1\\\":\\\"oldValue\\\"}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\", \"{\\\"key1\\\":\\\"newValue\\\"}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        assertEquals(1, result.size());\n        JsonNode resultNode = mapper.readTree(result.get(\"group1\"));\n        assertEquals(\"newValue\", resultNode.get(\"key1\").asText());\n    }\n\n    @Test\n    void mergeMapShouldHandleComplexNestedStructures() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"config\",\n            \"{\\\"server\\\":{\\\"port\\\":8080,\\\"host\\\":\\\"localhost\\\"},\\\"features\\\":{\\\"auth\\\":true}}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"config\",\n            \"{\\\"server\\\":{\\\"port\\\":9090},\\\"features\\\":{\\\"logging\\\":true}}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        JsonNode resultNode = mapper.readTree(result.get(\"config\"));\n        assertEquals(9090, resultNode.get(\"server\").get(\"port\").asInt());\n        assertEquals(\"localhost\", resultNode.get(\"server\").get(\"host\").asText());\n        assertTrue(resultNode.get(\"features\").get(\"auth\").asBoolean());\n        assertTrue(resultNode.get(\"features\").get(\"logging\").asBoolean());\n    }\n\n    @Test\n    void mergeMapShouldHandleArrayReplacement() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{\\\"items\\\":[1,2,3]}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\", \"{\\\"items\\\":[4,5,6]}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        JsonNode resultNode = mapper.readTree(result.get(\"group1\"));\n        JsonNode items = resultNode.get(\"items\");\n        assertEquals(3, items.size());\n        assertEquals(4, items.get(0).asInt());\n        assertEquals(5, items.get(1).asInt());\n        assertEquals(6, items.get(2).asInt());\n    }\n\n    @Test\n    void mergeMapShouldHandleNullValues() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\"}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\", \"{\\\"key1\\\":null}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        JsonNode resultNode = mapper.readTree(result.get(\"group1\"));\n        assertTrue(resultNode.get(\"key1\").isNull());\n    }\n\n    @Test\n    void mergeMapShouldThrowExceptionForInvalidJson() {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"invalid json\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\"}\");\n\n        assertThrows(JsonProcessingException.class,\n            () -> SystemConfigUtils.mergeMap(defaultMap, overrideMap));\n    }\n\n    @Test\n    void mergeConfigMapShouldMergeConfigMaps() throws JsonProcessingException {\n        ConfigMap defaultConfigMap = new ConfigMap();\n        Metadata defaultMetadata = new Metadata();\n        defaultMetadata.setName(\"default-config\");\n        defaultConfigMap.setMetadata(defaultMetadata);\n        Map<String, String> defaultData = new HashMap<>();\n        defaultData.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\"}\");\n        defaultConfigMap.setData(defaultData);\n\n        ConfigMap overrideConfigMap = new ConfigMap();\n        Metadata overrideMetadata = new Metadata();\n        overrideMetadata.setName(\"override-config\");\n        overrideConfigMap.setMetadata(overrideMetadata);\n        Map<String, String> overrideData = new HashMap<>();\n        overrideData.put(\"group1\", \"{\\\"key2\\\":\\\"value2\\\"}\");\n        overrideConfigMap.setData(overrideData);\n\n        ConfigMap result = SystemConfigUtils.mergeConfigMap(defaultConfigMap, overrideConfigMap);\n\n        assertNotNull(result);\n        assertEquals(\"override-config\", result.getMetadata().getName());\n        assertNotNull(result.getData());\n        assertEquals(1, result.getData().size());\n        JsonNode resultNode = mapper.readTree(result.getData().get(\"group1\"));\n        assertEquals(\"value1\", resultNode.get(\"key1\").asText());\n        assertEquals(\"value2\", resultNode.get(\"key2\").asText());\n    }\n\n    @Test\n    void mergeConfigMapShouldHandleNullData() throws JsonProcessingException {\n        ConfigMap defaultConfigMap = new ConfigMap();\n        Metadata defaultMetadata = new Metadata();\n        defaultMetadata.setName(\"default-config\");\n        defaultConfigMap.setMetadata(defaultMetadata);\n        defaultConfigMap.setData(null);\n\n        ConfigMap overrideConfigMap = new ConfigMap();\n        Metadata overrideMetadata = new Metadata();\n        overrideMetadata.setName(\"override-config\");\n        overrideConfigMap.setMetadata(overrideMetadata);\n        Map<String, String> overrideData = new HashMap<>();\n        overrideData.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\"}\");\n        overrideConfigMap.setData(overrideData);\n\n        ConfigMap result = SystemConfigUtils.mergeConfigMap(defaultConfigMap, overrideConfigMap);\n\n        assertNotNull(result);\n        assertNotNull(result.getData());\n        assertEquals(1, result.getData().size());\n        assertEquals(\"{\\\"key1\\\":\\\"value1\\\"}\", result.getData().get(\"group1\"));\n    }\n\n    @Test\n    void mergeConfigMapShouldHandleBothNullData() throws JsonProcessingException {\n        ConfigMap defaultConfigMap = new ConfigMap();\n        Metadata defaultMetadata = new Metadata();\n        defaultMetadata.setName(\"default-config\");\n        defaultConfigMap.setMetadata(defaultMetadata);\n        defaultConfigMap.setData(null);\n\n        ConfigMap overrideConfigMap = new ConfigMap();\n        Metadata overrideMetadata = new Metadata();\n        overrideMetadata.setName(\"override-config\");\n        overrideConfigMap.setMetadata(overrideMetadata);\n        overrideConfigMap.setData(null);\n\n        ConfigMap result = SystemConfigUtils.mergeConfigMap(defaultConfigMap, overrideConfigMap);\n\n        assertNotNull(result);\n        assertNotNull(result.getData());\n        assertTrue(result.getData().isEmpty());\n    }\n\n    @Test\n    void mergeConfigMapShouldUseOverrideMetadata() throws JsonProcessingException {\n        ConfigMap defaultConfigMap = new ConfigMap();\n        Metadata defaultMetadata = new Metadata();\n        defaultMetadata.setName(\"default-config\");\n        defaultConfigMap.setMetadata(defaultMetadata);\n        defaultConfigMap.setData(new HashMap<>());\n\n        ConfigMap overrideConfigMap = new ConfigMap();\n        Metadata overrideMetadata = new Metadata();\n        overrideMetadata.setName(\"override-config\");\n        overrideConfigMap.setMetadata(overrideMetadata);\n        overrideConfigMap.setData(new HashMap<>());\n\n        ConfigMap result = SystemConfigUtils.mergeConfigMap(defaultConfigMap, overrideConfigMap);\n\n        assertEquals(\"override-config\", result.getMetadata().getName());\n    }\n\n    @Test\n    void mergeMapShouldHandlePrimitiveValueReplacement() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{\\\"number\\\":42,\\\"boolean\\\":true,\\\"string\\\":\\\"old\\\"}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\", \"{\\\"number\\\":99,\\\"boolean\\\":false,\\\"string\\\":\\\"new\\\"}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        JsonNode resultNode = mapper.readTree(result.get(\"group1\"));\n        assertEquals(99, resultNode.get(\"number\").asInt());\n        assertFalse(resultNode.get(\"boolean\").asBoolean());\n        assertEquals(\"new\", resultNode.get(\"string\").asText());\n    }\n\n    @Test\n    void mergeMapShouldHandleEmptyJsonObjects() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\", \"{}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\", \"{\\\"key1\\\":\\\"value1\\\"}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        JsonNode resultNode = mapper.readTree(result.get(\"group1\"));\n        assertEquals(\"value1\", resultNode.get(\"key1\").asText());\n    }\n\n    @Test\n    void mergeMapShouldHandleDeepNesting() throws JsonProcessingException {\n        Map<String, String> defaultMap = new HashMap<>();\n        defaultMap.put(\"group1\",\n            \"{\\\"level1\\\":{\\\"level2\\\":{\\\"level3\\\":{\\\"key\\\":\\\"value1\\\"}}}}\");\n        Map<String, String> overrideMap = new HashMap<>();\n        overrideMap.put(\"group1\",\n            \"{\\\"level1\\\":{\\\"level2\\\":{\\\"level3\\\":{\\\"newKey\\\":\\\"value2\\\"}}}}\");\n\n        Map<String, String> result = SystemConfigUtils.mergeMap(defaultMap, overrideMap);\n\n        JsonNode resultNode = mapper.readTree(result.get(\"group1\"));\n        JsonNode level3 = resultNode.get(\"level1\").get(\"level2\").get(\"level3\");\n        assertEquals(\"value1\", level3.get(\"key\").asText());\n        assertEquals(\"value2\", level3.get(\"newKey\").asText());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/VersionUtilsTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link VersionUtils}.\n *\n * @author guqing\n * @since 2.2.0\n */\nclass VersionUtilsTest {\n\n    @Test\n    void satisfiesRequires() {\n        // match all requires\n        String systemVersion = \"0.0.0\";\n        String requires = \">=2.2.0\";\n        boolean result = VersionUtils.satisfiesRequires(systemVersion, requires);\n        assertThat(result).isTrue();\n\n        systemVersion = \"2.0.0\";\n        requires = \"*\";\n        result = VersionUtils.satisfiesRequires(systemVersion, requires);\n        assertThat(result).isTrue();\n\n        systemVersion = \"2.0.0\";\n        requires = \"\";\n        result = VersionUtils.satisfiesRequires(systemVersion, requires);\n        assertThat(result).isTrue();\n\n        // match exact version\n        systemVersion = \"2.0.0\";\n        requires = \">=2.0.0\";\n        result = VersionUtils.satisfiesRequires(systemVersion, requires);\n        assertThat(result).isTrue();\n\n        systemVersion = \"2.0.0\";\n        requires = \">2.0.0\";\n        result = VersionUtils.satisfiesRequires(systemVersion, requires);\n        assertThat(result).isFalse();\n\n        //an exact version x.y.z will implicitly mean the same as >=x.y.z\n        systemVersion = \"2.1.0\";\n        // means >=2.0.0\n        requires = \"2.0.0\";\n        result = VersionUtils.satisfiesRequires(systemVersion, requires);\n        assertThat(result).isTrue();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/infra/utils/YamlUnstructuredLoaderTest.java",
    "content": "package run.halo.app.infra.utils;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.List;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.core.io.Resource;\nimport org.springframework.security.util.InMemoryResource;\nimport run.halo.app.extension.Unstructured;\n\n/**\n * Tests for {@link YamlUnstructuredLoader}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass YamlUnstructuredLoaderTest {\n\n    private List<InMemoryResource> yamlResources;\n    private String notSpecYaml;\n\n    @BeforeEach\n    void setUp() {\n        String viewCategoriesRoleYaml = \"\"\"\n            apiVersion: v1alpha1\n            kind: Fake\n            metadata:\n              name: test1\n            hello:\n              world: halo\n            \"\"\";\n\n        String multipleRoleYaml = \"\"\"\n            apiVersion: v1alpha1\n            kind: Fake\n            metadata:\n              name: test2\n            hello:\n              world: haha\n            ---\n            apiVersion: v1alpha1\n            kind: Fake\n            metadata:\n              name: test2\n            hello:\n              world: bang\n            \"\"\";\n\n        notSpecYaml = \"\"\"\n            server:\n              port: 8090\n            spring:\n              jackson:\n                date-format: yyyy-MM-dd HH:mm:ss\n            \"\"\";\n\n        yamlResources = Stream.of(viewCategoriesRoleYaml, multipleRoleYaml, notSpecYaml)\n            .map(InMemoryResource::new)\n            .toList();\n    }\n\n    @Test\n    void loadTest() {\n        Resource[] resources = yamlResources.toArray(Resource[]::new);\n        YamlUnstructuredLoader yamlUnstructuredLoader = new YamlUnstructuredLoader(resources);\n        List<Unstructured> unstructuredList = yamlUnstructuredLoader.load();\n        assertThat(unstructuredList).isNotNull();\n        assertThat(unstructuredList).hasSize(3);\n\n        assertThat(JsonUtils.objectToJson(unstructuredList)).isEqualToIgnoringWhitespace(\"\"\"\n            [\n                 {\n                     \"apiVersion\": \"v1alpha1\",\n                     \"kind\": \"Fake\",\n                     \"metadata\": {\n                         \"name\": \"test1\"\n                     },\n                     \"hello\": {\n                         \"world\": \"halo\"\n                     }\n                 },\n                 {\n                     \"apiVersion\": \"v1alpha1\",\n                     \"kind\": \"Fake\",\n                     \"metadata\": {\n                         \"name\": \"test2\"\n                     },\n                     \"hello\": {\n                         \"world\": \"haha\"\n                     }\n                 },\n                 {\n                     \"apiVersion\": \"v1alpha1\",\n                     \"kind\": \"Fake\",\n                     \"metadata\": {\n                         \"name\": \"test2\"\n                     },\n                     \"hello\": {\n                         \"world\": \"bang\"\n                     }\n                 }\n             ]\n            \"\"\");\n    }\n\n    @Test\n    void loadIgnore() {\n        InMemoryResource resource = new InMemoryResource(notSpecYaml);\n        YamlUnstructuredLoader yamlUnstructuredLoader = new YamlUnstructuredLoader(resource);\n        List<Unstructured> unstructuredList = yamlUnstructuredLoader.load();\n        assertThat(unstructuredList).isEmpty();\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/migration/BackupReconcilerTest.java",
    "content": "package run.halo.app.migration;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.extension.ExtensionUtil.addFinalizers;\n\nimport java.io.IOException;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.Exceptions;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\n\n@ExtendWith(MockitoExtension.class)\nclass BackupReconcilerTest {\n\n    @Mock\n    MigrationService migrationService;\n\n    @Mock\n    ExtensionClient client;\n\n    @InjectMocks\n    BackupReconciler reconciler;\n\n    @Test\n    void whenFreshBackupIsComing() {\n        var name = \"fake-backup\";\n        var backup = createPureBackup(name);\n        backup.getSpec().setFormat(\"zip\");\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n        doNothing().when(client).update(backup);\n        when(migrationService.backup(backup)).thenReturn(Mono.fromRunnable(() -> {\n            var status = backup.getStatus();\n            status.setFilename(\"fake-backup-filename\");\n            status.setSize(1024L);\n        }));\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n\n        assertNotNull(result);\n        assertFalse(result.reEnqueue());\n\n        var status = backup.getStatus();\n        assertEquals(Backup.Phase.SUCCEEDED, status.getPhase());\n        assertNotNull(status.getStartTimestamp());\n        assertNotNull(status.getCompletionTimestamp());\n        assertEquals(\"fake-backup-filename\", status.getFilename());\n        assertEquals(1024L, status.getSize());\n\n        // 1. query\n        // 2. pending -> running\n        // 3. running -> succeeded\n        verify(client, times(3)).fetch(Backup.class, name);\n        verify(client, times(3)).update(backup);\n        verify(migrationService).backup(backup);\n    }\n\n    @Test\n    void whenBackupDeleted() {\n        var name = \"fake-deleted-backup\";\n        var backup = createPureBackup(name);\n        backup.getMetadata().setDeletionTimestamp(Instant.now());\n        addFinalizers(backup.getMetadata(), Set.of(Constant.HOUSE_KEEPER_FINALIZER));\n\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n        when(migrationService.cleanup(backup)).thenReturn(Mono.empty());\n        doNothing().when(client).update(backup);\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n\n        assertNotNull(result);\n        assertFalse(result.reEnqueue());\n\n        assertFalse(backup.getMetadata().getFinalizers().contains(Constant.HOUSE_KEEPER_FINALIZER));\n        verify(client).fetch(Backup.class, name);\n        verify(migrationService).cleanup(backup);\n        verify(client).update(backup);\n    }\n\n    @Test\n    void setPhaseToFailedIfPhaseIsRunning() {\n        var name = \"fake-backup\";\n        var backup = createPureBackup(name);\n        var status = backup.getStatus();\n        status.setPhase(Backup.Phase.RUNNING);\n\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n        doNothing().when(client).update(backup);\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n        assertNotNull(result);\n        assertFalse(result.reEnqueue());\n\n        assertEquals(Backup.Phase.FAILED, status.getPhase());\n        assertEquals(\"UnexpectedExit\", status.getFailureReason());\n        // 1. add finalizer\n        // 2. update status\n        verify(client, times(2)).fetch(Backup.class, name);\n        verify(client, times(2)).update(backup);\n    }\n\n    @Test\n    void shouldReQueueIfExpiresAtSetAndNotExpired() {\n        var now = Instant.now();\n        reconciler.setClock(Clock.fixed(now, ZoneId.systemDefault()));\n        var name = \"fake-backup\";\n        var backup = createPureBackup(name);\n        addFinalizers(backup.getMetadata(), Set.of(Constant.HOUSE_KEEPER_FINALIZER));\n        backup.getSpec().setExpiresAt(now.plus(Duration.ofSeconds(3)));\n        var status = backup.getStatus();\n        status.setPhase(Backup.Phase.SUCCEEDED);\n\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n        assertNotNull(result);\n        assertTrue(result.reEnqueue());\n        assertEquals(Duration.ofSeconds(3), result.retryAfter());\n\n        verify(client).fetch(Backup.class, name);\n        verify(client, never()).update(backup);\n        verify(client, never()).delete(backup);\n    }\n\n    @Test\n    void shouldDeleteIfExpiresAtSetAndExpired() {\n        var now = Instant.now();\n        reconciler.setClock(Clock.fixed(now, ZoneId.systemDefault()));\n        var name = \"fake-backup\";\n        var backup = createPureBackup(name);\n        addFinalizers(backup.getMetadata(), Set.of(Constant.HOUSE_KEEPER_FINALIZER));\n        backup.getSpec().setExpiresAt(now.minus(Duration.ofSeconds(3)));\n        var status = backup.getStatus();\n        status.setPhase(Backup.Phase.SUCCEEDED);\n\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n        doNothing().when(client).delete(backup);\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n        assertNotNull(result);\n        assertFalse(result.reEnqueue());\n\n        verify(client).fetch(Backup.class, name);\n        verify(client, never()).update(backup);\n        verify(client).delete(backup);\n    }\n\n    @Test\n    void whenBackupInterrupted() {\n        var name = \"fake-backup\";\n        var backup = createPureBackup(name);\n        backup.getSpec().setFormat(\"zip\");\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n        doNothing().when(client).update(backup);\n        when(migrationService.backup(backup)).thenReturn(\n            Mono.error(Exceptions.propagate(new InterruptedException())));\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n\n        assertNotNull(result);\n        assertFalse(result.reEnqueue());\n\n        var status = backup.getStatus();\n        assertEquals(Backup.Phase.FAILED, status.getPhase());\n        assertNotNull(status.getStartTimestamp());\n        assertNull(status.getCompletionTimestamp());\n        assertEquals(\"Interrupted\", status.getFailureReason());\n\n        // 1. query\n        // 2. pending -> running\n        // 3. running -> failed\n        verify(client, times(3)).fetch(Backup.class, name);\n        verify(client, times(3)).update(backup);\n        verify(migrationService).backup(backup);\n    }\n\n    @Test\n    void somethingWentWrongWhenBackup() {\n        var name = \"fake-backup\";\n        var backup = createPureBackup(name);\n        backup.getSpec().setFormat(\"zip\");\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n        doNothing().when(client).update(backup);\n        when(migrationService.backup(backup))\n            .thenReturn(Mono.error(Exceptions.propagate(new IOException(\"File not found\"))));\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n\n        assertNotNull(result);\n        assertFalse(result.reEnqueue());\n\n        var status = backup.getStatus();\n        assertEquals(Backup.Phase.FAILED, status.getPhase());\n        assertNotNull(status.getStartTimestamp());\n        assertNull(status.getCompletionTimestamp());\n        assertEquals(\"SystemError\", status.getFailureReason());\n\n        // 1. query\n        // 2. pending -> running\n        // 3. running -> failed\n        verify(client, times(3)).fetch(Backup.class, name);\n        verify(client, times(3)).update(backup);\n        verify(migrationService).backup(backup);\n    }\n\n    @Test\n    void whenBackupWasFailed() {\n        var name = \"fake-backup\";\n        var backup = createPureBackup(name);\n        backup.getStatus().setPhase(Backup.Phase.FAILED);\n\n        when(client.fetch(Backup.class, name)).thenReturn(Optional.of(backup));\n\n        var result = reconciler.reconcile(new Reconciler.Request(name));\n        assertNotNull(result);\n        assertFalse(result.reEnqueue());\n        Mockito.verify(migrationService, never()).backup(any(Backup.class));\n    }\n\n    Backup createPureBackup(String name) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        var backup = new Backup();\n        backup.setMetadata(metadata);\n        return backup;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/migration/impl/MigrationServiceImplTest.java",
    "content": "package run.halo.app.migration.impl;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.nio.file.attribute.FileTime;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.zip.ZipInputStream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.core.io.buffer.DefaultDataBufferFactory;\nimport org.springframework.data.r2dbc.core.R2dbcEntityTemplate;\nimport org.springframework.data.r2dbc.core.ReactiveSelectOperation.ReactiveSelect;\nimport org.springframework.data.r2dbc.core.ReactiveSelectOperation.SelectWithQuery;\nimport org.springframework.r2dbc.connection.R2dbcTransactionManager;\nimport org.springframework.transaction.ReactiveTransaction;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.store.ExtensionStore;\nimport run.halo.app.extension.store.ExtensionStoreRepository;\nimport run.halo.app.infra.BackupRootGetter;\nimport run.halo.app.infra.exception.NotFoundException;\nimport run.halo.app.infra.properties.HaloProperties;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.migration.Backup;\n\n@ExtendWith(MockitoExtension.class)\nclass MigrationServiceImplTest {\n\n    @Mock\n    ExtensionStoreRepository repository;\n\n    @Mock\n    HaloProperties haloProperties;\n\n    @Mock\n    BackupRootGetter backupRoot;\n\n    @Mock\n    R2dbcEntityTemplate entityTemplate;\n\n    @Mock\n    R2dbcTransactionManager txManager;\n\n    @InjectMocks\n    @Spy\n    MigrationServiceImpl migrationService;\n\n    @Mock\n    ReactiveSelect<ExtensionStore> reactiveSelect;\n\n    @Mock\n    SelectWithQuery<ExtensionStore> selectWithQuery;\n\n    @TempDir\n    Path tempDir;\n\n    @Test\n    void backupTest() throws IOException {\n        Files.writeString(tempDir.resolve(\"fake-file\"), \"halo\", StandardOpenOption.CREATE_NEW);\n        var extensionStores = List.of(\n            createExtensionStore(\"fake-extension-store\", \"fake-data\")\n        );\n        var tx = mock(ReactiveTransaction.class);\n        when(txManager.getReactiveTransaction(any())).thenReturn(Mono.just(tx));\n        when(txManager.commit(tx)).thenReturn(Mono.empty());\n\n        when(entityTemplate.select(ExtensionStore.class)).thenReturn(reactiveSelect);\n        when(reactiveSelect.withFetchSize(100)).thenReturn(selectWithQuery);\n        when(selectWithQuery.all())\n            .thenReturn(Flux.fromIterable(extensionStores))\n            .thenReturn(Flux.empty());\n\n        when(haloProperties.getWorkDir()).thenReturn(tempDir);\n        when(backupRoot.get()).thenReturn(tempDir.resolve(\"backups\"));\n        var startTimestamp = Instant.now();\n        var backup = createRunningBackup(\"fake-backup\", startTimestamp);\n        StepVerifier.create(migrationService.backup(backup))\n            .verifyComplete();\n\n        // 1. backup workdir\n        // 2. package backup\n        verify(haloProperties).getWorkDir();\n        verify(backupRoot).get();\n\n        var status = backup.getStatus();\n        var datetimePart = migrationService.getDateTimeFormatter().format(startTimestamp);\n        assertEquals(datetimePart + \"-fake-backup.zip\", status.getFilename());\n        var backupFile = migrationService.getBackupsRoot()\n            .resolve(status.getFilename());\n        assertTrue(Files.exists(backupFile));\n        assertEquals(Files.size(backupFile), status.getSize());\n\n        var target = tempDir.resolve(\"target\");\n        try (var zis = new ZipInputStream(\n            Files.newInputStream(backupFile, StandardOpenOption.READ))) {\n            FileUtils.unzip(zis, tempDir.resolve(\"target\"));\n        }\n\n        var extensionsFile = target.resolve(\"extensions.data\");\n        var workdir = target.resolve(\"workdir\");\n        assertTrue(Files.exists(extensionsFile));\n        assertTrue(Files.exists(workdir));\n\n        var objectMapper = migrationService.getObjectMapper();\n        var gotExtensionStores = objectMapper.readValue(extensionsFile.toFile(),\n            new TypeReference<List<ExtensionStore>>() {\n            });\n        assertEquals(gotExtensionStores, extensionStores);\n        assertEquals(\"halo\", Files.readString(workdir.resolve(\"fake-file\")));\n    }\n\n    @Test\n    void restoreTest() throws IOException, URISyntaxException {\n        var unpackedBackup =\n            getClass().getClassLoader().getResource(\"backups/backup-for-restoration\");\n        assertNotNull(unpackedBackup);\n        var backupFile = tempDir.resolve(\"backups\").resolve(\"fake-backup.zip\");\n        Files.createDirectories(backupFile.getParent());\n        FileUtils.zip(Path.of(unpackedBackup.toURI()), backupFile);\n        var workdir = tempDir.resolve(\"workdir-for-restoration\");\n        Files.createDirectory(workdir);\n\n\n        var expectStore = createExtensionStore(\"fake-extension-store\", \"fake-data\");\n        expectStore.setVersion(null);\n\n        when(haloProperties.getWorkDir()).thenReturn(workdir);\n        when(repository.deleteAll()).thenReturn(Mono.empty());\n        when(repository.saveAll(List.of(expectStore))).thenReturn(Flux.empty());\n\n        var tx = mock(ReactiveTransaction.class);\n        when(txManager.getReactiveTransaction(any())).thenReturn(Mono.just(tx));\n        when(txManager.commit(tx)).thenReturn(Mono.empty());\n\n        var content = DataBufferUtils.read(backupFile,\n            DefaultDataBufferFactory.sharedInstance,\n            2048,\n            StandardOpenOption.READ);\n        StepVerifier.create(migrationService.restore(content))\n            .verifyComplete();\n\n\n        verify(haloProperties).getWorkDir();\n        verify(repository).deleteAll();\n        verify(repository).saveAll(List.of(expectStore));\n\n        // make sure the workdir is recovered.\n        var fakeFile = workdir.resolve(\"fake-file\");\n        assertEquals(\"halo\", Files.readString(fakeFile));\n    }\n\n    @Test\n    void cleanupBackupTest() throws IOException {\n        var backupFile = tempDir.resolve(\"workdir\").resolve(\"backups\").resolve(\"backup.zip\");\n        Files.createDirectories(backupFile.getParent());\n        Files.createFile(backupFile);\n\n        when(backupRoot.get()).thenReturn(tempDir.resolve(\"workdir\").resolve(\"backups\"));\n        var backup = createSucceededBackup(\"fake-backup\", \"backup.zip\");\n        StepVerifier.create(migrationService.cleanup(backup))\n            .verifyComplete();\n        verify(haloProperties, never()).getWorkDir();\n        verify(backupRoot).get();\n        assertTrue(Files.notExists(backupFile));\n    }\n\n    @Test\n    void cleanupBackupWithNoFilename() {\n        var backup = createSucceededBackup(\"fake-backup\", null);\n        StepVerifier.create(migrationService.cleanup(backup))\n            .verifyComplete();\n        verify(haloProperties, never()).getWorkDir();\n        verify(backupRoot, never()).get();\n    }\n\n    @Test\n    void downloadBackupTest() throws IOException {\n        var backupFile = tempDir.resolve(\"workdir\").resolve(\"backups\").resolve(\"backup.zip\");\n        Files.createDirectories(backupFile.getParent());\n        Files.writeString(backupFile, \"this is a backup file.\", StandardOpenOption.CREATE_NEW);\n        when(backupRoot.get()).thenReturn(tempDir.resolve(\"workdir\").resolve(\"backups\"));\n        var backup = createSucceededBackup(\"fake-backup\", \"backup.zip\");\n\n        StepVerifier.create(migrationService.download(backup))\n            .assertNext(resource -> {\n                assertEquals(\"backup.zip\", resource.getFilename());\n                try {\n                    var content = resource.getContentAsString(UTF_8);\n                    assertEquals(\"this is a backup file.\", content);\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            })\n            .verifyComplete();\n\n        verify(haloProperties, never()).getWorkDir();\n        verify(backupRoot).get();\n    }\n\n    @Test\n    void downloadBackupWhichDoesNotExist() {\n        var backup = createSucceededBackup(\"fake-backup\", \"backup.zip\");\n        when(backupRoot.get()).thenReturn(tempDir.resolve(\"workdir\").resolve(\"backups\"));\n\n        StepVerifier.create(migrationService.download(backup))\n            .expectError(NotFoundException.class)\n            .verify();\n        verify(haloProperties, never()).getWorkDir();\n        verify(backupRoot).get();\n    }\n\n    @Test\n    void getBackupFilesTest() throws Exception {\n        var now = Instant.now();\n        var backup1 = tempDir.resolve(\"backup1.zip\");\n        Files.writeString(backup1, \"fake-content\");\n        Files.setLastModifiedTime(backup1, FileTime.from(now));\n\n        var backup2 = tempDir.resolve(\"backup2.zip\");\n        Files.writeString(backup2, \"fake--content\");\n        Files.setLastModifiedTime(\n            backup2,\n            FileTime.from(now.plus(Duration.ofSeconds(1)))\n        );\n\n        var backup3 = tempDir.resolve(\"backup3.not-a-zip\");\n        Files.writeString(backup3, \"fake-content\");\n        Files.setLastModifiedTime(\n            backup3,\n            FileTime.from(now.plus(Duration.ofSeconds(2)))\n        );\n        when(backupRoot.get()).thenReturn(tempDir);\n\n        migrationService.afterPropertiesSet();\n        migrationService.getBackupFiles()\n            .as(StepVerifier::create)\n            .assertNext(backupFile -> {\n                assertEquals(\"backup2.zip\", backupFile.getFilename());\n                assertEquals(13, backupFile.getSize());\n                assertEquals(now.plus(Duration.ofSeconds(1)), backupFile.getLastModifiedTime());\n            })\n            .assertNext(backupFile -> {\n                assertEquals(\"backup1.zip\", backupFile.getFilename());\n                assertEquals(12, backupFile.getSize());\n                assertEquals(now, backupFile.getLastModifiedTime());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void getBackupFileTest() throws Exception {\n        var now = Instant.now();\n        Files.writeString(tempDir.resolve(\"backup.zip\"), \"fake-content\");\n        Files.setLastModifiedTime(tempDir.resolve(\"backup.zip\"), FileTime.from(now));\n        when(backupRoot.get()).thenReturn(tempDir);\n\n        migrationService.afterPropertiesSet();\n        migrationService.getBackupFile(\"backup.zip\")\n            .as(StepVerifier::create)\n            .assertNext(backupFile -> {\n                assertEquals(\"backup.zip\", backupFile.getFilename());\n                assertEquals(12, backupFile.getSize());\n                assertEquals(now, backupFile.getLastModifiedTime());\n            })\n            .verifyComplete();\n\n        migrationService.getBackupFile(\"backup-not-exist.zip\")\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    Backup createSucceededBackup(String name, String filename) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        var backup = new Backup();\n        backup.setMetadata(metadata);\n        var status = backup.getStatus();\n        status.setPhase(Backup.Phase.SUCCEEDED);\n        status.setCompletionTimestamp(Instant.now());\n        status.setFilename(filename);\n        status.setSize(1024L);\n        return backup;\n    }\n\n    Backup createRunningBackup(String name, Instant startTimestamp) {\n        var metadata = new Metadata();\n        metadata.setName(name);\n        var backup = new Backup();\n        backup.setMetadata(metadata);\n        var status = backup.getStatus();\n        status.setPhase(Backup.Phase.RUNNING);\n        status.setStartTimestamp(startTimestamp);\n        return backup;\n    }\n\n    ExtensionStore createExtensionStore(String name, String data) {\n        var store = new ExtensionStore();\n        store.setName(name);\n        store.setData(data.getBytes(UTF_8));\n        store.setVersion(1024L);\n        return store;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/DefaultNotificationCenterTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport java.util.Locale;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.notification.Notification;\nimport run.halo.app.core.extension.notification.NotificationTemplate;\nimport run.halo.app.core.extension.notification.NotifierDescriptor;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n/**\n * Tests for {@link DefaultNotificationCenter}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultNotificationCenterTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private ReasonNotificationTemplateSelector notificationTemplateSelector;\n\n    @Mock\n    private UserNotificationPreferenceService userNotificationPreferenceService;\n\n    @Mock\n    private NotificationTemplateRender notificationTemplateRender;\n\n    @Mock\n    private NotificationSender notificationSender;\n\n    @Mock\n    private RecipientResolver recipientResolver;\n\n    @Mock\n    private SubscriptionService subscriptionService;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @InjectMocks\n    private DefaultNotificationCenter notificationCenter;\n\n    @Test\n    public void testNotify() {\n        final Reason reason = new Reason();\n        final Reason.Spec spec = new Reason.Spec();\n        Reason.Subject subject = new Reason.Subject();\n        subject.setApiVersion(\"content.halo.run/v1alpha1\");\n        subject.setKind(\"Comment\");\n        subject.setName(\"comment-a\");\n        spec.setSubject(subject);\n        spec.setReasonType(\"new-reply-on-comment\");\n        spec.setAttributes(null);\n        reason.setSpec(spec);\n        reason.setMetadata(new Metadata());\n        reason.getMetadata().setName(\"reason-a\");\n\n        var spyNotificationCenter = spy(notificationCenter);\n        var subscriber = new Subscriber(UserIdentity.anonymousWithEmail(\"A\"), \"fake-name\");\n        when(recipientResolver.resolve(reason)).thenReturn(Flux.just(subscriber));\n\n        doReturn(Mono.empty()).when(spyNotificationCenter)\n            .dispatchNotification(eq(reason), any());\n\n        spyNotificationCenter.notify(reason).block();\n\n        verify(spyNotificationCenter).dispatchNotification(eq(reason), any());\n        verify(recipientResolver).resolve(eq(reason));\n    }\n\n    List<Subscription> createSubscriptions() {\n        Subscription subscription = new Subscription();\n        subscription.setMetadata(new Metadata());\n        subscription.getMetadata().setName(\"subscription-a\");\n\n        subscription.setSpec(new Subscription.Spec());\n        subscription.getSpec().setSubscriber(new Subscription.Subscriber());\n        subscription.getSpec().getSubscriber().setName(\"anonymousUser#A\");\n\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(\"new-reply-on-comment\");\n        interestReason.setSubject(createNewReplyOnCommentSubject());\n        subscription.getSpec().setReason(interestReason);\n\n        return List.of(subscription);\n    }\n\n    Subscription.ReasonSubject createNewReplyOnCommentSubject() {\n        var reasonSubject = new Subscription.ReasonSubject();\n        reasonSubject.setApiVersion(\"content.halo.run/v1alpha1\");\n        reasonSubject.setKind(\"Comment\");\n        reasonSubject.setName(\"comment-a\");\n        return reasonSubject;\n    }\n\n    @Test\n    public void testSubscribe() {\n        var spyNotificationCenter = spy(notificationCenter);\n        Subscription subscription = createSubscriptions().get(0);\n\n        var subscriber = subscription.getSpec().getSubscriber();\n\n        var reason = subscription.getSpec().getReason();\n\n        doReturn(Mono.empty())\n            .when(spyNotificationCenter).unsubscribe(eq(subscriber), eq(reason));\n\n        when(client.create(any(Subscription.class))).thenReturn(Mono.empty());\n\n        spyNotificationCenter.subscribe(subscriber, reason).block();\n\n        verify(client).create(any(Subscription.class));\n    }\n\n    @Test\n    public void testGetNotifiersBySubscriber() {\n        UserNotificationPreference preference = new UserNotificationPreference();\n        when(userNotificationPreferenceService.getByUser(any()))\n            .thenReturn(Mono.just(preference));\n\n        var reason = new Reason();\n        reason.setMetadata(new Metadata());\n        reason.getMetadata().setName(\"reason-a\");\n        reason.setSpec(new Reason.Spec());\n        reason.getSpec().setReasonType(\"new-reply-on-comment\");\n        var subscriber = new Subscriber(UserIdentity.anonymousWithEmail(\"A\"), \"fake-name\");\n\n        notificationCenter.getNotifiersBySubscriber(subscriber, reason)\n            .collectList()\n            .as(StepVerifier::create)\n            .consumeNextWith(notifiers -> {\n                assertThat(notifiers).hasSize(1);\n                assertThat(notifiers.get(0)).isEqualTo(\"default-email-notifier\");\n            })\n            .verifyComplete();\n\n        verify(userNotificationPreferenceService).getByUser(eq(subscriber.name()));\n    }\n\n    @Test\n    public void testDispatchNotification() {\n        var spyNotificationCenter = spy(notificationCenter);\n\n        doReturn(Flux.just(\"email-notifier\"))\n            .when(spyNotificationCenter).getNotifiersBySubscriber(any(), any());\n\n        NotifierDescriptor notifierDescriptor = mock(NotifierDescriptor.class);\n        when(client.fetch(eq(NotifierDescriptor.class), eq(\"email-notifier\")))\n            .thenReturn(Mono.just(notifierDescriptor));\n\n        var notificationElement = mock(DefaultNotificationCenter.NotificationElement.class);\n        doReturn(Mono.just(notificationElement))\n            .when(spyNotificationCenter).prepareNotificationElement(any(), any(), any());\n\n        doReturn(Mono.empty()).when(spyNotificationCenter).sendNotification(any());\n\n        var reason = new Reason();\n        reason.setMetadata(new Metadata());\n        reason.getMetadata().setName(\"reason-a\");\n        reason.setSpec(new Reason.Spec());\n        reason.getSpec().setReasonType(\"new-reply-on-comment\");\n\n        var subscription = createSubscriptions().get(0);\n        var subscriptionName = subscription.getMetadata().getName();\n        var subscriber =\n            new Subscriber(UserIdentity.of(subscription.getSpec().getSubscriber().getName()),\n                subscriptionName);\n        spyNotificationCenter.dispatchNotification(reason, subscriber).block();\n\n        verify(client).fetch(eq(NotifierDescriptor.class), eq(\"email-notifier\"));\n        verify(spyNotificationCenter).sendNotification(any());\n        verify(spyNotificationCenter, times(0)).createNotification(any());\n    }\n\n    @Test\n    public void testPrepareNotificationElement() {\n        var spyNotificationCenter = spy(notificationCenter);\n\n        doReturn(Mono.just(Locale.getDefault()))\n            .when(spyNotificationCenter).getLocaleFromSubscriber(any());\n\n        var notificationContent = mock(DefaultNotificationCenter.NotificationContent.class);\n        doReturn(Mono.just(notificationContent))\n            .when(spyNotificationCenter).inferenceTemplate(any(), any(), any());\n\n        spyNotificationCenter.prepareNotificationElement(any(), any(), any())\n            .block();\n\n        verify(spyNotificationCenter).getLocaleFromSubscriber(any());\n        verify(spyNotificationCenter).inferenceTemplate(any(), any(), any());\n    }\n\n    @Test\n    public void testSendNotification() {\n        var spyNotificationCenter = spy(notificationCenter);\n\n        var context = mock(NotificationContext.class);\n        doReturn(Mono.just(context))\n            .when(spyNotificationCenter).notificationContextFrom(any());\n\n        when(notificationSender.sendNotification(eq(\"fake-notifier-ext\"), any()))\n            .thenReturn(Mono.empty());\n\n        var element = mock(DefaultNotificationCenter.NotificationElement.class);\n        var mockDescriptor = mock(NotifierDescriptor.class);\n        when(element.descriptor()).thenReturn(mockDescriptor);\n        when(element.subscriber()).thenReturn(mock(Subscriber.class));\n        var notifierDescriptorSpec = mock(NotifierDescriptor.Spec.class);\n        when(mockDescriptor.getSpec()).thenReturn(notifierDescriptorSpec);\n        when(notifierDescriptorSpec.getNotifierExtName()).thenReturn(\"fake-notifier-ext\");\n\n        spyNotificationCenter.sendNotification(element).block();\n\n        verify(spyNotificationCenter).notificationContextFrom(any());\n        verify(notificationSender).sendNotification(any(), any());\n    }\n\n    @Test\n    public void testCreateNotification() {\n        var element = mock(DefaultNotificationCenter.NotificationElement.class);\n        var subscription = createSubscriptions().get(0);\n        var user = mock(User.class);\n\n        var subscriptionName = subscription.getMetadata().getName();\n        var subscriber =\n            new Subscriber(UserIdentity.of(subscription.getSpec().getSubscriber().getName()),\n                subscriptionName);\n        when(client.fetch(eq(User.class), eq(subscriber.name()))).thenReturn(Mono.just(user));\n        when(element.subscriber()).thenReturn(subscriber);\n\n        when(client.create(any(Notification.class))).thenReturn(Mono.empty());\n\n        var reason = new Reason();\n        reason.setMetadata(new Metadata());\n        reason.getMetadata().setName(\"reason-a\");\n        reason.setSpec(new Reason.Spec());\n        reason.getSpec().setReasonType(\"new-reply-on-comment\");\n        when(element.reason()).thenReturn(reason);\n\n        notificationCenter.createNotification(element).block();\n\n        verify(client).fetch(eq(User.class), eq(subscriber.name()));\n        verify(client).create(any(Notification.class));\n    }\n\n    @Test\n    public void testInferenceTemplate() {\n        final var spyNotificationCenter = spy(notificationCenter);\n\n        final var reasonType = mock(ReasonType.class);\n\n        var reason = new Reason();\n        reason.setMetadata(new Metadata());\n        reason.getMetadata().setName(\"reason-a\");\n        reason.setSpec(new Reason.Spec());\n        reason.getSpec().setReasonType(\"new-reply-on-comment\");\n\n        var reasonTypeName = reason.getSpec().getReasonType();\n\n        doReturn(Mono.just(reasonType))\n            .when(spyNotificationCenter).getReasonType(eq(reasonTypeName));\n        doReturn(Mono.just(\"fake-unsubscribe-url\"))\n            .when(spyNotificationCenter).getUnsubscribeUrl(anyString());\n\n        final var locale = Locale.CHINESE;\n\n        var template = new NotificationTemplate();\n        template.setMetadata(new Metadata());\n        template.getMetadata().setName(\"notification-template-a\");\n        template.setSpec(new NotificationTemplate.Spec());\n        template.getSpec().setTemplate(new NotificationTemplate.Template());\n        template.getSpec().getTemplate().setRawBody(\"body\");\n        template.getSpec().getTemplate().setHtmlBody(\"html-body\");\n        template.getSpec().getTemplate().setTitle(\"title\");\n        template.getSpec().setReasonSelector(new NotificationTemplate.ReasonSelector());\n        template.getSpec().getReasonSelector().setReasonType(reasonTypeName);\n        template.getSpec().getReasonSelector().setLanguage(locale.getLanguage());\n\n        when(notificationTemplateRender.render(anyString(), any()))\n            .thenReturn(Mono.empty());\n        when(notificationTemplateSelector.select(eq(reasonTypeName), any()))\n            .thenReturn(Mono.just(template));\n\n        var subscriber = new Subscriber(UserIdentity.anonymousWithEmail(\"A\"), \"fake-name\");\n\n        spyNotificationCenter.inferenceTemplate(reason, subscriber, locale).block();\n\n        verify(spyNotificationCenter).getReasonType(eq(reasonTypeName));\n        verify(notificationTemplateSelector).select(eq(reasonTypeName), any());\n    }\n\n    @Test\n    void getLocaleFromSubscriberTest() {\n        var subscription = mock(Subscriber.class);\n\n        when(environmentFetcher.getBasic()).thenReturn(Mono.just(new SystemSetting.Basic()));\n        notificationCenter.getLocaleFromSubscriber(subscription)\n            .as(StepVerifier::create)\n            .expectNext(Locale.getDefault())\n            .verifyComplete();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/DefaultNotificationReasonEmitterTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link DefaultNotificationReasonEmitter}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultNotificationReasonEmitterTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private DefaultNotificationReasonEmitter emitter;\n\n    @Test\n    void testEmitWhenReasonTypeNotFound() {\n        var reasonType = createReasonType();\n        when(client.fetch(eq(ReasonType.class), eq(reasonType.getMetadata().getName())))\n            .thenReturn(Mono.empty());\n        doEmmit(reasonType, reasonAttributes())\n            .as(StepVerifier::create)\n            .verifyErrorMessage(\"404 NOT_FOUND \\\"ReasonType [\" + reasonType.getMetadata().getName()\n                + \"] not found, do you forget to register it?\\\"\");\n    }\n\n    @Test\n    void testEmitWhenMissingAttributeValue() {\n        var reasonType = createReasonType();\n        when(client.fetch(eq(ReasonType.class), eq(reasonType.getMetadata().getName())))\n            .thenReturn(Mono.just(reasonType));\n\n        var map = reasonAttributes();\n        map.put(\"commenter\", null);\n        doEmmit(reasonType, map)\n            .as(StepVerifier::create)\n            .verifyErrorMessage(\"Reason property [commenter] is required.\");\n    }\n\n    @Test\n    void testEmitWhenMissingOptionalAttribute() {\n        var reasonType = createReasonType();\n        when(client.fetch(eq(ReasonType.class), eq(reasonType.getMetadata().getName())))\n            .thenReturn(Mono.just(reasonType));\n\n        when(client.create(any(Reason.class))).thenReturn(Mono.empty());\n\n        var map = reasonAttributes();\n        map.put(\"postTitle\", null);\n        doEmmit(reasonType, map)\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void testCreateReasonOnEmit() {\n        var reasonType = createReasonType();\n        when(client.fetch(eq(ReasonType.class), eq(reasonType.getMetadata().getName())))\n            .thenReturn(Mono.just(reasonType));\n\n        when(client.create(any(Reason.class))).thenReturn(Mono.empty());\n\n        var spyEmitter = spy(emitter);\n        doAnswer(as -> {\n            var returnedValue = as.callRealMethod();\n            JSONAssert.assertEquals(createReasonJson(),\n                JsonUtils.objectToJson(returnedValue), true);\n            return returnedValue;\n        }).when(spyEmitter).createReason(any(), any());\n\n        spyEmitter.emit(reasonType.getMetadata().getName(),\n                builder -> builder.attributes(reasonAttributes())\n                    .subject(Reason.Subject.builder()\n                        .apiVersion(\"content.halo.run/v1alpha1\")\n                        .kind(\"Post\")\n                        .name(\"5152aea5-c2e8-4717-8bba-2263d46e19d5\")\n                        .title(\"Hello Halo\")\n                        .url(\"/archives/hello-halo\")\n                        .build()\n                    )\n            )\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    Map<String, Object> reasonAttributes() {\n        var map = new LinkedHashMap<String, Object>();\n        map.put(\"postName\", \"5152aea5-c2e8-4717-8bba-2263d46e19d5\");\n        map.put(\"postTitle\", \"Hello Halo\");\n        map.put(\"commenter\", \"guqing\");\n        map.put(\"commentName\", \"53a76c38-5df2-469d-ae1b-68f5ae21a398\");\n        map.put(\"content\", \"测试评论\");\n        return map;\n    }\n\n    private Mono<Void> doEmmit(ReasonType reasonType, Map<String, Object> map) {\n        return emitter.emit(reasonType.getMetadata().getName(), builder -> {\n            builder.attributes(map)\n                .subject(Reason.Subject.builder()\n                    .apiVersion(\"content.halo.run/v1alpha1\")\n                    .kind(\"Post\")\n                    .name(\"5152aea5-c2e8-4717-8bba-2263d46e19d5\")\n                    .title(\"Hello Halo\")\n                    .url(\"/archives/hello-halo\")\n                    .build()\n                );\n        });\n    }\n\n    String createReasonJson() {\n        return \"\"\"\n            {\n                \"spec\": {\n                    \"reasonType\": \"new-comment-on-post\",\n                    \"subject\": {\n                        \"apiVersion\": \"content.halo.run/v1alpha1\",\n                        \"kind\": \"Post\",\n                        \"name\": \"5152aea5-c2e8-4717-8bba-2263d46e19d5\",\n                        \"title\": \"Hello Halo\",\n                        \"url\": \"/archives/hello-halo\"\n                    },\n                    \"attributes\": {\n                        \"postName\": \"5152aea5-c2e8-4717-8bba-2263d46e19d5\",\n                        \"postTitle\": \"Hello Halo\",\n                        \"commentName\": \"53a76c38-5df2-469d-ae1b-68f5ae21a398\",\n                        \"content\": \"测试评论\",\n                        \"commenter\": \"guqing\"\n                    }\n                },\n                \"apiVersion\": \"notification.halo.run/v1alpha1\",\n                \"kind\": \"Reason\",\n                \"metadata\": {\n                    \"generateName\": \"reason-\"\n                }\n            }\n            \"\"\";\n    }\n\n    ReasonType createReasonType() {\n        return JsonUtils.jsonToObject(\"\"\"\n            {\n                \"apiVersion\": \"notification.halo.run/v1alpha1\",\n                \"kind\": \"ReasonType\",\n                \"metadata\": {\n                    \"name\": \"new-comment-on-post\"\n                },\n                \"spec\": {\n                    \"description\": \"当你的文章收到新评论时，触发事件\",\n                    \"displayName\": \"文章收到新评论\",\n                    \"properties\": [\n                        {\n                            \"name\": \"postName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"postTitle\",\n                            \"type\": \"string\",\n                            \"optional\": true\n                        },\n                        {\n                            \"name\": \"commenter\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"commentName\",\n                            \"type\": \"string\"\n                        },\n                        {\n                            \"name\": \"content\",\n                            \"type\": \"string\"\n                        }\n                    ]\n                }\n            }\n            \"\"\", ReasonType.class);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/DefaultNotificationSenderTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link DefaultNotificationSender}.\n *\n * @author guqing\n * @since 2.9.0\n */\nclass DefaultNotificationSenderTest {\n\n    @Nested\n    class QueueItemTest {\n\n        @Test\n        void equalsTest() {\n            var item1 =\n                new DefaultNotificationSender.QueueItem(\"1\",\n                    mock(Runnable.class), 0);\n            var item2 =\n                new DefaultNotificationSender.QueueItem(\"1\",\n                    mock(Runnable.class), 1);\n\n            assertThat(item1).isEqualTo(item2);\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/DefaultNotificationTemplateRenderTest.java",
    "content": "package run.halo.app.notification;\n\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\n\n/**\n * Tests for {@link DefaultNotificationTemplateRender}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultNotificationTemplateRenderTest {\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @InjectMocks\n    DefaultNotificationTemplateRender templateRender;\n\n    @BeforeEach\n    void setUp() throws MalformedURLException {\n        var uri = URI.create(\"http://localhost:8090\");\n        lenient().when(externalUrlSupplier.get()).thenReturn(uri);\n        lenient().when(externalUrlSupplier.getRaw()).thenReturn(uri.toURL());\n    }\n\n    @Test\n    void render() {\n        final String template = \"\"\"\n            亲爱的博主\n\n              [(${replier})] 在评论“[(${isQuoteReply ? quoteContent : commentContent})]”中回复了您，\n              以下是回复的具体内容：\n              \n              [(${content})]\n\n            [(${site.title})]\n            [(${site.url})]\n            祝好！\n            查看原文：[(${commentSubjectUrl})]\n            \"\"\";\n        final var model = Map.<String, Object>of(\n            \"replier\", \"guqing\",\n            \"isQuoteReply\", true,\n            \"quoteContent\", \"这是引用的内容\",\n            \"commentContent\", \"这是评论的内容\",\n            \"commentSubjectUrl\", \"/archives/1\",\n            \"content\", \"这是回复的内容\"\n        );\n\n        var basic = new SystemSetting.Basic();\n        basic.setTitle(\"Halo\");\n        basic.setLogo(\"https://halo.run/logo\");\n        basic.setSubtitle(\"Halo\");\n        when(environmentFetcher.fetch(eq(SystemSetting.Basic.GROUP), eq(SystemSetting.Basic.class)))\n            .thenReturn(Mono.just(basic));\n\n        templateRender.render(template, model)\n            .as(StepVerifier::create)\n            .consumeNextWith(render -> {\n                assertThat(render).isEqualTo(\"\"\"\n                    亲爱的博主\n\n                      guqing 在评论“这是引用的内容”中回复了您，\n                      以下是回复的具体内容：\n                      \n                      这是回复的内容\n                       \n                    Halo\n                    http://localhost:8090\n                    祝好！\n                    查看原文：/archives/1\n                    \"\"\");\n            })\n            .verifyComplete();\n\n        verify(environmentFetcher).fetch(eq(SystemSetting.Basic.GROUP),\n            eq(SystemSetting.Basic.class));\n        verify(externalUrlSupplier).getRaw();\n    }\n\n    @Test\n    void siteUrlTest() throws MalformedURLException {\n        when(environmentFetcher.fetch(eq(SystemSetting.Basic.GROUP), eq(SystemSetting.Basic.class)))\n            .thenReturn(Mono.just(new SystemSetting.Basic()));\n\n        var template = \"<a th:href=\\\"|${site.url}/uc/notifications|\\\">查看通知</a>\";\n        var expected = \"<a href=\\\"http://localhost:8090/uc/notifications\\\">查看通知</a>\";\n\n        when(externalUrlSupplier.getRaw()).thenReturn(new URL(\"http://localhost:8090/\"));\n        templateRender.render(template,\n                Map.of())\n            .as(StepVerifier::create)\n            .expectNext(expected)\n            .verifyComplete();\n\n        when(externalUrlSupplier.getRaw()).thenReturn(new URL(\"http://localhost:8090\"));\n        templateRender.render(template,\n                Map.of())\n            .as(StepVerifier::create)\n            .expectNext(expected)\n            .verifyComplete();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/DefaultNotifierConfigStoreTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.notification.DefaultNotifierConfigStore.RECEIVER_KEY;\nimport static run.halo.app.notification.DefaultNotifierConfigStore.SECRET_NAME;\nimport static run.halo.app.notification.DefaultNotifierConfigStore.SENDER_KEY;\n\nimport com.fasterxml.jackson.databind.node.JsonNodeFactory;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Secret;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link DefaultNotifierConfigStore}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultNotifierConfigStoreTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    DefaultNotifierConfigStore notifierConfigStore;\n\n    @Test\n    void fetchReceiverConfigTest() {\n        var objectNode = mock(ObjectNode.class);\n        var spyNotifierConfigStore = spy(notifierConfigStore);\n\n        doReturn(Mono.just(objectNode)).when(spyNotifierConfigStore)\n            .fetchConfig(eq(\"fake-notifier\"));\n        var receiverConfig = mock(ObjectNode.class);\n        when(objectNode.get(eq(RECEIVER_KEY))).thenReturn(receiverConfig);\n\n        spyNotifierConfigStore.fetchReceiverConfig(\"fake-notifier\")\n            .as(StepVerifier::create)\n            .consumeNextWith(actual -> assertThat(actual).isEqualTo(receiverConfig))\n            .verifyComplete();\n        verify(objectNode).get(eq(RECEIVER_KEY));\n    }\n\n    @Test\n    void fetchSenderConfigTest() {\n        var objectNode = mock(ObjectNode.class);\n        var spyNotifierConfigStore = spy(notifierConfigStore);\n\n        doReturn(Mono.just(objectNode)).when(spyNotifierConfigStore)\n            .fetchConfig(eq(\"fake-notifier\"));\n        var senderConfig = mock(ObjectNode.class);\n        when(objectNode.get(eq(DefaultNotifierConfigStore.SENDER_KEY))).thenReturn(senderConfig);\n\n        spyNotifierConfigStore.fetchSenderConfig(\"fake-notifier\")\n            .as(StepVerifier::create)\n            .consumeNextWith(actual -> assertThat(actual).isEqualTo(senderConfig))\n            .verifyComplete();\n        verify(objectNode).get(eq(DefaultNotifierConfigStore.SENDER_KEY));\n    }\n\n    @Test\n    void fetchConfigWhenSecretNotFound() {\n        var spyNotifierConfigStore = spy(notifierConfigStore);\n\n        var objectNode = JsonNodeFactory.instance.objectNode();\n        doReturn(Mono.just(objectNode)).when(spyNotifierConfigStore)\n            .fetchConfig(eq(\"fake-notifier\"));\n\n        spyNotifierConfigStore.fetchSenderConfig(\"fake-notifier\")\n            .as(StepVerifier::create)\n            .consumeNextWith(actual -> assertThat(actual).isNotNull())\n            .verifyComplete();\n\n        spyNotifierConfigStore.fetchReceiverConfig(\"fake-notifier\")\n            .as(StepVerifier::create)\n            .consumeNextWith(actual -> assertThat(actual).isNotNull())\n            .verifyComplete();\n    }\n\n    @Test\n    void saveReceiverConfigTest() {\n        var receiverConfig = mock(ObjectNode.class);\n        var spyNotifierConfigStore = spy(notifierConfigStore);\n\n        doReturn(Mono.empty()).when(spyNotifierConfigStore)\n            .saveConfig(eq(\"fake-notifier\"), eq(RECEIVER_KEY), eq(receiverConfig));\n\n        spyNotifierConfigStore.saveReceiverConfig(\"fake-notifier\", receiverConfig)\n            .as(StepVerifier::create)\n            .verifyComplete();\n\n        verify(spyNotifierConfigStore)\n            .saveConfig(eq(\"fake-notifier\"), eq(RECEIVER_KEY), eq(receiverConfig));\n    }\n\n    @Test\n    void saveSenderConfigTest() {\n        var senderConfig = mock(ObjectNode.class);\n        var spyNotifierConfigStore = spy(notifierConfigStore);\n\n        doReturn(Mono.empty()).when(spyNotifierConfigStore)\n            .saveConfig(eq(\"fake-notifier\"), eq(SENDER_KEY), eq(senderConfig));\n\n        spyNotifierConfigStore.saveSenderConfig(\"fake-notifier\", senderConfig)\n            .as(StepVerifier::create)\n            .verifyComplete();\n\n        verify(spyNotifierConfigStore)\n            .saveConfig(eq(\"fake-notifier\"), eq(SENDER_KEY), eq(senderConfig));\n\n    }\n\n    @Test\n    void saveConfigTest() {\n        when(client.fetch(eq(Secret.class), eq(SECRET_NAME))).thenReturn(Mono.empty());\n\n        when(client.create(any(Secret.class)))\n            .thenAnswer(answer -> Mono.just(answer.getArgument(0, Secret.class)));\n        when(client.update(any(Secret.class)))\n            .thenAnswer(answer -> Mono.just(answer.getArgument(0, Secret.class)));\n\n        var objectNode = JsonNodeFactory.instance.objectNode();\n        objectNode.put(\"k1\", \"v1\");\n        notifierConfigStore.saveConfig(\"fake-notifier\", \"fake-key\", objectNode)\n            .as(StepVerifier::create)\n            .verifyComplete();\n\n        verify(client).fetch(eq(Secret.class), eq(SECRET_NAME));\n        verify(client).create(assertArg(arg -> {\n            assertThat(arg).isInstanceOf(Secret.class);\n            var secret = (Secret) arg;\n            assertThat(secret.getMetadata().getName()).isEqualTo(SECRET_NAME);\n            assertThat(secret.getMetadata().getFinalizers())\n                .contains(MetadataUtil.SYSTEM_FINALIZER);\n            assertThat(secret.getStringData()).isNotNull();\n        }));\n        verify(client).update(assertArg(arg -> {\n            assertThat(arg).isInstanceOf(Secret.class);\n            var secret = (Secret) arg;\n            assertThat(secret.getStringData().get(\"fake-notifier.json\"))\n                .isEqualTo(\"{\\\"fake-key\\\":{\\\"k1\\\":\\\"v1\\\"}}\");\n        }));\n    }\n\n    @Test\n    void fetchConfigTest() {\n        String s = \"{\\\"fake-key\\\":{\\\"k1\\\":\\\"v1\\\"}}\";\n        var objectNode = JsonUtils.jsonToObject(s, ObjectNode.class);\n        var secret = new Secret();\n        secret.setStringData(Map.of(\"fake-notifier.json\", s));\n        when(client.fetch(eq(Secret.class), eq(SECRET_NAME)))\n            .thenReturn(Mono.just(secret));\n        notifierConfigStore.fetchConfig(\"fake-notifier\")\n            .as(StepVerifier::create)\n            .consumeNextWith(actual -> assertThat(actual).isEqualTo(objectNode))\n            .verifyComplete();\n    }\n\n    @Test\n    void resolveKeyTest() {\n        assertThat(notifierConfigStore.resolveKey(\"fake-notifier\"))\n            .isEqualTo(\"fake-notifier.json\");\n        assertThat(notifierConfigStore.resolveKey(\"other-notifier\"))\n            .isEqualTo(\"other-notifier.json\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/DefaultSubscriberEmailResolverTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.AnonymousUserConst;\n\n/**\n * Tests for {@link DefaultSubscriberEmailResolver}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultSubscriberEmailResolverTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    DefaultSubscriberEmailResolver subscriberEmailResolver;\n\n    @Test\n    void testResolve() {\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(AnonymousUserConst.PRINCIPAL + \"#test@example.com\");\n        subscriberEmailResolver.resolve(subscriber)\n            .as(StepVerifier::create)\n            .expectNext(\"test@example.com\")\n            .verifyComplete();\n\n        subscriber.setName(AnonymousUserConst.PRINCIPAL + \"#\");\n        subscriberEmailResolver.resolve(subscriber)\n            .as(StepVerifier::create)\n            .verifyErrorMessage(\"The subscriber does not have an email\");\n\n        var user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(\"fake-user\");\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setEmail(\"test@halo.run\");\n        user.getSpec().setEmailVerified(false);\n        when(client.fetch(eq(User.class), eq(\"fake-user\"))).thenReturn(Mono.just(user));\n\n        subscriber.setName(\"fake-user\");\n        subscriberEmailResolver.resolve(subscriber)\n            .as(StepVerifier::create)\n            .verifyComplete();\n\n        user.getSpec().setEmailVerified(true);\n        when(client.fetch(eq(User.class), eq(\"fake-user\"))).thenReturn(Mono.just(user));\n\n        subscriber.setName(\"fake-user\");\n        subscriberEmailResolver.resolve(subscriber)\n            .as(StepVerifier::create)\n            .expectNext(\"test@halo.run\")\n            .verifyComplete();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/LanguageUtilsTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport java.util.List;\nimport java.util.Locale;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link LanguageUtils}.\n *\n * @author guqing\n * @since 2.9.0\n */\nclass LanguageUtilsTest {\n\n    @Test\n    void computeLangFromLocale() {\n        List<String> languages = LanguageUtils.computeLangFromLocale(Locale.CHINA);\n        assertThat(languages).isEqualTo(List.of(\"default\", \"zh\", \"zh_CN\"));\n\n        languages = LanguageUtils.computeLangFromLocale(Locale.CHINESE);\n        assertThat(languages).isEqualTo(List.of(\"default\", \"zh\"));\n\n        languages = LanguageUtils.computeLangFromLocale(Locale.TAIWAN);\n        assertThat(languages).isEqualTo(List.of(\"default\", \"zh\", \"zh_TW\"));\n\n        languages = LanguageUtils.computeLangFromLocale(Locale.ENGLISH);\n        assertThat(languages).isEqualTo(List.of(\"default\", \"en\"));\n\n        languages = LanguageUtils.computeLangFromLocale(Locale.US);\n        assertThat(languages).isEqualTo(List.of(\"default\", \"en\", \"en_US\"));\n\n        languages =\n            LanguageUtils.computeLangFromLocale(Locale.forLanguageTag(\"en-US-x-lvariant-POSIX\"));\n        assertThat(languages).isEqualTo(List.of(\"default\", \"en\", \"en_US\", \"en_US-POSIX\"));\n    }\n\n    @Test\n    void computeLangFromLocaleWhenLanguageIsEmpty() {\n        assertThatThrownBy(() -> {\n            LanguageUtils.computeLangFromLocale(Locale.forLanguageTag(\"\"));\n        }).isInstanceOf(IllegalArgumentException.class)\n            .hasMessage(\"Locale \\\"\\\" cannot be used as it does not specify a language.\");\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/NotificationContextTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.time.Instant;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link NotificationContext}.\n *\n * @author guqing\n * @since 2.9.0\n */\nclass NotificationContextTest {\n\n    @Test\n    void constructTest() {\n        // Create a test message payload\n        NotificationContext.MessagePayload payload = new NotificationContext.MessagePayload();\n        payload.setTitle(\"Test Title\");\n        payload.setRawBody(\"Test Body\");\n        payload.setHtmlBody(\"Html body\");\n\n        // Create a test subject\n        NotificationContext.Subject subject = NotificationContext.Subject.builder()\n            .apiVersion(\"v1\")\n            .kind(\"test\")\n            .name(\"test-name\")\n            .title(\"Test Subject\")\n            .url(\"https://example.com\")\n            .build();\n\n        // Create a test message\n        NotificationContext.Message message = new NotificationContext.Message();\n        message.setPayload(payload);\n        message.setSubject(subject);\n        message.setRecipient(\"test-recipient\");\n        message.setTimestamp(Instant.now());\n\n        // Create a test receiver config\n        ObjectMapper mapper = new ObjectMapper();\n        ObjectNode receiverConfig = mapper.createObjectNode();\n        receiverConfig.put(\"key\", \"value\");\n\n        // Create a test sender config\n        ObjectNode senderConfig = mapper.createObjectNode();\n        senderConfig.put(\"key\", \"value\");\n\n        // Create a test notification context\n        NotificationContext notificationContext = new NotificationContext();\n        notificationContext.setMessage(message);\n        notificationContext.setReceiverConfig(receiverConfig);\n        notificationContext.setSenderConfig(senderConfig);\n\n        // Test getter methods\n        assertThat(notificationContext.getMessage()).isNotNull();\n        assertThat(notificationContext.getMessage().getPayload()).isEqualTo(payload);\n        assertThat(notificationContext.getMessage().getSubject()).isEqualTo(subject);\n        assertThat(\"test-recipient\").isEqualTo(notificationContext.getMessage().getRecipient());\n        assertThat(notificationContext.getMessage().getTimestamp()).isNotNull();\n        assertThat(notificationContext.getReceiverConfig()).isEqualTo(receiverConfig);\n        assertThat(notificationContext.getSenderConfig()).isEqualTo(senderConfig);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/NotificationTriggerTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.Optional;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\n\n/**\n * Test for {@link NotificationTrigger}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass NotificationTriggerTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    NotificationCenter notificationCenter;\n\n    @InjectMocks\n    NotificationTrigger notificationTrigger;\n\n    @Test\n    void shouldCleanUpAfterNotified() {\n        var reason = mock(Reason.class);\n        var metadata = mock(Metadata.class);\n        when(reason.getMetadata()).thenReturn(metadata);\n        when(metadata.getDeletionTimestamp()).thenReturn(null);\n        when(metadata.getFinalizers()).thenReturn(Set.of());\n\n        when(client.fetch(eq(Reason.class), eq(\"fake-reason\")))\n            .thenReturn(Optional.of(reason));\n\n        when(notificationCenter.notify(eq(reason))).thenReturn(Mono.empty());\n        notificationTrigger.reconcile(new Reconciler.Request(\"fake-reason\"));\n\n        verify(notificationCenter).notify(eq(reason));\n        verify(metadata).setFinalizers(eq(Set.of(NotificationTrigger.TRIGGERED_FINALIZER)));\n        verify(client).delete(any(Reason.class));\n    }\n\n    @Test\n    void shouldRemoveFinalizerAfterDeleted() {\n        var reason = mock(Reason.class);\n        var metadata = mock(Metadata.class);\n        when(reason.getMetadata()).thenReturn(metadata);\n        when(metadata.getDeletionTimestamp()).thenReturn(Instant.now());\n        when(metadata.getFinalizers())\n            .thenReturn(Set.of(NotificationTrigger.TRIGGERED_FINALIZER));\n\n        when(client.fetch(eq(Reason.class), eq(\"fake-reason\")))\n            .thenReturn(Optional.of(reason));\n\n        notificationTrigger.reconcile(new Reconciler.Request(\"fake-reason\"));\n\n        verify(metadata).setFinalizers(eq(Set.of()));\n        verify(client).update(eq(reason));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/ReasonNotificationTemplateSelectorImplTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.stream.Stream;\nimport lombok.NonNull;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Flux;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.notification.NotificationTemplate;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link ReasonNotificationTemplateSelectorImpl}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ReasonNotificationTemplateSelectorImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    ReasonNotificationTemplateSelectorImpl templateSelector;\n\n    @Test\n    void select() {\n        when(client.listAll(eq(NotificationTemplate.class), any(), any(Sort.class)))\n            .thenReturn(Flux.fromIterable(templates()));\n        // language priority: zh_CN -> zh -> default\n        // if language is same, then compare creationTimestamp to get the latest one\n        templateSelector.select(\"new-comment-on-post\", Locale.SIMPLIFIED_CHINESE)\n            .as(StepVerifier::create)\n            .consumeNextWith(template -> {\n                assertThat(template.getMetadata().getName()).isEqualTo(\"template-2\");\n                assertThat(template.getSpec().getTemplate().getTitle()).isEqualTo(\"B\");\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void lookupTemplateByLocaleTest() {\n        Map<String, Optional<NotificationTemplate>> map = new HashMap<>();\n        map.put(\"zh_CN\", Optional.of(createNotificationTemplate(\"zh_CN-template\")));\n        map.put(\"zh\", Optional.of(createNotificationTemplate(\"zh-template\")));\n        map.put(\"default\", Optional.of(createNotificationTemplate(\"default-template\")));\n\n        var sc = ReasonNotificationTemplateSelectorImpl\n            .lookupTemplateByLocale(Locale.SIMPLIFIED_CHINESE, map);\n        assertThat(sc).isNotNull();\n        assertThat(sc.getMetadata().getName()).isEqualTo(\"zh_CN-template\");\n\n        var c = ReasonNotificationTemplateSelectorImpl\n            .lookupTemplateByLocale(Locale.CHINESE, map);\n        assertThat(c).isNotNull();\n        assertThat(c.getMetadata().getName()).isEqualTo(\"zh-template\");\n\n        var e = ReasonNotificationTemplateSelectorImpl\n            .lookupTemplateByLocale(Locale.ENGLISH, map);\n        assertThat(e).isNotNull();\n        assertThat(e.getMetadata().getName()).isEqualTo(\"default-template\");\n    }\n\n    @Test\n    void matchReasonTypeTest() {\n        var template = createNotificationTemplate(\"fake-template\");\n        assertThat(ReasonNotificationTemplateSelectorImpl.matchReasonType(\"new-comment-on-post\")\n            .test(template)).isTrue();\n\n        assertThat(ReasonNotificationTemplateSelectorImpl.matchReasonType(\"fake-reason-type\")\n            .test(template)).isFalse();\n    }\n\n    @Test\n    void getLanguageKeyTest() {\n        final var languageKeyFunc = ReasonNotificationTemplateSelectorImpl.getLanguageKey();\n        var template = createNotificationTemplate(\"fake-template\");\n        assertThat(languageKeyFunc.apply(template)).isEqualTo(\"zh_CN\");\n\n        template.getSpec().getReasonSelector().setLanguage(\"\");\n        template.getSpec().getReasonSelector().setReasonType(\"new-comment-on-post\");\n        assertThat(languageKeyFunc.apply(template)).isEqualTo(\"default\");\n    }\n\n    @NonNull\n    private static NotificationTemplate createNotificationTemplate(String name) {\n        var template = new NotificationTemplate();\n        template.setMetadata(new Metadata());\n        template.getMetadata().setName(name);\n        template.setSpec(new NotificationTemplate.Spec());\n        template.getSpec().setReasonSelector(new NotificationTemplate.ReasonSelector());\n        template.getSpec().getReasonSelector().setLanguage(\"zh_CN\");\n        template.getSpec().getReasonSelector().setReasonType(\"new-comment-on-post\");\n        return template;\n    }\n\n    List<NotificationTemplate> templates() {\n        return Stream.of(\"\"\"\n                    {\n                        \"apiVersion\": \"notification.halo.run/v1alpha1\",\n                        \"kind\": \"NotificationTemplate\",\n                        \"metadata\": {\n                            \"name\": \"template-1\",\n                            \"creationTimestamp\": \"2023-01-01T00:00:00Z\"\n                        },\n                        \"spec\": {\n                            \"reasonSelector\": {\n                                \"language\": \"zh\",\n                                \"reasonType\": \"new-comment-on-post\"\n                            },\n                            \"template\": {\n                                \"body\": \"\",\n                                \"title\": \"A\"\n                            }\n                        }\n                    }\n                    \"\"\",\n                \"\"\"\n                    {\n                        \"apiVersion\": \"notification.halo.run/v1alpha1\",\n                        \"kind\": \"NotificationTemplate\",\n                        \"metadata\": {\n                            \"name\": \"template-2\",\n                            \"creationTimestamp\": \"2023-01-01T00:00:03Z\"\n                        },\n                        \"spec\": {\n                            \"reasonSelector\": {\n                                \"language\": \"zh_CN\",\n                                \"reasonType\": \"new-comment-on-post\"\n                            },\n                            \"template\": {\n                                \"body\": \"\",\n                                \"title\": \"B\"\n                            }\n                        }\n                    }\n                    \"\"\",\n                \"\"\"\n                    {\n                        \"apiVersion\": \"notification.halo.run/v1alpha1\",\n                        \"kind\": \"NotificationTemplate\",\n                        \"metadata\": {\n                            \"name\": \"template-3\",\n                            \"creationTimestamp\": \"2023-01-01T00:00:00Z\"\n                        },\n                        \"spec\": {\n                            \"reasonSelector\": {\n                                \"language\": \"zh_CN\",\n                                \"reasonType\": \"new-comment-on-post\"\n                            },\n                            \"template\": {\n                                \"body\": \"\",\n                                \"title\": \"C\"\n                            }\n                        }\n                    }\n                    \"\"\")\n            .map(json -> JsonUtils.jsonToObject(json, NotificationTemplate.class))\n            .toList();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/ReasonPayloadTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.core.extension.notification.Reason;\n\n/**\n * Tests for {@link ReasonPayload}.\n *\n * @author guqing\n * @since 2.9.0\n */\nclass ReasonPayloadTest {\n\n    @Test\n    public void testReasonPayloadBuilder() {\n        Reason.Subject subject = Reason.Subject.builder()\n            .kind(\"Post\")\n            .apiVersion(\"content.halo.run/v1alpha1\")\n            .name(\"fake-post\")\n            .title(\"Fake post title\")\n            .url(\"https://halo.run/fake-post\")\n            .build();\n        Map<String, Object> attributes = new HashMap<>();\n        attributes.put(\"key1\", \"value1\");\n        attributes.put(\"key2\", 2);\n        attributes.put(\"key3\", \"value3\");\n\n        ReasonPayload reasonPayload = ReasonPayload.builder()\n            .subject(subject)\n            .attribute(\"key1\", \"value1\")\n            .attribute(\"key2\", 2)\n            .attributes(Map.of(\"key3\", \"value3\"))\n            .build();\n\n        assertNotNull(reasonPayload);\n        assertThat(reasonPayload).isNotNull();\n        assertThat(reasonPayload.getSubject()).isEqualTo(subject);\n        assertThat(reasonPayload.getAttributes()).isEqualTo(attributes);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/RecipientResolverImplTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.notification.Reason;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.Metadata;\n\n/**\n * Tests for {@link RecipientResolverImpl}.\n *\n * @author guqing\n * @since 2.15.0\n */\n@ExtendWith(MockitoExtension.class)\nclass RecipientResolverImplTest {\n\n    @Mock\n    private SubscriptionService subscriptionService;\n\n    @InjectMocks\n    private RecipientResolverImpl recipientResolver;\n\n    @Test\n    void testExpressionMatch() {\n        var subscriber1 = new Subscription.Subscriber();\n        subscriber1.setName(\"test\");\n        final var subscription1 = createSubscription(subscriber1);\n        subscription1.getMetadata().setName(\"test-subscription\");\n        subscription1.getSpec().getReason().setSubject(null);\n        subscription1.getSpec().getReason().setExpression(\"props.owner == 'test'\");\n\n        var subscriber2 = new Subscription.Subscriber();\n        subscriber2.setName(\"guqing\");\n        final var subscription2 = createSubscription(subscriber2);\n        subscription2.getMetadata().setName(\"guqing-subscription\");\n        subscription2.getSpec().getReason().setSubject(null);\n        subscription2.getSpec().getReason().setExpression(\"props.owner == 'guqing'\");\n\n        var reason = new Reason();\n        reason.setSpec(new Reason.Spec());\n        reason.getSpec().setReasonType(\"new-comment-on-post\");\n        reason.getSpec().setSubject(new Reason.Subject());\n        reason.getSpec().getSubject().setApiVersion(\"content.halo.run/v1alpha1\");\n        reason.getSpec().getSubject().setKind(\"Post\");\n        reason.getSpec().getSubject().setName(\"fake-post\");\n        var reasonAttributes = new ReasonAttributes();\n        reasonAttributes.put(\"owner\", \"guqing\");\n        reason.getSpec().setAttributes(reasonAttributes);\n\n        when(subscriptionService.listByPerPage(anyString()))\n            .thenReturn(Flux.just(subscription1, subscription2));\n\n        recipientResolver.resolve(reason)\n            .as(StepVerifier::create)\n            .expectNext(new Subscriber(UserIdentity.of(\"guqing\"), \"guqing-subscription\"))\n            .verifyComplete();\n\n        verify(subscriptionService).listByPerPage(anyString());\n    }\n\n    @Test\n    void testSubjectMatch() {\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(\"test\");\n        Subscription subscription = createSubscription(subscriber);\n\n        when(subscriptionService.listByPerPage(anyString()))\n            .thenReturn(Flux.just(subscription));\n\n        var reason = new Reason();\n        reason.setSpec(new Reason.Spec());\n        reason.getSpec().setReasonType(\"new-comment-on-post\");\n        reason.getSpec().setSubject(new Reason.Subject());\n        reason.getSpec().getSubject().setApiVersion(\"content.halo.run/v1alpha1\");\n        reason.getSpec().getSubject().setKind(\"Post\");\n        reason.getSpec().getSubject().setName(\"fake-post\");\n\n        recipientResolver.resolve(reason)\n            .as(StepVerifier::create)\n            .expectNext(new Subscriber(UserIdentity.of(\"test\"), \"fake-subscription\"))\n            .verifyComplete();\n\n        verify(subscriptionService).listByPerPage(anyString());\n    }\n\n    @Test\n    void distinct() {\n        // same subscriber to different subscriptions\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(\"test\");\n\n        final var subscription1 = createSubscription(subscriber);\n        subscription1.getMetadata().setName(\"sub-1\");\n\n        final var subscription2 = createSubscription(subscriber);\n        subscription2.getMetadata().setName(\"sub-2\");\n        subscription2.getSpec().getReason().setSubject(null);\n        subscription2.getSpec().getReason().setExpression(\"props.owner == 'guqing'\");\n\n        when(subscriptionService.listByPerPage(anyString()))\n            .thenReturn(Flux.just(subscription1, subscription2));\n\n        var reason = new Reason();\n        reason.setSpec(new Reason.Spec());\n        reason.getSpec().setReasonType(\"new-comment-on-post\");\n        reason.getSpec().setSubject(new Reason.Subject());\n        reason.getSpec().getSubject().setApiVersion(\"content.halo.run/v1alpha1\");\n        reason.getSpec().getSubject().setKind(\"Post\");\n        reason.getSpec().getSubject().setName(\"fake-post\");\n        var reasonAttributes = new ReasonAttributes();\n        reasonAttributes.put(\"owner\", \"guqing\");\n        reason.getSpec().setAttributes(reasonAttributes);\n\n        recipientResolver.resolve(reason)\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n\n        verify(subscriptionService).listByPerPage(anyString());\n    }\n\n    @Test\n    void subjectMatchTest() {\n        var subscriber = new Subscription.Subscriber();\n        subscriber.setName(\"test\");\n\n        final var subscription = createSubscription(subscriber);\n\n        // match all name subscription\n        var subject = new Reason.Subject();\n        subject.setApiVersion(\"content.halo.run/v1alpha1\");\n        subject.setKind(\"Post\");\n        subject.setName(\"fake-post\");\n        assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isTrue();\n\n        // different kind\n        subject = new Reason.Subject();\n        subject.setApiVersion(\"content.halo.run/v1alpha1\");\n        subject.setKind(\"SinglePage\");\n        subject.setName(\"fake-post\");\n        assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isFalse();\n\n        // special case\n        subscription.getSpec().getReason().getSubject().setName(\"other-post\");\n        subject = new Reason.Subject();\n        subject.setApiVersion(\"content.halo.run/v1alpha1\");\n        subject.setKind(\"Post\");\n        subject.setName(\"fake-post\");\n        assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isFalse();\n        subject.setName(\"other-post\");\n        assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isTrue();\n    }\n\n    private static Subscription createSubscription(Subscription.Subscriber subscriber) {\n        Subscription subscription = new Subscription();\n        subscription.setMetadata(new Metadata());\n        subscription.getMetadata().setName(\"fake-subscription\");\n        subscription.setSpec(new Subscription.Spec());\n        subscription.getSpec().setSubscriber(subscriber);\n\n        var interestReason = new Subscription.InterestReason();\n        interestReason.setReasonType(\"new-comment-on-post\");\n        interestReason.setSubject(new Subscription.ReasonSubject());\n        interestReason.getSubject().setApiVersion(\"content.halo.run/v1alpha1\");\n        interestReason.getSubject().setKind(\"Post\");\n        subscription.getSpec().setReason(interestReason);\n        return subscription;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/SubscriptionServiceImplTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.concurrent.atomic.AtomicLong;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Tests for {@link SubscriptionServiceImpl}.\n *\n * @author guqing\n * @since 2.15.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SubscriptionServiceImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private SubscriptionServiceImpl subscriptionService;\n\n    @Test\n    void remove() {\n        var i = new AtomicLong(1L);\n        when(client.delete(any(Subscription.class))).thenAnswer(invocation -> {\n            var subscription = (Subscription) invocation.getArgument(0);\n            if (i.get() != subscription.getMetadata().getVersion()) {\n                return Mono.error(new OptimisticLockingFailureException(\"fake-exception\"));\n            }\n            return Mono.just(subscription);\n        });\n\n        var subscription = new Subscription();\n        subscription.setMetadata(new Metadata());\n        subscription.getMetadata().setName(\"fake-subscription\");\n        subscription.getMetadata().setVersion(0L);\n\n        when(client.fetch(eq(Subscription.class), eq(\"fake-subscription\")))\n            .thenAnswer(invocation -> {\n                if (i.incrementAndGet() > 3) {\n                    subscription.getMetadata().setVersion(i.get());\n                } else {\n                    subscription.getMetadata().setVersion(i.get() - 1);\n                }\n                return Mono.just(subscription);\n            });\n\n        subscriptionService.remove(subscription)\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n\n        // give version=0, but the real version is 1\n        // give version=1, but the real version is 2\n        // give version=2, but the real version is 3\n        // give version=3, but the real version is 3 (delete success)\n        verify(client, times(3)).fetch(eq(Subscription.class), eq(\"fake-subscription\"));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/UserIdentityTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link UserIdentity}.\n *\n * @author guqing\n * @since 2.9.0\n */\nclass UserIdentityTest {\n\n\n    @Test\n    void getEmailTest() {\n        var identity = UserIdentity.anonymousWithEmail(\"test@example.com\");\n        assertThat(identity.getEmail().orElse(null)).isEqualTo(\"test@example.com\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/UserNotificationPreferenceServiceImplTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map;\nimport java.util.Set;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link UserNotificationPreferenceServiceImpl}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass UserNotificationPreferenceServiceImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private UserNotificationPreferenceServiceImpl userNotificationPreferenceService;\n\n    @Test\n    void getByUser() {\n        var configMap = new ConfigMap();\n        configMap.setData(Map.of(\"notification\",\n            \"{\\\"reasonTypeNotifier\\\":{\\\"comment\\\":{\\\"notifiers\\\":[\\\"test-notifier\\\"]}}}\"));\n        when(client.fetch(ConfigMap.class, \"user-preferences-guqing\"))\n            .thenReturn(Mono.just(configMap));\n        userNotificationPreferenceService.getByUser(\"guqing\")\n            .as(StepVerifier::create)\n            .consumeNextWith(preference -> {\n                assertThat(preference.getReasonTypeNotifier()).isNotNull();\n                assertThat(preference.getReasonTypeNotifier().get(\"comment\")).isNotNull();\n                assertThat(preference.getReasonTypeNotifier().get(\"comment\").getNotifiers())\n                    .containsExactly(\"test-notifier\");\n            })\n            .verifyComplete();\n\n        verify(client).fetch(ConfigMap.class, \"user-preferences-guqing\");\n    }\n\n    @Test\n    void getByUserWhenNotFound() {\n        when(client.fetch(ConfigMap.class, \"user-preferences-guqing\"))\n            .thenReturn(Mono.empty());\n        userNotificationPreferenceService.getByUser(\"guqing\")\n            .as(StepVerifier::create)\n            .consumeNextWith(preference ->\n                assertThat(preference.getReasonTypeNotifier()).isNotNull()\n            )\n            .verifyComplete();\n\n        verify(client).fetch(ConfigMap.class, \"user-preferences-guqing\");\n    }\n\n    @Test\n    void getByUserWhenConfigDataNotFound() {\n        when(client.fetch(ConfigMap.class, \"user-preferences-guqing\"))\n            .thenReturn(Mono.just(new ConfigMap()));\n        userNotificationPreferenceService.getByUser(\"guqing\")\n            .as(StepVerifier::create)\n            .consumeNextWith(preference ->\n                assertThat(preference.getReasonTypeNotifier()).isNotNull()\n            )\n            .verifyComplete();\n\n        verify(client).fetch(ConfigMap.class, \"user-preferences-guqing\");\n    }\n\n\n    @Nested\n    class UserNotificationPreferenceTest {\n\n        @Test\n        void getNotifiers() {\n            var preference = new UserNotificationPreference();\n            preference.getReasonTypeNotifier().put(\"comment\", null);\n            // key doesn't exist\n            assertThat(preference.getReasonTypeNotifier().getNotifiers(\"comment\"))\n                .containsExactly(\"default-email-notifier\");\n\n            // key exists but the value is null\n            preference.getReasonTypeNotifier()\n                .put(\"comment\", new UserNotificationPreference.NotifierSetting());\n            assertThat(preference.getReasonTypeNotifier().getNotifiers(\"comment\")).isEmpty();\n\n            // key exists and the value is not null\n            preference.getReasonTypeNotifier().get(\"comment\").setNotifiers(Set.of(\"test-notifier\"));\n            assertThat(preference.getReasonTypeNotifier().getNotifiers(\"comment\"))\n                .containsExactly(\"test-notifier\");\n        }\n\n        @Test\n        void toJson() throws JSONException {\n            var preference = new UserNotificationPreference();\n            preference.getReasonTypeNotifier().put(\"comment\",\n                new UserNotificationPreference.NotifierSetting());\n            preference.getReasonTypeNotifier().get(\"comment\").setNotifiers(Set.of(\"test-notifier\"));\n\n            JSONAssert.assertEquals(\"\"\"\n                    {\n                      \"reasonTypeNotifier\": {\n                        \"comment\": {\n                            \"notifiers\": [\n                                \"test-notifier\"\n                            ]\n                        }\n                      }\n                    }\n                    \"\"\",\n                JsonUtils.objectToJson(preference),\n                true);\n        }\n    }\n\n    @Test\n    void buildUserPreferenceConfigMapName() {\n        var preferenceConfigMapName = UserNotificationPreferenceServiceImpl\n            .buildUserPreferenceConfigMapName(\"guqing\");\n        assertEquals(\"user-preferences-guqing\", preferenceConfigMapName);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/UserNotificationPreferenceTest.java",
    "content": "package run.halo.app.notification;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link UserNotificationPreference}.\n *\n * @author guqing\n * @since 2.9.0\n */\nclass UserNotificationPreferenceTest {\n\n    @Test\n    void preferenceCreation() {\n        String s = \"\"\"\n            {\n              \"reasonTypeNotifier\": {\n                \"comment\": {\n                  \"notifiers\": [\n                    \"email-notifier\",\n                    \"sms-notifier\"\n                  ]\n                },\n                \"new-post\": {\n                  \"notifiers\": [\n                    \"email-notifier\",\n                    \"webhook-router-notifier\"\n                  ]\n                }\n              }\n            }\n            \"\"\";\n        var preference = JsonUtils.jsonToObject(s, UserNotificationPreference.class);\n        assertThat(preference.getReasonTypeNotifier()).isNotNull();\n        assertThat(preference.getReasonTypeNotifier().get(\"comment\").getNotifiers())\n            .containsExactlyInAnyOrder(\"email-notifier\", \"sms-notifier\");\n        assertThat(preference.getReasonTypeNotifier().get(\"new-post\").getNotifiers())\n            .containsExactlyInAnyOrder(\"email-notifier\", \"webhook-router-notifier\");\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/endpoint/SubscriptionRouterTest.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.core.extension.notification.Subscription;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.ExternalUrlSupplier;\n\n/**\n * Tests for {@link SubscriptionRouter}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SubscriptionRouterTest {\n\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @InjectMocks\n    SubscriptionRouter subscriptionRouter;\n\n    @Test\n    void getUnsubscribeUrlTest() throws MalformedURLException {\n        when(externalUrlSupplier.getRaw()).thenReturn(URI.create(\"https://halo.run\").toURL());\n        var subscription = new Subscription();\n        subscription.setMetadata(new Metadata());\n        subscription.getMetadata().setName(\"fake-subscription\");\n        subscription.setSpec(new Subscription.Spec());\n        subscription.getSpec().setUnsubscribeToken(\"fake-unsubscribe-token\");\n\n        var url = subscriptionRouter.getUnsubscribeUrl(subscription);\n        assertThat(url).isEqualTo(\"https://halo.run/apis/api.notification.halo.run/v1alpha1\"\n            + \"/subscriptions/fake-subscription/unsubscribe\"\n            + \"?token=fake-unsubscribe-token\");\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpointTest.java",
    "content": "package run.halo.app.notification.endpoint;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.notification.NotifierDescriptor;\nimport run.halo.app.core.extension.notification.ReasonType;\nimport run.halo.app.extension.ExtensionUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.notification.UserNotificationPreferenceService;\n\n/**\n * Tests for {@link UserNotificationPreferencesEndpoint}.\n *\n * @author guqing\n * @since 2.10.0\n */\n@ExtendWith(MockitoExtension.class)\nclass UserNotificationPreferencesEndpointTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private UserNotificationPreferenceService userNotificationPreferenceService;\n\n    @InjectMocks\n    private UserNotificationPreferencesEndpoint userNotificationPreferencesEndpoint;\n\n    private WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        webTestClient = WebTestClient\n            .bindToRouterFunction(userNotificationPreferencesEndpoint.endpoint())\n            .build();\n\n        when(client.listAll(eq(ReasonType.class), assertArg(option ->\n                assertThat(option.toString())\n                    .isEqualTo(\"NOT EXISTS metadata.labels['halo.run/hidden']\")),\n            eq(ExtensionUtil.defaultSort()))\n        ).thenReturn(Flux.empty());\n    }\n\n    @Test\n    void listNotificationPreferences() {\n        when(client.list(eq(NotifierDescriptor.class), eq(null), any())).thenReturn(Flux.empty());\n        when(userNotificationPreferenceService.getByUser(any())).thenReturn(Mono.empty());\n        webTestClient.post()\n            .uri(\"/userspaces/{username}/notification-preferences\", \"guqing\")\n            .exchange()\n            .expectStatus()\n            .isOk();\n    }\n\n    @Test\n    void saveNotificationPreferences() {\n        when(client.list(eq(NotifierDescriptor.class), eq(null), any())).thenReturn(Flux.empty());\n        when(userNotificationPreferenceService.getByUser(any())).thenReturn(Mono.empty());\n        webTestClient.post()\n            .uri(\"/userspaces/{username}/notification-preferences\", \"guqing\")\n            .exchange()\n            .expectStatus()\n            .isOk();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/BuiltInPluginsInitializerTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.core.extension.Plugin.BUILT_IN_KEEPER_FINALIZER;\nimport static run.halo.app.core.extension.Plugin.SYSTEM_RESERVED_LABEL_KEY;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.support.ResourcePatternResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.ExtensionInitializedEvent;\nimport run.halo.app.infra.exception.PluginAlreadyExistsException;\n\n@ExtendWith(MockitoExtension.class)\nclass BuiltInPluginsInitializerTest {\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    PluginService pluginService;\n\n    @Mock\n    ResourcePatternResolver resourcePatternResolver;\n\n    @Mock\n    PluginFinder pluginFinder;\n\n    @InjectMocks\n    BuiltInPluginsInitializer initializer;\n\n    @BeforeEach\n    void setUp() {\n        initializer.setResourcePatternResolver(resourcePatternResolver);\n        initializer.setPluginFinder(pluginFinder);\n    }\n\n    @Test\n    void shouldCreateBuiltInPlugins() throws IOException {\n        var resource = mock(Resource.class);\n        when(resource.getFilename()).thenReturn(\"fake-plugin.jar\");\n        var pluginUri = URI.create(\"file:///fake-plugin.jar\");\n        var pluginPath = Path.of(pluginUri);\n        when(resource.getURI()).thenReturn(pluginUri);\n        when(resourcePatternResolver.getResources(isA(String.class)))\n            .thenReturn(new Resource[] {resource});\n\n        var fakePlugin = createPlugin();\n        when(pluginFinder.find(pluginPath)).thenReturn(fakePlugin);\n        when(pluginService.install(pluginPath)).thenReturn(Mono.just(fakePlugin));\n        doNothing().when(client).update(fakePlugin);\n        var event = mock(ExtensionInitializedEvent.class);\n\n        initializer.onApplicationEvent(event);\n\n        assertEquals(\"true\", fakePlugin.getMetadata().getLabels().get(SYSTEM_RESERVED_LABEL_KEY));\n        assertTrue(fakePlugin.getMetadata().getFinalizers().contains(BUILT_IN_KEEPER_FINALIZER));\n    }\n\n    @Test\n    void shouldUpgradeBuiltInPlugins() throws IOException {\n        var resource = mock(Resource.class);\n        when(resource.getFilename()).thenReturn(\"fake-plugin.jar\");\n        var pluginUri = URI.create(\"file:///fake-plugin.jar\");\n        var pluginPath = Path.of(pluginUri);\n        when(resource.getURI()).thenReturn(pluginUri);\n        when(resourcePatternResolver.getResources(isA(String.class)))\n            .thenReturn(new Resource[] {resource});\n\n        var fakePlugin = createPlugin();\n        when(pluginFinder.find(pluginPath)).thenReturn(fakePlugin);\n        when(pluginService.install(pluginPath))\n            .thenReturn(Mono.error(new PluginAlreadyExistsException(\"fake-plugin\")));\n        when(pluginService.upgrade(\"fake-plugin\", pluginPath))\n            .thenReturn(Mono.just(fakePlugin));\n        doNothing().when(client).update(fakePlugin);\n        var event = mock(ExtensionInitializedEvent.class);\n\n        initializer.onApplicationEvent(event);\n\n        assertEquals(\"true\", fakePlugin.getMetadata().getLabels().get(SYSTEM_RESERVED_LABEL_KEY));\n        assertTrue(fakePlugin.getMetadata().getFinalizers().contains(BUILT_IN_KEEPER_FINALIZER));\n    }\n\n    @Test\n    void shouldResetDeletionTimestamp() throws IOException {\n        var resource = mock(Resource.class);\n        when(resource.getFilename()).thenReturn(\"fake-plugin.jar\");\n        var pluginUri = URI.create(\"file:///fake-plugin.jar\");\n        var pluginPath = Path.of(pluginUri);\n        when(resource.getURI()).thenReturn(pluginUri);\n        when(resourcePatternResolver.getResources(isA(String.class)))\n            .thenReturn(new Resource[] {resource});\n\n        var fakePlugin = createPlugin();\n        fakePlugin.getMetadata().setDeletionTimestamp(Instant.now());\n        when(pluginFinder.find(pluginPath)).thenReturn(fakePlugin);\n        when(pluginService.install(pluginPath)).thenReturn(Mono.just(fakePlugin));\n        doNothing().when(client).update(fakePlugin);\n\n        var event = mock(ExtensionInitializedEvent.class);\n        initializer.onApplicationEvent(event);\n\n        assertNull(fakePlugin.getMetadata().getDeletionTimestamp());\n    }\n\n    Plugin createPlugin() {\n        var plugin = new Plugin();\n        plugin.setMetadata(new Metadata());\n        plugin.getMetadata().setName(\"fake-plugin\");\n        return plugin;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/DefaultDevelopmentPluginRepositoryTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.pf4j.PluginRepository;\n\n/**\n * Tests for {@link DefaultDevelopmentPluginRepository}.\n *\n * @author guqing\n * @since 2.8.0\n */\nclass DefaultDevelopmentPluginRepositoryTest {\n\n    private PluginRepository developmentPluginRepository;\n\n    @TempDir\n    private Path tempDir;\n\n    @BeforeEach\n    void setUp() {\n        var repository = new DefaultDevelopmentPluginRepository();\n        repository.setFixedPaths(List.of(tempDir));\n        this.developmentPluginRepository = repository;\n    }\n\n    @Test\n    void deletePluginPath() {\n        boolean deleted = developmentPluginRepository.deletePluginPath(null);\n        assertThat(deleted).isFalse();\n\n        // deletePluginPath is a no-op\n        deleted = developmentPluginRepository.deletePluginPath(tempDir);\n        assertThat(deleted).isTrue();\n        assertThat(Files.exists(tempDir)).isTrue();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/DefaultPluginApplicationContextFactoryTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.Mockito.withSettings;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.pf4j.Plugin;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport run.halo.app.search.SearchService;\n\n@SpringBootTest\nclass DefaultPluginApplicationContextFactoryTest {\n\n    @MockitoSpyBean\n    SpringPluginManager pluginManager;\n\n    DefaultPluginApplicationContextFactory factory;\n\n    @BeforeEach\n    void setUp() {\n        factory = new DefaultPluginApplicationContextFactory(pluginManager);\n    }\n\n    @Test\n    void shouldCreateCorrectly() {\n        var pw = mock(PluginWrapper.class);\n        when(pw.getPluginClassLoader()).thenReturn(this.getClass().getClassLoader());\n        var plugin = mock(Plugin.class, withSettings().extraInterfaces(SpringPlugin.class));\n        var sp = (SpringPlugin) plugin;\n        var pluginContext = new PluginContext.PluginContextBuilder()\n            .name(\"fake-plugin\")\n            .version(\"1.0.0\")\n            .build();\n        when(sp.getPluginContext()).thenReturn(pluginContext);\n        when(pw.getPlugin()).thenReturn(plugin);\n        when(pluginManager.getPlugin(\"fake-plugin\")).thenReturn(pw);\n        var context = factory.create(\"fake-plugin\");\n\n        assertEquals(pw.getPluginClassLoader(), Thread.currentThread().getContextClassLoader());\n\n        assertInstanceOf(PluginApplicationContext.class, context);\n        assertNotNull(context.getBeanProvider(SearchService.class).getIfUnique());\n        assertNotNull(context.getBeanProvider(PluginsRootGetter.class).getIfUnique());\n        // TODO Add more assertions here.\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistryTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\n\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\n/**\n * Tests for {@link DefaultPluginRouterFunctionRegistry}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultPluginRouterFunctionRegistryTest {\n\n    @InjectMocks\n    DefaultPluginRouterFunctionRegistry routerFunctionRegistry;\n\n    @Test\n    void shouldRegisterRouterFunction() {\n        RouterFunction<ServerResponse> routerFunction = mock(InvocationOnMock::getMock);\n        routerFunctionRegistry.register(Set.of(routerFunction));\n        assertEquals(Set.of(routerFunction), routerFunctionRegistry.getRouterFunctions());\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/DefaultSettingFetcherTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.test.context.bean.override.mockito.MockitoBean;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link DefaultSettingFetcher}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass DefaultSettingFetcherTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @MockitoBean\n    private final PluginContext pluginContext = PluginContext.builder()\n        .name(\"fake\")\n        .configMapName(\"fake-config\")\n        .build();\n\n    @Mock\n    private ApplicationContext applicationContext;\n\n    private DefaultReactiveSettingFetcher reactiveSettingFetcher;\n    private DefaultSettingFetcher settingFetcher;\n\n    @BeforeEach\n    void setUp() {\n        this.reactiveSettingFetcher = new DefaultReactiveSettingFetcher(pluginContext, client);\n        reactiveSettingFetcher.setApplicationContext(applicationContext);\n\n        settingFetcher = new DefaultSettingFetcher(reactiveSettingFetcher);\n\n        ConfigMap configMap = buildConfigMap();\n        when(client.fetch(eq(ConfigMap.class), eq(pluginContext.getConfigMapName())))\n            .thenReturn(Mono.just(configMap));\n    }\n\n    @Test\n    void getValues() throws JSONException {\n        Map<String, JsonNode> values = settingFetcher.getValues();\n\n        verify(client, times(1)).fetch(eq(ConfigMap.class), any());\n\n        assertThat(values).hasSize(2);\n        JSONAssert.assertEquals(getSns(), JsonUtils.objectToJson(values.get(\"sns\")), true);\n\n        // The extensionClient will only be called once\n        Map<String, JsonNode> callAgain = settingFetcher.getValues();\n        assertThat(callAgain).isNotNull();\n        verify(client, times(1)).fetch(eq(ConfigMap.class), any());\n    }\n\n    @Test\n    void getValuesWithUpdateCache() throws JSONException {\n        Map<String, JsonNode> values = settingFetcher.getValues();\n\n        verify(client, times(1)).fetch(eq(ConfigMap.class), any());\n        JSONAssert.assertEquals(getSns(), JsonUtils.objectToJson(values.get(\"sns\")), true);\n\n        ConfigMap configMap = buildConfigMap();\n        configMap.getData().put(\"sns\", \"\"\"\n            {\n                \"email\": \"abc@example.com\",\n                \"github\": \"abc\"\n            }\n            \"\"\");\n        when(client.fetch(eq(ConfigMap.class), eq(pluginContext.getConfigMapName())))\n            .thenReturn(Mono.just(configMap));\n        when(client.update(configMap)).thenReturn(Mono.just(configMap));\n        reactiveSettingFetcher.reconcile(new Reconciler.Request(pluginContext.getConfigMapName()));\n\n        // Make sure the method cache#put is called before the event is published\n        // to avoid the event listener to fetch the old value from the cache\n        verify(applicationContext).publishEvent(isA(PluginConfigUpdatedEvent.class));\n\n        Map<String, JsonNode> updatedValues = settingFetcher.getValues();\n        verify(client, times(3)).fetch(eq(ConfigMap.class), any());\n        assertThat(updatedValues).hasSize(2);\n        JSONAssert.assertEquals(configMap.getData().get(\"sns\"),\n            JsonUtils.objectToJson(updatedValues.get(\"sns\")), true);\n\n        updatedValues = settingFetcher.getValues();\n        assertThat(updatedValues).hasSize(2);\n        verify(client, times(3)).fetch(eq(ConfigMap.class), any());\n    }\n\n    @Test\n    void getGroupForObject() throws JSONException {\n        Optional<Sns> sns = settingFetcher.fetch(\"sns\", Sns.class);\n        assertThat(sns.isEmpty()).isFalse();\n        JSONAssert.assertEquals(getSns(), JsonUtils.objectToJson(sns.get()), true);\n    }\n\n    @Test\n    void getGroup() {\n        JsonNode jsonNode = settingFetcher.get(\"basic\");\n        assertThat(jsonNode).isNotNull();\n        assertThat(jsonNode.isObject()).isTrue();\n        assertThat(jsonNode.get(\"color\").asText()).isEqualTo(\"red\");\n        assertThat(jsonNode.get(\"width\").asInt()).isEqualTo(100);\n\n        // missing key will return empty json node\n        JsonNode emptyNode = settingFetcher.get(\"basic1\");\n        assertThat(emptyNode.isEmpty()).isTrue();\n    }\n\n    private ConfigMap buildConfigMap() {\n        ConfigMap configMap = new ConfigMap();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake\");\n        metadata.setLabels(Map.of(\"plugin.halo.run/plugin-name\", \"fake\"));\n        configMap.setMetadata(metadata);\n        configMap.setKind(\"ConfigMap\");\n        configMap.setApiVersion(\"v1alpha1\");\n        var map = new HashMap<String, String>();\n        map.put(\"sns\", getSns());\n        map.put(\"basic\", \"\"\"\n            {\n                \"color\": \"red\",\n                \"width\": \"100\"\n            }\n            \"\"\");\n        configMap.setData(map);\n        return configMap;\n    }\n\n    String getSns() {\n        return \"\"\"\n            {\n                \"email\": \"example@example.com\",\n                \"github\": \"example\",\n                \"instagram\": \"123\",\n                \"twitter\": \"halo-dev\",\n                \"user\": {\n                \"name\": \"guqing\",\n                \"age\": \"18\"\n                },\n                \"nums\": [1, 2, 3]\n            }\n            \"\"\";\n    }\n\n    record Sns(String email, String github, String instagram, String twitter,\n               Map<String, Object> user, List<Integer> nums) {\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/HaloPluginManagerTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.RuntimeMode;\nimport org.springframework.context.ApplicationContext;\nimport run.halo.app.infra.SystemVersionSupplier;\n\n@ExtendWith(MockitoExtension.class)\nclass HaloPluginManagerTest {\n\n    @Mock\n    PluginProperties pluginProperties;\n\n    @Mock\n    SystemVersionSupplier systemVersionSupplier;\n\n    @Mock\n    PluginsRootGetter pluginsRootGetter;\n\n    @Mock\n    ApplicationContext rootContext;\n\n    @InjectMocks\n    HaloPluginManager pluginManager;\n\n    @TempDir\n    Path tempDir;\n\n    @Test\n    void shouldGetDependentsWhilePluginsNotResolved() throws Exception {\n        when(pluginProperties.getRuntimeMode()).thenReturn(RuntimeMode.DEPLOYMENT);\n        when(systemVersionSupplier.get()).thenReturn(Version.of(1, 2, 3));\n        when(pluginsRootGetter.get()).thenReturn(tempDir);\n        pluginManager.afterPropertiesSet();\n        // if we don't invoke resolves\n        var dependents = pluginManager.getDependents(\"fake-plugin\");\n        assertTrue(dependents.isEmpty());\n    }\n\n    @Test\n    void shouldGetDependentsWhilePluginsResolved() throws Exception {\n        when(pluginProperties.getRuntimeMode()).thenReturn(RuntimeMode.DEPLOYMENT);\n        when(systemVersionSupplier.get()).thenReturn(Version.of(1, 2, 3));\n        when(pluginsRootGetter.get()).thenReturn(tempDir);\n        pluginManager.afterPropertiesSet();\n        pluginManager.loadPlugins();\n        // if we don't invoke resolves\n        var dependents = pluginManager.getDependents(\"fake-plugin\");\n        assertTrue(dependents.isEmpty());\n    }\n\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/OptionalDependentResolverTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.pf4j.PluginDependency;\nimport org.pf4j.PluginDescriptor;\n\n/**\n * Tests for {@link OptionalDependentResolver}.\n *\n * @author guqing\n * @since 2.20.11\n */\nclass OptionalDependentResolverTest {\n\n    @Test\n    void testNoPlugins() {\n        OptionalDependentResolver resolver = new OptionalDependentResolver(List.of());\n        assertTrue(resolver.getOptionalDependents(\"nonexistent\").isEmpty(),\n            \"No dependents expected for non-existent plugin\");\n    }\n\n    @Test\n    void testSinglePluginNoDependencies() {\n        var pluginA = createpluginDescriptor(\"A\", List.of());\n        OptionalDependentResolver resolver = new OptionalDependentResolver(List.of(pluginA));\n\n        assertTrue(resolver.getOptionalDependents(\"A\").isEmpty(), \"A has no dependents\");\n    }\n\n    @Test\n    void testSingleOptionalDependency() {\n        var pluginA = createpluginDescriptor(\"A\", List.of(new PluginDependency(\"B?\")));\n        var pluginB = createpluginDescriptor(\"B\", List.of());\n        OptionalDependentResolver resolver =\n            new OptionalDependentResolver(List.of(pluginA, pluginB));\n\n        assertEquals(List.of(\"A\"), resolver.getOptionalDependents(\"B\"),\n            \"B should have A as dependent\");\n        assertTrue(resolver.getOptionalDependents(\"A\").isEmpty(), \"A has no dependents\");\n    }\n\n    @Test\n    void testMultipleOptionalDependencies() {\n        var pluginA = createpluginDescriptor(\"A\", List.of(\n            new PluginDependency(\"B?\"),\n            new PluginDependency(\"C?\"))\n        );\n\n        var pluginB = createpluginDescriptor(\"B\", List.of());\n\n        var pluginC = createpluginDescriptor(\"C\", List.of());\n\n        OptionalDependentResolver resolver =\n            new OptionalDependentResolver(List.of(pluginA, pluginB, pluginC));\n\n        assertEquals(List.of(\"A\"), resolver.getOptionalDependents(\"B\"),\n            \"B should have A as dependent\");\n        assertEquals(List.of(\"A\"), resolver.getOptionalDependents(\"C\"),\n            \"C should have A as dependent\");\n        assertTrue(resolver.getOptionalDependents(\"A\").isEmpty(), \"A has no dependents\");\n    }\n\n    @Test\n    void testNestedDependencies() {\n        var pluginA = createpluginDescriptor(\"A\", List.of(\n            new PluginDependency(\"B?\")\n        ));\n        var pluginB = createpluginDescriptor(\"B\", List.of(\n            new PluginDependency(\"C?\")\n        ));\n        var pluginC = createpluginDescriptor(\"C\", List.of());\n\n        OptionalDependentResolver resolver =\n            new OptionalDependentResolver(List.of(pluginA, pluginB, pluginC));\n\n        assertEquals(List.of(\"B\"), resolver.getOptionalDependents(\"C\"),\n            \"C should have B as dependent\");\n        assertEquals(List.of(\"A\"), resolver.getOptionalDependents(\"B\"),\n            \"B should have A as dependent\");\n        assertTrue(resolver.getOptionalDependents(\"A\").isEmpty(), \"A has no dependents\");\n    }\n\n    @Test\n    void testCircularDependencies() {\n        var pluginA = createpluginDescriptor(\"A\", List.of(\n            new PluginDependency(\"B?\")\n        ));\n        var pluginB = createpluginDescriptor(\"B\", List.of(\n            new PluginDependency(\"A?\")\n        ));\n        OptionalDependentResolver resolver =\n            new OptionalDependentResolver(List.of(pluginA, pluginB));\n\n        assertEquals(List.of(\"B\"), resolver.getOptionalDependents(\"A\"),\n            \"A should have B as dependent\");\n        assertEquals(List.of(\"A\"), resolver.getOptionalDependents(\"B\"),\n            \"B should have A as dependent\");\n    }\n\n    @Test\n    void testNonOptionalDependencies() {\n        var pluginA = createpluginDescriptor(\"A\", List.of(\n            new PluginDependency(\"B\")\n        ));\n        var pluginB = createpluginDescriptor(\"B\", List.of());\n        OptionalDependentResolver resolver =\n            new OptionalDependentResolver(List.of(pluginA, pluginB));\n\n        assertTrue(resolver.getOptionalDependents(\"B\").isEmpty(), \"B should have no dependents\");\n        assertTrue(resolver.getOptionalDependents(\"A\").isEmpty(), \"A should have no dependents\");\n    }\n\n    PluginDescriptor createpluginDescriptor(String pluginName,\n        List<PluginDependency> dependencies) {\n        var descriptor = mock(PluginDescriptor.class);\n        lenient().when(descriptor.getPluginId()).thenReturn(pluginName);\n        lenient().when(descriptor.getDependencies()).thenReturn(dependencies);\n        return descriptor;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/PluginExtensionLoaderUtilsTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static run.halo.app.plugin.PluginExtensionLoaderUtils.isSetting;\nimport static run.halo.app.plugin.PluginExtensionLoaderUtils.lookupExtensions;\n\nimport java.io.IOException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.util.Arrays;\nimport java.util.Objects;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.core.io.DefaultResourceLoader;\nimport run.halo.app.infra.utils.YamlUnstructuredLoader;\n\nclass PluginExtensionLoaderUtilsTest {\n\n    @Test\n    void lookupExtensionsAndIsSettingTest() throws IOException {\n        var resourceLoader = new DefaultResourceLoader();\n        var rootResource = resourceLoader.getResource(\"classpath:plugin/plugin-0.0.1/\");\n        var classLoader = new URLClassLoader(new URL[] {rootResource.getURL()}, null);\n        var resources = lookupExtensions(classLoader);\n        assertTrue(resources.length >= 1);\n        var settingResource = Arrays.stream(resources)\n            .filter(r -> Objects.equals(\"setting.yaml\", r.getFilename()))\n            .findFirst()\n            .orElseThrow();\n\n        var loader = new YamlUnstructuredLoader(settingResource);\n        var unstructuredList = loader.load();\n        assertEquals(1, unstructuredList.size());\n        assertTrue(isSetting(\"fake-setting\").test(unstructuredList.get(0)));\n        assertFalse(isSetting(\"non-fake-setting\").test(unstructuredList.get(0)));\n        assertFalse(isSetting(\"\").test(unstructuredList.get(0)));\n        assertFalse(isSetting(null).test(unstructuredList.get(0)));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/PluginRequestMappingHandlerMappingTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\nimport static org.springframework.mock.http.server.reactive.MockServerHttpRequest.get;\nimport static org.springframework.mock.http.server.reactive.MockServerHttpRequest.post;\nimport static org.springframework.web.bind.annotation.RequestMethod.GET;\nimport static org.springframework.web.bind.annotation.RequestMethod.HEAD;\n\nimport java.lang.reflect.Method;\nimport java.security.Principal;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.stream.Collectors;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.core.MethodIntrospector;\nimport org.springframework.core.annotation.AnnotatedElementUtils;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.util.Assert;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.context.support.StaticWebApplicationContext;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.reactive.result.method.RequestMappingInfo;\nimport org.springframework.web.server.MethodNotAllowedException;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.util.pattern.PathPatternParser;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\n/**\n * Tests for {@link PluginRequestMappingHandlerMapping}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass PluginRequestMappingHandlerMappingTest {\n\n    private final StaticWebApplicationContext wac = new StaticWebApplicationContext();\n\n    private PluginRequestMappingHandlerMapping handlerMapping;\n\n\n    @BeforeEach\n    public void setup() {\n        handlerMapping = new PluginRequestMappingHandlerMapping();\n        this.handlerMapping.setApplicationContext(wac);\n    }\n\n    @Test\n    public void shouldAddPathPrefixWhenExistingApiVersion() throws Exception {\n        Method method = UserController.class.getMethod(\"getUser\");\n        RequestMappingInfo info =\n            this.handlerMapping.getPluginMappingForMethod(\"fakePlugin\", method,\n                UserController.class);\n\n        assertThat(info).isNotNull();\n        assertThat(info.getPatternsCondition().getPatterns()).isEqualTo(\n            Collections.singleton(\n                new PathPatternParser().parse(\n                    \"/apis/api.plugin.halo.run/v1alpha1/plugins/fakePlugin/user/{id}\")));\n    }\n\n    @Test\n    public void shouldKeepRawWhenMissingApiVersion() throws Exception {\n        Method method = AppleMissingApiVersionController.class.getMethod(\"getName\");\n        RequestMappingInfo info =\n            this.handlerMapping.getPluginMappingForMethod(\"fakePlugin\", method,\n                AppleMissingApiVersionController.class);\n\n        assertThat(info.getPatternsCondition().getPatterns())\n            .isEqualTo(Collections.singleton(new PathPatternParser().parse(\"/apples\")));\n    }\n\n    @Test\n    void registerHandlerMethods() {\n        assertThat(handlerMapping.getMappings(\"fakePlugin\")).isEmpty();\n\n        UserController userController = mock(UserController.class);\n        handlerMapping.registerHandlerMethods(\"fakePlugin\", userController);\n\n        List<RequestMappingInfo> mappings = handlerMapping.getMappings(\"fakePlugin\");\n        assertThat(mappings).hasSize(1);\n        assertThat(mappings.get(0).toString()).isEqualTo(\n            \"{GET /apis/api.plugin.halo.run/v1alpha1/plugins/fakePlugin/user/{id}}\");\n    }\n\n    @Test\n    void unregister() {\n        UserController userController = mock(UserController.class);\n        // register handler methods first\n        handlerMapping.registerHandlerMethods(\"fakePlugin\", userController);\n        assertThat(handlerMapping.getMappings(\"fakePlugin\")).hasSize(1);\n\n        // unregister\n        handlerMapping.unregister(\"fakePlugin\");\n        assertThat(handlerMapping.getMappings(\"fakePlugin\")).isEmpty();\n    }\n\n    @Test\n    public void getHandlerDirectMatch() {\n        // register handler methods first\n        handlerMapping.registerHandlerMethods(\"fakePlugin\", new TestController());\n\n        // resolve an expected method from TestController\n        Method expected =\n            ResolvableMethod.on(TestController.class).annot(getMapping(\"/foo\")).build();\n\n        // get handler by mock exchange\n        ServerWebExchange exchange =\n            MockServerWebExchange.from(\n                get(\"/apis/api.plugin.halo.run/v1alpha1/plugins/fakePlugin/foo\"));\n        HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();\n\n        assertThat(hm).isNotNull();\n        assertThat(hm.getMethod()).isEqualTo(expected);\n    }\n\n    @Test\n    public void getHandlerBestMatch() {\n        // register handler methods first\n        handlerMapping.registerHandlerMethods(\"fakePlugin\", new TestController());\n\n        Method expected =\n            ResolvableMethod.on(TestController.class).annot(getMapping(\"/foo\").params(\"p\")).build();\n\n        String requestPath = \"/apis/api.plugin.halo.run/v1alpha1/plugins/fakePlugin/foo?p=anything\";\n        ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));\n        HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();\n\n        assertThat(hm).isNotNull();\n        assertThat(hm.getMethod()).isEqualTo(expected);\n    }\n\n    @Test\n    public void getHandlerRootPathMatch() {\n        // register handler methods first\n        handlerMapping.registerHandlerMethods(\"fakePlugin\", new TestController());\n        Method expected =\n            ResolvableMethod.on(TestController.class).annot(getMapping(\"\")).build();\n\n        String requestPath = \"/apis/api.plugin.halo.run/v1alpha1/plugins/fakePlugin\";\n        ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));\n        HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();\n\n        assertThat(hm).isNotNull();\n        assertThat(hm.getMethod()).isEqualTo(expected);\n    }\n\n    @Test\n    public void getHandlerRequestMethodNotAllowed() {\n        // register handler methods first\n        handlerMapping.registerHandlerMethods(\"fakePlugin\", new TestController());\n\n        String requestPath = \"/apis/api.plugin.halo.run/v1alpha1/plugins/fakePlugin/bar\";\n        ServerWebExchange exchange = MockServerWebExchange.from(post(requestPath));\n        Mono<Object> mono = this.handlerMapping.getHandler(exchange);\n\n        assertError(mono, MethodNotAllowedException.class,\n            ex -> assertThat(ex.getSupportedMethods()).isEqualTo(\n                Set.of(HttpMethod.GET, HttpMethod.HEAD)));\n    }\n\n    @Test\n    void buildPrefix() {\n        String s = handlerMapping.buildPrefix(\"fakePlugin\", \"v1\");\n        assertThat(s).isEqualTo(\"/apis/api.plugin.halo.run/v1/plugins/fakePlugin\");\n\n        s = handlerMapping.buildPrefix(\"fakePlugin\", \"fake.halo.run/v1alpha1\");\n        assertThat(s).isEqualTo(\"/apis/fake.halo.run/v1alpha1\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <T> void assertError(Mono<Object> mono, final Class<T> exceptionClass,\n        final Consumer<T> consumer) {\n        StepVerifier.create(mono)\n            .consumeErrorWith(error -> {\n                assertThat(error.getClass()).isEqualTo(exceptionClass);\n                consumer.accept((T) error);\n            })\n            .verify();\n    }\n\n    private RequestMappingPredicate getMapping(String... path) {\n        return new RequestMappingPredicate(path).method(GET).params();\n    }\n\n    public static class ResolvableMethod {\n        private final Class<?> objectClass;\n        private final List<Predicate<Method>> filters = new ArrayList<>(4);\n\n        public ResolvableMethod(Class<?> objectClass) {\n            this.objectClass = objectClass;\n        }\n\n        public static ResolvableMethod on(Class<?> objectClass) {\n            return new ResolvableMethod(objectClass);\n        }\n\n        public ResolvableMethod annot(Predicate<Method> predicate) {\n            filters.add(predicate);\n            return this;\n        }\n\n        public Method build() {\n            Set<Method> methods = MethodIntrospector.selectMethods(this.objectClass, this::isMatch);\n            Assert.state(!methods.isEmpty(), () -> \"No matching method: \" + this);\n            Assert.state(methods.size() == 1,\n                () -> \"Multiple matching methods: \" + this + formatMethods(methods));\n            return methods.iterator().next();\n        }\n\n        private String formatMethods(Set<Method> methods) {\n            return \"\\nMatched:\\n\" + methods.stream()\n                .map(Method::toGenericString).collect(Collectors.joining(\",\\n\\t\", \"[\\n\\t\", \"\\n]\"));\n        }\n\n        private boolean isMatch(Method method) {\n            return this.filters.stream().allMatch(p -> p.test(method));\n        }\n    }\n\n    public static class RequestMappingPredicate implements Predicate<Method> {\n\n        private final String[] path;\n\n        private RequestMethod[] method = {};\n\n        private String[] params;\n\n\n        private RequestMappingPredicate(String... path) {\n            this.path = path;\n        }\n\n\n        public RequestMappingPredicate method(RequestMethod... methods) {\n            this.method = methods;\n            return this;\n        }\n\n        public RequestMappingPredicate params(String... params) {\n            this.params = params;\n            return this;\n        }\n\n        @Override\n        public boolean test(Method method) {\n            RequestMapping annot =\n                AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);\n            return annot != null\n                && Arrays.equals(this.path, annot.path())\n                && Arrays.equals(this.method, annot.method())\n                && (this.params == null || Arrays.equals(this.params, annot.params()));\n        }\n    }\n\n    @ApiVersion(\"v1alpha1\")\n    @RestController\n    @RequestMapping(\"/user\")\n    static class UserController {\n\n        @GetMapping(\"/{id}\")\n        public Principal getUser() {\n            return mock(Principal.class);\n        }\n    }\n\n    @RestController\n    @RequestMapping(\"/apples\")\n    static class AppleMissingApiVersionController {\n\n        @GetMapping\n        public String getName() {\n            return mock(String.class);\n        }\n    }\n\n    @ApiVersion(\"v1alpha1\")\n    @Controller\n    @RequestMapping\n    static class TestController {\n        @GetMapping(\"/foo\")\n        public void foo() {\n        }\n\n        @GetMapping(path = \"/foo\", params = \"p\")\n        public void fooParam() {\n        }\n\n        @RequestMapping(path = \"/ba*\", method = {GET, HEAD})\n        public void bar() {\n        }\n\n        @GetMapping(\"\")\n        public void empty() {\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/PluginServiceImplTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.core.io.buffer.DefaultDataBufferFactory.sharedInstance;\n\nimport com.github.zafarkhaja.semver.Version;\nimport com.google.common.hash.Hashing;\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Clock;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.function.Consumer;\nimport java.util.stream.IntStream;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.RepeatedTest;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.PluginDescriptor;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.util.FileSystemUtils;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport reactor.test.publisher.PublisherProbe;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.infra.exception.PluginAlreadyExistsException;\nimport run.halo.app.infra.utils.FileUtils;\n\n@ExtendWith(MockitoExtension.class)\nclass PluginServiceImplTest {\n\n    @Mock\n    SystemVersionSupplier systemVersionSupplier;\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    PluginsRootGetter pluginsRootGetter;\n\n    @Mock\n    SpringPluginManager pluginManager;\n\n    @Spy\n    @InjectMocks\n    PluginServiceImpl pluginService;\n\n    @Nested\n    class InstallUpdateReloadTest {\n\n        Path fakePluginPath;\n\n        @TempDir\n        Path tempDirectory;\n\n        @BeforeEach\n        void setUp() throws URISyntaxException, IOException {\n            fakePluginPath = tempDirectory.resolve(\"plugin-0.0.2.jar\");\n            var fakePluingUri = requireNonNull(\n                getClass().getClassLoader().getResource(\"plugin/plugin-0.0.2\")).toURI();\n            FileUtils.jar(Paths.get(fakePluingUri), tempDirectory.resolve(\"plugin-0.0.2.jar\"));\n\n            lenient().when(systemVersionSupplier.get()).thenReturn(Version.parse(\"0.0.0\"));\n        }\n\n        @Test\n        void installWhenPluginExists() {\n            var existingPlugin = new YamlPluginFinder().find(fakePluginPath);\n            when(client.fetch(Plugin.class, \"fake-plugin\")).thenReturn(Mono.just(existingPlugin));\n            var plugin = pluginService.install(fakePluginPath);\n            StepVerifier.create(plugin)\n                .expectError(PluginAlreadyExistsException.class)\n                .verify();\n\n            verify(client).fetch(Plugin.class, \"fake-plugin\");\n            verify(systemVersionSupplier).get();\n        }\n\n        @Test\n        void installWhenPluginNotExist() {\n            when(pluginsRootGetter.get()).thenReturn(tempDirectory.resolve(\"plugins\"));\n            when(client.fetch(Plugin.class, \"fake-plugin\")).thenReturn(Mono.empty());\n            var createdPlugin = mock(Plugin.class);\n            when(client.create(isA(Plugin.class))).thenReturn(Mono.just(createdPlugin));\n            var plugin = pluginService.install(fakePluginPath);\n            StepVerifier.create(plugin)\n                .expectNext(createdPlugin)\n                .verifyComplete();\n\n            verify(client).fetch(Plugin.class, \"fake-plugin\");\n            verify(systemVersionSupplier).get();\n            verify(client).create(isA(Plugin.class));\n        }\n\n        @Test\n        void upgradeWhenPluginNameMismatch() {\n            var plugin = pluginService.upgrade(\"non-fake-plugin\", fakePluginPath);\n            StepVerifier.create(plugin)\n                .expectError(ServerWebInputException.class)\n                .verify();\n\n            verify(client, never()).fetch(Plugin.class, \"fake-plugin\");\n        }\n\n        @Test\n        void upgradeWhenPluginNotFound() {\n            when(client.fetch(Plugin.class, \"fake-plugin\")).thenReturn(Mono.empty());\n            var plugin = pluginService.upgrade(\"fake-plugin\", fakePluginPath);\n            StepVerifier.create(plugin)\n                .expectError(ServerWebInputException.class)\n                .verify();\n\n            verify(client).fetch(Plugin.class, \"fake-plugin\");\n        }\n\n        @Test\n        void upgradeNormally() {\n            when(pluginsRootGetter.get()).thenReturn(tempDirectory.resolve(\"plugins\"));\n\n            var oldFakePlugin = createPlugin(\"fake-plugin\", plugin -> {\n                plugin.getSpec().setEnabled(true);\n                plugin.getSpec().setVersion(\"0.0.1\");\n            });\n\n            when(client.fetch(Plugin.class, \"fake-plugin\"))\n                .thenReturn(Mono.just(oldFakePlugin))\n                .thenReturn(Mono.just(oldFakePlugin))\n                .thenReturn(Mono.empty());\n\n            when(client.update(oldFakePlugin)).thenReturn(Mono.just(oldFakePlugin));\n\n            var plugin = pluginService.upgrade(\"fake-plugin\", fakePluginPath);\n\n            StepVerifier.create(plugin)\n                .expectNext(oldFakePlugin)\n                .verifyComplete();\n\n            verify(client).fetch(Plugin.class, \"fake-plugin\");\n            verify(client).update(oldFakePlugin);\n            assertTrue(oldFakePlugin.getSpec().getEnabled());\n            assertEquals(\"0.0.2\", oldFakePlugin.getSpec().getVersion());\n            assertEquals(\n                tempDirectory.resolve(\"plugins\").resolve(\"fake-plugin-0.0.2.jar\").toString(),\n                oldFakePlugin.getMetadata().getAnnotations().get(PluginConst.PLUGIN_PATH));\n        }\n\n        @Test\n        void shouldNotReloadIfLoadLocationIsNotReady() {\n            var pluginName = \"test-plugin\";\n\n            var testPlugin = createPlugin(pluginName, plugin -> {\n            });\n\n            when(client.get(Plugin.class, pluginName)).thenReturn(Mono.just(testPlugin));\n\n            pluginService.reload(pluginName)\n                .as(StepVerifier::create)\n                .consumeErrorWith(t -> {\n                    assertInstanceOf(IllegalStateException.class, t);\n                    assertEquals(\"Load location of plugin has not been populated.\",\n                        t.getMessage());\n                })\n                .verify();\n\n            verify(client).get(Plugin.class, pluginName);\n        }\n\n        @Test\n        void shouldReloadIfLoadLocationReady() {\n            var pluginName = \"test-plugin\";\n\n            var testPlugin = createPlugin(pluginName, plugin -> {\n                plugin.getStatus().setLoadLocation(fakePluginPath.toUri());\n            });\n\n            when(client.get(Plugin.class, pluginName)).thenReturn(Mono.just(testPlugin));\n            when(client.update(testPlugin)).thenReturn(Mono.just(testPlugin));\n\n            pluginService.reload(pluginName)\n                .as(StepVerifier::create)\n                .expectNext(testPlugin)\n                .verifyComplete();\n\n            assertEquals(fakePluginPath.toString(),\n                testPlugin.getMetadata().getAnnotations().get(PluginConst.PLUGIN_PATH));\n            verify(client).get(Plugin.class, pluginName);\n            verify(client).update(testPlugin);\n        }\n\n    }\n\n    @Test\n    void generateBundleVersionTest() {\n        var plugin1 = mock(PluginWrapper.class);\n        var plugin2 = mock(PluginWrapper.class);\n        var plugin3 = mock(PluginWrapper.class);\n        when(pluginManager.startedPlugins()).thenReturn(List.of(plugin1, plugin2, plugin3));\n\n        var descriptor1 = mock(PluginDescriptor.class);\n        var descriptor2 = mock(PluginDescriptor.class);\n        var descriptor3 = mock(PluginDescriptor.class);\n        when(plugin1.getDescriptor()).thenReturn(descriptor1);\n        when(plugin2.getDescriptor()).thenReturn(descriptor2);\n        when(plugin3.getDescriptor()).thenReturn(descriptor3);\n\n        when(plugin1.getPluginId()).thenReturn(\"fake-1\");\n        when(plugin2.getPluginId()).thenReturn(\"fake-2\");\n        when(plugin3.getPluginId()).thenReturn(\"fake-3\");\n\n        when(descriptor1.getVersion()).thenReturn(\"1.0.0\");\n        when(descriptor2.getVersion()).thenReturn(\"2.0.0\");\n        when(descriptor3.getVersion()).thenReturn(\"3.0.0\");\n\n        var str = \"fake-1:1.0.0fake-2:2.0.0fake-3:3.0.0\";\n        var result = Hashing.sha256().hashUnencodedChars(str).toString();\n        assertThat(result.length()).isEqualTo(64);\n\n        pluginService.generateBundleVersion()\n            .as(StepVerifier::create)\n            .consumeNextWith(version -> assertThat(version).isEqualTo(result))\n            .verifyComplete();\n\n        var plugin4 = mock(PluginWrapper.class);\n        var descriptor4 = mock(PluginDescriptor.class);\n        when(plugin4.getDescriptor()).thenReturn(descriptor4);\n        when(plugin4.getPluginId()).thenReturn(\"fake-4\");\n        when(descriptor4.getVersion()).thenReturn(\"3.0.0\");\n        var str2 = \"fake-1:1.0.0fake-2:2.0.0fake-4:3.0.0\";\n        var result2 = Hashing.sha256().hashUnencodedChars(str2).toString();\n        when(pluginManager.startedPlugins()).thenReturn(List.of(plugin1, plugin2, plugin4));\n        pluginService.generateBundleVersion()\n            .as(StepVerifier::create)\n            .consumeNextWith(version -> assertThat(version).isEqualTo(result2))\n            .verifyComplete();\n\n        assertThat(result).isNotEqualTo(result2);\n    }\n\n    @Test\n    void shouldGenerateRandomBundleVersionInDevelopment() {\n        var clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());\n        pluginService.setClock(clock);\n        when(pluginManager.isDevelopment()).thenReturn(true);\n        pluginService.generateBundleVersion()\n            .as(StepVerifier::create)\n            .expectNext(String.valueOf(clock.instant().toEpochMilli()))\n            .verifyComplete();\n\n        verify(pluginManager, never()).startedPlugins();\n    }\n\n    @Test\n    void shouldGetStartedPluginNames() {\n        var plugin1 = mock(PluginWrapper.class);\n        when(plugin1.getPluginId()).thenReturn(\"plugin-1\");\n        var plugin2 = mock(PluginWrapper.class);\n        when(plugin2.getPluginId()).thenReturn(\"plugin-2\");\n        when(pluginManager.startedPlugins()).thenReturn(List.of(plugin1, plugin2));\n\n        pluginService.getStartedPluginNames()\n            .as(StepVerifier::create)\n            .expectNext(\"plugin-1\", \"plugin-2\")\n            .verifyComplete();\n    }\n\n    @Nested\n    class PluginStateChangeTest {\n\n        @Test\n        void shouldEnablePluginIfPluginWasNotStarted() {\n            var plugin = createPlugin(\"fake-plugin\", p -> {\n                p.getSpec().setEnabled(false);\n                p.statusNonNull().setPhase(Plugin.Phase.RESOLVED);\n            });\n\n            when(client.get(Plugin.class, \"fake-plugin\")).thenReturn(Mono.just(plugin))\n                .thenReturn(Mono.fromSupplier(() -> {\n                    plugin.statusNonNull().setPhase(Plugin.Phase.STARTED);\n                    return plugin;\n                }));\n            when(client.update(plugin)).thenReturn(Mono.just(plugin));\n\n            pluginService.changeState(\"fake-plugin\", true, false)\n                .as(StepVerifier::create)\n                .expectNext(plugin)\n                .verifyComplete();\n\n            assertTrue(plugin.getSpec().getEnabled());\n        }\n\n        @Test\n        void shouldDisablePluginIfAlreadyStarted() {\n            var plugin = createPlugin(\"fake-plugin\", p -> {\n                p.getSpec().setEnabled(true);\n                p.statusNonNull().setPhase(Plugin.Phase.STARTED);\n            });\n\n            when(client.get(Plugin.class, \"fake-plugin\")).thenReturn(Mono.just(plugin))\n                .thenReturn(Mono.fromSupplier(() -> {\n                    plugin.getStatus().setPhase(Plugin.Phase.STOPPED);\n                    return plugin;\n                }));\n            when(client.update(plugin)).thenReturn(Mono.just(plugin));\n\n            pluginService.changeState(\"fake-plugin\", false, false)\n                .as(StepVerifier::create)\n                .expectNext(plugin)\n                .verifyComplete();\n            assertFalse(plugin.getSpec().getEnabled());\n        }\n    }\n\n    @Nested\n    class BundleCacheTest {\n\n        PluginServiceImpl.BundleCache cache;\n\n        @TempDir\n        Path tempDir;\n\n        @BeforeEach\n        void setUp() {\n            pluginService.setTempDir(tempDir);\n            cache = pluginService.new BundleCache(\".js\");\n        }\n\n        @Test\n        void shouldComputeBundleFileIfAbsent() {\n            doReturn(Mono.just(\"different-version\")).when(pluginService).generateBundleVersion();\n            var fakeContent = Mono.<DataBuffer>just(sharedInstance.wrap(\"fake-content\".getBytes(\n                UTF_8)));\n            cache.computeIfAbsent(\"fake-version\", fakeContent)\n                .as(StepVerifier::create)\n                .assertNext(resource -> {\n                    try {\n                        assertEquals(tempDir.resolve(\"different-version.js\"),\n                            resource.getFile().toPath());\n                        assertEquals(\"different-version.js\", resource.getFilename());\n                        assertEquals(\"fake-content\", resource.getContentAsString(UTF_8));\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                })\n                .verifyComplete();\n\n            try {\n                FileSystemUtils.deleteRecursively(tempDir);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n            cache.computeIfAbsent(\"fake-version\", fakeContent)\n                .as(StepVerifier::create)\n                .assertNext(resource -> {\n                    try {\n                        assertThat(Files.exists(tempDir)).isTrue();\n                        assertEquals(tempDir.resolve(\"different-version.js\"),\n                            resource.getFile().toPath());\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        void shouldNotComputeBundleFileIfPresentAndVersionIsMatch() {\n            shouldComputeBundleFileIfAbsent();\n\n            var fakeContent = Mono.<DataBuffer>just(\n                sharedInstance.wrap(\"another-fake-content\".getBytes(UTF_8)));\n\n            cache.computeIfAbsent(\"different-version\", fakeContent)\n                .as(StepVerifier::create)\n                .assertNext(resource -> {\n                    try {\n                        assertEquals(\"different-version.js\", resource.getFilename());\n                        // The content won't be changed if the version is matched.\n                        assertEquals(\"fake-content\", resource.getContentAsString(UTF_8));\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        void shouldComputeBundleFileIfPresentButVersionMismatch() {\n            shouldComputeBundleFileIfAbsent();\n\n            var fakeContent = Mono.<DataBuffer>just(\n                sharedInstance.wrap(\"another-fake-content\".getBytes(UTF_8)));\n\n            doReturn(Mono.just(\"updated-version\")).when(pluginService).generateBundleVersion();\n\n            cache.computeIfAbsent(\"mismatch-version\", fakeContent)\n                .as(StepVerifier::create)\n                .assertNext(resource -> {\n                    try {\n                        assertTrue(Files.notExists(tempDir.resolve(\"different-version.js\")));\n                        assertEquals(\"updated-version.js\", resource.getFilename());\n                        assertEquals(\"another-fake-content\", resource.getContentAsString(UTF_8));\n                    } catch (IOException e) {\n                        throw new RuntimeException(e);\n                    }\n                })\n                .verifyComplete();\n        }\n\n        @RepeatedTest(10)\n        void concurrentComputeBundleFileIfAbsent() {\n            lenient().doReturn(Mono.just(\"different-version\"))\n                .when(pluginService)\n                .generateBundleVersion();\n\n            var executorService = Executors.newCachedThreadPool();\n\n            var probes = new ArrayList<PublisherProbe<DataBuffer>>();\n            List<? extends Future<?>> futures = IntStream.range(0, 10)\n                .mapToObj(i -> {\n                    var fakeContent = Mono.<DataBuffer>just(sharedInstance.wrap(\n                        (\"fake-content-\" + i).getBytes(UTF_8)\n                    ));\n                    var probe = PublisherProbe.of(fakeContent);\n                    probes.add(probe);\n                    return executorService.submit(\n                        () -> {\n                            cache.computeIfAbsent(\"fake-version\", probe.mono())\n                                .as(StepVerifier::create)\n                                .expectNextCount(1)\n                                .verifyComplete();\n                        });\n                })\n                .toList();\n            executorService.shutdown();\n            futures.forEach(future -> {\n                try {\n                    future.get();\n                } catch (InterruptedException | ExecutionException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n\n            // ensure only one probe was subscribed\n            var subscribedCount = probes.stream()\n                .filter(PublisherProbe::wasSubscribed)\n                .count();\n            assertEquals(1, subscribedCount);\n        }\n    }\n\n    Plugin createPlugin(String name, Consumer<Plugin> pluginConsumer) {\n        var plugin = new Plugin();\n        plugin.setMetadata(new Metadata());\n        plugin.getMetadata().setName(name);\n        plugin.setSpec(new Plugin.PluginSpec());\n        plugin.setStatus(new Plugin.PluginStatus());\n        pluginConsumer.accept(plugin);\n        return plugin;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/PluginsRootGetterImplTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.when;\n\nimport java.nio.file.Paths;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.infra.properties.HaloProperties;\n\n@ExtendWith(MockitoExtension.class)\nclass PluginsRootGetterImplTest {\n\n    @Mock\n    HaloProperties haloProperties;\n\n    @InjectMocks\n    PluginsRootGetterImpl pluginsRootGetter;\n\n    @Test\n    void shouldGetterPluginsRootCorrectly() {\n        var haloWorkDir = Paths.get(\"halo-work-dir\");\n        when(haloProperties.getWorkDir()).thenReturn(haloWorkDir);\n        assertEquals(haloWorkDir.resolve(\"plugins\"), pluginsRootGetter.get());\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/SharedApplicationContextFactoryTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.ApplicationContext;\n\n/**\n * Tests for {@link SharedApplicationContextFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@SpringBootTest\nclass SharedApplicationContextFactoryTest {\n\n    @Autowired\n    ApplicationContext applicationContext;\n\n    @Test\n    void createSharedApplicationContext() {\n        var sharedContext =  SharedApplicationContextFactory.create(applicationContext);\n        assertNotNull(sharedContext);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/SharedEventDispatcherTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.Mockito.withSettings;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.Plugin;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationEvent;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.context.Lifecycle;\n\n@ExtendWith(MockitoExtension.class)\nclass SharedEventDispatcherTest {\n\n    @Mock\n    SpringPluginManager pluginManager;\n\n    @Mock\n    ApplicationEventPublisher publisher;\n\n    @InjectMocks\n    SharedEventDispatcher dispatcher;\n\n    @Test\n    void shouldNotDispatchEventIfNotSharedEvent() {\n        dispatcher.onApplicationEvent(new FakeEvent(this));\n        verify(pluginManager, never()).getStartedPlugins();\n    }\n\n    @Test\n    void shouldDispatchEventToAllStartedPlugins() {\n        var pw = mock(PluginWrapper.class);\n        var plugin = mock(Plugin.class, withSettings().extraInterfaces(SpringPlugin.class));\n        var context =\n            mock(ApplicationContext.class, withSettings().extraInterfaces(Lifecycle.class));\n        when(((Lifecycle) context).isRunning()).thenReturn(true);\n        when(((SpringPlugin) plugin).getApplicationContext()).thenReturn(context);\n        when(pw.getPlugin()).thenReturn(plugin);\n        when(pluginManager.startedPlugins()).thenReturn(List.of(pw));\n\n        var event = new FakeSharedEvent(this);\n        dispatcher.onApplicationEvent(event);\n\n        verify(context).publishEvent(new HaloSharedEventDelegator(dispatcher, event));\n    }\n\n    @Test\n    void shouldNotDispatchEventToAllStartedPluginsWhilePluginContextIsNotRunning() {\n        var pw = mock(PluginWrapper.class);\n        var plugin = mock(Plugin.class, withSettings().extraInterfaces(SpringPlugin.class));\n        var context =\n            mock(ApplicationContext.class, withSettings().extraInterfaces(Lifecycle.class));\n        when(((Lifecycle) context).isRunning()).thenReturn(false);\n        when(((SpringPlugin) plugin).getApplicationContext()).thenReturn(context);\n        when(pw.getPlugin()).thenReturn(plugin);\n        when(pluginManager.startedPlugins()).thenReturn(List.of(pw));\n        var event = new FakeSharedEvent(this);\n        dispatcher.onApplicationEvent(event);\n        verify(context, never()).publishEvent(event);\n    }\n\n    @Test\n    void shouldNotDispatchEventToAllStartedPluginsWhilePluginContextIsNotLifecycle() {\n        var pw = mock(PluginWrapper.class);\n        var plugin = mock(Plugin.class, withSettings().extraInterfaces(SpringPlugin.class));\n        var context = mock(ApplicationContext.class);\n        when(((SpringPlugin) plugin).getApplicationContext()).thenReturn(context);\n        when(pw.getPlugin()).thenReturn(plugin);\n        when(pluginManager.startedPlugins()).thenReturn(List.of(pw));\n        var event = new FakeSharedEvent(this);\n        dispatcher.onApplicationEvent(event);\n        verify(context, never()).publishEvent(event);\n    }\n\n    @Test\n    void shouldUnwrapPluginSharedEventAndRepublish() {\n        var event = new PluginSharedEventDelegator(this, new FakeSharedEvent(this));\n        dispatcher.onApplicationEvent(event);\n        verify(publisher).publishEvent(event.getDelegate());\n    }\n\n    class FakeEvent extends ApplicationEvent {\n\n        public FakeEvent(Object source) {\n            super(source);\n        }\n\n    }\n\n    @SharedEvent\n    class FakeSharedEvent extends ApplicationEvent {\n\n        public FakeSharedEvent(Object source) {\n            super(source);\n        }\n\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java",
    "content": "package run.halo.app.plugin;\n\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.FileNotFoundException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginStateEvent;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.util.ResourceUtils;\n\n/**\n * Tests for {@link SpringComponentsFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SpringComponentsFinderTest {\n\n    @Mock\n    private PluginManager pluginManager;\n\n    @InjectMocks\n    private SpringComponentsFinder finder;\n\n    @Test\n    void shouldNotInvokeReadClasspathStorages() {\n        assertThrows(UnsupportedOperationException.class,\n            () -> finder.readClasspathStorages()\n        );\n    }\n\n    @Test\n    void shouldNotInvokeReadPluginsStorages() {\n        assertThrows(UnsupportedOperationException.class,\n            () -> finder.readPluginsStorages()\n        );\n    }\n\n    @Test\n    void shouldPutEntryIfPluginCreated() throws FileNotFoundException {\n        var pluginWrapper = mockPluginWrapper();\n        when(pluginWrapper.getPluginState()).thenReturn(PluginState.CREATED);\n\n        var event = new PluginStateEvent(pluginManager, pluginWrapper, null);\n        finder.pluginStateChanged(event);\n\n        var classNames = finder.findClassNames(\"fake-plugin\");\n        assertEquals(Set.of(\"run.halo.fake.FakePlugin\"), classNames);\n    }\n\n    @Test\n    void shouldRemoveEntryIfPluginUnloaded() throws FileNotFoundException {\n        var pluginWrapper = mockPluginWrapper();\n        when(pluginWrapper.getPluginState()).thenReturn(PluginState.CREATED);\n\n        var event = new PluginStateEvent(pluginManager, pluginWrapper, null);\n        finder.pluginStateChanged(event);\n\n        var classNames = finder.findClassNames(\"fake-plugin\");\n        assertFalse(classNames.isEmpty());\n\n        when(pluginWrapper.getPluginState()).thenReturn(PluginState.UNLOADED);\n        event = new PluginStateEvent(pluginManager, pluginWrapper, null);\n        finder.pluginStateChanged(event);\n\n        classNames = finder.findClassNames(\"fake-plugin\");\n        assertTrue(classNames.isEmpty());\n    }\n\n    private PluginWrapper mockPluginWrapper() throws FileNotFoundException {\n        var pluginWrapper = mock(PluginWrapper.class);\n        when(pluginWrapper.getPluginId()).thenReturn(\"fake-plugin\");\n\n        var pluginRootUrl = ResourceUtils.getURL(\"classpath:plugin/plugin-for-finder/\");\n        var classLoader = new URLClassLoader(new URL[] {pluginRootUrl});\n        when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);\n        return pluginWrapper;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/YamlPluginDescriptorFinderTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.pf4j.PluginDescriptor;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.util.FileSystemUtils;\nimport org.springframework.util.ResourceUtils;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link YamlPluginDescriptorFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass YamlPluginDescriptorFinderTest {\n\n    private YamlPluginDescriptorFinder yamlPluginDescriptorFinder;\n\n    private File testFile;\n    private Path tempDirectory;\n\n    @BeforeEach\n    void setUp() throws IOException {\n        yamlPluginDescriptorFinder = new YamlPluginDescriptorFinder();\n        tempDirectory = Files.createTempDirectory(\"halo-plugin\");\n        var plugin002Uri = requireNonNull(\n            ResourceUtils.getFile(\"classpath:plugin/plugin-0.0.2\")).toURI();\n\n        Path targetJarPath = tempDirectory.resolve(\"plugin-0.0.2.jar\");\n        FileUtils.jar(Paths.get(plugin002Uri), targetJarPath);\n        testFile = targetJarPath.toFile();\n    }\n\n    @AfterEach\n    void tearDown() throws IOException {\n        FileSystemUtils.deleteRecursively(tempDirectory);\n    }\n\n    @Test\n    void isApplicable() throws IOException {\n        // File not exists\n        boolean applicable =\n            yamlPluginDescriptorFinder.isApplicable(Path.of(\"/some/path/test.jar\"));\n        assertThat(applicable).isFalse();\n\n        // jar file is applicable\n        Path tempJarFile = Files.createTempFile(\"test\", \".jar\");\n        Path tempZipFile = Files.createTempFile(\"test\", \".zip\");\n        try {\n            applicable =\n                yamlPluginDescriptorFinder.isApplicable(tempJarFile);\n            assertThat(applicable).isTrue();\n            // zip file is not applicable\n            applicable =\n                yamlPluginDescriptorFinder.isApplicable(tempZipFile);\n            assertThat(applicable).isFalse();\n\n            // directory is applicable\n            applicable =\n                yamlPluginDescriptorFinder.isApplicable(tempJarFile.getParent());\n            assertThat(applicable).isTrue();\n        } finally {\n            FileUtils.deleteRecursivelyAndSilently(tempJarFile);\n            FileUtils.deleteRecursivelyAndSilently(tempZipFile);\n        }\n    }\n\n    @Test\n    void find() throws JSONException {\n        PluginDescriptor pluginDescriptor = yamlPluginDescriptorFinder.find(testFile.toPath());\n        String actual = JsonUtils.objectToJson(pluginDescriptor);\n        JSONAssert.assertEquals(\"\"\"\n                {\n                     \"pluginId\": \"fake-plugin\",\n                     \"pluginDescription\": \"Fake description\",\n                     \"pluginClass\": \"run.halo.app.plugin.BasePlugin\",\n                     \"version\": \"0.0.2\",\n                     \"requires\": \">=2.0.0\",\n                     \"provider\": \"johnniang\",\n                     \"dependencies\": [],\n                     \"license\": \"GPLv3\"\n                 }\n                \"\"\",\n            actual,\n            false);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/YamlPluginFinderTest.java",
    "content": "package run.halo.app.plugin;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.pf4j.PluginRuntimeException;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.core.io.Resource;\nimport org.springframework.security.util.InMemoryResource;\nimport org.springframework.util.FileCopyUtils;\nimport org.springframework.util.FileSystemUtils;\nimport org.springframework.util.ResourceUtils;\nimport run.halo.app.core.extension.Plugin;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.infra.utils.FileUtils;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link YamlPluginDescriptorFinder}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass YamlPluginFinderTest {\n    private YamlPluginFinder pluginFinder;\n\n    private File testFile;\n\n    @BeforeEach\n    void setUp() throws FileNotFoundException {\n        pluginFinder = new YamlPluginFinder();\n        testFile = ResourceUtils.getFile(\"classpath:plugin/plugin.yaml\");\n    }\n\n    @Test\n    void find() throws IOException {\n        var tempDirectory = Files.createTempDirectory(\"halo-test-plugin\");\n        try {\n            var directories =\n                Files.createDirectories(tempDirectory.resolve(\"build/resources/main\"));\n            FileCopyUtils.copy(testFile, directories.resolve(\"plugin.yaml\").toFile());\n\n            var plugin = pluginFinder.find(tempDirectory);\n            assertThat(plugin).isNotNull();\n            var status = plugin.getStatus();\n            assertEquals(Plugin.Phase.PENDING, status.getPhase());\n            assertEquals(tempDirectory.toUri(), status.getLoadLocation());\n        } finally {\n            FileUtils.deleteRecursivelyAndSilently(tempDirectory);\n        }\n    }\n\n    @Test\n    void findFromJar() throws IOException, URISyntaxException {\n        Path tempDirectory = Files.createTempDirectory(\"halo-plugin\");\n        try {\n            var plugin002Uri = requireNonNull(\n                getClass().getClassLoader().getResource(\"plugin/plugin-0.0.2\")).toURI();\n\n            Path targetJarPath = tempDirectory.resolve(\"plugin-0.0.2.jar\");\n            FileUtils.jar(Paths.get(plugin002Uri), targetJarPath);\n            Plugin plugin = pluginFinder.find(targetJarPath);\n            assertThat(plugin).isNotNull();\n            assertThat(plugin.getMetadata().getName()).isEqualTo(\"fake-plugin\");\n        } finally {\n            FileSystemUtils.deleteRecursively(tempDirectory);\n        }\n    }\n\n    @Test\n    void unstructuredToPluginTest() throws JSONException {\n        Plugin plugin = pluginFinder.unstructuredToPlugin(new FileSystemResource(testFile));\n        assertThat(plugin).isNotNull();\n        JSONAssert.assertEquals(\"\"\"\n                  {\n                    \"spec\": {\n                        \"displayName\": \"a name to show\",\n                        \"version\": \"0.0.1\",\n                        \"author\": {\n                            \"name\": \"guqing\"\n                        },\n                        \"logo\": \"https://guqing.xyz/avatar\",\n                        \"pluginDependencies\": {\n                            \"banana\": \"0.0.1\"\n                        },\n                        \"homepage\": \"https://github.com/guqing/halo-plugin-1\",\n                        \"description\": \"Tell me more about this plugin.\",\n                        \"license\": [\n                            {\n                                \"name\": \"MIT\"\n                            }\n                        ],\n                        \"requires\": \">=2.0.0\",\n                        \"enabled\": false\n                    },\n                    \"apiVersion\": \"plugin.halo.run/v1alpha1\",\n                    \"kind\": \"Plugin\",\n                    \"metadata\": {\n                        \"name\": \"plugin-1\"\n                    }\n                }\n                  \"\"\",\n            JsonUtils.objectToJson(plugin),\n            true);\n    }\n\n    @Test\n    void findFailedWhenFileNotFound() {\n        var test = Paths.get(\"\");\n        assertThatThrownBy(() -> pluginFinder.find(test))\n            .isInstanceOf(PluginRuntimeException.class)\n            .hasMessage(\"Unable to find plugin descriptor file: plugin.yaml\");\n    }\n\n    @Test\n    void acceptArrayObjectLicense() throws JSONException {\n        Resource pluginResource = new InMemoryResource(\"\"\"\n            apiVersion: v1\n            kind: Plugin\n            metadata:\n              name: plugin-1\n            spec:\n              license:\n                - name: MIT\n                  url: https://exmple.com\n            \"\"\");\n        Plugin plugin = pluginFinder.unstructuredToPlugin(pluginResource);\n        assertThat(plugin.getSpec()).isNotNull();\n        JSONAssert.assertEquals(\"\"\"\n            [{\n                \"name\": \"MIT\",\n                \"url\": \"https://exmple.com\"\n            }]\n            \"\"\", JsonUtils.objectToJson(plugin.getSpec().getLicense()), false);\n    }\n\n    @Test\n    void deserializeLicense() throws JSONException, JsonProcessingException {\n        String pluginJson = \"\"\"\n            {\n                \"apiVersion\": \"plugin.halo.run/v1alpha1\",\n                \"kind\": \"Plugin\",\n                \"metadata\": {\n                    \"name\": \"plugin-1\"\n                },\n                \"spec\": {\n                    \"license\": [\n                        {\n                            \"name\": \"MIT\",\n                            \"url\": \"https://exmple.com\"\n                        }\n                    ]\n                }\n            }\n            \"\"\";\n        Plugin plugin = Unstructured.OBJECT_MAPPER.readValue(pluginJson, Plugin.class);\n        assertThat(plugin.getSpec()).isNotNull();\n        JSONAssert.assertEquals(pluginJson, JsonUtils.objectToJson(plugin), false);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetterTest.java",
    "content": "package run.halo.app.plugin.extensionpoint;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.infra.SystemSetting.ExtensionPointEnabled.GROUP;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.ExtensionPoint;\nimport org.pf4j.PluginManager;\nimport org.springframework.beans.factory.BeanFactory;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.core.annotation.Order;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting.ExtensionPointEnabled;\nimport run.halo.app.plugin.extensionpoint.ExtensionPointDefinition.ExtensionPointType;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultExtensionGetterTest {\n\n    @Mock\n    ExtensionPointDefinitionGetter extensionPointDefinitionGetter;\n\n    @Mock\n    ExtensionDefinitionGetter extensionDefinitionGetter;\n\n    @Mock\n    PluginManager pluginManager;\n\n    @Mock\n    SystemConfigFetcher configFetcher;\n\n    @Mock\n    BeanFactory beanFactory;\n\n    @Mock\n    ObjectProvider<FakeExtensionPoint> extensionPointObjectProvider;\n\n    @InjectMocks\n    DefaultExtensionGetter getter;\n\n    @Test\n    void shouldGetExtensionBySingletonDefinitionWhenExtensionPointEnabledSet() {\n        // prepare extension point definition\n        when(extensionPointDefinitionGetter.getByClassName(any()))\n            .thenReturn(Mono.fromSupplier(\n                () -> createExtensionPointDefinition(\"fake-extension-point\",\n                    FakeExtensionPoint.class,\n                    ExtensionPointType.SINGLETON))\n            );\n\n        when(extensionDefinitionGetter.get(eq(\"fake-extension\")))\n            .thenReturn(Mono.fromSupplier(() -> createExtensionDefinition(\n                \"fake-extension\",\n                FakeExtensionPointImpl.class,\n                \"fake-extension-point\")));\n\n        when(configFetcher.fetch(GROUP, ExtensionPointEnabled.class))\n            .thenReturn(Mono.fromSupplier(() -> {\n                var extensionPointEnabled = new ExtensionPointEnabled();\n                extensionPointEnabled.put(\"fake-extension-point\",\n                    new LinkedHashSet<>(List.of(\"fake-extension\")));\n                return extensionPointEnabled;\n            }));\n\n        @SuppressWarnings(\"unchecked\")\n        ObjectProvider<FakeExtensionPoint> objectProvider = mock(ObjectProvider.class);\n        when(objectProvider.orderedStream())\n            .thenReturn(Stream.of(new FakeExtensionPointDefaultImpl()));\n        when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);\n\n        var extensionImpl = new FakeExtensionPointImpl();\n\n        var spyGetter = spy(getter);\n        doReturn(List.of(extensionImpl)).when(spyGetter)\n            .lookExtensions(eq(FakeExtensionPoint.class));\n\n        spyGetter.getEnabledExtensions(FakeExtensionPoint.class)\n            .as(StepVerifier::create)\n            .expectNext(extensionImpl)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetDefaultSingletonDefinitionWhileExtensionPointEnabledNotSet() {\n        when(extensionPointDefinitionGetter.getByClassName(any()))\n            .thenReturn(Mono.fromSupplier(\n                () -> createExtensionPointDefinition(\"fake-extension-point\",\n                    FakeExtensionPoint.class,\n                    ExtensionPointType.SINGLETON))\n            );\n\n        when(configFetcher.fetch(GROUP, ExtensionPointEnabled.class))\n            .thenReturn(Mono.empty());\n\n        @SuppressWarnings(\"unchecked\")\n        ObjectProvider<FakeExtensionPoint> objectProvider = mock(ObjectProvider.class);\n        var extensionDefaultImpl = new FakeExtensionPointDefaultImpl();\n        when(objectProvider.orderedStream())\n            .thenReturn(Stream.of(extensionDefaultImpl));\n        when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);\n\n        var spyGetter = spy(getter);\n        doReturn(List.of()).when(spyGetter)\n            .lookExtensions(eq(FakeExtensionPoint.class));\n\n        spyGetter.getEnabledExtensions(FakeExtensionPoint.class)\n            .as(StepVerifier::create)\n            .expectNext(extensionDefaultImpl)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetMultiInstanceExtensionWhileExtensionPointEnabledSet() {\n        // prepare extension point definition\n        when(extensionPointDefinitionGetter.getByClassName(any()))\n            .thenReturn(Mono.fromSupplier(\n                () -> createExtensionPointDefinition(\"fake-extension-point\",\n                    FakeExtensionPoint.class,\n                    ExtensionPointType.MULTI_INSTANCE))\n            );\n\n        when(extensionDefinitionGetter.get(eq(\"fake-extension\")))\n            .thenReturn(Mono.fromSupplier(() -> createExtensionDefinition(\n                \"fake-extension\",\n                FakeExtensionPointImpl.class,\n                \"fake-extension-point\")));\n\n        when(extensionDefinitionGetter.get(eq(\"default-fake-extension\")))\n            .thenReturn(Mono.fromSupplier(() -> createExtensionDefinition(\n                \"default-fake-extension\",\n                FakeExtensionPointDefaultImpl.class,\n                \"fake-extension-point\")));\n\n        when(configFetcher.fetch(GROUP, ExtensionPointEnabled.class))\n            .thenReturn(Mono.fromSupplier(() -> {\n                var extensionPointEnabled = new ExtensionPointEnabled();\n                extensionPointEnabled.put(\"fake-extension-point\",\n                    new LinkedHashSet<>(List.of(\"default-fake-extension\", \"fake-extension\")));\n                return extensionPointEnabled;\n            }));\n\n        @SuppressWarnings(\"unchecked\")\n        ObjectProvider<FakeExtensionPoint> objectProvider = mock(ObjectProvider.class);\n        var extensionDefaultImpl = new FakeExtensionPointDefaultImpl();\n        when(objectProvider.orderedStream())\n            .thenReturn(Stream.of(extensionDefaultImpl));\n        when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);\n\n        var extensionImpl = new FakeExtensionPointImpl();\n        var anotherExtensionImpl = new FakeExtensionPoint() {\n        };\n\n        var spyGetter = spy(getter);\n        doReturn(List.of(extensionImpl, anotherExtensionImpl)).when(spyGetter)\n            .lookExtensions(eq(FakeExtensionPoint.class));\n\n        spyGetter.getEnabledExtensions(FakeExtensionPoint.class)\n            .as(StepVerifier::create)\n            // should keep the order of enabled extensions\n            .expectNext(extensionDefaultImpl)\n            .expectNext(extensionImpl)\n            .verifyComplete();\n    }\n\n\n    @Test\n    void shouldGetMultiInstanceExtensionWhileExtensionPointEnabledNotSet() {\n        // prepare extension point definition\n        when(extensionPointDefinitionGetter.getByClassName(any()))\n            .thenReturn(Mono.fromSupplier(\n                () -> createExtensionPointDefinition(\"fake-extension-point\",\n                    FakeExtensionPoint.class,\n                    ExtensionPointType.MULTI_INSTANCE))\n            );\n\n        when(configFetcher.fetch(GROUP, ExtensionPointEnabled.class))\n            .thenReturn(Mono.empty());\n\n        @SuppressWarnings(\"unchecked\")\n        ObjectProvider<FakeExtensionPoint> objectProvider = mock(ObjectProvider.class);\n        var extensionDefaultImpl = new FakeExtensionPointDefaultImpl();\n        when(objectProvider.orderedStream())\n            .thenReturn(Stream.of(extensionDefaultImpl));\n        when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);\n\n        var extensionImpl = new FakeExtensionPointImpl();\n        var anotherExtensionImpl = new FakeExtensionPoint() {\n        };\n\n        var spyGetter = spy(getter);\n        doReturn(List.of(extensionImpl, anotherExtensionImpl)).when(spyGetter)\n            .lookExtensions(eq(FakeExtensionPoint.class));\n\n        spyGetter.getEnabledExtensions(FakeExtensionPoint.class)\n            .as(StepVerifier::create)\n            // should keep the order according to @Order annotation\n            // order is 1\n            .expectNext(extensionImpl)\n            // order is 2\n            .expectNext(extensionDefaultImpl)\n            // order is not set\n            .expectNext(anotherExtensionImpl)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldGetExtensionsFromPluginManagerAndApplicationContext() {\n        var extensionFromPlugin = new FakeExtensionPointDefaultImpl();\n        var extensionFromAppContext = new FakeExtensionPointImpl();\n\n        var spyGetter = spy(getter);\n        doReturn(List.of(extensionFromPlugin)).when(spyGetter)\n            .lookExtensions(eq(FakeExtensionPoint.class));\n\n        when(beanFactory.getBeanProvider(FakeExtensionPoint.class))\n            .thenReturn(extensionPointObjectProvider);\n        when(extensionPointObjectProvider.orderedStream())\n            .thenReturn(Stream.of(extensionFromAppContext));\n        var extensions = spyGetter.getExtensionList(FakeExtensionPoint.class);\n        assertEquals(List.of(extensionFromAppContext, extensionFromPlugin), extensions);\n    }\n\n    interface FakeExtensionPoint extends ExtensionPoint {\n\n    }\n\n    @Order(1)\n    static class FakeExtensionPointImpl implements FakeExtensionPoint {\n    }\n\n    @Order(2)\n    static class FakeExtensionPointDefaultImpl implements FakeExtensionPoint {\n    }\n\n    ExtensionDefinition createExtensionDefinition(String name, Class<?> clazz, String epdName) {\n        var ed = new ExtensionDefinition();\n        var metadata = new Metadata();\n        metadata.setName(name);\n        ed.setMetadata(metadata);\n        var spec = new ExtensionDefinition.ExtensionSpec();\n        spec.setClassName(clazz.getName());\n        spec.setExtensionPointName(epdName);\n        ed.setSpec(spec);\n        return ed;\n    }\n\n    ExtensionPointDefinition createExtensionPointDefinition(String name,\n        Class<?> clazz,\n        ExtensionPointType type) {\n        var epd = new ExtensionPointDefinition();\n        var metadata = new Metadata();\n        metadata.setName(name);\n        epd.setMetadata(metadata);\n        var spec = new ExtensionPointDefinition.ExtensionPointSpec();\n        spec.setClassName(clazz.getName());\n        spec.setType(type);\n        epd.setSpec(spec);\n        return epd;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/resources/BundleResourceUtilsTest.java",
    "content": "package run.halo.app.plugin.resources;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.PluginClassLoader;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.core.io.Resource;\nimport run.halo.app.infra.exception.AccessDeniedException;\n\n/**\n * Tests for {@link BundleResourceUtils}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass BundleResourceUtilsTest {\n\n    @Mock\n    private PluginManager pluginManager;\n\n    @BeforeEach\n    void setUp() throws MalformedURLException {\n        PluginWrapper pluginWrapper = Mockito.mock(PluginWrapper.class);\n        PluginClassLoader pluginClassLoader = Mockito.mock(PluginClassLoader.class);\n        lenient().when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader);\n        lenient().when(pluginManager.getPlugin(eq(\"fake-plugin\"))).thenReturn(pluginWrapper);\n\n        lenient().when(pluginClassLoader.getResource(eq(\"console/main.js\"))).thenReturn(\n            new URL(\"file://console/main.js\"));\n        lenient().when(pluginClassLoader.getResource(eq(\"console/style.css\"))).thenReturn(\n            new URL(\"file://console/style.css\"));\n    }\n\n    @Test\n    void getJsBundleResource() {\n        Resource jsBundleResource =\n            BundleResourceUtils.getJsBundleResource(pluginManager, \"fake-plugin\", \"main.js\");\n        assertThat(jsBundleResource).isNotNull();\n        assertThat(jsBundleResource.exists()).isTrue();\n\n        jsBundleResource =\n            BundleResourceUtils.getJsBundleResource(pluginManager, \"fake-plugin\", \"test.js\");\n        assertThat(jsBundleResource).isNull();\n\n        jsBundleResource =\n            BundleResourceUtils.getJsBundleResource(pluginManager, \"nothing-plugin\", \"main.js\");\n        assertThat(jsBundleResource).isNull();\n\n        assertThatThrownBy(() -> {\n            BundleResourceUtils.getJsBundleResource(pluginManager, \"fake-plugin\",\n                \"../test/main.js\");\n        }).isInstanceOf(AccessDeniedException.class);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java",
    "content": "package run.halo.app.plugin.resources;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.Mockito.when;\n\nimport java.io.FileNotFoundException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.Spy;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginWrapper;\nimport org.springframework.boot.autoconfigure.web.WebProperties;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.http.CacheControl;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.util.ResourceUtils;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;\nimport run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.plugin.PluginConst;\n\n/**\n * Tests for {@link ReverseProxyRouterFunctionFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ReverseProxyRouterFunctionFactoryTest {\n\n    @Mock\n    private PluginManager pluginManager;\n\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Spy\n    WebProperties webProperties = new WebProperties();\n\n    @InjectMocks\n    private ReverseProxyRouterFunctionFactory factory;\n\n    @Test\n    void shouldProxyStaticResourceWithCacheControl() throws FileNotFoundException {\n        var cache = webProperties.getResources().getCache();\n        cache.setUseLastModified(true);\n        cache.getCachecontrol().setMaxAge(Duration.ofDays(7));\n\n        var routerFunction = factory.create(mockReverseProxy(), \"fakeA\");\n        assertNotNull(routerFunction);\n        var webClient = WebTestClient.bindToRouterFunction(routerFunction).build();\n\n        var pluginWrapper = Mockito.mock(PluginWrapper.class);\n        var pluginRoot = ResourceUtils.getURL(\"classpath:plugin/plugin-for-reverseproxy/\");\n        var classLoader = new URLClassLoader(new URL[] {pluginRoot});\n        when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);\n        when(pluginManager.getPlugin(\"fakeA\")).thenReturn(pluginWrapper);\n\n        webClient.get().uri(\"/plugins/fakeA/assets/static/test.txt\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().cacheControl(CacheControl.maxAge(Duration.ofDays(7)))\n            .expectHeader().value(HttpHeaders.LAST_MODIFIED, Assertions::assertNotNull)\n            .expectBody(String.class).isEqualTo(\"Fake content.\");\n    }\n\n    @Test\n    void shouldProxyStaticResourceWithoutLastModified() throws FileNotFoundException {\n        var cache = webProperties.getResources().getCache();\n        cache.setUseLastModified(false);\n        cache.getCachecontrol().setMaxAge(Duration.ofDays(7));\n\n        var routerFunction = factory.create(mockReverseProxy(), \"fakeA\");\n        assertNotNull(routerFunction);\n        var webClient = WebTestClient.bindToRouterFunction(routerFunction).build();\n\n        var pluginWrapper = Mockito.mock(PluginWrapper.class);\n        var pluginRoot = ResourceUtils.getURL(\"classpath:plugin/plugin-for-reverseproxy/\");\n        var classLoader = new URLClassLoader(new URL[] {pluginRoot});\n        when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);\n        when(pluginManager.getPlugin(\"fakeA\")).thenReturn(pluginWrapper);\n\n        webClient.get().uri(\"/plugins/fakeA/assets/static/test.txt\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectHeader().cacheControl(CacheControl.maxAge(Duration.ofDays(7)))\n            .expectHeader().lastModified(-1)\n            .expectBody(String.class).isEqualTo(\"Fake content.\");\n    }\n\n    @Test\n    void shouldReturnNotFoundIfResourceNotFound() throws FileNotFoundException {\n        var routerFunction = factory.create(mockReverseProxy(), \"fakeA\");\n        assertNotNull(routerFunction);\n        var webClient = WebTestClient.bindToRouterFunction(routerFunction).build();\n\n        var pluginWrapper = Mockito.mock(PluginWrapper.class);\n        var pluginRoot = ResourceUtils.getURL(\"classpath:plugin/plugin-for-reverseproxy/\");\n        var classLoader = new URLClassLoader(new URL[] {pluginRoot});\n        when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);\n        when(pluginManager.getPlugin(\"fakeA\")).thenReturn(pluginWrapper);\n\n        webClient.get().uri(\"/plugins/fakeA/assets/static/non-existing-file.txt\")\n            .exchange()\n            .expectHeader().cacheControl(CacheControl.empty())\n            .expectStatus().isNotFound();\n    }\n\n    private ReverseProxy mockReverseProxy() {\n        var reverseProxyRule = new ReverseProxyRule(\"/static/**\",\n            new FileReverseProxyProvider(\"static\", \"\"));\n        var reverseProxy = new ReverseProxy();\n        var metadata = new Metadata();\n        metadata.setLabels(\n            Map.of(PluginConst.PLUGIN_NAME_LABEL_NAME, \"fakeA\"));\n        reverseProxy.setMetadata(metadata);\n        reverseProxy.setRules(List.of(reverseProxyRule));\n        return reverseProxy;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistryTest.java",
    "content": "package run.halo.app.plugin.resources;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.ReverseProxy;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.plugin.PluginRouterFunctionRegistry;\n\n/**\n * Tests for {@link ReverseProxyRouterFunctionRegistry}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ReverseProxyRouterFunctionRegistryTest {\n\n    @InjectMocks\n    ReverseProxyRouterFunctionRegistry registry;\n\n    @Mock\n    ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;\n\n    @Mock\n    PluginRouterFunctionRegistry pluginRouterFunctionRegistry;\n\n    @Test\n    void register() {\n        ReverseProxy mock = getMockReverseProxy();\n        registry.register(\"fake-plugin\", mock);\n\n        assertThat(registry.reverseProxySize(\"fake-plugin\")).isEqualTo(1);\n\n        // repeat register a same reverse proxy\n        registry.register(\"fake-plugin\", mock);\n\n        assertThat(registry.reverseProxySize(\"fake-plugin\")).isEqualTo(1);\n\n        verify(reverseProxyRouterFunctionFactory, times(2)).create(any(), any());\n    }\n\n    @Test\n    void removeByKeyValue() {\n        ReverseProxy mock = getMockReverseProxy();\n        registry.register(\"fake-plugin\", mock);\n\n        registry.remove(\"fake-plugin\", \"test-reverse-proxy\");\n\n        assertThat(registry.reverseProxySize(\"fake-plugin\")).isEqualTo(0);\n    }\n\n    private ReverseProxy getMockReverseProxy() {\n        ReverseProxy mock = Mockito.mock(ReverseProxy.class);\n        Metadata metadata = new Metadata();\n        metadata.setName(\"test-reverse-proxy\");\n        when(mock.getMetadata()).thenReturn(metadata);\n        RouterFunction<ServerResponse> routerFunction = request -> Mono.empty();\n\n        when(reverseProxyRouterFunctionFactory.create(any(), any()))\n            .thenReturn(routerFunction);\n        return mock;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/HaloDocumentEventsListenerTest.java",
    "content": "package run.halo.app.search;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.search.event.HaloDocumentAddRequestEvent;\nimport run.halo.app.search.event.HaloDocumentDeleteRequestEvent;\nimport run.halo.app.search.event.HaloDocumentRebuildRequestEvent;\n\n@ExtendWith(MockitoExtension.class)\nclass HaloDocumentEventsListenerTest {\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    @InjectMocks\n    HaloDocumentEventsListener listener;\n\n    @Test\n    void shouldRebuildIndicesWhenReceivingRebuildRequestEvent() {\n        listener.setBufferSize(1);\n        var searchEngine = mock(SearchEngine.class);\n        when(searchEngine.available()).thenReturn(true);\n        when(extensionGetter.getEnabledExtension(SearchEngine.class))\n            .thenReturn(Mono.just(searchEngine));\n        var docsProvider = mock(HaloDocumentsProvider.class);\n\n        var docs = List.of(new HaloDocument(), new HaloDocument(), new HaloDocument());\n\n        when(docsProvider.fetchAll()).thenReturn(Flux.fromIterable(docs));\n        when(extensionGetter.getExtensions(HaloDocumentsProvider.class))\n            .thenReturn(Flux.just(docsProvider));\n        listener.onApplicationEvent(new HaloDocumentRebuildRequestEvent(this));\n        verify(searchEngine, times(3)).addOrUpdate(any());\n    }\n\n    @Test\n    void shouldAddDocsWhenReceivingAddRequestEvent() {\n        var searchEngine = mock(SearchEngine.class);\n        when(searchEngine.available()).thenReturn(true);\n        when(extensionGetter.getEnabledExtension(SearchEngine.class))\n            .thenReturn(Mono.just(searchEngine));\n        var docs = List.of(new HaloDocument());\n        listener.onApplicationEvent(new HaloDocumentAddRequestEvent(this, docs));\n        verify(searchEngine).addOrUpdate(docs);\n    }\n\n    @Test\n    void shouldDeleteDocsWhenReceivingDeleteRequestEvent() {\n        var searchEngine = mock(SearchEngine.class);\n        when(searchEngine.available()).thenReturn(true);\n        when(extensionGetter.getEnabledExtension(SearchEngine.class))\n            .thenReturn(Mono.just(searchEngine));\n        var docIds = List.of(\"1\", \"2\", \"3\");\n        listener.onApplicationEvent(new HaloDocumentDeleteRequestEvent(this, docIds));\n        verify(searchEngine).deleteDocument(docIds);\n    }\n\n    @Test\n    void shouldFailWhenSearchEngineIsUnavailable() {\n        var searchEngine = mock(SearchEngine.class);\n        when(searchEngine.available()).thenReturn(false);\n        when(extensionGetter.getEnabledExtension(SearchEngine.class))\n            .thenReturn(Mono.just(searchEngine));\n        assertThrows(\n            SearchEngineUnavailableException.class,\n            () -> listener.onApplicationEvent(new HaloDocumentRebuildRequestEvent(this))\n        );\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/IndexEndpointTest.java",
    "content": "package run.halo.app.search;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.validation.Errors;\nimport org.springframework.web.reactive.function.server.HandlerStrategies;\nimport org.springframework.web.server.handler.ResponseStatusExceptionHandler;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.exception.RequestBodyValidationException;\n\n@ExtendWith(MockitoExtension.class)\nclass IndexEndpointTest {\n\n    @Mock\n    SearchService searchService;\n\n    @InjectMocks\n    IndexEndpoint endpoint;\n\n    WebTestClient client;\n\n    @BeforeEach\n    void setUp() {\n        client = WebTestClient.bindToRouterFunction(endpoint.endpoint())\n            .handlerStrategies(HandlerStrategies.builder()\n                .exceptionHandler(new ResponseStatusExceptionHandler())\n                .build())\n            .build();\n    }\n\n    @Test\n    void shouldResponseBadRequestIfNotRequestBody() {\n        client.post().uri(\"/indices/-/search\")\n            .exchange()\n            .expectStatus().isBadRequest();\n    }\n\n    @Test\n    void shouldResponseBadRequestIfRequestBodyValidationFailed() {\n        var option = new SearchOption();\n        var errors = mock(Errors.class);\n        when(searchService.search(any(SearchOption.class)))\n            .thenReturn(Mono.error(new RequestBodyValidationException(errors)));\n\n        client.post().uri(\"/indices/-/search\")\n            .bodyValue(option)\n            .exchange()\n            .expectStatus().isBadRequest();\n    }\n\n    @Test\n    void shouldSearchCorrectly() {\n        var option = new SearchOption();\n        option.setKeyword(\"halo\");\n        var searchResult = new SearchResult();\n        when(searchService.search(any(SearchOption.class))).thenReturn(Mono.just(searchResult));\n\n        client.post().uri(\"/indices/-/search\")\n            .bodyValue(option)\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(SearchResult.class)\n            .isEqualTo(searchResult);\n\n        verify(searchService).search(assertArg(o -> {\n            assertEquals(\"halo\", o.getKeyword());\n            // make sure the filters are overwritten\n            assertTrue(o.getFilterExposed());\n            assertTrue(o.getFilterPublished());\n            assertFalse(o.getFilterRecycled());\n        }));\n    }\n\n    @Test\n    void shouldFailWhenSearchEngineIsUnavailable() {\n        when(searchService.search(any(SearchOption.class)))\n            .thenReturn(Mono.error(new SearchEngineUnavailableException()));\n\n        client.post().uri(\"/indices/-/search\")\n            .bodyValue(new SearchOption())\n            .exchange()\n            .expectStatus().is4xxClientError();\n    }\n\n    @Test\n    void ensureGroupVersionNotModified() {\n        assertEquals(\"api.halo.run/v1alpha1\", endpoint.groupVersion().toString());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/IndicesEndpointTest.java",
    "content": "package run.halo.app.search;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.Mockito.verify;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationEventPublisher;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport run.halo.app.search.event.HaloDocumentRebuildRequestEvent;\n\n@ExtendWith(MockitoExtension.class)\nclass IndicesEndpointTest {\n\n    @Mock\n    ApplicationEventPublisher publisher;\n\n    @InjectMocks\n    IndicesEndpoint endpoint;\n\n    WebTestClient client;\n\n    @BeforeEach\n    void setUp() {\n        client = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build();\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\"/indices/-/rebuild\"})\n    void shouldRebuildIndices(String uri) {\n        client.post().uri(uri)\n            .exchange()\n            .expectStatus().isAccepted();\n        verify(publisher).publishEvent(assertArg(event -> {\n            assertInstanceOf(HaloDocumentRebuildRequestEvent.class, event);\n            assertEquals(endpoint, event.getSource());\n        }));\n    }\n\n    @Test\n    void ensureGroupVersionNotChanged() {\n        assertEquals(\"api.console.halo.run/v1alpha1\", endpoint.groupVersion().toString());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/SearchServiceImplTest.java",
    "content": "package run.halo.app.search;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.validation.Errors;\nimport org.springframework.validation.Validator;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.infra.exception.RequestBodyValidationException;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n@ExtendWith(MockitoExtension.class)\nclass SearchServiceImplTest {\n\n    @Mock\n    Validator validator;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    @InjectMocks\n    SearchServiceImpl searchService;\n\n    @Test\n    void shouldThrowValidationErrorIfOptionIsInvalid() {\n        var option = new SearchOption();\n        option.setKeyword(\"halo\");\n\n        var errors = mock(Errors.class);\n        when(errors.hasErrors()).thenReturn(true);\n        when(validator.validateObject(option)).thenReturn(errors);\n\n        searchService.search(option)\n            .as(StepVerifier::create)\n            .expectError(RequestBodyValidationException.class)\n            .verify();\n    }\n\n    @Test\n    void shouldThrowSearchEngineUnavailableExceptionIfNoSearchEngineFound() {\n        var option = new SearchOption();\n        option.setKeyword(\"halo\");\n\n        var errors = mock(Errors.class);\n        when(errors.hasErrors()).thenReturn(false);\n        when(validator.validateObject(option)).thenReturn(errors);\n\n        when(extensionGetter.getEnabledExtension(SearchEngine.class)).thenReturn(Mono.empty());\n\n        searchService.search(option)\n            .as(StepVerifier::create)\n            .expectError(SearchEngineUnavailableException.class)\n            .verify();\n    }\n\n    @Test\n    void shouldThrowSearchEngineUnavailableExceptionIfNoSearchEngineAvailable() {\n        var option = new SearchOption();\n        option.setKeyword(\"halo\");\n\n        var errors = mock(Errors.class);\n        when(errors.hasErrors()).thenReturn(false);\n        when(validator.validateObject(option)).thenReturn(errors);\n\n        when(extensionGetter.getEnabledExtension(SearchEngine.class))\n            .thenAnswer(invocation -> Mono.fromSupplier(() -> {\n                var searchEngine = mock(SearchEngine.class);\n                when(searchEngine.available()).thenReturn(false);\n                return searchEngine;\n            }));\n\n        searchService.search(option)\n            .as(StepVerifier::create)\n            .expectError(SearchEngineUnavailableException.class);\n    }\n\n    @Test\n    void shouldSearch() {\n        var option = new SearchOption();\n        option.setKeyword(\"halo\");\n\n        var errors = mock(Errors.class);\n        when(errors.hasErrors()).thenReturn(false);\n        when(validator.validateObject(option)).thenReturn(errors);\n\n        var searchResult = mock(SearchResult.class);\n        when(extensionGetter.getEnabledExtension(SearchEngine.class))\n            .thenAnswer(invocation -> Mono.fromSupplier(() -> {\n                var searchEngine = mock(SearchEngine.class);\n                when(searchEngine.available()).thenReturn(true);\n                when(searchEngine.search(option)).thenReturn(searchResult);\n                return searchEngine;\n            }));\n\n        searchService.search(option)\n            .as(StepVerifier::create)\n            .expectNext(searchResult)\n            .verifyComplete();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/lucene/LuceneSearchEngineIntegrationTest.java",
    "content": "package run.halo.app.search.lucene;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static run.halo.app.core.extension.content.Post.VisibleEnum.PRIVATE;\nimport static run.halo.app.core.extension.content.Post.VisibleEnum.PUBLIC;\n\nimport java.time.Duration;\nimport java.util.List;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.opentest4j.AssertionFailedError;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.core.retry.RetryException;\nimport org.springframework.core.retry.RetryPolicy;\nimport org.springframework.core.retry.RetryTemplate;\nimport org.springframework.dao.OptimisticLockingFailureException;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.test.StepVerifier;\nimport reactor.util.retry.Retry;\nimport run.halo.app.content.Content;\nimport run.halo.app.content.ContentUpdateParam;\nimport run.halo.app.content.PostRequest;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.search.SearchEngine;\nimport run.halo.app.search.SearchOption;\nimport run.halo.app.search.SearchResult;\n\n@DirtiesContext\n@SpringBootTest(properties = {\n    \"halo.search-engine.lucene.enabled=true\",\n    \"halo.extension.controller.disabled=false\"})\n@AutoConfigureWebTestClient\npublic class LuceneSearchEngineIntegrationTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @Autowired\n    PostService postService;\n\n    @Autowired\n    ReactiveExtensionClient client;\n\n    @Autowired\n    SearchEngine searchEngine;\n\n    @BeforeEach\n    @AfterEach\n    void cleanUp() {\n        searchEngine.deleteAll();\n    }\n\n    @Test\n    @WithMockUser(username = \"admin\", roles = AnonymousUserConst.Role)\n    void shouldSearchPostAfterPostPublished() throws RetryException {\n        var postName = \"first-post\";\n        assertNoResult(1);\n        createPost(postName);\n        assertHasResult(5);\n        unpublishPost(postName);\n        assertNoResult(5);\n        publishPost(postName);\n        assertHasResult(5);\n        privatePost(postName);\n        assertNoResult(5);\n        publicPost(postName);\n        assertHasResult(5);\n        recyclePost(postName);\n        assertNoResult(5);\n        recoverPost(postName);\n        assertHasResult(5);\n        deletePostPermanently(postName);\n        assertNoResult(5);\n    }\n\n    void assertHasResult(int maxAttempts) throws RetryException {\n        var retryTemplate = new RetryTemplate(RetryPolicy.builder()\n            .delay(Duration.ofSeconds(1))\n            .maxRetries(maxAttempts)\n            .predicate(AssertionFailedError.class::isInstance)\n            .build());\n        var option = new SearchOption();\n        option.setKeyword(\"halo\");\n        option.setHighlightPreTag(\"<my-tag>\");\n        option.setHighlightPostTag(\"</my-tag>\");\n        retryTemplate.execute(() -> {\n            webClient.post().uri(\"/apis/api.halo.run/v1alpha1/indices/-/search\")\n                .bodyValue(option)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(SearchResult.class).value(result -> {\n                    assertEquals(1, result.getTotal());\n                    assertEquals(\"halo\", result.getKeyword());\n                    var hits = result.getHits();\n                    assertEquals(1, hits.size());\n                    var doc = hits.get(0);\n                    assertEquals(\"post.content.halo.run-first-post\", doc.getId());\n                    assertEquals(\"post.content.halo.run\", doc.getType());\n                    assertEquals(\"first <my-tag>halo</my-tag> post\", doc.getTitle());\n                    assertNull(doc.getDescription());\n                    assertEquals(\"<my-tag>halo</my-tag>\", doc.getContent());\n                });\n            return null;\n        });\n    }\n\n    void assertNoResult(int maxAttempts) throws RetryException {\n        var retryTemplate = new RetryTemplate(RetryPolicy.builder()\n            .delay(Duration.ofSeconds(1))\n            .maxRetries(maxAttempts)\n            .predicate(AssertionFailedError.class::isInstance)\n            .build());\n\n        var option = new SearchOption();\n        option.setKeyword(\"halo\");\n        option.setHighlightPreTag(\"<my-tag>\");\n        option.setHighlightPostTag(\"</my-tag>\");\n        option.setIncludeTagNames(List.of(\"search\"));\n        option.setIncludeCategoryNames(List.of(\"halo\"));\n        option.setIncludeOwnerNames(List.of(\"admin\"));\n        retryTemplate.execute(() -> {\n            webClient.post().uri(\"/apis/api.halo.run/v1alpha1/indices/-/search\")\n                .bodyValue(option)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(SearchResult.class).value(result -> {\n                    assertEquals(0, result.getTotal());\n                    assertEquals(\"halo\", result.getKeyword());\n                });\n            return null;\n        });\n    }\n\n    void deletePostPermanently(String postName) {\n        client.get(Post.class, postName)\n            .flatMap(client::delete)\n            .retryWhen(optimisticLockRetry())\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    void recoverPost(String postName) {\n        client.get(Post.class, postName)\n            .doOnNext(post -> post.getSpec().setDeleted(false))\n            .flatMap(client::update)\n            .retryWhen(optimisticLockRetry())\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    void recyclePost(String postName) {\n        client.get(Post.class, postName)\n            .doOnNext(post -> post.getSpec().setDeleted(true))\n            .flatMap(client::update)\n            .retryWhen(optimisticLockRetry())\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    void publicPost(String postName) {\n        client.get(Post.class, postName)\n            .doOnNext(post -> post.getSpec().setVisible(PUBLIC))\n            .flatMap(client::update)\n            .retryWhen(optimisticLockRetry())\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    void privatePost(String postName) {\n        client.get(Post.class, postName)\n            .doOnNext(post -> post.getSpec().setVisible(PRIVATE))\n            .flatMap(client::update)\n            .retryWhen(optimisticLockRetry())\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    void publishPost(String postName) {\n        client.get(Post.class, postName)\n            .flatMap(postService::publish)\n            .retryWhen(optimisticLockRetry())\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    void unpublishPost(String postName) {\n        client.get(Post.class, postName)\n            .flatMap(postService::unpublish)\n            .retryWhen(optimisticLockRetry())\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    void createPost(String postName) {\n        var post = new Post();\n        var metadata = new Metadata();\n        post.setMetadata(metadata);\n        metadata.setName(postName);\n        var spec = new Post.PostSpec();\n        post.setSpec(spec);\n        spec.setPublish(true);\n        spec.setOwner(\"admin\");\n        spec.setTitle(\"first halo post\");\n        spec.setVisible(PUBLIC);\n        spec.setAllowComment(true);\n        spec.setPinned(false);\n        spec.setPriority(0);\n        spec.setSlug(\"/first-post\");\n        spec.setDeleted(false);\n        spec.setTags(List.of(\"search\"));\n        spec.setCategories(List.of(\"halo\"));\n        var excerpt = new Post.Excerpt();\n        excerpt.setRaw(\"first post description\");\n        excerpt.setAutoGenerate(false);\n        spec.setExcerpt(excerpt);\n        var content = new Content(\"halo\", \"halo\", \"Markdown\");\n        var contentParam = ContentUpdateParam.from(content);\n        var postRequest = new PostRequest(post, contentParam);\n        postService.draftPost(postRequest)\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    Retry optimisticLockRetry() {\n        return Retry.backoff(5, Duration.ofMillis(100))\n            .filter(OptimisticLockingFailureException.class::isInstance);\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/lucene/LuceneSearchEngineTest.java",
    "content": "package run.halo.app.search.lucene;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.stream.IntStream;\nimport org.apache.lucene.index.DirectoryReader;\nimport org.apache.lucene.store.AlreadyClosedException;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.search.HaloDocument;\nimport run.halo.app.search.SearchOption;\n\n@ExtendWith(MockitoExtension.class)\nclass LuceneSearchEngineTest {\n\n    LuceneSearchEngine searchEngine;\n\n    @TempDir\n    Path tempDir;\n\n    @BeforeEach\n    void setUp() throws Exception {\n        this.searchEngine = new LuceneSearchEngine(tempDir);\n        this.searchEngine.afterPropertiesSet();\n    }\n\n    @AfterEach\n    void cleanUp() throws Exception {\n        this.searchEngine.destroy();\n    }\n\n    @Test\n    void shouldAddOrUpdateDocument() throws IOException {\n        var haloDoc = createFakeHaloDoc();\n        searchEngine.addOrUpdate(List.of(haloDoc));\n        // validate the index\n        var reader = DirectoryReader.open(searchEngine.getDirectory());\n        assertEquals(1, reader.getDocCount(\"id\"));\n    }\n\n    @Test\n    void shouldDeleteDocument() throws IOException {\n        var haloDoc = createFakeHaloDoc();\n        searchEngine.addOrUpdate(List.of(haloDoc));\n\n        var reader = DirectoryReader.open(searchEngine.getDirectory());\n        assertEquals(1, reader.getDocCount(\"id\"));\n\n        this.searchEngine.deleteDocument(List.of(\"fake-id\"));\n\n        reader = DirectoryReader.open(searchEngine.getDirectory());\n        assertEquals(0, reader.getDocCount(\"id\"));\n    }\n\n    @Test\n    void shouldDeleteAll() throws IOException {\n        var haloDoc = createFakeHaloDoc();\n        searchEngine.addOrUpdate(List.of(haloDoc));\n\n        var reader = DirectoryReader.open(searchEngine.getDirectory());\n        assertEquals(1, reader.getDocCount(\"id\"));\n\n        this.searchEngine.deleteAll();\n\n        reader = DirectoryReader.open(searchEngine.getDirectory());\n        assertEquals(0, reader.getDocCount(\"id\"));\n    }\n\n    @Test\n    void shouldAddOrUpdateDocumentConcurrently()\n        throws ExecutionException, InterruptedException, TimeoutException {\n        runConcurrently(() -> {\n            var haloDoc = createFakeHaloDoc();\n            searchEngine.addOrUpdate(List.of(haloDoc));\n        });\n    }\n\n    @Test\n    void shouldDeleteDocumentConcurrently()\n        throws ExecutionException, InterruptedException, TimeoutException {\n        runConcurrently(() -> {\n            var haloDoc = createFakeHaloDoc();\n            searchEngine.addOrUpdate(List.of(haloDoc));\n            searchEngine.deleteDocument(List.of(haloDoc.getId()));\n        });\n    }\n\n    @Test\n    void shouldDeleteAllConcurrently()\n        throws ExecutionException, InterruptedException, TimeoutException {\n        runConcurrently(() -> {\n            var haloDoc = createFakeHaloDoc();\n            searchEngine.addOrUpdate(List.of(haloDoc));\n            searchEngine.deleteAll();\n        });\n    }\n\n    @Test\n    void shouldDestroy() throws Exception {\n        var directory = this.searchEngine.getDirectory();\n        this.searchEngine.destroy();\n        assertThrows(AlreadyClosedException.class, () -> DirectoryReader.open(directory));\n    }\n\n    @Test\n    void shouldSearchNothingIfIndexNotFound() {\n        var option = new SearchOption();\n        option.setKeyword(\"fake\");\n        option.setLimit(123);\n        option.setHighlightPreTag(\"<fake-tag>\");\n        option.setHighlightPostTag(\"</fake-tag>\");\n        var result = this.searchEngine.search(option);\n        assertEquals(0, result.getTotal());\n        assertEquals(\"fake\", result.getKeyword());\n        assertEquals(123, result.getLimit());\n        assertEquals(0, result.getHits().size());\n    }\n\n    @Test\n    void shouldSearch() {\n        this.searchEngine.addOrUpdate(List.of(createFakeHaloDoc()));\n\n        var option = new SearchOption();\n        option.setKeyword(\"fake\");\n        option.setLimit(123);\n        option.setHighlightPreTag(\"<fake-tag>\");\n        option.setHighlightPostTag(\"</fake-tag>\");\n        var result = this.searchEngine.search(option);\n        assertEquals(1, result.getTotal());\n        assertEquals(\"fake\", result.getKeyword());\n        assertEquals(123, result.getLimit());\n        assertEquals(1, result.getHits().size());\n        var gotHaloDoc = result.getHits().get(0);\n        assertEquals(\"fake-id\", gotHaloDoc.getId());\n        assertEquals(\"<fake-tag>fake</fake-tag>-title\", gotHaloDoc.getTitle());\n        assertNull(gotHaloDoc.getDescription());\n        assertEquals(\"<fake-tag>fake</fake-tag>-content\", gotHaloDoc.getContent());\n    }\n\n    void runConcurrently(Runnable runnable)\n        throws ExecutionException, InterruptedException, TimeoutException {\n        var executorService = Executors.newFixedThreadPool(10);\n        var futures = IntStream.of(0, 10)\n            .mapToObj(i -> CompletableFuture.runAsync(runnable, executorService))\n            .toArray(CompletableFuture[]::new);\n        CompletableFuture.allOf(futures).get(10, TimeUnit.SECONDS);\n        executorService.shutdownNow();\n        assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS));\n    }\n\n    HaloDocument createFakeHaloDoc() {\n        var haloDoc = new HaloDocument();\n        haloDoc.setId(\"fake-id\");\n        haloDoc.setMetadataName(\"fake-name\");\n        haloDoc.setTitle(\"fake-title\");\n        haloDoc.setDescription(null);\n        haloDoc.setContent(\"fake-content\");\n        haloDoc.setType(\"fake-type\");\n        haloDoc.setOwnerName(\"fake-owner\");\n        var now = Instant.now();\n        haloDoc.setCreationTimestamp(now);\n        haloDoc.setUpdateTimestamp(null);\n        haloDoc.setPermalink(\"/fake-permalink\");\n        haloDoc.setAnnotations(Map.of(\"fake-anno-key\", \"fake-anno-value\"));\n        return haloDoc;\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/post/PostEventsListenerTest.java",
    "content": "package run.halo.app.search.post;\n\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.assertArg;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationEventPublisher;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.event.post.PostDeletedEvent;\nimport run.halo.app.event.post.PostUpdatedEvent;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.search.event.HaloDocumentAddRequestEvent;\nimport run.halo.app.search.event.HaloDocumentDeleteRequestEvent;\n\n@ExtendWith(MockitoExtension.class)\nclass PostEventsListenerTest {\n\n    @Mock\n    ApplicationEventPublisher publisher;\n\n    @Mock\n    PostService postService;\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @InjectMocks\n    PostEventsListener listener;\n\n    @Nested\n    class PostUpdatedEventTest {\n\n        @Test\n        void shouldDoNothingIfPostIsDeleted() {\n            when(client.fetch(Post.class, \"fake-post\"))\n                .thenReturn(Mono.empty());\n            var event = new PostUpdatedEvent(this, \"fake-post\");\n            listener.onApplicationEvent(event)\n                .as(StepVerifier::create)\n                .verifyComplete();\n\n            verify(publisher, never()).publishEvent(any());\n        }\n\n        @Test\n        void shouldRequestDeleteWhilePostIsDeleting() {\n            var post = new Post();\n            var metadata = new Metadata();\n            metadata.setName(\"fake-post\");\n            metadata.setDeletionTimestamp(Instant.now());\n            post.setMetadata(metadata);\n            when(client.fetch(Post.class, \"fake-post\"))\n                .thenReturn(Mono.just(post));\n            var event = new PostUpdatedEvent(this, \"fake-post\");\n            listener.onApplicationEvent(event)\n                .as(StepVerifier::create)\n                .verifyComplete();\n\n            verify(publisher).publishEvent(\n                assertArg(e -> assertInstanceOf(HaloDocumentDeleteRequestEvent.class, e))\n            );\n        }\n\n        @Test\n        void shouldRequestAddWhilePostIsNotDeleted() {\n            var post = new Post();\n            var metadata = new Metadata();\n            metadata.setName(\"fake-post\");\n            post.setMetadata(metadata);\n            var spec = new Post.PostSpec();\n            post.setSpec(spec);\n            var status = new Post.PostStatus();\n            post.setStatus(status);\n            when(client.fetch(Post.class, \"fake-post\"))\n                .thenReturn(Mono.just(post));\n            var content = ContentWrapper.builder()\n                .content(\"fake-content\")\n                .raw(\"fake-content\")\n                .build();\n            when(postService.getReleaseContent(post)).thenReturn(Mono.just(content));\n            var event = new PostUpdatedEvent(this, \"fake-post\");\n            listener.onApplicationEvent(event)\n                .as(StepVerifier::create)\n                .verifyComplete();\n\n            verify(publisher).publishEvent(\n                assertArg(e -> assertInstanceOf(HaloDocumentAddRequestEvent.class, e))\n            );\n        }\n    }\n\n    @Nested\n    class PostDeleteEventTest {\n\n        @Test\n        void shouldRequestDelete() {\n            var post = new Post();\n            var metadata = new Metadata();\n            metadata.setName(\"fake-post\");\n            post.setMetadata(metadata);\n            var event = new PostDeletedEvent(this, post);\n            listener.onApplicationEvent(event);\n\n            verify(publisher).publishEvent(\n                assertArg(e -> assertInstanceOf(HaloDocumentDeleteRequestEvent.class, e))\n            );\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/search/post/PostHaloDocumentsProviderTest.java",
    "content": "package run.halo.app.search.post;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.ReactiveExtensionPaginatedOperator;\n\n@ExtendWith(MockitoExtension.class)\nclass PostHaloDocumentsProviderTest {\n\n    @Mock\n    PostService postService;\n\n    @Mock\n    ReactiveExtensionPaginatedOperator paginatedOperator;\n\n    @InjectMocks\n    PostHaloDocumentsProvider provider;\n\n    @Test\n    void ensureTypeNotModified() {\n        assertEquals(\"post.content.halo.run\", provider.getType());\n    }\n\n    @Test\n    void shouldFetchAll() {\n        var post = createFakePost();\n        when(paginatedOperator.list(same(Post.class), any(ListOptions.class)))\n            .thenReturn(Flux.just(post));\n        var content = ContentWrapper.builder()\n            .content(\"fake-content\")\n            .raw(\"fake-content\")\n            .build();\n        when(postService.getReleaseContent(post)).thenReturn(Mono.just(content));\n        provider.fetchAll()\n            .as(StepVerifier::create)\n            .assertNext(doc -> {\n                assertEquals(\"post.content.halo.run\", doc.getType());\n                assertEquals(\"fake-post\", doc.getMetadataName());\n                assertEquals(\"post.content.halo.run-fake-post\", doc.getId());\n                assertEquals(\"fake-content\", doc.getContent());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFetchAllIfNoContent() {\n        var post = createFakePost();\n        when(paginatedOperator.list(same(Post.class), any(ListOptions.class)))\n            .thenReturn(Flux.just(post));\n        when(postService.getReleaseContent(post)).thenReturn(Mono.empty());\n        provider.fetchAll()\n            .as(StepVerifier::create)\n            .assertNext(doc -> {\n                assertEquals(\"post.content.halo.run\", doc.getType());\n                assertEquals(\"fake-post\", doc.getMetadataName());\n                assertEquals(\"post.content.halo.run-fake-post\", doc.getId());\n                assertEquals(\"\", doc.getContent());\n            })\n            .verifyComplete();\n    }\n\n    Post createFakePost() {\n        var post = new Post();\n        var metadata = new Metadata();\n        metadata.setName(\"fake-post\");\n        post.setMetadata(metadata);\n        var spec = new Post.PostSpec();\n        var status = new Post.PostStatus();\n        post.setSpec(spec);\n        post.setStatus(status);\n        return post;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/AuthProviderServiceImplTest.java",
    "content": "package run.halo.app.security;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.same;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.HashMap;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.AuthProvider;\nimport run.halo.app.core.extension.UserConnection;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link AuthProviderServiceImpl}.\n *\n * @author guqing\n * @since 2.4.0\n */\n@ExtendWith({SpringExtension.class, MockitoExtension.class})\nclass AuthProviderServiceImplTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    ObjectProvider<SystemConfigFetcher> systemFetchProvider;\n\n    @Mock\n    SystemConfigFetcher systemConfigFetcher;\n\n    @InjectMocks\n    AuthProviderServiceImpl authProviderService;\n\n    @BeforeEach\n    void setUp() {\n        when(systemFetchProvider.getIfUnique()).thenReturn(systemConfigFetcher);\n    }\n\n    @Test\n    void testEnable() throws JSONException {\n        // Create a test auth provider\n        AuthProvider authProvider = createAuthProvider(\"github\");\n        when(client.get(eq(AuthProvider.class), eq(\"github\"))).thenReturn(Mono.just(authProvider));\n\n        ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);\n        when(client.update(captor.capture())).thenReturn(Mono.empty());\n\n        pileSystemConfigMap();\n\n        // Call the method being tested\n        authProviderService.enable(\"github\")\n            .as(StepVerifier::create)\n            .expectNext(authProvider)\n            .verifyComplete();\n\n        ConfigMap value = captor.getValue();\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"states\": [\n                        {\n                            \"name\": \"github\",\n                            \"enabled\": true,\n                            \"priority\": 0\n                        }\n                    ]\n                }\n                \"\"\",\n            value.getData().get(SystemSetting.AuthProvider.GROUP),\n            true);\n        // Verify the result\n        verify(client).get(AuthProvider.class, \"github\");\n    }\n\n    @Test\n    void testDisable() throws JSONException {\n        // Create a test auth provider\n        AuthProvider authProvider = createAuthProvider(\"github\");\n        when(client.get(eq(AuthProvider.class), eq(\"github\"))).thenReturn(Mono.just(authProvider));\n\n        AuthProvider local = createAuthProvider(\"local\");\n        local.getMetadata().getLabels().put(AuthProvider.PRIVILEGED_LABEL, \"true\");\n\n        ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);\n        when(client.update(captor.capture())).thenReturn(Mono.empty());\n\n        pileSystemConfigMap();\n\n        // Call the method being tested\n        Mono<AuthProvider> result = authProviderService.disable(\"github\");\n\n        assertEquals(authProvider, result.block());\n        ConfigMap value = captor.getValue();\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"states\": [\n                        {\n                            \"name\": \"github\",\n                            \"enabled\": false,\n                            \"priority\": 0\n                        }\n                    ]\n                }\n                \"\"\",\n            value.getData().get(SystemSetting.AuthProvider.GROUP),\n            true);\n        // Verify the result\n        verify(client).get(AuthProvider.class, \"github\");\n    }\n\n    @Test\n    @WithMockUser(username = \"admin\")\n    void listAll() {\n        AuthProvider github = createAuthProvider(\"github\");\n        github.getSpec().setBindingUrl(\"fake-binding-url\");\n\n        AuthProvider gitlab = createAuthProvider(\"gitlab\");\n        gitlab.getSpec().setBindingUrl(\"fake-binding-url\");\n\n        AuthProvider gitee = createAuthProvider(\"gitee\");\n\n        when(client.listAll(same(AuthProvider.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.just(github, gitlab, gitee));\n        when(client.listAll(same(UserConnection.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.empty());\n\n        pileSystemConfigMap();\n\n        authProviderService.listAll()\n            .as(StepVerifier::create)\n            .consumeNextWith(result -> {\n                assertThat(result).hasSize(3);\n                try {\n                    JSONAssert.assertEquals(\"\"\"\n                            [{\n                                 \"name\": \"gitee\",\n                                 \"displayName\": \"gitee\",\n                                 \"authType\": \"OAUTH2\",\n                                 \"isBound\": false,\n                                 \"enabled\": false,\n                                 \"priority\": 0,\n                                 \"supportsBinding\": false,\n                                 \"privileged\": false\n                             },\n                             {\n                                 \"name\": \"github\",\n                                 \"displayName\": \"github\",\n                                 \"bindingUrl\": \"fake-binding-url\",\n                                 \"authType\": \"OAUTH2\",\n                                 \"isBound\": false,\n                                 \"enabled\": false,\n                                 \"priority\": 0,\n                                 \"supportsBinding\": false,\n                                 \"privileged\": false\n                             },\n                             {\n                                 \"name\": \"gitlab\",\n                                 \"displayName\": \"gitlab\",\n                                 \"bindingUrl\": \"fake-binding-url\",\n                                 \"authType\": \"OAUTH2\",\n                                 \"isBound\": false,\n                                 \"enabled\": false,\n                                 \"priority\": 0,\n                                 \"supportsBinding\": false,\n                                 \"privileged\": false\n                            }]\n                            \"\"\",\n                        JsonUtils.objectToJson(result),\n                        true);\n                } catch (JSONException e) {\n                    throw new RuntimeException(e);\n                }\n            })\n            .verifyComplete();\n    }\n\n    AuthProvider createAuthProvider(String name) {\n        AuthProvider authProvider = new AuthProvider();\n        authProvider.setMetadata(new Metadata());\n        authProvider.getMetadata().setName(name);\n        authProvider.getMetadata().setLabels(new HashMap<>());\n        authProvider.setSpec(new AuthProvider.AuthProviderSpec());\n        authProvider.getSpec().setDisplayName(name);\n        authProvider.getSpec().setAuthType(AuthProvider.AuthType.OAUTH2);\n        return authProvider;\n    }\n\n    void pileSystemConfigMap() {\n        ConfigMap configMap = new ConfigMap();\n        configMap.setData(new HashMap<>());\n        when(systemConfigFetcher.getConfigMap())\n            .thenReturn(Mono.just(configMap));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/CsrfSecurityTest.java",
    "content": "package run.halo.app.security;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.test.web.reactive.server.WebTestClient;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass CsrfSecurityTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @Test\n    void shouldNotCheckCsrfForPatAuthentication() {\n        webClient.post()\n            .uri(\"/fake\")\n            .headers(headers -> headers.setBearerAuth(\"pat_invalid\"))\n            .exchange()\n            .expectStatus()\n            .isUnauthorized()\n            .expectHeader()\n            .exists(HttpHeaders.WWW_AUTHENTICATE);\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/DefaultServerAuthenticationEntryPointTest.java",
    "content": "package run.halo.app.security;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.springframework.http.HttpHeaders.WWW_AUTHENTICATE;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;\nimport org.springframework.security.web.server.savedrequest.ServerRequestCache;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultServerAuthenticationEntryPointTest {\n\n    @Mock\n    ServerRequestCache requestCache;\n\n    @InjectMocks\n    DefaultServerAuthenticationEntryPoint entryPoint;\n\n    @Test\n    void commenceForXhrRequest() {\n        var mockReq = MockServerHttpRequest.get(\"/protected\")\n            .header(\"X-Requested-With\", \"XMLHttpRequest\")\n            .build();\n        var mockExchange = MockServerWebExchange.builder(mockReq)\n            .build();\n        var commenceMono = entryPoint.commence(mockExchange,\n            new AuthenticationCredentialsNotFoundException(\"Not Found\"));\n        StepVerifier.create(commenceMono)\n            .verifyComplete();\n        var headers = mockExchange.getResponse().getHeaders();\n        assertEquals(\"FormLogin realm=\\\"console\\\"\", headers.getFirst(WWW_AUTHENTICATE));\n    }\n\n    @Test\n    void commenceForNormalRequest() {\n        var mockReq = MockServerHttpRequest.get(\"/protected\")\n            .build();\n        var mockExchange = MockServerWebExchange.builder(mockReq)\n            .build();\n        Mockito.when(requestCache.saveRequest(mockExchange)).thenReturn(Mono.empty());\n        var commenceMono = entryPoint.commence(mockExchange,\n            new AuthenticationCredentialsNotFoundException(\"Not Found\"));\n        StepVerifier.create(commenceMono)\n            .verifyComplete();\n        assertEquals(URI.create(\"/login?authentication_required\"),\n            mockExchange.getResponse().getHeaders().getLocation());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/DefaultSuperAdminInitializerTest.java",
    "content": "package run.halo.app.security;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.mock;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.security.crypto.password.PasswordEncoder;\nimport run.halo.app.extension.MetadataUtil;\n\n/**\n * Tests for {@link DefaultSuperAdminInitializer}.\n */\nclass DefaultSuperAdminInitializerTest {\n\n    @Test\n    void createAdminShouldSetSystemProtectionFinalizer() {\n        var passwordEncoder = mock(PasswordEncoder.class);\n        var initializer = new DefaultSuperAdminInitializer(null, passwordEncoder);\n\n        var admin = initializer.createAdmin(\"admin\", \"password\", \"admin@example.com\");\n\n        assertThat(admin.getMetadata()).isNotNull();\n        assertThat(admin.getMetadata().getFinalizers()).isNotNull();\n        assertThat(admin.getMetadata().getFinalizers())\n            .contains(MetadataUtil.SYSTEM_FINALIZER);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java",
    "content": "package run.halo.app.security;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.core.authority.AuthorityUtils.authorityListToSet;\n\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.exception.UserNotFoundException;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultUserDetailServiceTest {\n\n    @Mock\n    UserService userService;\n\n    @Mock\n    RoleService roleService;\n\n    @InjectMocks\n    DefaultUserDetailService userDetailService;\n\n    @Test\n    void shouldUpdatePasswordSuccessfully() {\n        var fakeUser = createFakeUserDetails();\n\n        var user = new run.halo.app.core.extension.User();\n\n        when(userService.updatePassword(\"faker\", \"new-fake-password\")).thenReturn(\n            Mono.just(user)\n        );\n\n        var userDetailsMono = userDetailService.updatePassword(fakeUser, \"new-fake-password\");\n\n        StepVerifier.create(userDetailsMono)\n            .expectSubscription()\n            .assertNext(userDetails -> assertEquals(\"new-fake-password\", userDetails.getPassword()))\n            .verifyComplete();\n\n        verify(userService, times(1)).updatePassword(eq(\"faker\"), eq(\"new-fake-password\"));\n    }\n\n    @Test\n    void shouldReturnErrorWhenFailedToUpdatePassword() {\n        var fakeUser = createFakeUserDetails();\n\n        var exception = new RuntimeException(\"failed to update password\");\n        when(userService.updatePassword(\"faker\", \"new-fake-password\")).thenReturn(\n            Mono.error(exception)\n        );\n\n        var userDetailsMono = userDetailService.updatePassword(fakeUser, \"new-fake-password\");\n\n        StepVerifier.create(userDetailsMono)\n            .expectSubscription()\n            .expectErrorMatches(throwable -> throwable == exception)\n            .verify();\n        verify(userService, times(1)).updatePassword(eq(\"faker\"), eq(\"new-fake-password\"));\n    }\n\n    @Test\n    void shouldFindUserDetailsByExistingUsername() {\n        var foundUser = createFakeUser();\n\n        when(userService.getUser(\"faker\")).thenReturn(Mono.just(foundUser));\n        when(roleService.getRolesByUsername(\"faker\")).thenReturn(Flux.just(\"fake-role\"));\n\n        var userDetailsMono = userDetailService.findByUsername(\"faker\");\n\n        StepVerifier.create(userDetailsMono)\n            .expectSubscription()\n            .assertNext(gotUser -> {\n                assertEquals(foundUser.getMetadata().getName(), gotUser.getUsername());\n                assertEquals(foundUser.getSpec().getPassword(), gotUser.getPassword());\n                assertEquals(\n                    Set.of(\"ROLE_fake-role\", \"ROLE_authenticated\", \"ROLE_anonymous\"),\n                    authorityListToSet(gotUser.getAuthorities()));\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFindHaloUserDetailsWith2faDisabledWhen2faNotEnabled() {\n        var fakeUser = createFakeUser();\n        when(userService.getUser(\"faker\")).thenReturn(Mono.just(fakeUser));\n        when(roleService.getRolesByUsername(\"faker\")).thenReturn(Flux.empty());\n        userDetailService.findByUsername(\"faker\")\n            .as(StepVerifier::create)\n            .assertNext(userDetails -> {\n                assertInstanceOf(HaloUserDetails.class, userDetails);\n                assertFalse(((HaloUserDetails) userDetails).isTwoFactorAuthEnabled());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFindHaloUserDetailsWith2faDisabledWhen2faEnabledButNoTotpConfigured() {\n        var fakeUser = createFakeUser();\n        fakeUser.getSpec().setTwoFactorAuthEnabled(true);\n        when(userService.getUser(\"faker\")).thenReturn(Mono.just(fakeUser));\n        when(roleService.getRolesByUsername(\"faker\")).thenReturn(Flux.empty());\n        userDetailService.findByUsername(\"faker\")\n            .as(StepVerifier::create)\n            .assertNext(userDetails -> {\n                assertInstanceOf(HaloUserDetails.class, userDetails);\n                assertFalse(((HaloUserDetails) userDetails).isTwoFactorAuthEnabled());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFindHaloUserDetailsWith2faEnabledWhen2faEnabledAndTotpConfigured() {\n        var fakeUser = createFakeUser();\n        fakeUser.getSpec().setTwoFactorAuthEnabled(true);\n        fakeUser.getSpec().setTotpEncryptedSecret(\"fake-totp-encrypted-secret\");\n        when(userService.getUser(\"faker\")).thenReturn(Mono.just(fakeUser));\n        when(roleService.getRolesByUsername(\"faker\")).thenReturn(Flux.empty());\n        userDetailService.findByUsername(\"faker\")\n            .as(StepVerifier::create)\n            .assertNext(userDetails -> {\n                assertInstanceOf(HaloUserDetails.class, userDetails);\n                assertTrue(((HaloUserDetails) userDetails).isTwoFactorAuthEnabled());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFindHaloUserDetailsWith2faDisabledWhen2faDisabledGlobally() {\n        userDetailService.setTwoFactorAuthDisabled(true);\n        var fakeUser = createFakeUser();\n        fakeUser.getSpec().setTwoFactorAuthEnabled(true);\n        fakeUser.getSpec().setTotpEncryptedSecret(\"fake-totp-encrypted-secret\");\n        when(userService.getUser(\"faker\")).thenReturn(Mono.just(fakeUser));\n        when(roleService.getRolesByUsername(\"faker\")).thenReturn(Flux.empty());\n        userDetailService.findByUsername(\"faker\")\n            .as(StepVerifier::create)\n            .assertNext(userDetails -> {\n                assertInstanceOf(HaloUserDetails.class, userDetails);\n                assertFalse(((HaloUserDetails) userDetails).isTwoFactorAuthEnabled());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFindUserDetailsByExistingUsernameButWithoutAnyRoles() {\n        var foundUser = createFakeUser();\n\n        when(userService.getUser(\"faker\")).thenReturn(Mono.just(foundUser));\n        when(roleService.getRolesByUsername(\"faker\")).thenReturn(Flux.empty());\n\n        StepVerifier.create(userDetailService.findByUsername(\"faker\"))\n            .expectSubscription()\n            .assertNext(gotUser -> {\n                assertEquals(foundUser.getMetadata().getName(), gotUser.getUsername());\n                assertEquals(foundUser.getSpec().getPassword(), gotUser.getPassword());\n                assertEquals(\n                    Set.of(\"ROLE_anonymous\", \"ROLE_authenticated\"),\n                    authorityListToSet(gotUser.getAuthorities()));\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldNotFindUserDetailsByNonExistingUsername() {\n        when(userService.getUser(\"non-existing-user\")).thenReturn(\n            Mono.error(() -> new UserNotFoundException(\"non-existing-user\")));\n\n        var userDetailsMono = userDetailService.findByUsername(\"non-existing-user\");\n\n        StepVerifier.create(userDetailsMono)\n            .expectError(AuthenticationException.class)\n            .verify();\n    }\n\n    @Test\n    void shouldFindUserDetailsByEmail() {\n        var foundUser = createFakeUser();\n\n        when(userService.findUserByVerifiedEmail(\"faker@halo.run\"))\n            .thenReturn(Mono.just(foundUser));\n        when(roleService.getRolesByUsername(\"faker\")).thenReturn(Flux.just(\"fake-role\"));\n\n        var userDetailsMono = userDetailService.findByUsername(\"faker@halo.run\");\n\n        StepVerifier.create(userDetailsMono)\n            .expectSubscription()\n            .assertNext(gotUser -> {\n                assertEquals(foundUser.getMetadata().getName(), gotUser.getUsername());\n                assertEquals(foundUser.getSpec().getPassword(), gotUser.getPassword());\n                assertEquals(\n                    Set.of(\"ROLE_fake-role\", \"ROLE_authenticated\", \"ROLE_anonymous\"),\n                    authorityListToSet(gotUser.getAuthorities()));\n            })\n            .verifyComplete();\n\n        verify(userService, never()).getUser(any());\n    }\n\n    @Test\n    void shouldReturnNotFoundWhenEmailNotExists() {\n        when(userService.findUserByVerifiedEmail(\"non-existing-email@halo.run\"))\n            .thenReturn(Mono.error(new UserNotFoundException(\"non-existing-email@halo.run\")));\n\n        var userDetailsMono = userDetailService.findByUsername(\"non-existing-email@halo.run\");\n\n        StepVerifier.create(userDetailsMono)\n            .expectError(BadCredentialsException.class)\n            .verify();\n    }\n\n    UserDetails createFakeUserDetails() {\n        return User.builder()\n            .username(\"faker\")\n            .password(\"fake-password\")\n            .roles(\"fake-role\")\n            .build();\n    }\n\n    run.halo.app.core.extension.User createFakeUser() {\n        var metadata = new Metadata();\n        metadata.setName(\"faker\");\n\n        var userSpec = new run.halo.app.core.extension.User.UserSpec();\n        userSpec.setPassword(\"fake-password\");\n\n        var user = new run.halo.app.core.extension.User();\n        user.setMetadata(metadata);\n        user.setSpec(userSpec);\n        return user;\n\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/HaloServerRequestCacheTest.java",
    "content": "package run.halo.app.security;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.web.server.session.DefaultWebSessionManager;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\nclass HaloServerRequestCacheTest {\n\n    HaloServerRequestCache requestCache;\n\n    @BeforeEach\n    void setUp() {\n        requestCache = new HaloServerRequestCache();\n    }\n\n    @Test\n    void shouldNotSaveIfPageNotCacheable() {\n        var mockExchange =\n            MockServerWebExchange.from(MockServerHttpRequest.get(\"/login\"));\n        requestCache.saveRequest(mockExchange)\n            .then(requestCache.getRedirectUri(mockExchange))\n            .as(StepVerifier::create)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldSaveIfPageCacheable() {\n        var mockExchange = MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"/archives\")\n                .queryParam(\"q\", \"v\")\n                .accept(MediaType.TEXT_HTML)\n        );\n        requestCache.saveRequest(mockExchange)\n            .then(requestCache.getRedirectUri(mockExchange))\n            .as(StepVerifier::create)\n            .expectNext(URI.create(\"/archives?q=v\"))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldSaveIfRedirectUriPresent() {\n        var mockExchange = MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"/login\")\n                .queryParam(\"redirect_uri\", \"/halo?q=v#fragment\")\n        );\n        requestCache.saveRequest(mockExchange)\n            .then(requestCache.getRedirectUri(mockExchange))\n            .as(StepVerifier::create)\n            .expectNext(URI.create(\"/halo?q=v#fragment\"))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldRemoveIfRedirectUriFound() {\n        var sessionManager = new DefaultWebSessionManager();\n        var mockExchange = MockServerWebExchange.builder(MockServerHttpRequest.get(\"/login\")\n                .queryParam(\"redirect_uri\", \"/halo\")\n            )\n            .sessionManager(sessionManager)\n            .build();\n        var removeExchange = mockExchange.mutate()\n            .request(builder -> builder.uri(URI.create(\"/halo\")))\n            .build();\n        requestCache.saveRequest(mockExchange)\n            .then(Mono.defer(() -> requestCache.removeMatchingRequest(removeExchange)))\n            .as(StepVerifier::create)\n            .assertNext(request -> {\n                Assertions.assertEquals(URI.create(\"/halo\"), request.getURI());\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldRemoveIfRedirectUriFoundAndContainsFragment() {\n        var sessionManager = new DefaultWebSessionManager();\n        var mockExchange =\n            MockServerWebExchange.builder(MockServerHttpRequest.get(\"/login\")\n                    .queryParam(\"redirect_uri\", \"/halo#fragment\")\n                )\n                .sessionManager(sessionManager)\n                .build();\n        var removeExchange = mockExchange.mutate()\n            .request(builder -> builder.uri(URI.create(\"/halo\")))\n            .build();\n        requestCache.saveRequest(mockExchange)\n            .then(Mono.defer(() -> requestCache.removeMatchingRequest(removeExchange)))\n            .as(StepVerifier::create)\n            .assertNext(request -> {\n                Assertions.assertEquals(URI.create(\"/halo\"), request.getURI());\n            })\n            .verifyComplete();\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java",
    "content": "package run.halo.app.security;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.security.web.server.ServerRedirectStrategy;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.infra.InitializationStateGetter;\n\n/**\n * Tests for {@link InitializeRedirectionWebFilter}.\n *\n * @author guqing\n * @since 2.5.2\n */\n@ExtendWith(MockitoExtension.class)\nclass InitializeRedirectionWebFilterTest {\n\n    @Mock\n    private InitializationStateGetter initializationStateGetter;\n\n    @Mock\n    private ServerRedirectStrategy serverRedirectStrategy;\n\n    @InjectMocks\n    private InitializeRedirectionWebFilter filter;\n\n    @BeforeEach\n    void setUp() {\n        filter.setRedirectStrategy(serverRedirectStrategy);\n    }\n\n    @Test\n    void shouldRedirectWhenSystemNotInitialized() {\n        when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(false));\n\n        WebFilterChain chain = mock(WebFilterChain.class);\n        var paths = new String[] {\"/\", \"/console/test\", \"/uc/test\", \"/login\", \"/signup\"};\n        for (String path : paths) {\n            MockServerHttpRequest request = MockServerHttpRequest.get(path)\n                .accept(MediaType.TEXT_HTML).build();\n            MockServerWebExchange exchange = MockServerWebExchange.from(request);\n\n            when(serverRedirectStrategy.sendRedirect(any(), any())).thenReturn(Mono.empty().then());\n\n            Mono<Void> result = filter.filter(exchange, chain);\n\n            StepVerifier.create(result)\n                .expectNextCount(0)\n                .expectComplete()\n                .verify();\n\n            verify(serverRedirectStrategy).sendRedirect(eq(exchange),\n                eq(URI.create(\"/system/setup\")));\n            verify(chain, never()).filter(eq(exchange));\n        }\n    }\n\n    @Test\n    void shouldNotRedirectWhenSystemInitialized() {\n        lenient().when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));\n\n        WebFilterChain chain = mock(WebFilterChain.class);\n\n        var paths = new String[] {\"/test\", \"/apis/test\", \"system/setup\", \"/logout\"};\n        for (String path : paths) {\n            MockServerHttpRequest request = MockServerHttpRequest.get(path)\n                .accept(MediaType.TEXT_HTML).build();\n            MockServerWebExchange exchange = MockServerWebExchange.from(request);\n            when(chain.filter(any())).thenReturn(Mono.empty().then());\n            Mono<Void> result = filter.filter(exchange, chain);\n\n            StepVerifier.create(result)\n                .expectNextCount(0)\n                .expectComplete()\n                .verify();\n\n            verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange), any());\n            verify(chain).filter(eq(exchange));\n        }\n    }\n\n    @Test\n    void shouldNotRedirectTest() {\n        WebFilterChain chain = mock(WebFilterChain.class);\n\n        MockServerHttpRequest request = MockServerHttpRequest.get(\"/test\")\n            .accept(MediaType.TEXT_HTML).build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        when(chain.filter(any())).thenReturn(Mono.empty().then());\n        Mono<Void> result = filter.filter(exchange, chain);\n\n        StepVerifier.create(result)\n            .expectNextCount(0)\n            .expectComplete()\n            .verify();\n\n        verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange), any());\n        verify(chain).filter(eq(exchange));\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/ResponseMap.java",
    "content": "package run.halo.app.security;\n\nimport java.util.HashMap;\n\npublic class ResponseMap extends HashMap<String, Object> {\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/WebExchangeMatchersTest.java",
    "content": "package run.halo.app.security.authentication;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.springframework.http.MediaType.ALL;\nimport static org.springframework.http.MediaType.APPLICATION_JSON;\nimport static org.springframework.http.MediaType.TEXT_HTML;\nimport static run.halo.app.security.authentication.WebExchangeMatchers.ignoringMediaTypeAll;\n\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport reactor.test.StepVerifier;\n\nclass WebExchangeMatchersTest {\n\n    @Test\n    void shouldNotMatchMediaTypeAll() {\n        assertion(Set.of(APPLICATION_JSON), Set.of(APPLICATION_JSON, ALL), true);\n        assertion(Set.of(APPLICATION_JSON), Set.of(ALL), false);\n        assertion(Set.of(APPLICATION_JSON), Set.of(APPLICATION_JSON), true);\n        assertion(Set.of(APPLICATION_JSON), Set.of(APPLICATION_JSON, TEXT_HTML), true);\n    }\n\n    void assertion(Set<MediaType> matchingMediaTypes,\n        Set<MediaType> acceptMediaTypes,\n        boolean expectMatch) {\n        var matcher = ignoringMediaTypeAll(matchingMediaTypes.toArray(new MediaType[0]));\n        MockServerHttpRequest request = MockServerHttpRequest.get(\"/fake\")\n            .accept(acceptMediaTypes.toArray(new MediaType[0]))\n            .build();\n        var webExchange = MockServerWebExchange.from(request);\n        StepVerifier.create(matcher.matches(webExchange))\n            .consumeNextWith(matchResult -> assertEquals(expectMatch, matchResult.isMatch()))\n            .verifyComplete();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/impl/RsaKeyServiceTest.java",
    "content": "package run.halo.app.security.authentication.impl;\n\nimport static com.nimbusds.jose.jwk.KeyOperation.SIGN;\nimport static com.nimbusds.jose.jwk.KeyOperation.VERIFY;\nimport static org.junit.jupiter.api.Assertions.assertArrayEquals;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWSAlgorithm;\nimport com.nimbusds.jose.jwk.KeyUse;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.InvalidKeyException;\nimport java.security.KeyFactory;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.interfaces.RSAPrivateCrtKey;\nimport java.security.interfaces.RSAPublicKey;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.Set;\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.Cipher;\nimport javax.crypto.IllegalBlockSizeException;\nimport javax.crypto.NoSuchPaddingException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.util.StringUtils;\nimport reactor.core.Exceptions;\nimport reactor.test.StepVerifier;\nimport run.halo.app.security.authentication.login.InvalidEncryptedMessageException;\n\n@ExtendWith(MockitoExtension.class)\nclass RsaKeyServiceTest {\n\n    RsaKeyService service;\n\n    @TempDir\n    Path tempDir;\n\n    @BeforeEach\n    void setUp() throws JOSEException {\n        service = new RsaKeyService(tempDir);\n        service.afterPropertiesSet();\n    }\n\n    @Test\n    void shouldGenerateKeyPair()\n        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {\n        byte[] privKeyBytes = Files.readAllBytes(tempDir.resolve(\"pat_id_rsa\"));\n        byte[] pubKeyBytes = Files.readAllBytes(tempDir.resolve(\"pat_id_rsa.pub\"));\n\n        var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);\n        var privKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);\n        var keyFactory = KeyFactory.getInstance(RsaKeyService.ALGORITHM);\n        var privKey = (RSAPrivateCrtKey) keyFactory.generatePrivate(privKeySpec);\n        var pubKey = (RSAPublicKey) keyFactory.generatePublic(pubKeySpec);\n        assertEquals(privKey.getModulus(), pubKey.getModulus());\n        assertEquals(privKey.getPublicExponent(), pubKey.getPublicExponent());\n    }\n\n    @Test\n    void shouldReadPublicKey() throws IOException {\n        var realPubKeyBytes = Files.readAllBytes(tempDir.resolve(\"pat_id_rsa.pub\"));\n\n        StepVerifier.create(service.readPublicKey())\n            .assertNext(bytes -> assertArrayEquals(realPubKeyBytes, bytes))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldDecryptMessageCorrectly() {\n        final String message = \"halo\";\n\n        var mono = service.readPublicKey()\n            .map(pubKeyBytes -> {\n                var pubKeySpec = new X509EncodedKeySpec(pubKeyBytes);\n                try {\n                    var keyFactory = KeyFactory.getInstance(RsaKeyService.ALGORITHM);\n                    var pubKey = keyFactory.generatePublic(pubKeySpec);\n                    var cipher = Cipher.getInstance(RsaKeyService.TRANSFORMATION);\n                    cipher.init(Cipher.ENCRYPT_MODE, pubKey);\n                    return cipher.doFinal(message.getBytes());\n                } catch (NoSuchAlgorithmException | InvalidKeySpecException\n                         | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException\n                         | BadPaddingException e) {\n                    throw Exceptions.propagate(e);\n                }\n            })\n            .flatMap(service::decrypt)\n            .map(String::new);\n\n        StepVerifier.create(mono)\n            .expectNext(message)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFailToDecryptMessage() {\n        StepVerifier.create(service.decrypt(\"invalid-bytes\".getBytes()))\n            .verifyError(InvalidEncryptedMessageException.class);\n    }\n\n    @Test\n    void shouldGetKeyIdFromJwk() {\n        assertTrue(StringUtils.hasText(service.getKeyId()));\n    }\n\n    @Test\n    void shouldGetJwk() {\n        var jwk = service.getJwk();\n        assertEquals(\"RSA\", jwk.getKeyType().getValue());\n        assertEquals(JWSAlgorithm.RS256, jwk.getAlgorithm());\n        assertEquals(KeyUse.SIGNATURE, jwk.getKeyUse());\n        assertEquals(Set.of(SIGN, VERIFY), jwk.getKeyOperations());\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/login/LoginAuthenticationConverterTest.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport io.github.resilience4j.ratelimiter.RateLimiter;\nimport io.github.resilience4j.ratelimiter.RateLimiterConfig;\nimport io.github.resilience4j.ratelimiter.RateLimiterRegistry;\nimport java.time.Duration;\nimport java.util.Base64;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.security.authentication.BadCredentialsException;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.util.LinkedMultiValueMap;\nimport org.springframework.util.MultiValueMap;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.security.authentication.CryptoService;\nimport run.halo.app.security.authentication.exception.TooManyRequestsException;\n\n@ExtendWith(MockitoExtension.class)\nclass LoginAuthenticationConverterTest {\n\n    @Mock\n    ServerWebExchange exchange;\n\n    @Mock\n    CryptoService cryptoService;\n\n    @Mock\n    RateLimiterRegistry rateLimiterRegistry;\n\n    @InjectMocks\n    LoginAuthenticationConverter converter;\n\n    MultiValueMap<String, String> formData;\n\n    @BeforeEach\n    void setUp() {\n        formData = new LinkedMultiValueMap<>();\n        lenient().when(exchange.getFormData()).thenReturn(Mono.just(formData));\n        var request = mock(ServerHttpRequest.class);\n        var headers = new HttpHeaders();\n\n        when(request.getHeaders()).thenReturn(headers);\n        when(exchange.getRequest()).thenReturn(request);\n        when(rateLimiterRegistry.rateLimiter(\"authentication-from-ip-unknown\",\n            \"authentication\"))\n            .thenReturn(RateLimiter.ofDefaults(\"authentication\"));\n    }\n\n    @Test\n    void shouldTriggerRateLimit() {\n        var username = \"username\";\n        var password = \"password\";\n\n        formData.add(\"username\", username);\n        formData.add(\"password\", Base64.getEncoder().encodeToString(password.getBytes()));\n        var rateLimiter = RateLimiter.of(\"authentication\", RateLimiterConfig.custom()\n            .limitForPeriod(1)\n            .limitRefreshPeriod(Duration.ofSeconds(1))\n            .timeoutDuration(Duration.ofMillis(0))\n            .build());\n        assertTrue(rateLimiter.acquirePermission(1));\n        when(rateLimiterRegistry.rateLimiter(\"authentication-from-ip-unknown\", \"authentication\"))\n            .thenReturn(rateLimiter);\n        StepVerifier.create(converter.convert(exchange))\n            .expectError(TooManyRequestsException.class)\n            .verify();\n\n        verify(cryptoService, never()).decrypt(password.getBytes());\n    }\n\n    @Test\n    void applyUsernameAndPasswordThenCreatesTokenSuccess() {\n        var username = \"username\";\n        var password = \"password\";\n        var decryptedPassword = \"decrypted password\";\n\n        formData.add(\"username\", username);\n        formData.add(\"password\", Base64.getEncoder().encodeToString(password.getBytes()));\n\n        when(cryptoService.decrypt(password.getBytes()))\n            .thenReturn(Mono.just(decryptedPassword.getBytes()));\n        StepVerifier.create(converter.convert(exchange))\n            .expectNext(new UsernamePasswordAuthenticationToken(username, decryptedPassword))\n            .verifyComplete();\n\n        verify(cryptoService).decrypt(password.getBytes());\n    }\n\n    @Test\n    void applyPasswordWithoutBase64FormatThenBadCredentialsException() {\n        var username = \"username\";\n        var password = \"+invalid-base64-format-password\";\n\n        formData.add(\"username\", username);\n        formData.add(\"password\", password);\n\n        StepVerifier.create(converter.convert(exchange))\n            .verifyError(BadCredentialsException.class);\n    }\n\n    @Test\n    void applyUsernameAndInvalidPasswordThenBadCredentialsException() {\n        var username = \"username\";\n        var password = \"password\";\n\n        formData.add(\"username\", username);\n        formData.add(\"password\", Base64.getEncoder().encodeToString(password.getBytes()));\n\n        when(cryptoService.decrypt(password.getBytes()))\n            .thenReturn(Mono.error(() -> new InvalidEncryptedMessageException(\"invalid message\")));\n        StepVerifier.create(converter.convert(exchange))\n            .verifyError(BadCredentialsException.class);\n        verify(cryptoService).decrypt(password.getBytes());\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/login/PublicKeyRouteBuilderTest.java",
    "content": "package run.halo.app.security.authentication.login;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Base64;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.authentication.CryptoService;\n\n@ExtendWith(MockitoExtension.class)\nclass PublicKeyRouteBuilderTest {\n\n    WebTestClient webClient;\n\n    @Mock\n    CryptoService cryptoService;\n\n    @BeforeEach\n    void setUp() {\n        webClient = WebTestClient.bindToRouterFunction(\n            new PublicKeyRouteBuilder(cryptoService).build()\n        ).build();\n    }\n\n    @Test\n    void shouldReadPublicKey() {\n        var publicKeyStr = \"public-key\";\n        var encoder = Base64.getEncoder();\n        when(cryptoService.readPublicKey()).thenReturn(Mono.just(publicKeyStr.getBytes()));\n        webClient.get().uri(\"/login/public-key\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(PublicKeyRouteBuilder.PublicKeyResponse.class)\n            .consumeWith(result -> {\n                var response = result.getResponseBody();\n                assertNotNull(response);\n                assertEquals(encoder.encodeToString(publicKeyStr.getBytes()),\n                    response.getBase64Format());\n            });\n\n        verify(cryptoService).readPublicKey();\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/pat/PatTest.java",
    "content": "package run.halo.app.security.authentication.pat;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport run.halo.app.security.PersonalAccessToken;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass PatTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @Test\n    @WithMockUser(username = \"faker\", password = \"${noop}password\", roles = \"super-role\")\n    void generatePat() {\n        var requestPat = new PersonalAccessToken();\n        var spec = requestPat.getSpec();\n        spec.setRoles(List.of(\"super-role\"));\n        spec.setName(\"Fake PAT\");\n        webClient.post()\n            .uri(\"/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens\")\n            .bodyValue(requestPat)\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(PersonalAccessToken.class)\n            .value(pat -> {\n                var annotations = pat.getMetadata().getAnnotations();\n                assertTrue(annotations.containsKey(\"security.halo.run/access-token\"));\n            });\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/rememberme/PersistentTokenBasedRememberMeServicesTest.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Date;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.web.authentication.rememberme.CookieTheftException;\nimport org.springframework.security.web.authentication.rememberme.InvalidCookieException;\nimport org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;\nimport org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;\nimport org.springframework.security.web.server.WebFilterExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * Tests for {@link PersistentTokenBasedRememberMeServices}.\n *\n * @author guqing\n * @since 2.17.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PersistentTokenBasedRememberMeServicesTest {\n    @Mock\n    private CookieSignatureKeyResolver cookieSignatureKeyResolver;\n\n    @Mock\n    private ReactiveUserDetailsService userDetailsService;\n\n    @Mock\n    private RememberMeCookieResolver rememberMeCookieResolver;\n\n    @Mock\n    private PersistentRememberMeTokenRepository tokenRepository;\n\n    @InjectMocks\n    private PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices;\n\n    @Nested\n    class ProcessAutoLoginCookieTest {\n        @Test\n        void invalidCookieTest() {\n            var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/\"));\n            assertThatThrownBy(() -> persistentTokenBasedRememberMeServices.processAutoLoginCookie(\n                new String[] {\"test\"},\n                exchange).block())\n                .isInstanceOf(InvalidCookieException.class)\n                .hasMessage(\"Cookie token did not contain 2 tokens, but contained '[test]'\");\n        }\n\n        @Test\n        void noPersistentTokenFoundTest() {\n            var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/\"));\n            when(tokenRepository.getTokenForSeries(eq(\"test-series\")))\n                .thenReturn(Mono.empty());\n\n            assertThatThrownBy(() -> persistentTokenBasedRememberMeServices.processAutoLoginCookie(\n                new String[] {\"test-series\", \"test\"},\n                exchange).block()\n            ).isInstanceOf(RememberMeAuthenticationException.class)\n                .hasMessage(\"No persistent token found for series id: test-series\");\n        }\n\n        @Test\n        void tokenMismatchTest() {\n            var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/\"));\n            when(tokenRepository.getTokenForSeries(eq(\"fake-series\")))\n                .thenReturn(Mono.just(\n                    new PersistentRememberMeToken(\"test\", \"fake-series\", \"other-token-value\",\n                        new Date()))\n                );\n            when(tokenRepository.removeUserTokens(eq(\"test\"))).thenReturn(Mono.empty());\n            assertThatThrownBy(() -> persistentTokenBasedRememberMeServices.processAutoLoginCookie(\n                new String[] {\"fake-series\", \"token-value\"},\n                exchange).block())\n                .isInstanceOf(CookieTheftException.class)\n                .hasMessage(\n                    \"Invalid remember-me token (Series/token) mismatch. Implies previous cookie \"\n                        + \"theft attack.\");\n        }\n\n        @Test\n        void rememberMeLoginExpiredTest() {\n            var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/\"));\n            when(tokenRepository.getTokenForSeries(eq(\"fake-series\")))\n                .thenReturn(Mono.just(\n                    new PersistentRememberMeToken(\"test\", \"fake-series\", \"token-value\",\n                        new Date(Instant.now().minusSeconds(10).toEpochMilli())))\n                );\n            when(rememberMeCookieResolver.getCookieMaxAge()).thenReturn(Duration.ofSeconds(5));\n            assertThatThrownBy(() -> persistentTokenBasedRememberMeServices.processAutoLoginCookie(\n                new String[] {\"fake-series\", \"token-value\"},\n                exchange).block())\n                .isInstanceOf(RememberMeAuthenticationException.class)\n                .hasMessage(\"Remember-me login has expired\");\n        }\n\n        @Test\n        void successfulTest() {\n            var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/\"));\n            when(tokenRepository.getTokenForSeries(eq(\"fake-series\")))\n                .thenReturn(Mono.just(\n                    new PersistentRememberMeToken(\"test\", \"fake-series\", \"token-value\",\n                        new Date()))\n                );\n            when(rememberMeCookieResolver.getCookieMaxAge()).thenReturn(Duration.ofSeconds(5));\n\n            var generatedTokenValue = new AtomicReference<String>();\n            when(tokenRepository.updateToken(eq(\"fake-series\"), any(), any()))\n                .thenAnswer(invocation -> {\n                    var tokenValue = (String) invocation.getArgument(1);\n                    generatedTokenValue.compareAndSet(null, tokenValue);\n                    return Mono.empty();\n                });\n\n            when(userDetailsService.findByUsername(eq(\"test\"))).thenReturn(Mono.empty());\n\n            persistentTokenBasedRememberMeServices.processAutoLoginCookie(\n                    new String[] {\"fake-series\", \"token-value\"}, exchange)\n                .block();\n\n            verify(rememberMeCookieResolver).setRememberMeCookie(eq(exchange),\n                eq(persistentTokenBasedRememberMeServices.encodeCookie(\n                    new String[] {\"fake-series\", generatedTokenValue.get()})));\n        }\n    }\n\n    @Test\n    void onLoginSuccessTest() {\n        var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/\"));\n        var authentication = new UsernamePasswordAuthenticationToken(\"test\", \"test\");\n\n        when(tokenRepository.createNewToken(any())).thenReturn(Mono.empty());\n        persistentTokenBasedRememberMeServices.onLoginSuccess(exchange, authentication).block();\n\n        verify(rememberMeCookieResolver).setRememberMeCookie(eq(exchange), any());\n    }\n\n    @Test\n    void onLogoutTest() {\n        var authentication = new UsernamePasswordAuthenticationToken(\"test\", \"test\");\n\n        when(tokenRepository.removeUserTokens(eq(\"test\"))).thenReturn(Mono.empty());\n\n        var filterExchange = mock(WebFilterExchange.class);\n        persistentTokenBasedRememberMeServices.onLogout(filterExchange, authentication).block();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/rememberme/RememberTokenCleanerTest.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.spy;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n/**\n * Test for {@link RememberTokenCleaner}.\n *\n * @author guqing\n * @since 2.17.0\n */\n@ExtendWith(MockitoExtension.class)\nclass RememberTokenCleanerTest {\n    @InjectMocks\n    private RememberTokenCleaner rememberTokenCleaner;\n\n    @Test\n    void test() {\n        var spyRememberTokenCleaner = spy(rememberTokenCleaner);\n        Mockito.doReturn(Duration.ofSeconds(30)).when(spyRememberTokenCleaner).getTokenValidity();\n        var expiredTime = spyRememberTokenCleaner.getExpirationThreshold();\n\n        var creationTime = Instant.now().minus(Duration.ofSeconds(31));\n        // creationTime < expirationThreshold means it has expired\n        assertThat(creationTime).isBefore(expiredTime);\n\n        // not expired\n        creationTime = Instant.now().minus(Duration.ofSeconds(29));\n        assertThat(creationTime).isAfter(expiredTime);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/rememberme/TokenBasedRememberMeServicesTest.java",
    "content": "package run.halo.app.security.authentication.rememberme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.security.authentication.TestingAuthenticationToken;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\n/**\n * Tests for {@link TokenBasedRememberMeServices}.\n *\n * @author guqing\n * @since 2.16.0\n */\n@ExtendWith({SpringExtension.class, MockitoExtension.class})\nclass TokenBasedRememberMeServicesTest {\n\n    @Mock\n    CookieSignatureKeyResolver cookieSignatureKeyResolver;\n\n    @InjectMocks\n    private TokenBasedRememberMeServices tokenBasedRememberMeServices;\n\n    @Test\n    void retrieveUserName() {\n        var authentication = new TestingAuthenticationToken(\"fake-user\", \"test\");\n        var username = tokenBasedRememberMeServices.retrieveUserName(authentication);\n\n        var userDetails = new User(\"zhangsan\", \"test\", List.of());\n        authentication = new TestingAuthenticationToken(userDetails, \"test\");\n        username = tokenBasedRememberMeServices.retrieveUserName(authentication);\n        assertThat(username).isEqualTo(\"zhangsan\");\n    }\n\n    @Test\n    void makeTokenSignatureTest() {\n        when(cookieSignatureKeyResolver.resolveSigningKey()).thenReturn(Mono.just(\"fake-key\"));\n        var expireMs = 1716435187323L;\n        tokenBasedRememberMeServices.makeTokenSignature(expireMs, \"fake-user\", \"pwd-1\",\n                TokenBasedRememberMeServices.DEFAULT_ALGORITHM)\n            .as(StepVerifier::create)\n            .expectNext(\"29f1c7ccbb489741392d27ba5c30f30d05c79ee66289b6d6da5b431bba99a0c7\")\n            .verifyComplete();\n    }\n\n    @Test\n    void encodeCookieTest() {\n        var expireMs = 1716435187323L;\n        var cookieTokens = new String[] {\"fake-user\", Long.toString(expireMs),\n            TokenBasedRememberMeServices.DEFAULT_ALGORITHM,\n            \"29f1c7ccbb489741392d27ba5c30f30d05c79ee66289b6d6da5b431bba99a0c7\"};\n        var encode = tokenBasedRememberMeServices.encodeCookie(cookieTokens);\n        assertThat(encode)\n            .isEqualTo(\"ZmFrZS11c2VyOjE3MTY0MzUxODczMjM6U0hBLTI1NjoyOWYxYzdjY2JiNDg5NzQxMz\"\n                + \"kyZDI3YmE1YzMwZjMwZDA1Yzc5ZWU2NjI4OWI2ZDZkYTViNDMxYmJhOTlhMGM3\");\n    }\n\n    @Test\n    void decodeCookieTest() {\n        var cookieValue = \"YWRtaW46MTcxODk2NDE3NDgwODpTSEE\"\n            + \"tMjU2OmNkOTM0ZTAyZWQ4NGJmMzc1ZTA4MmE1OWU4YTA3NTNiMzA3ODg1MjZmYzA3Yjgy\"\n            + \"YzVmY2Y3YmJiYzdjYzRkNWU\";\n        // 123 % 4 = 3, so we need to add 1 '=' to make it a multiple of 4 for\n        // spring-security/gh-15127\n        assertThat(cookieValue.length()).isEqualTo(123);\n        var cookie = tokenBasedRememberMeServices.decodeCookie(cookieValue);\n        assertThat(cookie).containsExactly(\"admin\", \"1718964174808\", \"SHA-256\",\n            \"cd934e02ed84bf375e082a59e8a0753b30788526fc07b82c5fcf7bbbc7cc4d5e\");\n\n        cookieValue = \"ZmFrZS11c2VyOjE3MTY0MzUxODczMjM6U0hBLTI1NjoyOWYxYzdjY2JiNDg5NzQxMz\"\n            + \"kyZDI3YmE1YzMwZjMwZDA1Yzc5ZWU2NjI4OWI2ZDZkYTViNDMxYmJhOTlhMGM3\";\n        assertThat(cookieValue.length()).isEqualTo(128);\n        cookie = tokenBasedRememberMeServices.decodeCookie(cookieValue);\n        assertThat(cookie).containsExactly(\"fake-user\", \"1716435187323\", \"SHA-256\",\n            \"29f1c7ccbb489741392d27ba5c30f30d05c79ee66289b6d6da5b431bba99a0c7\");\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authentication/twofactor/TwoFactorAuthSettingsTest.java",
    "content": "package run.halo.app.security.authentication.twofactor;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.params.provider.Arguments.arguments;\n\nimport java.util.stream.Stream;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nclass TwoFactorAuthSettingsTest {\n\n    @ParameterizedTest\n    @MethodSource(\"isAvailableCases\")\n    void isAvailableTest(TwoFactorAuthSettings settings, boolean expectAvailable) {\n        assertEquals(expectAvailable, settings.isAvailable());\n    }\n\n    static Stream<Arguments> isAvailableCases() {\n        return Stream.of(\n            arguments(settings(false, true, true), false),\n            arguments(settings(false, false, false), false),\n            arguments(settings(false, false, true), false),\n            arguments(settings(false, true, false), false),\n            arguments(settings(true, true, true), true),\n            arguments(settings(true, false, false), false),\n            arguments(settings(true, false, true), true),\n            arguments(settings(true, true, false), false)\n        );\n    }\n\n    static TwoFactorAuthSettings settings(boolean enabled, boolean emailVerified,\n        boolean totpConfigured) {\n        var settings = new TwoFactorAuthSettings();\n        settings.setEnabled(enabled);\n        settings.setEmailVerified(emailVerified);\n        settings.setTotpConfigured(totpConfigured);\n        return settings;\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authorization/AuthorityUtilsTest.java",
    "content": "package run.halo.app.security.authorization;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles;\nimport static run.halo.app.security.authorization.AuthorityUtils.containsSuperRole;\n\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.security.core.authority.SimpleGrantedAuthority;\n\nclass AuthorityUtilsTest {\n\n    @Test\n    void authoritiesToRolesTest() {\n        var authorities = List.of(\n            new SimpleGrantedAuthority(\"ROLE_admin\"),\n            new SimpleGrantedAuthority(\"ROLE_owner\"),\n            new SimpleGrantedAuthority(\"ROLE_manager\"),\n            new SimpleGrantedAuthority(\"faker\"),\n            new SimpleGrantedAuthority(\"SCOPE_system:read\")\n        );\n\n        var roles = authoritiesToRoles(authorities);\n\n        assertEquals(Set.of(\"admin\", \"owner\", \"manager\"), roles);\n    }\n\n    @Test\n    void containsSuperRoleTest() {\n        assertTrue(containsSuperRole(Set.of(\"super-role\")));\n        assertTrue(containsSuperRole(Set.of(\"super-role\", \"admin\")));\n        assertFalse(containsSuperRole(Set.of(\"admin\")));\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authorization/AuthorizationTest.java",
    "content": "package run.halo.app.security.authorization;\n\nimport static org.mockito.ArgumentMatchers.anySet;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;\nimport static run.halo.app.core.extension.Role.ROLE_AGGREGATE_LABEL_PREFIX;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.TestConfiguration;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.http.MediaType;\nimport org.springframework.lang.NonNull;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.Role.PolicyRule;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.AnonymousUserConst;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\n@DirtiesContext\n@Import(AuthorizationTest.TestConfig.class)\nclass AuthorizationTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @MockitoSpyBean\n    ReactiveUserDetailsService userDetailsService;\n\n    @MockitoSpyBean\n    ReactiveUserDetailsPasswordService userDetailsPasswordService;\n\n    @MockitoSpyBean\n    RoleService roleService;\n\n    @Autowired\n    ExtensionClient client;\n\n    @BeforeEach\n    void setUp() {\n        webClient = webClient.mutateWith(csrf());\n    }\n\n    @Test\n    void anonymousUserAccessProtectedApi() {\n        when(userDetailsService.findByUsername(eq(AnonymousUserConst.PRINCIPAL)))\n            .thenReturn(Mono.empty());\n\n        webClient.get().uri(\"/apis/fake.halo.run/v1/posts\")\n            .header(\"X-Requested-With\", \"XMLHttpRequest\")\n            .exchange()\n            .expectStatus().isUnauthorized();\n\n        webClient.get().uri(\"/apis/fake.halo.run/v1/posts\")\n            .exchange()\n            .expectStatus().isFound()\n            .expectHeader().location(\"/login?authentication_required\");\n\n        verify(roleService, times(2)).listDependenciesFlux(anySet());\n    }\n\n    @Test\n    void anonymousUserAccessAuthenticationFreeApi() {\n        when(userDetailsService.findByUsername(eq(AnonymousUserConst.PRINCIPAL)))\n            .thenReturn(Mono.empty());\n        Role role = new Role();\n        role.setMetadata(new Metadata());\n        role.getMetadata().setName(AnonymousUserConst.Role);\n        role.setRules(new ArrayList<>());\n        PolicyRule policyRule = new PolicyRule.Builder()\n            .apiGroups(\"fake.halo.run\")\n            .verbs(\"list\")\n            .resources(\"posts\")\n            .build();\n        role.getRules().add(policyRule);\n        when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));\n        webClient.get().uri(\"/apis/fake.halo.run/v1/posts\").exchange().expectStatus()\n            .isOk()\n            .expectBody(String.class).isEqualTo(\"returned posts\");\n\n        webClient.get().uri(\"/apis/fake.halo.run/v1/posts/hello-halo\")\n            .header(\"X-Requested-With\", \"XMLHttpRequest\")\n            .exchange()\n            .expectStatus()\n            .isUnauthorized();\n\n        webClient.get().uri(\"/apis/fake.halo.run/v1/posts/hello-halo\")\n            .exchange()\n            .expectStatus()\n            .isFound()\n            .expectHeader().location(\"/login?authentication_required\");\n\n        verify(roleService, times(3)).listDependenciesFlux(anySet());\n    }\n\n    @Test\n    @WithMockUser(username = \"user\", roles = \"post.read\")\n    void authenticatedUserAccessAuthenticationFreeApi() {\n        Role role = new Role();\n        role.setMetadata(new Metadata());\n        role.getMetadata().setName(AnonymousUserConst.Role);\n        role.setRules(new ArrayList<>());\n        PolicyRule policyRule = new PolicyRule.Builder()\n            .apiGroups(\"fake.halo.run\")\n            .verbs(\"list\")\n            .resources(\"posts\")\n            .build();\n        role.getRules().add(policyRule);\n\n        when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(role));\n\n        webClient.get().uri(\"/apis/fake.halo.run/v1/posts\").exchange().expectStatus()\n            .isOk()\n            .expectBody(String.class).isEqualTo(\"returned posts\");\n        verify(roleService).listDependenciesFlux(anySet());\n    }\n\n    @Test\n    void anonymousUserShouldAccessResourcesByAggregatedRoles() {\n        // create a role\n        var role = new Role();\n        role.setMetadata(new Metadata());\n        role.getMetadata().setName(\"fake-role-with-aggregate-to-anonymous\");\n        role.getMetadata().setLabels(new HashMap<>(Map.of(\n            ROLE_AGGREGATE_LABEL_PREFIX + AnonymousUserConst.Role, \"true\"\n        )));\n        role.setRules(new ArrayList<>());\n        var policyRule = new PolicyRule.Builder()\n            .apiGroups(\"fake.halo.run\")\n            .verbs(\"list\")\n            .resources(\"fakes\")\n            .build();\n        role.getRules().add(policyRule);\n        client.create(role);\n\n        webClient.get().uri(\"/apis/fake.halo.run/v1/fakes\").exchange()\n            .expectStatus()\n            .isOk();\n    }\n\n    @TestConfiguration\n    static class TestConfig {\n\n        @Bean\n        public RouterFunction<ServerResponse> postRoute() {\n            return RouterFunctions.route()\n                .GET(\"/apis/fake.halo.run/v1/posts\", request -> ServerResponse.ok()\n                    .contentType(MediaType.TEXT_PLAIN)\n                    .bodyValue(\"returned posts\")\n                )\n                .PUT(\"/apis/fake.halo.run/v1/posts/{name}\", request -> ServerResponse.ok()\n                    .contentType(MediaType.TEXT_PLAIN)\n                    .bodyValue(\"updated post \" + request.pathVariable(\"name\"))\n                )\n                .GET(\"/apis/fake.halo.run/v1/fakes\", request -> ServerResponse.ok().build())\n                .build();\n        }\n\n        @NonNull\n        Mono<ServerResponse> queryPosts(ServerRequest request) {\n            return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)\n                .bodyValue(\"returned posts\");\n        }\n\n        @NonNull\n        Mono<ServerResponse> updatePost(ServerRequest request) {\n            var name = request.pathVariable(\"name\");\n            return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)\n                .bodyValue(\"updated post \" + name);\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authorization/DefaultRuleResolverTest.java",
    "content": "package run.halo.app.security.authorization;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.mock.http.server.reactive.MockServerHttpRequest.method;\nimport static org.springframework.security.authentication.UsernamePasswordAuthenticationToken.authenticated;\nimport static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;\n\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.security.core.userdetails.User;\nimport reactor.core.publisher.Flux;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.core.extension.Role.PolicyRule;\nimport run.halo.app.core.user.service.RoleService;\nimport run.halo.app.extension.Metadata;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultRuleResolverTest {\n\n    @Mock\n    RoleService roleService;\n\n    @InjectMocks\n    DefaultRuleResolver ruleResolver;\n\n    @Test\n    void visitRules() {\n        when(roleService.listDependenciesFlux(Set.of(\"ruleReadPost\")))\n            .thenReturn(Flux.just(mockRole()));\n        var fakeUser = new User(\"admin\", \"123456\", createAuthorityList(\"ROLE_ruleReadPost\"));\n        var authentication = authenticated(fakeUser, fakeUser.getPassword(),\n            fakeUser.getAuthorities());\n\n        var cases = getRequestResolveCases();\n        cases.forEach(requestResolveCase -> {\n            var httpMethod = HttpMethod.valueOf(requestResolveCase.method);\n            var request = method(httpMethod, requestResolveCase.url).build();\n            var requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n            StepVerifier.create(ruleResolver.visitRules(authentication, requestInfo))\n                .assertNext(\n                    visitor -> assertEquals(requestResolveCase.expected, visitor.isAllowed()))\n                .verifyComplete();\n        });\n\n        verify(roleService, times(cases.size())).listDependenciesFlux(Set.of(\"ruleReadPost\"));\n    }\n\n    @Test\n    void visitRulesForUserspaceScope() {\n        when(roleService.listDependenciesFlux(Set.of(\"ruleReadPost\")))\n            .thenReturn(Flux.just(mockRole()));\n        var fakeUser = new User(\"admin\", \"123456\", createAuthorityList(\"ROLE_ruleReadPost\"));\n        var authentication =\n            authenticated(fakeUser, fakeUser.getPassword(), fakeUser.getAuthorities());\n        var cases = List.of(\n            new RequestResolveCase(\"/api/v1/categories\", \"POST\", true),\n            new RequestResolveCase(\"/api/v1/categories\", \"DELETE\", true),\n            new RequestResolveCase(\"/api/v1/userspaces/bar/categories\", \"DELETE\", false),\n            new RequestResolveCase(\"/api/v1/userspaces/admin/categories\", \"DELETE\", true),\n            new RequestResolveCase(\"/api/v1/posts\", \"GET\", true),\n\n            new RequestResolveCase(\"/api/v1/userspaces/foo/posts\", \"GET\", false),\n            new RequestResolveCase(\"/api/v1/userspaces/admin/posts\", \"GET\", true)\n        );\n        cases.forEach(requestResolveCase -> {\n            var httpMethod = HttpMethod.valueOf(requestResolveCase.method);\n            var request = method(httpMethod, requestResolveCase.url).build();\n            var requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n            StepVerifier.create(ruleResolver.visitRules(authentication, requestInfo))\n                .assertNext(\n                    visitor -> assertEquals(requestResolveCase.expected, visitor.isAllowed()))\n                .verifyComplete();\n        });\n    }\n\n    Role mockRole() {\n        var role = new Role();\n        var rules = List.of(\n            new PolicyRule.Builder().apiGroups(\"\").resources(\"posts\").verbs(\"list\", \"get\").build(),\n            new PolicyRule.Builder().apiGroups(\"\").resources(\"categories\").verbs(\"*\").build(),\n            new PolicyRule.Builder().apiGroups(\"api.plugin.halo.run\")\n                .resources(\"plugins/users\")\n                .resourceNames(\"foo/bar\").verbs(\"*\").build(),\n            new PolicyRule.Builder().apiGroups(\"api.plugin.halo.run\")\n                .resources(\"plugins/users\")\n                .resourceNames(\"foo\").verbs(\"*\").build(),\n            new PolicyRule.Builder().nonResourceURLs(\"/healthy\").verbs(\"get\", \"post\", \"head\")\n                .build());\n        role.setRules(rules);\n        var metadata = new Metadata();\n        metadata.setName(\"ruleReadPost\");\n        role.setMetadata(metadata);\n        return role;\n    }\n\n    List<RequestResolveCase> getRequestResolveCases() {\n        return List.of(new RequestResolveCase(\"/api/v1/tags\", \"GET\", false),\n            new RequestResolveCase(\"/api/v1/tags/tagName\", \"GET\", false),\n\n            new RequestResolveCase(\"/api/v1/categories/aName\", \"GET\", true),\n            new RequestResolveCase(\"/api/v1//categories\", \"POST\", true),\n            new RequestResolveCase(\"/api/v1/categories\", \"DELETE\", true),\n            new RequestResolveCase(\"/api/v1/posts\", \"GET\", true),\n            new RequestResolveCase(\"/api/v1/posts/aName\", \"GET\", true),\n\n            new RequestResolveCase(\"/api/v1/posts\", \"DELETE\", false),\n            new RequestResolveCase(\"/api/v1/posts/aName\", \"UPDATE\", false),\n\n            // group resource url\n            new RequestResolveCase(\"/apis/group/v1/posts\", \"GET\", false),\n\n            // plugin custom resource url\n            new RequestResolveCase(\"/apis/api.plugin.halo.run/v1alpha1/plugins/foo/users\", \"GET\",\n                true),\n            new RequestResolveCase(\"/apis/api.plugin.halo.run/v1alpha1/plugins/foo/users/bar\",\n                \"GET\", true),\n            new RequestResolveCase(\"/apis/api.plugin.halo.run/v1alpha1/plugins/foo/posts/bar\",\n                \"GET\", false),\n\n            // non resource url\n            new RequestResolveCase(\"/healthy\", \"GET\", true),\n            new RequestResolveCase(\"/healthy\", \"POST\", true),\n            new RequestResolveCase(\"/healthy\", \"HEAD\", true),\n            new RequestResolveCase(\"//healthy\", \"GET\", false),\n            new RequestResolveCase(\"/healthy/name\", \"GET\", false),\n            new RequestResolveCase(\"/healthy1\", \"GET\", false),\n\n            new RequestResolveCase(\"//healthy//name\", \"GET\", false),\n            new RequestResolveCase(\"/\", \"GET\", false));\n    }\n\n    record RequestResolveCase(String url, String method, boolean expected) {\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authorization/PolicyRuleTest.java",
    "content": "package run.halo.app.security.authorization;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.core.extension.Role;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link Role.PolicyRule}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass PolicyRuleTest {\n    private ObjectMapper objectMapper;\n\n    @BeforeEach\n    void setUp() {\n        objectMapper = JsonUtils.DEFAULT_JSON_MAPPER;\n    }\n\n    @Test\n    public void constructPolicyRule() throws JsonProcessingException, JSONException {\n        Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null);\n        assertThat(policyRule).isNotNull();\n        JSONAssert.assertEquals(\"\"\"\n            {\n                \"apiGroups\": [],\n                \"resources\": [],\n                \"resourceNames\": [],\n                \"nonResourceURLs\": [],\n                \"verbs\": []\n            }\n            \"\"\",\n            JsonUtils.objectToJson(policyRule),\n            true);\n\n        Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build();\n        JSONAssert.assertEquals(\"\"\"\n            {\n                \"apiGroups\": [],\n                \"resources\": [],\n                \"resourceNames\": [],\n                \"nonResourceURLs\": [],\n                \"verbs\": []\n            }\n            \"\"\",\n            JsonUtils.objectToJson(policyByBuilder),\n            true);\n\n        Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder()\n            .apiGroups(\"group\")\n            .resources(\"resource-1\", \"resource-2\")\n            .resourceNames(\"resourceName\")\n            .nonResourceURLs(\"non resource url\")\n            .verbs(\"verbs\")\n            .build();\n\n        JsonNode expected = objectMapper.readTree(\"\"\"\n            {\n                \"apiGroups\": [\n                    \"group\"\n                ],\n                \"resources\": [\n                    \"resource-1\",\n                    \"resource-2\"\n                ],\n                \"resourceNames\": [\n                    \"resourceName\"\n                ],\n                \"nonResourceURLs\": [\n                    \"non resource url\"\n                ],\n                \"verbs\": [\n                    \"verbs\"\n                ]\n            }\n            \"\"\");\n        JsonNode policyNonNullJson = objectMapper.valueToTree(policyNonNull);\n        assertThat(policyNonNullJson).isEqualTo(expected);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authorization/RbacRequestEvaluationTest.java",
    "content": "package run.halo.app.security.authorization;\n\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThat;\n\nimport org.junit.jupiter.api.Test;\nimport run.halo.app.core.extension.Role;\n\n/**\n * Tests for {@link RbacRequestEvaluation}.\n *\n * @author guqing\n * @since 2.4.0\n */\nclass RbacRequestEvaluationTest {\n\n    @Test\n    void resourceNameMatches() {\n        RbacRequestEvaluation rbacRequestEvaluation = new RbacRequestEvaluation();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"\", \"fake/test\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"\", \"fake\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"\", \"\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*\", null)).isTrue();\n\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*/test\", \"fake/test\")).isTrue();\n\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*/test\", \"hello/test\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*/test\", \"hello/fake\")).isFalse();\n\n        assertThat(matchResourceName(rbacRequestEvaluation, \"test/*\", \"hello/fake\")).isFalse();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"test/*\", \"test/fake\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"test/*\", \"test\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"test/*\", \"hello\")).isFalse();\n\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*/*\", \"test/fake\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*/*\", \"test\")).isTrue();\n\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*\", \"test\")).isTrue();\n        assertThat(matchResourceName(rbacRequestEvaluation, \"*\", \"hello\")).isTrue();\n    }\n\n    boolean matchResourceName(RbacRequestEvaluation rbacRequestEvaluation, String rule,\n        String requestedName) {\n        return rbacRequestEvaluation.resourceNameMatches(new Role.PolicyRule.Builder()\n            .resourceNames(rule)\n            .build(), requestedName);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/authorization/RequestInfoResolverTest.java",
    "content": "package run.halo.app.security.authorization;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.springframework.mock.http.server.reactive.MockServerHttpRequest.method;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.http.HttpMethod;\n\n/**\n * Tests for {@link RequestInfoFactory}.\n *\n * @author guqing\n * @see RequestInfo\n * @since 2.0.0\n */\npublic class RequestInfoResolverTest {\n\n    @Test\n    void shouldResolveAsWatchRequestWhenRequestIsWebSocket() {\n        var request = method(HttpMethod.GET, \"/apis/fake.halo.run/v1alpha1/fakes\")\n            .header(\"Upgrade\", \"websocket\")\n            .header(\"Connection\", \"Upgrade\")\n            .build();\n        RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n        assertThat(requestInfo).isNotNull();\n        assertThat(requestInfo.getVerb()).isEqualTo(\"watch\");\n    }\n\n    @Test\n    void shouldNotResolveAsWatchRequestWhenRequestIsNotWebSocket() {\n        var request = method(HttpMethod.GET, \"/apis/fake.halo.run/v1alpha1/fakes\")\n            .header(\"Upgrade\", \"websocket\")\n            .build();\n        RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n        assertThat(requestInfo).isNotNull();\n        assertThat(requestInfo.getVerb()).isEqualTo(\"list\");\n    }\n\n    @Test\n    public void requestInfoTest() {\n        for (SuccessCase successCase : getTestRequestInfos()) {\n            final var request = method(HttpMethod.valueOf(successCase.method), successCase.url)\n                .build();\n\n            RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n\n            assertNotNull(requestInfo, successCase::toString);\n            assertEquals(successCase.expectedVerb, requestInfo.getVerb(), successCase::toString);\n            assertThat(requestInfo.getApiPrefix()).isEqualTo(successCase.expectedAPIPrefix);\n            assertThat(requestInfo.getApiGroup()).isEqualTo(successCase.expectedAPIGroup);\n            assertThat(requestInfo.getApiVersion()).isEqualTo(successCase.expectedAPIVersion);\n            assertThat(requestInfo.getResource()).isEqualTo(successCase.expectedResource);\n            assertThat(requestInfo.getSubresource()).isEqualTo(successCase.expectedSubresource);\n            assertThat(requestInfo.getName()).isEqualTo(successCase.expectedName);\n            assertThat(requestInfo.getParts()).isEqualTo(successCase.expectedParts);\n        }\n    }\n\n    @Test\n    public void nonApiRequestInfoTest() {\n        Map<String, NonApiCase> map = new HashMap<>();\n        map.put(\"simple groupless\", new NonApiCase(\"/api/version/resource\", true));\n        map.put(\"simple group\",\n            new NonApiCase(\"/apis/group/version/resource/name/subresource\", true));\n        map.put(\"more steps\", new NonApiCase(\"/api/version/resource/name/subresource\", true));\n        map.put(\"group list\", new NonApiCase(\"/apis/batch/v1/job\", true));\n        map.put(\"group get\", new NonApiCase(\"/apis/batch/v1/job/foo\", true));\n        map.put(\"group subresource\", new NonApiCase(\"/apis/batch/v1/job/foo/scale\", true));\n\n        // bad case\n        map.put(\"bad root\", new NonApiCase(\"/not-api/version/resource\", false));\n        map.put(\"group without enough steps\", new NonApiCase(\"/apis/extensions/v1beta1\", false));\n        map.put(\"group without enough steps 2\", new NonApiCase(\"/apis/extensions/v1beta1/\", false));\n        map.put(\"not enough steps\", new NonApiCase(\"/api/version\", false));\n        map.put(\"one step\", new NonApiCase(\"/api\", false));\n        map.put(\"zero step\", new NonApiCase(\"/\", false));\n        map.put(\"empty\", new NonApiCase(\"\", false));\n\n        map.forEach((k, v) -> {\n            var request = method(HttpMethod.GET, v.url).build();\n            RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n            if (requestInfo.isResourceRequest() != v.expected) {\n                throw new RuntimeException(\n                    String.format(\"%s: expected %s, actual %s\", k, v.expected,\n                        requestInfo.isResourceRequest()));\n            }\n        });\n    }\n\n    @Test\n    void pluginsScopedAndPluginManage() {\n        List<CustomSuccessCase> testCases =\n            List.of(\n                new CustomSuccessCase(\"DELETE\", \"/apis/api.plugin.halo.run/v1/plugins/other/posts\",\n                    \"delete\", \"apis\", \"api.plugin.halo.run\", \"v1\", \"\", \"plugins\", \"posts\", \"\", \"\",\n                    new String[] {\"plugins\", \"other\", \"posts\"}),\n\n                // api group identification\n                new CustomSuccessCase(\"POST\",\n                    \"/apis/api.plugin.halo.run/v1/plugins/other/posts/foo\",\n                    \"create\", \"apis\",\n                    \"api.plugin.halo.run\", \"v1\", \"\", \"plugins\", \"posts\", \"other\", \"foo\",\n                    new String[] {\"plugins\", \"other\", \"posts\", \"foo\"}),\n\n                // api version identification\n                new CustomSuccessCase(\"POST\",\n                    \"/apis/api.plugin.halo.run/v1beta3/plugins/other/posts/bar\", \"create\",\n                    \"apis\", \"api.plugin.halo.run\", \"v1beta3\", \"\", \"plugins\", \"posts\", \"other\",\n                    \"bar\",\n                    new String[] {\"plugins\", \"other\", \"posts\", \"bar\"}));\n\n        // 以 /apis 开头的 plugins 资源为 core 中管理插件使用的资源\n        for (CustomSuccessCase successCase : testCases) {\n            var request =\n                method(HttpMethod.valueOf(successCase.method),\n                    successCase.url).build();\n            RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n            assertThat(requestInfo).isNotNull();\n            assertRequestInfoCase(successCase, requestInfo);\n        }\n\n        List<CustomSuccessCase> pluginScopedCases =\n            List.of(\n                new CustomSuccessCase(\"DELETE\", \"/apis/api.plugin.halo.run/v1/plugins/other/posts\",\n                    \"delete\", \"apis\", \"api.plugin.halo.run\", \"v1\", \"\", \"plugins\", \"posts\",\n                    \"other\", \"\", new String[] {\"plugins\", \"other\", \"posts\"}),\n\n                // api group identification\n                new CustomSuccessCase(\"POST\",\n                    \"/apis/api.plugin.halo.run/v1/plugins/other/posts/some-name\", \"create\", \"apis\",\n                    \"api.plugin.halo.run\", \"v1\", \"other\", \"plugins\", \"posts\", \"other\", \"some-name\",\n                    new String[] {\"plugins\", \"other\", \"posts\", \"some-name\"}));\n\n        for (CustomSuccessCase pluginScopedCase : pluginScopedCases) {\n            var request =\n                method(HttpMethod.valueOf(pluginScopedCase.method),\n                    pluginScopedCase.url).build();\n            RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n            assertThat(requestInfo).isNotNull();\n            assertRequestInfoCase(pluginScopedCase, requestInfo);\n        }\n    }\n\n    private void assertRequestInfoCase(CustomSuccessCase pluginScopedCase,\n        RequestInfo requestInfo) {\n        assertThat(requestInfo.getVerb()).isEqualTo(pluginScopedCase.expectedVerb);\n        assertThat(requestInfo.getParts()).isEqualTo(pluginScopedCase.expectedParts);\n        assertThat(requestInfo.getApiGroup()).isEqualTo(pluginScopedCase.expectedAPIGroup);\n        assertThat(requestInfo.getResource()).isEqualTo(pluginScopedCase.expectedResource);\n        assertThat(requestInfo.getSubresource())\n            .isEqualTo(pluginScopedCase.expectedSubresource());\n        assertThat(requestInfo.getSubName())\n            .isEqualTo(pluginScopedCase.expectedSubName());\n    }\n\n    @Test\n    public void errorCaseTest() {\n        List<ErrorCases> errorCases = List.of(new ErrorCases(\"no resource path\", \"/\"),\n            new ErrorCases(\"just apiversion\", \"/api/version/\"),\n            new ErrorCases(\"just prefix, group, version\", \"/apis/group/version/\"),\n            new ErrorCases(\"apiversion with no resource\", \"/api/version/\"),\n            new ErrorCases(\"bad prefix\", \"/badprefix/version/resource\"),\n            new ErrorCases(\"missing api group\", \"/apis/version/resource\"));\n        for (ErrorCases errorCase : errorCases) {\n            var request =\n                method(HttpMethod.GET, errorCase.url).build();\n            RequestInfo apiRequestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n            if (apiRequestInfo.isResourceRequest()) {\n                throw new RuntimeException(\n                    String.format(\"%s: expected non-resource request\", errorCase.desc));\n            }\n        }\n\n        List<ErrorCases> postCases =\n            List.of(new ErrorCases(\"api resource has name and no subresource but post\",\n                    \"/api/version/themes/install\"),\n                new ErrorCases(\"apis resource has name and no subresource but post\",\n                    \"/apis/api.halo.run/v1alpha1/themes/install\"));\n        for (ErrorCases errorCase : postCases) {\n            var request =\n                method(HttpMethod.POST, errorCase.url).build();\n            RequestInfo apiRequestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);\n            if (apiRequestInfo.isResourceRequest()) {\n                throw new RuntimeException(\n                    String.format(\"%s: expected non-resource request\", errorCase.desc));\n            }\n        }\n    }\n\n    public record NonApiCase(String url, boolean expected) {\n    }\n\n    public record ErrorCases(String desc, String url) {\n    }\n\n\n    public record SuccessCase(String method, String url, String expectedVerb,\n                              String expectedAPIPrefix, String expectedAPIGroup,\n                              String expectedAPIVersion, String expectedNamespace,\n                              String expectedResource, String expectedSubresource,\n                              String expectedName, String[] expectedParts) {\n    }\n\n    public record CustomSuccessCase(String method, String url, String expectedVerb,\n                                    String expectedAPIPrefix, String expectedAPIGroup,\n                                    String expectedAPIVersion, String expectedNamespace,\n                                    String expectedResource, String expectedSubresource,\n                                    String expectedName, String expectedSubName,\n                                    String[] expectedParts) {\n    }\n\n    List<SuccessCase> getTestRequestInfos() {\n        String namespaceAll = \"*\";\n        return List.of(\n            new SuccessCase(\"GET\", \"/api/v1/namespaces\", \"list\", \"api\", \"\", \"v1\", \"\", \"namespaces\",\n                \"\", \"\", new String[] {\"namespaces\"}),\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other\", \"get\", \"api\", \"\", \"v1\", \"other\",\n                \"namespaces\", \"\", \"other\", new String[] {\"namespaces\", \"other\"}),\n\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts\", \"list\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts/foo\", \"get\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"foo\", new String[] {\"posts\", \"foo\"}),\n            new SuccessCase(\"HEAD\", \"/api/v1/namespaces/other/posts/foo\", \"get\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"foo\", new String[] {\"posts\", \"foo\"}),\n            new SuccessCase(\"GET\", \"/api/v1/posts\", \"list\", \"api\", \"\", \"v1\", namespaceAll, \"posts\",\n                \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"HEAD\", \"/api/v1/posts\", \"list\", \"api\", \"\", \"v1\", namespaceAll, \"posts\",\n                \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts/foo\", \"get\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"foo\", new String[] {\"posts\", \"foo\"}),\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts\", \"list\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n\n            // special verbs\n            new SuccessCase(\"GET\", \"/api/v1/proxy/namespaces/other/posts/foo\", \"proxy\", \"api\", \"\",\n                \"v1\", \"other\", \"posts\", \"\", \"foo\", new String[] {\"posts\", \"foo\"}),\n            new SuccessCase(\"GET\",\n                \"/api/v1/proxy/namespaces/other/posts/foo/subpath/not/a/subresource\", \"proxy\",\n                \"api\", \"\", \"v1\", \"other\", \"posts\", \"\", \"foo\",\n                new String[] {\"posts\", \"foo\", \"subpath\", \"not\", \"a\", \"subresource\"}),\n            new SuccessCase(\"GET\", \"/api/v1/watch/posts\", \"watch\", \"api\", \"\", \"v1\", namespaceAll,\n                \"posts\", \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"GET\", \"/api/v1/posts?watch=true\", \"watch\", \"api\", \"\", \"v1\",\n                namespaceAll, \"posts\", \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"GET\", \"/api/v1/posts?watch=false\", \"list\", \"api\", \"\", \"v1\",\n                namespaceAll, \"posts\", \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"GET\", \"/api/v1/watch/namespaces/other/posts\", \"watch\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts?watch=true\", \"watch\", \"api\", \"\",\n                \"v1\", \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts?watch=false\", \"list\", \"api\", \"\",\n                \"v1\", \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n\n            // subresource identification\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts/foo/status\", \"get\", \"api\", \"\",\n                \"v1\", \"other\", \"posts\", \"status\", \"foo\", new String[] {\"posts\", \"foo\", \"status\"}),\n            new SuccessCase(\"GET\", \"/api/v1/namespaces/other/posts/foo/proxy/subpath\", \"get\", \"api\",\n                \"\", \"v1\", \"other\", \"posts\", \"proxy\", \"foo\",\n                new String[] {\"posts\", \"foo\", \"proxy\", \"subpath\"}),\n            new SuccessCase(\"PUT\", \"/api/v1/namespaces/other/finalize\", \"update\", \"api\", \"\", \"v1\",\n                \"other\", \"namespaces\", \"finalize\", \"other\",\n                new String[] {\"namespaces\", \"other\", \"finalize\"}),\n            new SuccessCase(\"PUT\", \"/api/v1/namespaces/other/status\", \"update\", \"api\", \"\", \"v1\",\n                \"other\", \"namespaces\", \"status\", \"other\",\n                new String[] {\"namespaces\", \"other\", \"status\"}),\n\n            // verb identification\n            new SuccessCase(\"PATCH\", \"/api/v1/namespaces/other/posts/foo\", \"patch\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"foo\", new String[] {\"posts\", \"foo\"}),\n            new SuccessCase(\"DELETE\", \"/api/v1/namespaces/other/posts/foo\", \"delete\", \"api\", \"\",\n                \"v1\", \"other\", \"posts\", \"\", \"foo\", new String[] {\"posts\", \"foo\"}),\n            new SuccessCase(\"POST\", \"/api/v1/namespaces/other/posts\", \"create\", \"api\", \"\", \"v1\",\n                \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n\n            // deletecollection verb identification\n            new SuccessCase(\"DELETE\", \"/api/v1/nodes?all=true\", \"deletecollection\", \"api\", \"\", \"v1\",\n                \"\",\n                \"nodes\", \"\", \"\", new String[] {\"nodes\"}),\n            new SuccessCase(\"DELETE\", \"/api/v1/namespaces?all=false\", \"delete\", \"api\", \"\",\n                \"v1\", \"\",\n                \"namespaces\", \"\", \"\", new String[] {\"namespaces\"}),\n            new SuccessCase(\"DELETE\", \"/api/v1/namespaces/other/posts?all=true\", \"deletecollection\",\n                \"api\",\n                \"\", \"v1\", \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n            new SuccessCase(\"DELETE\", \"/apis/extensions/v1/namespaces/other/posts?all=true\",\n                \"deletecollection\", \"apis\", \"extensions\", \"v1\", \"other\", \"posts\", \"\", \"\",\n                new String[] {\"posts\"}),\n\n            // api group identification\n            new SuccessCase(\"POST\", \"/apis/extensions/v1/namespaces/other/posts\", \"create\", \"apis\",\n                \"extensions\", \"v1\", \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}),\n\n            // api version identification\n            new SuccessCase(\"POST\", \"/apis/extensions/v1beta3/namespaces/other/posts\", \"create\",\n                \"apis\", \"extensions\", \"v1beta3\", \"other\", \"posts\", \"\", \"\", new String[] {\"posts\"}));\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/device/DeviceServiceImplTest.java",
    "content": "package run.halo.app.security.device;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.stream.Stream;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\n\n/**\n * Tests for {@link DeviceServiceImpl}.\n *\n * @author guqing\n * @since 2.17.0\n */\nclass DeviceServiceImplTest {\n\n    static Stream<Arguments> deviceInfoParseTest() {\n        return Stream.of(\n            Arguments.of(\n                \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like \"\n                    + \"Gecko) Chrome/126.0.0.0 Safari/537.36\",\n                \"Mac OS X 10.15.7\",\n                \"Chrome 126.0\"\n            ),\n            Arguments.of(\n                \"Mozilla/5.0 (Phone; OpenHarmony 5.0) AppleWebKit/537.36 (KHTML, like Gecko) \"\n                    + \"Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile HuaweiBrowser/5.0.4\"\n                    + \".300\",\n                \"OpenHarmony 5.0\",\n                \"Chrome 114.0\"\n            )\n        );\n    }\n\n    @ParameterizedTest\n    @MethodSource\n    void deviceInfoParseTest(String userAgent, String expectedOs, String expectedBrowser) {\n        var info = DeviceServiceImpl.DeviceInfo.parse(userAgent);\n        assertThat(info.os()).isEqualTo(expectedOs);\n        assertThat(info.browser()).isEqualTo(expectedBrowser);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/jackson2/HaloSecurityJacksonModuleTest.java",
    "content": "package run.halo.app.security.jackson2;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.core.context.SecurityContextImpl;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.security.jackson2.SecurityJackson2Modules;\nimport org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;\nimport org.springframework.security.oauth2.core.user.DefaultOAuth2User;\nimport org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;\nimport run.halo.app.security.authentication.login.HaloUser;\nimport run.halo.app.security.authentication.oauth2.HaloOAuth2AuthenticationToken;\nimport run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;\n\nclass HaloSecurityJacksonModuleTest {\n\n    ObjectMapper objectMapper;\n\n    @BeforeEach\n    void setUp() {\n        this.objectMapper = Jackson2ObjectMapperBuilder.json()\n            .modules(SecurityJackson2Modules.getModules(this.getClass().getClassLoader()))\n            .modules(modules -> modules.add(new HaloSecurityJackson2Module()))\n            .indentOutput(true)\n            .build();\n    }\n\n    @Test\n    void codecHaloUserTest() throws JsonProcessingException {\n        codecAssert(haloUser -> UsernamePasswordAuthenticationToken.authenticated(haloUser,\n            haloUser.getPassword(),\n            haloUser.getAuthorities()));\n    }\n\n    @Test\n    void codecTwoFactorAuthenticationTokenTest() throws JsonProcessingException {\n        codecAssert(haloUser -> {\n            var authentication = UsernamePasswordAuthenticationToken.authenticated(haloUser,\n                haloUser.getPassword(),\n                haloUser.getAuthorities());\n            return new TwoFactorAuthentication(authentication);\n        });\n    }\n\n    @Test\n    void codecHaloOAuth2AuthenticationTokenTest() throws JsonProcessingException {\n        codecAssert(haloUser -> {\n            var oauth2User = new DefaultOAuth2User(List.of(), Map.of(\"name\", \"halo\"), \"name\");\n            var oauth2Token = new OAuth2AuthenticationToken(oauth2User, List.of(), \"github\");\n            return new HaloOAuth2AuthenticationToken(haloUser, oauth2Token);\n        });\n    }\n\n    @Test\n    void shouldReadSwitchUserGrantedAuthority() throws JsonProcessingException {\n        codecAssert(haloUser -> {\n            var authentication = UsernamePasswordAuthenticationToken.authenticated(\n                haloUser.getUsername(), haloUser.getPassword(), haloUser.getAuthorities()\n            );\n            var switchUserGrantedAuthority =\n                new SwitchUserGrantedAuthority(\"ADMIN\", authentication);\n            var extendedAuthorities = new ArrayList<>(authentication.getAuthorities());\n            extendedAuthorities.add(switchUserGrantedAuthority);\n            authentication = UsernamePasswordAuthenticationToken.authenticated(\n                authentication.getPrincipal(), authentication.getCredentials(), extendedAuthorities\n            );\n            return authentication;\n        });\n    }\n\n    void codecAssert(Function<HaloUser, Authentication> authenticationConverter)\n        throws JsonProcessingException {\n        var userDetails = User.withUsername(\"faker\")\n            .password(\"123456\")\n            .authorities(\"ROLE_USER\")\n            .build();\n        var haloUser = new HaloUser(userDetails, true, \"fake-encrypted-secret\");\n\n        var authentication = authenticationConverter.apply(haloUser);\n\n        var securityContext = new SecurityContextImpl(authentication);\n        var securityContextJson = objectMapper.writeValueAsString(securityContext);\n\n        var deserializedSecurityContext =\n            objectMapper.readValue(securityContextJson, SecurityContext.class);\n\n        assertEquals(deserializedSecurityContext, securityContext);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/preauth/SystemSetupEndpointTest.java",
    "content": "package run.halo.app.security.preauth;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.Properties;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link SystemSetupEndpoint}.\n *\n * @author guqing\n * @since 2.20.0\n */\nclass SystemSetupEndpointTest {\n\n    @Test\n    void placeholderTest() {\n        var properties = new Properties();\n        properties.setProperty(\"username\", \"guqing\");\n        properties.setProperty(\"timestamp\", \"2024-09-30\");\n        var str = SystemSetupEndpoint.PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(\"\"\"\n            ${username}\n            ${timestamp}\n            \"\"\", properties);\n        assertThat(str).isEqualTo(\"\"\"\n            guqing\n            2024-09-30\n            \"\"\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/session/InMemoryReactiveIndexedSessionRepositoryTest.java",
    "content": "package run.halo.app.security.session;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.springframework.session.ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;\n\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport reactor.test.StepVerifier;\n\n/**\n * Tests for {@link InMemoryReactiveIndexedSessionRepository}.\n *\n * @author guqing\n * @since 2.15.0\n */\nclass InMemoryReactiveIndexedSessionRepositoryTest {\n    private InMemoryReactiveIndexedSessionRepository sessionRepository;\n\n    @BeforeEach\n    void setUp() {\n        sessionRepository = new InMemoryReactiveIndexedSessionRepository(new ConcurrentHashMap<>());\n    }\n\n    @Test\n    void principalNameIndexTest() {\n        sessionRepository.createSession()\n            .doOnNext(session -> {\n                session.setAttribute(PRINCIPAL_NAME_INDEX_NAME,\n                    \"test\");\n            })\n            .map(session -> sessionRepository.indexResolver.resolveIndexesFor(session))\n            .as(StepVerifier::create)\n            .consumeNextWith(map -> {\n                assertThat(map).containsEntry(\n                    PRINCIPAL_NAME_INDEX_NAME,\n                    \"test\");\n            });\n\n        sessionRepository.findByPrincipalName(\"test\")\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n\n        sessionRepository.findByIndexNameAndIndexValue(\n                PRINCIPAL_NAME_INDEX_NAME, \"test\")\n            .as(StepVerifier::create)\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    @Test\n    void saveTest() {\n        var indexKey = createSession(\"fake-session-1\", \"test\");\n\n        assertThat(sessionRepository.getSessionIdIndexMap()).hasSize(1);\n        assertThat(\n            sessionRepository.getSessionIdIndexMap().containsValue(Set.of(indexKey))).isTrue();\n\n        assertThat(sessionRepository.getIndexSessionIdMap()).hasSize(1);\n        assertThat(sessionRepository.getIndexSessionIdMap().containsKey(indexKey)).isTrue();\n        assertThat(sessionRepository.getIndexSessionIdMap().get(indexKey)).isEqualTo(\n            Set.of(\"fake-session-1\"));\n    }\n\n    @Test\n    void saveToUpdateTest() {\n        // same session id will update the index\n        createSession(\"fake-session-1\", \"test\");\n        var indexKey2 = createSession(\"fake-session-1\", \"test2\");\n\n        assertThat(sessionRepository.getSessionIdIndexMap()).hasSize(1);\n        assertThat(\n            sessionRepository.getSessionIdIndexMap().containsValue(Set.of(indexKey2))).isTrue();\n\n        assertThat(sessionRepository.getIndexSessionIdMap()).hasSize(1);\n        assertThat(sessionRepository.getIndexSessionIdMap().containsKey(indexKey2)).isTrue();\n        assertThat(sessionRepository.getIndexSessionIdMap().get(indexKey2)).isEqualTo(\n            Set.of(\"fake-session-1\"));\n    }\n\n    @Test\n    void deleteByIdTest() {\n        createSession(\"fake-session-2\", \"test1\");\n        sessionRepository.deleteById(\"fake-session-2\")\n            .as(StepVerifier::create)\n            .verifyComplete();\n        assertThat(sessionRepository.getSessionIdIndexMap()).isEmpty();\n        assertThat(sessionRepository.getIndexSessionIdMap()).isEmpty();\n    }\n\n    InMemoryReactiveIndexedSessionRepository.IndexKey createSession(String sessionId,\n        String principalName) {\n        var indexKey = new InMemoryReactiveIndexedSessionRepository.IndexKey(\n            PRINCIPAL_NAME_INDEX_NAME, principalName);\n        sessionRepository.createSession()\n            .doOnNext(session -> {\n                session.setAttribute(indexKey.attributeName(), indexKey.attributeValue());\n                session.setId(sessionId);\n            })\n            .flatMap(sessionRepository::save)\n            .as(StepVerifier::create)\n            .verifyComplete();\n        return indexKey;\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/switchuser/SwitchUserConfigurerTest.java",
    "content": "package run.halo.app.security.switchuser;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.security.core.userdetails.ReactiveUserDetailsService;\nimport org.springframework.security.core.userdetails.User;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\n\n\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass SwitchUserConfigurerTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @MockitoSpyBean\n    ReactiveUserDetailsService userDetailsService;\n\n    @Test\n    @WithMockUser(username = \"admin\", roles = \"super-role\")\n    void shouldSwitchWithSuperRole() {\n        when(userDetailsService.findByUsername(\"faker\"))\n            .thenReturn(Mono.fromSupplier(() -> User.withUsername(\"faker\")\n                .password(\"password\")\n                .roles(\"user\")\n                .build()));\n        var result = webClient.mutateWith(csrf())\n            .post()\n            .uri(\"/login/impersonate?username={username}&redirect_uri={redirect_uri}\",\n                \"faker\", \"/fake-success\"\n            )\n            .exchange()\n            .expectStatus().isFound()\n            .expectHeader().location(\"/fake-success\")\n            .expectCookie().exists(\"SESSION\")\n            .expectBody().returnResult();\n        var session = result.getResponseCookies().getFirst(\"SESSION\");\n        assertNotNull(session);\n    }\n\n    @Test\n    @WithSwitchUser(\n        username = \"admin\", roles = {\"super-role\"},\n        targetUsername = \"faker\", targetRoles = {\"user\"}\n    )\n    void shouldLogoutSuccessfully() {\n        webClient.mutateWith(csrf())\n            .post().uri(\"/logout/impersonate?redirect_uri={redirect_uri}\", \"/fake-logout-success\")\n            .exchange()\n            .expectStatus().isFound()\n            .expectHeader().location(\"/fake-logout-success\");\n    }\n\n    @Test\n    @WithMockUser(username = \"admin\", roles = \"non-super-role\")\n    void shouldNotSwitchWithoutSuperRole() {\n        webClient.mutateWith(csrf())\n            .post()\n            .uri(\"/login/impersonate?username={username}&redirect_uri={redirect_uri}\",\n                \"faker\", \"/fake-success\"\n            )\n            .exchange()\n            .expectStatus().isForbidden();\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/switchuser/SwitchUserSecurityContextFactory.java",
    "content": "package run.halo.app.security.switchuser;\n\nimport static org.springframework.security.authentication.UsernamePasswordAuthenticationToken.authenticated;\nimport static org.springframework.security.web.server.authentication.SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR;\n\nimport org.springframework.security.core.authority.AuthorityUtils;\nimport org.springframework.security.core.context.SecurityContext;\nimport org.springframework.security.core.context.SecurityContextImpl;\nimport org.springframework.security.test.context.support.WithSecurityContextFactory;\nimport org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;\n\nclass SwitchUserSecurityContextFactory implements WithSecurityContextFactory<WithSwitchUser> {\n\n    @Override\n    public SecurityContext createSecurityContext(WithSwitchUser annotation) {\n        var username = annotation.username();\n        var roles = annotation.roles();\n        var switchToUsername = annotation.targetUsername();\n        var switchToRoles = annotation.targetRoles();\n\n        var currentAuthentication =\n            authenticated(username, \"password\", AuthorityUtils.createAuthorityList(roles));\n        var switchAuthority =\n            new SwitchUserGrantedAuthority(ROLE_PREVIOUS_ADMINISTRATOR, currentAuthentication);\n\n        var targetAuthorities = AuthorityUtils.createAuthorityList(switchToRoles);\n        targetAuthorities.add(switchAuthority);\n        var authentication = authenticated(switchToUsername, \"password\",\n            targetAuthorities);\n        return new SecurityContextImpl(authentication);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/security/switchuser/WithSwitchUser.java",
    "content": "package run.halo.app.security.switchuser;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Inherited;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.security.test.context.support.WithSecurityContext;\n\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@Inherited\n@Documented\n@WithSecurityContext(factory = SwitchUserSecurityContextFactory.class)\n@interface WithSwitchUser {\n\n    String username();\n\n    String targetUsername();\n\n    String[] roles() default {};\n\n    String[] targetRoles() default {};\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/CompositeTemplateResolverTest.java",
    "content": "package run.halo.app.theme;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.thymeleaf.templateresolver.ITemplateResolver;\nimport org.thymeleaf.templateresolver.TemplateResolution;\nimport run.halo.app.infra.exception.NotFoundException;\n\nclass CompositeTemplateResolverTest {\n\n    @Test\n    void shouldGetBlankNameIfNoResolvers() {\n        var resolver = new CompositeTemplateResolver(null);\n        assertEquals(\"\", resolver.getName());\n    }\n\n    @Test\n    void shouldGetNameIfResolversProvided() {\n        var resolverA = mock(ITemplateResolver.class);\n        when(resolverA.getName()).thenReturn(\"A\");\n        var resolverB = mock(ITemplateResolver.class);\n        when(resolverB.getName()).thenReturn(\"B\");\n        var resolver = new CompositeTemplateResolver(List.of(resolverB, resolverA));\n        assertEquals(\"B, A\", resolver.getName());\n    }\n\n    @Test\n    void shouldGetNullOrder() {\n        var resolver = new CompositeTemplateResolver(null);\n        assertNull(resolver.getOrder());\n    }\n\n    @Test\n    void shouldThrowNotFoundExceptionIfNoResolvers() {\n        var resolver = new CompositeTemplateResolver(null);\n        assertThrows(\n            NotFoundException.class,\n            () -> resolver.resolveTemplate(null, null, null, null)\n        );\n    }\n\n    @Test\n    void shouldThrowNotFoundExceptionIfAllResolversReturnNull() {\n        var resolverA = mock(ITemplateResolver.class);\n        when(resolverA.resolveTemplate(null, null, null, null)).thenReturn(null);\n        var resolverB = mock(ITemplateResolver.class);\n        when(resolverB.resolveTemplate(null, null, null, null)).thenReturn(null);\n        var resolver = new CompositeTemplateResolver(List.of(resolverA, resolverB));\n        assertThrows(\n            NotFoundException.class,\n            () -> resolver.resolveTemplate(null, null, null, null)\n        );\n    }\n\n    @Test\n    void shouldResolveTemplateIfResolvedByOneOfResolvers() {\n        var resolverA = mock(ITemplateResolver.class);\n        var resolution = mock(TemplateResolution.class);\n        when(resolverA.resolveTemplate(null, null, null, null))\n            .thenReturn(resolution);\n        var resolverB = mock(ITemplateResolver.class);\n        when(resolverB.resolveTemplate(null, null, null, null))\n            .thenReturn(null);\n        var resolver = new CompositeTemplateResolver(List.of(resolverA, resolverB));\n        assertEquals(resolution, resolver.resolveTemplate(null, null, null, null));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java",
    "content": "package run.halo.app.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.when;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.context.ApplicationContext;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.spring6.dialect.SpringStandardDialect;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\nimport org.thymeleaf.templateresource.StringTemplateResource;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.dialect.HaloProcessorDialect;\n\n/**\n * Tests expression parser for reactive return value.\n *\n * @author guqing\n * @see ReactiveSpelVariableExpressionEvaluator\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\npublic class ReactiveFinderExpressionParserTests {\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private ObjectProvider<ExtensionGetter> extensionGetterProvider;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    private TemplateEngine templateEngine;\n\n    @BeforeEach\n    void setUp() {\n        HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();\n        templateEngine = new TemplateEngine();\n        templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect() {\n            @Override\n            public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {\n                return new ReactiveSpelVariableExpressionEvaluator();\n            }\n        }));\n        templateEngine.addTemplateResolver(new TestTemplateResolver());\n        lenient().when(applicationContext.getBean(eq(SystemConfigFetcher.class)))\n            .thenReturn(environmentFetcher);\n        when(applicationContext.getBeanProvider(ExtensionGetter.class))\n            .thenReturn(extensionGetterProvider);\n        when(extensionGetterProvider.getIfUnique()).thenReturn(null);\n        lenient().when(environmentFetcher.fetchComment())\n            .thenReturn(Mono.just(new SystemSetting.Comment()));\n    }\n\n    @Test\n    void javascriptInlineParser() {\n        Context context = getContext();\n        context.setVariable(\"target\", new TestReactiveFinder());\n        context.setVariable(\"genericMap\", Map.of(\"key\", \"value\"));\n        String result = templateEngine.process(\"javascriptInline\", context);\n        assertThat(result).isEqualTo(\"\"\"\n            <p>value</p>\n            <p>ruibaby</p>\n            <p>guqing</p>\n            <p>bar</p>\n            <script>\n                var genericValue = \"value\";\n                var name = \"guqing\";\n                var names = [\"guqing\",\"johnniang\",\"ruibaby\"];\n                var users = [{\"name\":\"guqing\"},{\"name\":\"ruibaby\"},{\"name\":\"johnniang\"}];\n                var userListItem = \"guqing\";\n                var objectJsonNodeFlux = [{\"name\":\"guqing\"}];\n                var objectJsonNodeFluxChain = \"guqing\";\n                var mapMono = \"bar\";\n                var arrayNodeMono = \"bar\";\n            </script>\n            \"\"\");\n    }\n\n    static class TestReactiveFinder {\n        public Mono<String> getName() {\n            return Mono.just(\"guqing\");\n        }\n\n        public Flux<String> names() {\n            return Flux.just(\"guqing\", \"johnniang\", \"ruibaby\");\n        }\n\n        public Flux<TestUser> users() {\n            return Flux.just(\n                new TestUser(\"guqing\"), new TestUser(\"ruibaby\"), new TestUser(\"johnniang\")\n            );\n        }\n\n        public Flux<JsonNode> objectJsonNodeFlux() {\n            ObjectNode objectNode = JsonUtils.DEFAULT_JSON_MAPPER.createObjectNode();\n            objectNode.put(\"name\", \"guqing\");\n            return Flux.just(objectNode);\n        }\n\n        public Mono<Map<String, Object>> mapMono() {\n            return Mono.just(Map.of(\"foo\", \"bar\"));\n        }\n\n        public Mono<JsonNode> arrayNodeMono() {\n            ArrayNode arrayNode = JsonUtils.DEFAULT_JSON_MAPPER.createArrayNode();\n            arrayNode.add(arrayNode.objectNode().put(\"foo\", \"bar\"));\n            return Mono.just(arrayNode);\n        }\n    }\n\n    record TestUser(String name) {\n    }\n\n    private Context getContext() {\n        Context context = new Context();\n        context.setVariable(\n            ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,\n            new ThymeleafEvaluationContext(applicationContext, null));\n        return context;\n    }\n\n    static class TestTemplateResolver extends StringTemplateResolver {\n        @Override\n        protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,\n            String ownerTemplate, String template,\n            Map<String, Object> templateResolutionAttributes) {\n            return new StringTemplateResource(\"\"\"\n                <p th:text=\"${genericMap.key}\"></p>\n                <p th:text=\"${target.users[1].name}\"></p>\n                <p th:text=\"${target.objectJsonNodeFlux[0].name}\"></p>\n                <p th:text=\"${target.arrayNodeMono.get(0).foo}\"></p>\n                <script th:inline=\"javascript\">\n                    var genericValue = /*[[${genericMap.key}]]*/;\n                    var name = /*[[${target.getName()}]]*/;\n                    var names = /*[[${target.names()}]]*/;\n                    var users = /*[[${target.users()}]]*/;\n                    var userListItem = /*[[${target.users[0].name}]]*/;\n                    var objectJsonNodeFlux = /*[[${target.objectJsonNodeFlux()}]]*/;\n                    var objectJsonNodeFluxChain = /*[[${target.objectJsonNodeFlux[0].name}]]*/;\n                    var mapMono = /*[[${target.mapMono.foo}]]*/;\n                    var arrayNodeMono = /*[[${target.arrayNodeMono.get(0).foo}]]*/;\n                </script>\n                \"\"\");\n        }\n\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/SiteSettingVariablesAcquirerTest.java",
    "content": "package run.halo.app.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.infra.ExternalUrlSupplier;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.theme.finders.vo.SiteSettingVo;\n\n/**\n * Tests for {@link SiteSettingVariablesAcquirer}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\npublic class SiteSettingVariablesAcquirerTest {\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @Mock\n    private SystemVersionSupplier systemVersionSupplier;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @InjectMocks\n    private SiteSettingVariablesAcquirer siteSettingVariablesAcquirer;\n\n    @Test\n    void acquireWhenExternalUrlSet() throws MalformedURLException {\n        var url = new URL(\"https://halo.run\");\n        when(externalUrlSupplier.getURL(any())).thenReturn(url);\n        when(systemVersionSupplier.get()).thenReturn(Version.parse(\"0.0.0-alpha.1\"));\n        when(environmentFetcher.getConfig()).thenReturn(Mono.just(Map.of()));\n\n        siteSettingVariablesAcquirer.acquire(mock(ServerWebExchange.class))\n            .as(StepVerifier::create)\n            .consumeNextWith(result -> {\n                assertThat(result).containsKey(\"site\");\n                assertThat(result.get(\"site\")).isInstanceOf(SiteSettingVo.class);\n                var site = (SiteSettingVo) result.get(\"site\");\n                assertThat(site)\n                    .extracting(SiteSettingVo::url)\n                    .isEqualTo(url);\n                assertThat(site)\n                    .extracting(SiteSettingVo::version)\n                    .isEqualTo(\"0.0.0-alpha.1\");\n            })\n            .verifyComplete();\n        verify(externalUrlSupplier).getURL(any());\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/ThemeContextTest.java",
    "content": "package run.halo.app.theme;\n\nimport java.nio.file.Path;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link ThemeContext}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass ThemeContextTest {\n\n    @Test\n    void constructorBuilderTest() throws JSONException {\n        var path = Path.of(\"/tmp/themes/testTheme\");\n        var testTheme = ThemeContext.builder()\n            .name(\"testTheme\")\n            .path(path)\n            .active(true)\n            .build();\n        var got = JsonUtils.objectToJson(testTheme);\n        var expect = String.format(\"\"\"\n                {\n                    \"name\": \"testTheme\",\n                    \"path\": \"%s\",\n                    \"active\": true\n                }\n                \"\"\", path.toUri());\n        JSONAssert.assertEquals(expect, got, false);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/ThemeIntegrationTest.java",
    "content": "package run.halo.app.theme;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport org.hamcrest.Matchers;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.TestConfiguration;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;\nimport org.springframework.test.annotation.DirtiesContext;\nimport org.springframework.test.context.bean.override.mockito.MockitoBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Menu;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.InitializationStateGetter;\nimport run.halo.app.infra.utils.HaloUtils;\nimport run.halo.app.security.AfterSecurityWebFilter;\nimport run.halo.app.theme.router.ModelConst;\n\n@SpringBootTest\n@Import(ThemeIntegrationTest.TestConfig.class)\n@AutoConfigureWebTestClient\n@DirtiesContext\npublic class ThemeIntegrationTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @MockitoBean\n    InitializationStateGetter initializationStateGetter;\n\n    @Autowired\n    ExtensionClient client;\n\n    @BeforeEach\n    void setUp() {\n        when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));\n\n        // create a menu item\n        var menuItem = new MenuItem();\n        menuItem.setMetadata(new Metadata());\n        menuItem.getMetadata().setName(\"main-menu-home\");\n        menuItem.setSpec(new MenuItem.MenuItemSpec());\n        menuItem.getSpec().setDisplayName(\"Home\");\n        menuItem.getSpec().setHref(\"/\");\n        client.create(menuItem);\n\n        // create a primary menu\n        var menu = new Menu();\n        menu.setMetadata(new Metadata());\n        menu.getMetadata().setName(\"main-menu\");\n        menu.setSpec(new Menu.Spec());\n        menu.getSpec().setDisplayName(\"Mail Menu\");\n        menu.getSpec().setMenuItems(new LinkedHashSet<>(List.of(\"main-menu-home\")));\n        client.create(menu);\n    }\n\n    @TestConfiguration\n    static class TestConfig {\n\n        @Bean\n        RouterFunction<ServerResponse> noTemplateExistsRoute() {\n            return RouterFunctions.route()\n                .GET(\n                    \"/no-template-exists\",\n                    request -> ServerResponse.ok().render(\"no-template-exists\")\n                )\n                .build();\n        }\n\n        @Bean\n        RouterFunction<ServerResponse> noCacheRoute() {\n            return RouterFunctions.route()\n                .GET(\n                    \"/should-not-cache\",\n                    request -> ServerResponse.ok().render(\"no-template-exists\")\n                )\n                .before(HaloUtils.noCache())\n                .build();\n        }\n\n        @Bean\n        AfterSecurityWebFilter poweredByHaloTemplateEngineCheckFilter() {\n            var matcher = pathMatchers(HttpMethod.GET, \"/should-not-cache\");\n            return (exchange, chain) -> chain.filter(exchange)\n                .flatMap(v -> matcher.matches(exchange)\n                    .filter(MatchResult::isMatch)\n                    .switchIfEmpty(Mono.fromRunnable(() -> {\n                        assertNull(exchange.getAttribute(ModelConst.NO_CACHE));\n                        assertTrue(exchange.getRequiredAttribute(\n                            ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE)\n                        );\n                    }).then(Mono.empty()))\n                    .doOnNext(m -> {\n                        assertTrue(exchange.getRequiredAttribute(ModelConst.NO_CACHE));\n                        assertFalse(exchange.getRequiredAttribute(\n                            ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE)\n                        );\n                    })\n                )\n                .then();\n        }\n\n    }\n\n    @Test\n    void shouldRespondNotFoundIfNoTemplateFound() {\n        webClient.get()\n            .uri(\"/no-template-exists\")\n            .accept(MediaType.TEXT_HTML)\n            .exchange()\n            .expectStatus().isNotFound()\n            .expectBody(String.class)\n            .value(Matchers.containsString(\"Template no-template-exists was not found\"));\n\n        webClient.get()\n            .uri(\"/should-not-cache\")\n            .accept(MediaType.TEXT_HTML)\n            .exchange()\n            .expectStatus().isNotFound()\n            .expectBody(String.class)\n            .value(Matchers.containsString(\"Template no-template-exists was not found\"));\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/ThemeLinkBuilderTest.java",
    "content": "package run.halo.app.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.lenient;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.file.Paths;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport run.halo.app.infra.ExternalUrlSupplier;\n\n/**\n * Tests for {@link ThemeLinkBuilder}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ThemeLinkBuilderTest {\n    @Mock\n    private ExternalUrlSupplier externalUrlSupplier;\n\n    @BeforeEach\n    void setUp() {\n        // Mock external url supplier\n        lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(\"\"));\n    }\n\n    @Test\n    void processTemplateLinkWithNoActive() {\n        ThemeLinkBuilder themeLinkBuilder =\n            new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);\n\n        String link = \"/post\";\n        String processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(\"/post?preview-theme=test-theme\");\n\n        processed = themeLinkBuilder.processLink(null, \"/post?foo=bar\");\n        assertThat(processed).isEqualTo(\"/post?foo=bar&preview-theme=test-theme\");\n    }\n\n    @Test\n    void processTemplateLinkWithActive() {\n        ThemeLinkBuilder themeLinkBuilder =\n            new ThemeLinkBuilder(getTheme(true), externalUrlSupplier);\n\n        String link = \"/post\";\n        String processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(\"/post\");\n    }\n\n    @Test\n    void processAssetsLink() {\n        // activated theme\n        ThemeLinkBuilder themeLinkBuilder =\n            new ThemeLinkBuilder(getTheme(true), externalUrlSupplier);\n\n        String link = \"/assets/css/style.css\";\n        String processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(\"/themes/test-theme/assets/css/style.css\");\n\n        // preview theme\n        getTheme(false);\n        link = \"/assets/js/main.js\";\n        processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(\"/themes/test-theme/assets/js/main.js\");\n    }\n\n    @Test\n    void processNullLink() {\n        ThemeLinkBuilder themeLinkBuilder =\n            new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);\n\n        String link = null;\n        String processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(null);\n\n        // empty link\n        link = \"\";\n        processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(\"/?preview-theme=test-theme\");\n    }\n\n    @Test\n    void processAbsoluteLink() {\n        ThemeLinkBuilder themeLinkBuilder =\n            new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);\n        String link = \"https://github.com/halo-dev\";\n        String processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(link);\n\n        link = \"http://example.com\";\n        processed = themeLinkBuilder.processLink(null, link);\n        assertThat(processed).isEqualTo(link);\n    }\n\n    @Test\n    void linkInSite() throws URISyntaxException {\n        URI uri = new URI(\"\");\n        // relative link is always in site\n        assertThat(ThemeLinkBuilder.linkInSite(uri, \"/post\")).isTrue();\n\n        // absolute link is not in site\n        assertThat(ThemeLinkBuilder.linkInSite(uri, \"https://example.com\")).isFalse();\n\n        uri = new URI(\"https://example.com\");\n        // link in externalUrl is in site link\n        assertThat(ThemeLinkBuilder.linkInSite(uri, \"http://example.com/hello/world\")).isTrue();\n        // scheme is different but authority is same\n        assertThat(ThemeLinkBuilder.linkInSite(uri, \"https://example.com/hello/world\")).isTrue();\n\n        // scheme is same and authority is different\n        assertThat(ThemeLinkBuilder.linkInSite(uri, \"http://halo.run/hello/world\")).isFalse();\n        // scheme is different and authority is different\n        assertThat(ThemeLinkBuilder.linkInSite(uri, \"https://halo.run/hello/world\")).isFalse();\n\n        // port is different\n        uri = new URI(\"http://localhost:8090\");\n        assertThat(ThemeLinkBuilder.linkInSite(uri, \"http://localhost:3000\")).isFalse();\n    }\n\n    private ThemeContext getTheme(boolean isActive) {\n        return ThemeContext.builder()\n            .name(\"test-theme\")\n            .path(Paths.get(\"/themes/test-theme\"))\n            .active(isActive)\n            .build();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/ThemeLocaleContextResolverTest.java",
    "content": "package run.halo.app.theme;\n\nimport static java.util.Locale.CANADA;\nimport static java.util.Locale.CHINA;\nimport static java.util.Locale.CHINESE;\nimport static java.util.Locale.ENGLISH;\nimport static java.util.Locale.GERMAN;\nimport static java.util.Locale.GERMANY;\nimport static java.util.Locale.JAPAN;\nimport static java.util.Locale.JAPANESE;\nimport static java.util.Locale.KOREA;\nimport static java.util.Locale.UK;\nimport static java.util.Locale.US;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_COOKIE_NAME;\nimport static run.halo.app.theme.ThemeLocaleContextResolver.TIME_ZONE_COOKIE_NAME;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Locale;\nimport java.util.TimeZone;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.context.i18n.TimeZoneAwareLocaleContext;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.web.server.ServerWebExchange;\n\n/**\n * Test for {@link ThemeLocaleContextResolver}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass ThemeLocaleContextResolverTest {\n    private final ThemeLocaleContextResolver resolver = new ThemeLocaleContextResolver();\n\n    @Test\n    public void resolveTimeZone() {\n        TimeZoneAwareLocaleContext localeContext =\n            (TimeZoneAwareLocaleContext) this.resolver.resolveLocaleContext(\n                exchangeTimeZone(CHINA));\n        assertThat(localeContext.getTimeZone()).isNotNull();\n        assertThat(localeContext.getTimeZone())\n            .isEqualTo(TimeZone.getTimeZone(\"America/Adak\"));\n        assertThat(localeContext.getLocale()).isNotNull();\n        assertThat(localeContext.getLocale().getLanguage()).isEqualTo(\"en\");\n    }\n\n    @Test\n    public void resolve() {\n        assertThat(this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale())\n            .isEqualTo(CANADA);\n        assertThat(this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale())\n            .isEqualTo(US);\n    }\n\n    @Test\n    public void resolveFromParam() {\n        assertThat(this.resolver.resolveLocaleContext(exchangeForParam(\"en\")).getLocale())\n            .isEqualTo(ENGLISH);\n        assertThat(this.resolver.resolveLocaleContext(exchangeForParam(\"zh\")).getLocale())\n            .isEqualTo(CHINESE);\n        assertThat(this.resolver.resolveLocaleContext(exchangeForParam(\"zh-CN\")).getLocale())\n            .isEqualTo(CHINA);\n        assertThat(this.resolver.resolveLocaleContext(exchangeForParam(\"zh-cn\")).getLocale())\n            .isEqualTo(CHINA);\n    }\n\n    @Test\n    public void resolvePreferredSupported() {\n        this.resolver.setSupportedLocales(Collections.singletonList(CANADA));\n        assertThat(this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale()).isEqualTo(\n            CANADA);\n    }\n\n    @Test\n    public void resolvePreferredNotSupported() {\n        this.resolver.setSupportedLocales(Collections.singletonList(CANADA));\n        assertThat(this.resolver.resolveLocaleContext(exchange(US, UK)).getLocale()).isEqualTo(US);\n    }\n\n    @Test\n    public void resolvePreferredNotSupportedWithDefault() {\n        this.resolver.setSupportedLocales(Arrays.asList(US, JAPAN));\n        this.resolver.setDefaultLocale(JAPAN);\n        assertThat(this.resolver.resolveLocaleContext(exchange(KOREA)).getLocale()).isEqualTo(\n            JAPAN);\n    }\n\n    @Test\n    public void resolvePreferredAgainstLanguageOnly() {\n        this.resolver.setSupportedLocales(Collections.singletonList(ENGLISH));\n        assertThat(\n            this.resolver.resolveLocaleContext(exchange(GERMANY, US, UK)).getLocale()).isEqualTo(\n            ENGLISH);\n    }\n\n    @Test\n    public void resolvePreferredAgainstCountryIfPossible() {\n        this.resolver.setSupportedLocales(Arrays.asList(ENGLISH, UK));\n        assertThat(\n            this.resolver.resolveLocaleContext(exchange(GERMANY, US, UK)).getLocale()).isEqualTo(\n            UK);\n    }\n\n    @Test\n    public void resolvePreferredAgainstLanguageWithMultipleSupportedLocales() {\n        this.resolver.setSupportedLocales(Arrays.asList(GERMAN, US));\n        assertThat(\n            this.resolver.resolveLocaleContext(exchange(GERMANY, US, UK)).getLocale()).isEqualTo(\n            GERMAN);\n    }\n\n    @Test\n    public void resolveMissingAcceptLanguageHeader() {\n        MockServerHttpRequest request = MockServerHttpRequest.get(\"/\").build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isNull();\n    }\n\n    @Test\n    public void resolveMissingAcceptLanguageHeaderWithDefault() {\n        this.resolver.setDefaultLocale(US);\n\n        MockServerHttpRequest request = MockServerHttpRequest.get(\"/\").build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isEqualTo(US);\n    }\n\n    @Test\n    public void resolveEmptyAcceptLanguageHeader() {\n        MockServerHttpRequest request =\n            MockServerHttpRequest.get(\"/\").header(HttpHeaders.ACCEPT_LANGUAGE, \"\").build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isNull();\n    }\n\n    @Test\n    public void resolveEmptyAcceptLanguageHeaderWithDefault() {\n        this.resolver.setDefaultLocale(US);\n\n        MockServerHttpRequest request =\n            MockServerHttpRequest.get(\"/\").header(HttpHeaders.ACCEPT_LANGUAGE, \"\").build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isEqualTo(US);\n    }\n\n    @Test\n    public void resolveInvalidAcceptLanguageHeader() {\n        MockServerHttpRequest request =\n            MockServerHttpRequest.get(\"/\").header(HttpHeaders.ACCEPT_LANGUAGE, \"en_US\").build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isNull();\n    }\n\n    @Test\n    public void resolveInvalidAcceptLanguageHeaderWithDefault() {\n        this.resolver.setDefaultLocale(US);\n\n        MockServerHttpRequest request =\n            MockServerHttpRequest.get(\"/\").header(HttpHeaders.ACCEPT_LANGUAGE, \"en_US\").build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isEqualTo(US);\n    }\n\n    @Test\n    public void defaultLocale() {\n        this.resolver.setDefaultLocale(JAPANESE);\n        MockServerHttpRequest request = MockServerHttpRequest.get(\"/\").build();\n        MockServerWebExchange exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isEqualTo(JAPANESE);\n\n        request = MockServerHttpRequest.get(\"/\").acceptLanguageAsLocales(US).build();\n        exchange = MockServerWebExchange.from(request);\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isEqualTo(US);\n    }\n\n    @Test\n    void resolveUnderminedLocale() {\n        var request = MockServerHttpRequest.get(\"/\")\n            .header(HttpHeaders.ACCEPT_LANGUAGE, \"und\")\n            .build();\n        var exchange = MockServerWebExchange.from(request);\n\n        assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isNull();\n    }\n\n    private ServerWebExchange exchange(Locale... locales) {\n        return MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"\").acceptLanguageAsLocales(locales));\n    }\n\n    private ServerWebExchange exchangeTimeZone(Locale... locales) {\n        return MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"\").acceptLanguageAsLocales(locales)\n                .cookie(new HttpCookie(TIME_ZONE_COOKIE_NAME, \"America/Adak\"))\n                .cookie(new HttpCookie(LANGUAGE_COOKIE_NAME, \"en\")));\n    }\n\n    private ServerWebExchange exchangeForParam(String language) {\n        return MockServerWebExchange.from(\n            MockServerHttpRequest.get(\"/index?language=\" + language));\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/ViewNameResolverTest.java",
    "content": "package run.halo.app.theme;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.Mockito.when;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.boot.thymeleaf.autoconfigure.ThymeleafProperties;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\n\n/**\n * Tests for {@link DefaultViewNameResolver}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith({SpringExtension.class, MockitoExtension.class})\nclass ViewNameResolverTest {\n\n    @Mock\n    ThemeResolver themeResolver;\n\n    @Mock\n    ThymeleafProperties thymeleafProperties;\n\n    @InjectMocks\n    DefaultViewNameResolver viewNameResolver;\n\n    @TempDir\n    Path themePath;\n\n    @BeforeEach\n    void setUp() {\n        when(thymeleafProperties.getSuffix()).thenReturn(ThymeleafProperties.DEFAULT_SUFFIX);\n    }\n\n    @Test\n    void resolveViewNameOrDefault() throws URISyntaxException, IOException {\n        var templatesPath = themePath.resolve(\"templates\");\n        if (!Files.exists(templatesPath)) {\n            Files.createDirectory(templatesPath);\n        }\n        Files.createFile(templatesPath.resolve(\"post_news.html\"));\n        Files.createFile(templatesPath.resolve(\"post_docs.html\"));\n\n\n        var exchange = Mockito.mock(ServerWebExchange.class);\n        when(themeResolver.getTheme(exchange))\n            .thenReturn(Mono.fromSupplier(() -> ThemeContext.builder()\n                .name(\"fake-theme\")\n                .path(themePath)\n                .active(true)\n                .build())\n            );\n\n        MockServerRequest request = MockServerRequest.builder()\n            .uri(new URI(\"/\")).method(HttpMethod.GET)\n            .exchange(exchange)\n            .build();\n\n        viewNameResolver.resolveViewNameOrDefault(request, \"post_news\", \"post\")\n            .as(StepVerifier::create)\n            .expectNext(\"post_news\")\n            .verifyComplete();\n\n        // post_docs.html\n        String viewName = \"post_docs\" + thymeleafProperties.getSuffix();\n        viewNameResolver.resolveViewNameOrDefault(request, viewName, \"post\")\n            .as(StepVerifier::create)\n            .expectNext(viewName)\n            .verifyComplete();\n\n        viewNameResolver.resolveViewNameOrDefault(request, \"post_nothing\", \"post\")\n            .as(StepVerifier::create)\n            .expectNext(\"post\")\n            .verifyComplete();\n    }\n\n    @Test\n    void processName() {\n        var suffix = thymeleafProperties.getSuffix();\n        assertThat(viewNameResolver.computeResourceName(\"post_news\"))\n            .isEqualTo(\"post_news\" + suffix);\n        assertThat(\n            viewNameResolver.computeResourceName(\"post_news\" + suffix))\n            .isEqualTo(\"post_news\" + suffix);\n        assertThat(viewNameResolver.computeResourceName(\"post_news.test\"))\n            .isEqualTo(\"post_news.test\" + suffix);\n\n        assertThatThrownBy(() -> viewNameResolver.computeResourceName(null))\n            .isInstanceOf(IllegalArgumentException.class)\n            .hasMessage(\"Name must not be null\");\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/CommentElementTagProcessorTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.context.ApplicationContext;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.spring6.dialect.SpringStandardDialect;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\nimport org.thymeleaf.templateresource.StringTemplateResource;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Tests for {@link CommentElementTagProcessor}.\n *\n * @author guqing\n * @see ExtensionComponentsFinder\n * @see HaloProcessorDialect\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CommentElementTagProcessorTest {\n\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private ExtensionGetter extensionGetter;\n\n    @Mock\n    private ObjectProvider<ExtensionGetter>  extensionGetterProvider;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    private TemplateEngine templateEngine;\n\n    @BeforeEach\n    void setUp() {\n        HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();\n        templateEngine = new TemplateEngine();\n        templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));\n        templateEngine.addTemplateResolver(new TestTemplateResolver());\n        lenient().when(applicationContext.getBean(eq(ExtensionGetter.class)))\n            .thenReturn(extensionGetter);\n        when(applicationContext.getBeanProvider(ExtensionGetter.class))\n            .thenReturn(extensionGetterProvider);\n        when(extensionGetterProvider.getIfUnique()).thenReturn(null);\n    }\n\n    @Test\n    void doProcess() {\n        Context context = getContext();\n\n        when(applicationContext.getBean(eq(SystemConfigFetcher.class)))\n            .thenReturn(environmentFetcher);\n        var commentSetting = mock(SystemSetting.Comment.class);\n        when(environmentFetcher.fetchComment())\n            .thenReturn(Mono.just(commentSetting));\n        when(commentSetting.getEnable()).thenReturn(true);\n\n        when(extensionGetter.getEnabledExtensions(eq(CommentWidget.class)))\n            .thenReturn(Flux.empty());\n        String result = templateEngine.process(\"commentWidget\", context);\n        assertThat(result).isEqualTo(\"\"\"\n            <!DOCTYPE html>\n            <html lang=\"en\">\n              <body>\n                <p>comment widget:</p>\n               \\s\n              </body>\n            </html>\n            \"\"\");\n\n        when(extensionGetter.getEnabledExtensions(eq(CommentWidget.class)))\n            .thenReturn(Flux.just(new DefaultCommentWidget()));\n        result = templateEngine.process(\"commentWidget\", context);\n        assertThat(result).isEqualTo(\"\"\"\n            <!DOCTYPE html>\n            <html lang=\"en\">\n              <body>\n                <p>comment widget:</p>\n                <p>Comment in default widget</p>\n              </body>\n            </html>\n            \"\"\");\n    }\n\n    static class DefaultCommentWidget implements CommentWidget {\n\n        @Override\n        public void render(ITemplateContext context, IProcessableElementTag tag,\n            IElementTagStructureHandler structureHandler) {\n            structureHandler.replaceWith(\"<p>Comment in default widget</p>\", false);\n        }\n    }\n\n    private Context getContext() {\n        Context context = new Context();\n        context.setVariable(\n            ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,\n            new ThymeleafEvaluationContext(applicationContext, null));\n        return context;\n    }\n\n    static class TestTemplateResolver extends StringTemplateResolver {\n        @Override\n        protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,\n            String ownerTemplate, String template,\n            Map<String, Object> templateResolutionAttributes) {\n            if (template.equals(\"commentWidget\")) {\n                return new StringTemplateResource(commentWidget());\n            }\n            return null;\n        }\n\n        private String commentWidget() {\n            return \"\"\"\n                <!DOCTYPE html>\n                <html lang=\"en\">\n                  <body>\n                    <p>comment widget:</p>\n                    <halo:comment/>\n                  </body>\n                </html>\n                \"\"\";\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessorTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationContext;\nimport org.thymeleaf.context.WebEngineContext;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport org.thymeleaf.web.IWebExchange;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Tests for {@link CommentEnabledVariableProcessor}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CommentEnabledVariableProcessorTest {\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private ExtensionGetter extensionGetter;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @BeforeEach\n    void setUp() {\n        lenient().when(applicationContext.getBean(eq(ExtensionGetter.class)))\n            .thenReturn(extensionGetter);\n    }\n\n    @Test\n    void getCommentWidget() {\n        when(applicationContext.getBean(eq(SystemConfigFetcher.class)))\n            .thenReturn(environmentFetcher);\n        SystemSetting.Comment commentSetting = mock(SystemSetting.Comment.class);\n        when(environmentFetcher.fetchComment())\n            .thenReturn(Mono.just(commentSetting));\n\n        CommentWidget commentWidget = mock(CommentWidget.class);\n        when(extensionGetter.getEnabledExtensions(CommentWidget.class))\n            .thenReturn(Flux.just(commentWidget));\n        WebEngineContext webContext = mock(WebEngineContext.class);\n        var evaluationContext = mock(ThymeleafEvaluationContext.class);\n        when(webContext.getVariable(\n            eq(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME)))\n            .thenReturn(evaluationContext);\n        when(evaluationContext.getApplicationContext()).thenReturn(applicationContext);\n        IWebExchange webExchange = mock(IWebExchange.class);\n        when(webContext.getExchange()).thenReturn(webExchange);\n\n        // comment disabled\n        when(commentSetting.getEnable()).thenReturn(true);\n        assertThat(\n            CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue();\n\n        // comment enabled\n        when(commentSetting.getEnable()).thenReturn(false);\n        assertThat(\n            CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse();\n\n        // comment enabled and ENABLE_COMMENT_ATTRIBUTE is true\n        when(commentSetting.getEnable()).thenReturn(true);\n        when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))\n            .thenReturn(true);\n        assertThat(\n            CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue();\n\n        // comment enabled and ENABLE_COMMENT_ATTRIBUTE is false\n        when(commentSetting.getEnable()).thenReturn(true);\n        when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))\n            .thenReturn(false);\n        assertThat(\n            CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse();\n\n        // comment enabled and ENABLE_COMMENT_ATTRIBUTE is null\n        when(commentSetting.getEnable()).thenReturn(true);\n        when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))\n            .thenReturn(null);\n        assertThat(\n            CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue();\n\n        // comment enabled and ENABLE_COMMENT_ATTRIBUTE is 'false'\n        when(commentSetting.getEnable()).thenReturn(true);\n        when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))\n            .thenReturn(\"false\");\n        assertThat(\n            CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse();\n    }\n\n    @Test\n    void populateAllowCommentAttribute() {\n        WebEngineContext webContext = mock(WebEngineContext.class);\n        IWebExchange webExchange = mock(IWebExchange.class);\n        when(webContext.getExchange()).thenReturn(webExchange);\n\n        CommentEnabledVariableProcessor.populateAllowCommentAttribute(webContext, true);\n        verify(webExchange).setAttributeValue(\n            eq(CommentEnabledVariableProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(true));\n\n        CommentEnabledVariableProcessor.populateAllowCommentAttribute(webContext, false);\n        verify(webExchange).setAttributeValue(\n            eq(CommentEnabledVariableProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(false));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessorIntegrationTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.core.annotation.AnnotationAwareOrderComparator;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.spring6.dialect.SpringStandardDialect;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\nimport org.thymeleaf.templateresource.StringTemplateResource;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.SinglePageFinder;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * Integration tests for {@link ContentTemplateHeadProcessor}.\n *\n * @author guqing\n * @see HaloProcessorDialect\n * @see GlobalHeadInjectionProcessor\n * @see ContentTemplateHeadProcessor\n * @see TemplateHeadProcessor\n * @see TemplateGlobalHeadProcessor\n * @see TemplateFooterElementTagProcessor\n * @since 2.7.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ContentTemplateHeadProcessorIntegrationTest {\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private PostFinder postFinder;\n\n    @Mock\n    private SinglePageFinder singlePageFinder;\n\n    @Mock\n    private SystemConfigFetcher fetcher;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    private TemplateEngine templateEngine;\n\n    @BeforeEach\n    void setUp() {\n        HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();\n        templateEngine = new TemplateEngine();\n        templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));\n        templateEngine.addTemplateResolver(new TestTemplateResolver());\n\n        Map<String, TemplateHeadProcessor> map = new HashMap<>();\n        map.put(\"postTemplateHeadProcessor\",\n            new ContentTemplateHeadProcessor(postFinder, singlePageFinder));\n        map.put(\"templateGlobalHeadProcessor\", new TemplateGlobalHeadProcessor(fetcher));\n        map.put(\"seoProcessor\", new GlobalSeoProcessor(fetcher));\n        map.put(\"duplicateMetaTagProcessor\", new DuplicateMetaTagProcessor());\n        lenient().when(applicationContext.getBeansOfType(eq(TemplateHeadProcessor.class)))\n            .thenReturn(map);\n\n        SystemSetting.Seo seo = new SystemSetting.Seo();\n        seo.setKeywords(\"global keywords\");\n        seo.setDescription(\"global description\");\n        lenient().when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), eq(SystemSetting.Seo.class)))\n            .thenReturn(Mono.just(seo));\n\n        SystemSetting.CodeInjection codeInjection = new SystemSetting.CodeInjection();\n        codeInjection.setGlobalHead(\n            \"<meta name=\\\"description\\\" content=\\\"global-head-description\\\"/>\");\n        codeInjection.setContentHead(\n            \"<meta name=\\\"description\\\" content=\\\"content-head-description\\\"/>\");\n        lenient().when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP),\n            eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection));\n\n        lenient().when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), eq(SystemSetting.Seo.class)))\n            .thenReturn(Mono.empty());\n\n        lenient().when(applicationContext.getBeanProvider(ExtensionGetter.class))\n            .thenAnswer(invocation -> {\n                var objectProvider = mock(ObjectProvider.class);\n                when(objectProvider.getIfUnique()).thenReturn(extensionGetter);\n                return objectProvider;\n            });\n        lenient().when(extensionGetter.getExtensions(TemplateHeadProcessor.class)).thenReturn(\n            Flux.fromIterable(map.values()).sort(AnnotationAwareOrderComparator.INSTANCE)\n        );\n        lenient().when(applicationContext.getBean(eq(SystemConfigFetcher.class)))\n            .thenReturn(fetcher);\n        lenient().when(fetcher.fetchComment()).thenReturn(Mono.just(new SystemSetting.Comment()));\n    }\n\n\n    @Test\n    void overrideGlobalMetaTest() {\n        Context context = getContext();\n        context.setVariable(\"name\", \"fake-post\");\n        // template id flag is used by TemplateGlobalHeadProcessor\n        context.setVariable(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());\n\n        List<Map<String, String>> htmlMetas = new ArrayList<>();\n        htmlMetas.add(mutableMetaMap(\"keyword\", \"postK1,postK2\"));\n        htmlMetas.add(mutableMetaMap(\"description\", \"post-description\"));\n        htmlMetas.add(mutableMetaMap(\"other\", \"post-other-meta\"));\n        Post.PostSpec postSpec = new Post.PostSpec();\n        postSpec.setHtmlMetas(htmlMetas);\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake-post\");\n        PostVo postVo = PostVo.builder().spec(postSpec).metadata(metadata).build();\n        when(postFinder.getByName(eq(\"fake-post\"))).thenReturn(Mono.just(postVo));\n\n        String result = templateEngine.process(\"post\", context);\n        /*\n          this test case shows:\n            1. global seo meta keywords and description is overridden by content head meta\n            2. global head meta is overridden by content head meta\n            3. but global head meta is not overridden by global seo meta\n         */\n        var outputSettings = new Document.OutputSettings().prettyPrint(true);\n        var actual = Jsoup.parse(result).outputSettings(outputSettings).html();\n        var expected = Jsoup.parse(\"\"\"\n            <!doctype html>\n            <html lang=\"en\">\n             <head>\n              <meta charset=\"UTF-8\">\n              <title>Post detail</title>\n              <meta name=\"keyword\" content=\"postK1,postK2\">\n              <meta name=\"description\" content=\"post-description\">\n              <meta name=\"other\" content=\"post-other-meta\">\n             </head>\n             <body>this is body</body>\n            </html>\"\"\"\n        ).outputSettings(outputSettings).html();\n        assertThat(actual).isEqualTo(expected);\n    }\n\n    Map<String, String> mutableMetaMap(String nameValue, String contentValue) {\n        Map<String, String> map = new HashMap<>();\n        map.put(\"name\", nameValue);\n        map.put(\"content\", contentValue);\n        return map;\n    }\n\n    private Context getContext() {\n        Context context = new Context();\n        context.setVariable(\n            ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,\n            new ThymeleafEvaluationContext(applicationContext, null));\n        return context;\n    }\n\n    static class TestTemplateResolver extends StringTemplateResolver {\n        @Override\n        protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,\n            String ownerTemplate, String template,\n            Map<String, Object> templateResolutionAttributes) {\n            if (template.equals(\"post\")) {\n                return new StringTemplateResource(postTemplate());\n            }\n            return null;\n        }\n\n        private String postTemplate() {\n            return \"\"\"\n                <!DOCTYPE html>\n                <html lang=\"en\" xmlns:th=\"http://www.thymeleaf.org\">\n                  <head>\n                    <meta charset=\"UTF-8\" />\n                    <title>Post detail</title>\n                  </head>\n                  <body>\n                    this is body\n                  </body>\n                </html>\n                \"\"\";\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessorTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link ContentTemplateHeadProcessor}.\n *\n * @author guqing\n * @since 2.5.0\n */\nclass ContentTemplateHeadProcessorTest {\n\n    @Nested\n    class ExcerptToMetaDescriptionTest {\n        @Test\n        void toMetaWhenExcerptIsNull() {\n            List<Map<String, String>> htmlMetas = new ArrayList<>();\n            htmlMetas.add(createMetaMap(\"keywords\", \"test\"));\n            var result = ContentTemplateHeadProcessor.excerptToMetaDescriptionIfAbsent(htmlMetas,\n                null);\n            assertThat(result).hasSize(2);\n            assertThat(result.get(0)).containsEntry(\"name\", \"keywords\");\n            assertThat(result.get(1)).containsEntry(\"name\", \"description\")\n                .containsEntry(\"content\", \"\");\n        }\n\n        @Test\n        void toMetaWhenWhenHtmlMetaIsNull() {\n            var result = ContentTemplateHeadProcessor.excerptToMetaDescriptionIfAbsent(null,\n                null);\n            assertThat(result).hasSize(1);\n            assertThat(result.get(0)).containsEntry(\"name\", \"description\")\n                .containsEntry(\"content\", \"\");\n        }\n\n        @Test\n        void toMetaWhenWhenExcerptNotEmpty() {\n            List<Map<String, String>> htmlMetas = new ArrayList<>();\n            htmlMetas.add(createMetaMap(\"keywords\", \"test\"));\n            var result = ContentTemplateHeadProcessor.excerptToMetaDescriptionIfAbsent(htmlMetas,\n                \"test excerpt\");\n            assertThat(result).hasSize(2);\n            assertThat(result.get(0)).containsEntry(\"name\", \"keywords\");\n            assertThat(result.get(1)).containsEntry(\"name\", \"description\")\n                .containsEntry(\"content\", \"test excerpt\");\n        }\n\n        @Test\n        void toMetaWhenWhenDescriptionExistsAndEmpty() {\n            List<Map<String, String>> htmlMetas = new ArrayList<>();\n            htmlMetas.add(createMetaMap(\"keywords\", \"test\"));\n            htmlMetas.add(createMetaMap(\"description\", \"\"));\n            var result = ContentTemplateHeadProcessor.excerptToMetaDescriptionIfAbsent(htmlMetas,\n                \"test excerpt\");\n            assertThat(result).hasSize(2);\n            assertThat(result.get(0)).containsEntry(\"name\", \"keywords\");\n            assertThat(result.get(1)).containsEntry(\"name\", \"description\")\n                .containsEntry(\"content\", \"test excerpt\");\n        }\n\n        @Test\n        void toMetaWhenWhenDescriptionExistsAndNotEmpty() {\n            List<Map<String, String>> htmlMetas = new ArrayList<>();\n            htmlMetas.add(createMetaMap(\"keywords\", \"test\"));\n            htmlMetas.add(createMetaMap(\"description\", \"test description\"));\n            var result = ContentTemplateHeadProcessor.excerptToMetaDescriptionIfAbsent(htmlMetas,\n                \"test excerpt\");\n            assertThat(result).hasSize(2);\n            assertThat(result.get(0)).containsEntry(\"name\", \"keywords\");\n            assertThat(result.get(1)).containsEntry(\"name\", \"description\")\n                .containsEntry(\"content\", \"test description\");\n        }\n\n        Map<String, String> createMetaMap(String nameValue, String contentValue) {\n            Map<String, String> metaMap = new HashMap<>();\n            metaMap.put(\"name\", nameValue);\n            metaMap.put(\"content\", contentValue);\n            return metaMap;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/DuplicateMetaTagProcessorTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.util.regex.Matcher;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link DuplicateMetaTagProcessor}.\n *\n * @author guqing\n * @since 2.8.0\n */\nclass DuplicateMetaTagProcessorTest {\n\n    @Test\n    void extractMetaTag() {\n        // normal\n        String text = \"<meta name=\\\"description\\\" content=\\\"a description\\\"/>\";\n        Matcher matcher = DuplicateMetaTagProcessor.META_PATTERN.matcher(text);\n        assertThat(matcher.find()).isTrue();\n        assertThat(matcher.group(1)).isEqualTo(\"description\");\n\n        // name and content are not in the general order\n        text = \"<meta content=\\\"K1,K2\\\" name=\\\"keywords\\\"/>\";\n        matcher = DuplicateMetaTagProcessor.META_PATTERN.matcher(text);\n        assertThat(matcher.find()).isTrue();\n        assertThat(matcher.group(1)).isEqualTo(\"keywords\");\n\n        // no closing slash\n        text = \"<meta content=\\\"K1,K2\\\" name=\\\"keywords\\\">\";\n        matcher = DuplicateMetaTagProcessor.META_PATTERN.matcher(text);\n        assertThat(matcher.find()).isTrue();\n        assertThat(matcher.group(1)).isEqualTo(\"keywords\");\n\n        // multiple line breaks and other stuff\n        text = \"\"\"\n            <meta content=\"全局 Head description\" name=\"description\" />\n            \n            <style>\n              .moment .momemt-content pre.notranslate {\n                background: #f3f3f3;\n                color: #444;\n              }\n            </style>\n            \"\"\";\n        matcher = DuplicateMetaTagProcessor.META_PATTERN.matcher(text);\n        assertThat(matcher.find()).isTrue();\n        assertThat(matcher.group(1)).isEqualTo(\"description\");\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.when;\n\nimport java.io.FileNotFoundException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.test.context.bean.override.mockito.MockitoBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.util.ResourceUtils;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.InitializationStateGetter;\nimport run.halo.app.theme.ThemeContext;\nimport run.halo.app.theme.ThemeResolver;\n\n@SpringBootTest\n@AutoConfigureWebTestClient\nclass GeneratorMetaProcessorTest {\n\n    @Autowired\n    WebTestClient webClient;\n\n    @MockitoBean\n    InitializationStateGetter initializationStateGetter;\n\n    @MockitoBean\n    ThemeResolver themeResolver;\n\n    @BeforeEach\n    void setUp() throws FileNotFoundException, URISyntaxException {\n        when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));\n        var themeContext = ThemeContext.builder()\n            .name(\"default\")\n            .path(Path.of(ResourceUtils.getURL(\"classpath:themes/default\").toURI()))\n            .active(true)\n            .build();\n        when(themeResolver.getTheme(any(ServerWebExchange.class)))\n            .thenReturn(Mono.just(themeContext));\n    }\n\n    @Test\n    void requestIndexPage() {\n        webClient.get().uri(\"/\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody()\n            .consumeWith(System.out::println)\n            .xpath(\"/html/head/meta[@name=\\\"generator\\\"][starts-with(@content, \\\"Halo \\\")]\")\n            .exists();\n    }\n\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/HaloPostTemplateHandlerTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.thymeleaf.spring6.expression.ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME;\n\nimport java.util.List;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.context.ApplicationContext;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.ITemplateHandler;\nimport org.thymeleaf.model.IOpenElementTag;\nimport org.thymeleaf.model.IStandaloneElementTag;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n@ExtendWith(MockitoExtension.class)\nclass HaloPostTemplateHandlerTest {\n\n    HaloPostTemplateHandler postHandler;\n\n    @Mock\n    ITemplateContext templateContext;\n\n    @Mock\n    ITemplateHandler next;\n\n    @Mock\n    ApplicationContext applicationContext;\n\n    @Mock\n    IStandaloneElementTag standaloneElementTag;\n\n    @Mock\n    IOpenElementTag openElementTag;\n\n    @Mock\n    ObjectProvider<ExtensionGetter> extensionGetterProvider;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n\n    @BeforeEach\n    void setUp() {\n        postHandler = new HaloPostTemplateHandler();\n        var evaluationContext = mock(ThymeleafEvaluationContext.class);\n        when(evaluationContext.getApplicationContext()).thenReturn(applicationContext);\n        when(templateContext.getVariable(THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME))\n            .thenReturn(evaluationContext);\n        when(applicationContext.getBeanProvider(ExtensionGetter.class))\n            .thenReturn(extensionGetterProvider);\n        when(extensionGetterProvider.getIfUnique()).thenReturn(extensionGetter);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideEmptyElementTagProcessors\")\n    void shouldHandleStandaloneElementIfNoElementTagProcessors(\n        List<ElementTagPostProcessor> processors\n    ) {\n        when(extensionGetter.getExtensionList(ElementTagPostProcessor.class))\n            .thenReturn(processors);\n\n        postHandler.setContext(templateContext);\n        postHandler.setNext(next);\n        postHandler.handleStandaloneElement(standaloneElementTag);\n        verify(next).handleStandaloneElement(standaloneElementTag);\n    }\n\n    @Test\n    void shouldHandleStandaloneElementIfOneElementTagProcessorProvided() {\n        var processor = mock(ElementTagPostProcessor.class);\n        var newTag = mock(IStandaloneElementTag.class);\n        when(processor.process(SecureTemplateContextWrapper.wrap(templateContext),\n            standaloneElementTag))\n            .thenReturn(Mono.just(newTag));\n        when(extensionGetter.getExtensionList(ElementTagPostProcessor.class))\n            .thenReturn(List.of(processor));\n\n        postHandler.setContext(templateContext);\n        postHandler.setNext(next);\n        postHandler.handleStandaloneElement(standaloneElementTag);\n        verify(next).handleStandaloneElement(newTag);\n    }\n\n    @Test\n    void shouldHandleStandaloneElementIfTagTypeChanged() {\n        var processor = mock(ElementTagPostProcessor.class);\n        var newTag = mock(IStandaloneElementTag.class);\n        when(processor.process(SecureTemplateContextWrapper.wrap(templateContext),\n            standaloneElementTag))\n            .thenReturn(Mono.just(newTag));\n        when(extensionGetter.getExtensionList(ElementTagPostProcessor.class))\n            .thenReturn(List.of(processor));\n\n        postHandler.setContext(templateContext);\n        postHandler.setNext(next);\n        postHandler.handleStandaloneElement(standaloneElementTag);\n        verify(next).handleStandaloneElement(newTag);\n    }\n\n    @Test\n    void shouldHandleStandaloneElementIfMoreElementTagProcessorsProvided() {\n        var processor1 = mock(ElementTagPostProcessor.class);\n        var processor2 = mock(ElementTagPostProcessor.class);\n        var newTag1 = mock(IStandaloneElementTag.class);\n        var newTag2 = mock(IStandaloneElementTag.class);\n        when(processor1.process(SecureTemplateContextWrapper.wrap(templateContext),\n            standaloneElementTag))\n            .thenReturn(Mono.just(newTag1));\n        when(processor2.process(SecureTemplateContextWrapper.wrap(templateContext), newTag1))\n            .thenReturn(Mono.just(newTag2));\n        when(extensionGetter.getExtensionList(ElementTagPostProcessor.class))\n            .thenReturn(List.of(processor1, processor2));\n\n        postHandler.setContext(templateContext);\n        postHandler.setNext(next);\n        postHandler.handleStandaloneElement(standaloneElementTag);\n        verify(next).handleStandaloneElement(newTag2);\n    }\n\n    @Test\n    void shouldNotHandleIfProcessedTagTypeChanged() {\n        var processor = mock(ElementTagPostProcessor.class);\n        var newTag = mock(IOpenElementTag.class);\n        when(processor.process(SecureTemplateContextWrapper.wrap(templateContext),\n            standaloneElementTag))\n            .thenReturn(Mono.just(newTag));\n        when(extensionGetter.getExtensionList(ElementTagPostProcessor.class))\n            .thenReturn(List.of(processor));\n\n        postHandler.setContext(templateContext);\n        postHandler.setNext(next);\n        assertThrows(ClassCastException.class,\n            () -> postHandler.handleStandaloneElement(standaloneElementTag)\n        );\n    }\n\n    static Stream<List<ElementTagPostProcessor>> provideEmptyElementTagProcessors() {\n        return Stream.of(\n            null,\n            List.of()\n        );\n    }\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport com.google.common.collect.ImmutableSortedMap;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.core.annotation.AnnotationAwareOrderComparator;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.spring6.dialect.SpringStandardDialect;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\nimport org.thymeleaf.templateresource.StringTemplateResource;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemSetting.CodeInjection;\nimport run.halo.app.infra.SystemSetting.Seo;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.Constant;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.SinglePageFinder;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.finders.vo.UserVo;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * Tests for {@link HaloProcessorDialect}.\n *\n * @author guqing\n * @see HaloProcessorDialect\n * @see GlobalHeadInjectionProcessor\n * @see ContentTemplateHeadProcessor\n * @see TemplateHeadProcessor\n * @see TemplateGlobalHeadProcessor\n * @see TemplateFooterElementTagProcessor\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass HaloProcessorDialectTest {\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    private PostFinder postFinder;\n\n    @Mock\n    private SinglePageFinder singlePageFinder;\n\n    @Mock\n    private SystemConfigFetcher fetcher;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    private TemplateEngine templateEngine;\n\n    @BeforeEach\n    void setUp() {\n        HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();\n        templateEngine = new TemplateEngine();\n        templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));\n        templateEngine.addTemplateResolver(new TestTemplateResolver());\n\n        Map<String, TemplateHeadProcessor> map = new HashMap<>();\n        map.put(\"postTemplateHeadProcessor\",\n            new ContentTemplateHeadProcessor(postFinder, singlePageFinder));\n        map.put(\"templateGlobalHeadProcessor\", new TemplateGlobalHeadProcessor(fetcher));\n        map.put(\"faviconHeadProcessor\", new DefaultFaviconHeadProcessor(fetcher));\n        map.put(\"globalSeoProcessor\", new GlobalSeoProcessor(fetcher));\n        map.put(\"indexSeoProcessor\", new IndexSeoProcessor(fetcher));\n\n        CodeInjection codeInjection = new CodeInjection();\n        codeInjection.setContentHead(\"<meta name=\\\"content-head-test\\\" content=\\\"test\\\" />\");\n        codeInjection.setGlobalHead(\"<meta name=\\\"global-head-test\\\" content=\\\"test\\\" />\");\n        codeInjection.setFooter(\"<footer>hello this is global footer.</footer>\");\n        lenient().when(fetcher.fetch(eq(CodeInjection.GROUP), eq(CodeInjection.class)))\n            .thenReturn(Mono.just(codeInjection));\n\n        lenient().when(applicationContext.getBean(eq(SystemConfigFetcher.class)))\n            .thenReturn(fetcher);\n        lenient().when(fetcher.fetch(eq(Seo.GROUP), eq(Seo.class)))\n            .thenReturn(Mono.empty());\n\n        lenient().when(applicationContext.getBeanProvider(ExtensionGetter.class))\n            .then(invocation -> {\n                @SuppressWarnings(\"unchecked\")\n                ObjectProvider<ExtensionGetter> objectProvider = mock(ObjectProvider.class);\n                when(objectProvider.getIfUnique()).thenReturn(extensionGetter);\n                return objectProvider;\n            });\n        lenient().when(extensionGetter.getExtensions(TemplateHeadProcessor.class)).thenReturn(\n            Flux.fromIterable(map.values()).sort(AnnotationAwareOrderComparator.INSTANCE)\n        );\n\n        lenient().when(fetcher.fetchComment())\n            .thenReturn(Mono.just(new SystemSetting.Comment()));\n    }\n\n    @Test\n    void globalHeadAndFooterProcessors() {\n        SystemSetting.Basic basic = new SystemSetting.Basic();\n        basic.setFavicon(\"favicon.ico\");\n        when(fetcher.fetch(eq(SystemSetting.Basic.GROUP),\n            eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic));\n\n        when(extensionGetter.getExtensions(TemplateFooterProcessor.class))\n            .thenReturn(Flux.empty());\n\n        Context context = getContext();\n\n        String result = templateEngine.process(\"index\", context);\n        assertThat(result).isEqualTo(\"\"\"\n            <!DOCTYPE html>\n            <html lang=\"en\">\n              <head>\n                <meta charset=\"UTF-8\" />\n                <title>Index</title>\n              <meta name=\"global-head-test\" content=\"test\" />\n            <link rel=\"icon\" href=\"favicon.ico\" />\n            </head>\n              <body>\n                <p>index</p>\n            <div class=\"footer\">\n              <footer>hello this is global footer.</footer>\n            </div>\n            \n              </body>\n            </html>\n            \"\"\");\n    }\n\n    @Test\n    void contentHeadAndFooterAndPostProcessors() {\n        Context context = getContext();\n        context.setVariable(\"name\", \"fake-post\");\n        // template id flag is used by TemplateGlobalHeadProcessor\n        context.setVariable(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());\n\n        List<Map<String, String>> htmlMetas = new ArrayList<>();\n        htmlMetas.add(ImmutableSortedMap.of(\"name\", \"post-meta-V1\", \"content\", \"post-meta-V1\"));\n        htmlMetas.add(ImmutableSortedMap.of(\"name\", \"post-meta-V2\", \"content\", \"post-meta-V2\"));\n        Post.PostSpec postSpec = new Post.PostSpec();\n        postSpec.setHtmlMetas(htmlMetas);\n        Metadata metadata = new Metadata();\n        metadata.setName(\"fake-post\");\n        PostVo postVo = PostVo.builder()\n            .spec(postSpec)\n            .metadata(metadata).build();\n        when(postFinder.getByName(eq(\"fake-post\"))).thenReturn(Mono.just(postVo));\n\n        SystemSetting.Basic basic = new SystemSetting.Basic();\n        basic.setFavicon(null);\n        when(fetcher.fetch(eq(SystemSetting.Basic.GROUP),\n            eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic));\n\n        when(extensionGetter.getExtensions(TemplateFooterProcessor.class))\n            .thenReturn(Flux.empty());\n\n        String result = templateEngine.process(\"post\", context);\n        assertThat(result).isEqualTo(\"\"\"\n            <!DOCTYPE html>\n            <html lang=\"en\">\n              <head>\n                <meta charset=\"UTF-8\" />\n                <title>Post</title>\n              <meta name=\"global-head-test\" content=\"test\" />\n            <meta name=\"content-head-test\" content=\"test\" />\n            <meta content=\"post-meta-V1\" name=\"post-meta-V1\"/>\\\n            <meta content=\"post-meta-V2\" name=\"post-meta-V2\"/>\\\n            <meta name=\"description\" content=\"\"/>\\\n            </head>\n              <body>\n                <p>post</p>\n            <div class=\"footer\">\n              <footer>hello this is global footer.</footer>\n            </div>\n            \n              </body>\n            </html>\n            \"\"\");\n    }\n\n    @Test\n    void shouldSetMetaDescriptionIfContainingMetaDescriptionVariable() {\n        var context = getContext();\n        context.setVariable(Constant.META_DESCRIPTION_VARIABLE_NAME, \"Fake description\");\n        when(fetcher.fetch(Seo.GROUP, Seo.class)).thenReturn(Mono.empty());\n        when(fetcher.fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class))\n            .thenReturn(Mono.empty());\n        var result = templateEngine.process(\"seo\", context);\n        assertTrue(result.contains(\"\"\"\n            <meta name=\"description\" content=\"Fake description\"/>\\\n            \"\"\"));\n    }\n\n    @Test\n    void blockSeo() {\n        final Context context = getContext();\n        Seo seo = new Seo();\n        seo.setBlockSpiders(true);\n        when(fetcher.fetch(eq(Seo.GROUP),\n            eq(Seo.class))).thenReturn(Mono.just(seo));\n        SystemSetting.Basic basic = new SystemSetting.Basic();\n        basic.setFavicon(\"favicon.ico\");\n        when(fetcher.fetch(eq(SystemSetting.Basic.GROUP),\n            eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic));\n\n        String result = templateEngine.process(\"seo\", context);\n        assertThat(result).isEqualTo(\"\"\"\n            <!DOCTYPE html>\n            <html lang=\"en\">\n              <head>\n                <meta charset=\"UTF-8\" />\n                <title>Seo Test</title>\n              <meta name=\"robots\" content=\"noindex\"/>\\\n            <meta name=\"global-head-test\" content=\"test\" />\n            <link rel=\"icon\" href=\"favicon.ico\" />\n            </head>\n              <body>\n                seo setting test.\n              </body>\n            </html>\n            \"\"\");\n    }\n\n    @Test\n    void indexSeoWithKeywordsAndDescription() {\n        Context context = getContext();\n        context.setVariable(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.INDEX.getValue());\n        Seo seo = new Seo();\n        seo.setKeywords(\"K1, K2, K3\");\n        seo.setDescription(\"This is a description.\");\n        when(fetcher.fetch(eq(Seo.GROUP),\n            eq(Seo.class))).thenReturn(Mono.just(seo));\n        SystemSetting.Basic basic = new SystemSetting.Basic();\n        basic.setFavicon(\"favicon.ico\");\n        when(fetcher.fetch(eq(SystemSetting.Basic.GROUP),\n            eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic));\n\n        String result = templateEngine.process(\"seo\", context);\n        assertThat(result).isEqualTo(\"\"\"\n            <!DOCTYPE html>\n            <html lang=\"en\">\n              <head>\n                <meta charset=\"UTF-8\" />\n                <title>Seo Test</title>\n              <meta name=\"global-head-test\" content=\"test\" />\n            <meta name=\"keywords\" content=\"K1, K2, K3\" />\n            <meta name=\"description\" content=\"This is a description.\" />\n            <link rel=\"icon\" href=\"favicon.ico\" />\n            </head>\n              <body>\n                seo setting test.\n              </body>\n            </html>\n            \"\"\");\n    }\n\n    @Nested\n    class AnnotationExpressionObjectFactoryTest {\n\n        @Test\n        void getWhenAnnotationsIsNull() {\n            Context context = getContext();\n            context.setVariable(\"user\", createUser());\n\n            String result = templateEngine.process(\"annotationsGetExpression\", context);\n            assertThat(result).isEqualTo(\"<p></p>\\n\");\n        }\n\n        @Test\n        void getWhenAnnotationsExists() {\n            Context context = getContext();\n            UserVo user = createUser();\n            user.getMetadata().setAnnotations(Map.of(\"background\", \"fake-background\"));\n            context.setVariable(\"user\", user);\n\n            String result = templateEngine.process(\"annotationsGetExpression\", context);\n            assertThat(result).isEqualTo(\"<p>fake-background</p>\\n\");\n        }\n\n        @Test\n        void getOrDefaultWhenAnnotationsIsNull() {\n            Context context = getContext();\n            UserVo user = createUser();\n            user.getMetadata().setAnnotations(Map.of(\"background\", \"red\"));\n            context.setVariable(\"user\", user);\n\n            String result = templateEngine.process(\"annotationsGetOrDefaultExpression\", context);\n            assertThat(result).isEqualTo(\"<p>red</p>\\n\");\n        }\n\n        @Test\n        void getOrDefaultWhenAnnotationsExists() {\n            Context context = getContext();\n            context.setVariable(\"user\", createUser());\n\n            String result = templateEngine.process(\"annotationsGetOrDefaultExpression\", context);\n            assertThat(result).isEqualTo(\"<p>default-value</p>\\n\");\n        }\n\n        @Test\n        void containsWhenAnnotationsIsNull() {\n            Context context = getContext();\n            context.setVariable(\"user\", createUser());\n\n            String result = templateEngine.process(\"annotationsContainsExpression\", context);\n            assertThat(result).isEqualTo(\"<p>false</p>\\n\");\n        }\n\n        @Test\n        void containsWhenAnnotationsIsNotNull() {\n            Context context = getContext();\n            UserVo user = createUser();\n            user.getMetadata().setAnnotations(Map.of(\"background\", \"\"));\n            context.setVariable(\"user\", user);\n\n            String result = templateEngine.process(\"annotationsContainsExpression\", context);\n            assertThat(result).isEqualTo(\"<p>true</p>\\n\");\n        }\n\n        UserVo createUser() {\n            User user = new User();\n            user.setMetadata(new Metadata());\n            user.getMetadata().setName(\"fake-user\");\n            user.setSpec(new User.UserSpec());\n            return UserVo.from(user);\n        }\n    }\n\n    private Context getContext() {\n        Context context = new Context();\n        context.setVariable(\n            ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,\n            new ThymeleafEvaluationContext(applicationContext, null));\n        return context;\n    }\n\n    static class TestTemplateResolver extends StringTemplateResolver {\n        @Override\n        protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,\n            String ownerTemplate, String template,\n            Map<String, Object> templateResolutionAttributes) {\n            if (template.equals(DefaultTemplateEnum.INDEX.getValue())) {\n                return new StringTemplateResource(indexTemplate());\n            }\n\n            if (template.equals(DefaultTemplateEnum.POST.getValue())) {\n                return new StringTemplateResource(postTemplate());\n            }\n\n            if (template.equals(\"seo\")) {\n                return new StringTemplateResource(seoTemplate());\n            }\n\n            if (template.equals(\"annotationsGetExpression\")) {\n                return new StringTemplateResource(annotationsGetExpression());\n            }\n            if (template.equals(\"annotationsGetOrDefaultExpression\")) {\n                return new StringTemplateResource(annotationsGetOrDefaultExpression());\n            }\n            if (template.equals(\"annotationsContainsExpression\")) {\n                return new StringTemplateResource(annotationsContainsExpression());\n            }\n            return null;\n        }\n\n        private String indexTemplate() {\n            return commonTemplate().formatted(\"Index\", \"\"\"\n                <p>index</p>\n                <div class=\"footer\">\n                  <halo:footer></halo:footer>\n                </div>\n                \"\"\");\n        }\n\n        private String postTemplate() {\n            return commonTemplate().formatted(\"Post\", \"\"\"\n                <p>post</p>\n                <div class=\"footer\">\n                  <halo:footer></halo:footer>\n                </div>\n                \"\"\");\n        }\n\n        private String commonTemplate() {\n            return \"\"\"\n                <!DOCTYPE html>\n                <html lang=\"en\" xmlns:th=\"http://www.thymeleaf.org\">\n                  <head>\n                    <meta charset=\"UTF-8\" />\n                    <title>%s</title>\n                  </head>\n                  <body>\n                    %s\n                  </body>\n                </html>\n                \"\"\";\n        }\n\n        private String seoTemplate() {\n            return \"\"\"\n                <!DOCTYPE html>\n                <html lang=\"en\" xmlns:th=\"http://www.thymeleaf.org\">\n                  <head>\n                    <meta charset=\"UTF-8\" />\n                    <title>Seo Test</title>\n                  </head>\n                  <body>\n                    seo setting test.\n                  </body>\n                </html>\n                \"\"\";\n        }\n\n        private String annotationsGetExpression() {\n            return \"\"\"\n                <p th:text=\"${#annotations.get(user, 'background')}\"></p>\n                \"\"\";\n        }\n\n        private String annotationsGetOrDefaultExpression() {\n            return \"\"\"\n                <p th:text=\"${#annotations.getOrDefault(user, 'background', 'default-value')}\"></p>\n                \"\"\";\n        }\n\n        private String annotationsContainsExpression() {\n            return \"\"\"\n                <p th:text=\"${#annotations.contains(user, 'background')}\"></p>\n                \"\"\";\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/HaloSpringSecurityDialectTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.params.provider.Arguments.arguments;\nimport static org.springframework.http.MediaType.TEXT_HTML;\n\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.Arguments;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.authority.AuthorityUtils;\nimport org.springframework.security.core.context.SecurityContextImpl;\nimport org.springframework.security.web.server.context.ServerSecurityContextRepository;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.WebContext;\nimport org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;\nimport org.thymeleaf.spring6.SpringWebFluxTemplateEngine;\nimport org.thymeleaf.spring6.web.webflux.SpringWebFluxWebApplication;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\n\n// @ExtendWith(MockitoExtension.class)\n@SpringBootTest\nclass HaloSpringSecurityDialectTest {\n\n    TemplateEngine templateEngine;\n\n    @Autowired\n    ServerSecurityContextRepository securityContextRepository;\n\n    @Autowired\n    ObjectProvider<MethodSecurityExpressionHandler> expressionHandler;\n\n\n    @BeforeEach\n    void setUp() {\n        var haloSpringSecurityDialect =\n            new HaloSpringSecurityDialect(securityContextRepository, expressionHandler);\n        templateEngine = new SpringWebFluxTemplateEngine();\n        templateEngine.addTemplateResolver(new StringTemplateResolver());\n        templateEngine.addDialect(haloSpringSecurityDialect);\n    }\n\n    static Stream<Arguments> shouldEvaluateSecAuthorizeAttr() {\n        return Stream.of(\n            arguments(\n                \"Evaluate sec:authorize to true when role match\",\n                List.of(\"ROLE_ADMIN\"),\n                \"\"\"\n                    <p sec:authorize=\"hasRole('ROLE_ADMIN')\">Admin</p>\\\n                    \"\"\",\n                \"\"\"\n                    <p>Admin</p>\\\n                    \"\"\"),\n            arguments(\n                \"Evaluate sec:authorize to false when role not match\",\n                List.of(\"ROLE_USER\"),\n                \"\"\"\n                    <p sec:authorize=\"hasRole('ROLE_ADMIN')\"></p>\\\n                    \"\"\",\n                \"\")\n        );\n    }\n\n    @ParameterizedTest(name = \"{0}\")\n    @MethodSource\n    void shouldEvaluateSecAuthorizeAttr(String name, List<String> authorities, String template,\n        String expected) {\n        var request = MockServerHttpRequest.get(\"/halo-sec-authorize\").build();\n        var exchange = new MockServerWebExchange.Builder(request).build();\n        var webExchange = SpringWebFluxWebApplication.buildApplication(null)\n            .buildExchange(exchange, Locale.getDefault(), TEXT_HTML, UTF_8);\n        var context = new WebContext(webExchange);\n        var authentication = new UsernamePasswordAuthenticationToken(\"fake-user\", \"fake-credential\",\n            AuthorityUtils.createAuthorityList(authorities));\n        var securityContext = new SecurityContextImpl(authentication);\n        context.setVariable(SpringSecurityContextUtils.SECURITY_CONTEXT_MODEL_ATTRIBUTE_NAME,\n            securityContext);\n        var result = templateEngine.process(template, context);\n        assertEquals(expected, result);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/InjectionExcluderProcessorTest.java",
    "content": "package run.halo.app.theme.dialect;\n\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link InjectionExcluderProcessor}.\n *\n * @author guqing\n * @since 2.20.0\n */\nclass InjectionExcluderProcessorTest {\n\n    @Nested\n    class PageInjectionExcluderTest {\n        final InjectionExcluderProcessor.PageInjectionExcluder pageInjectionExcluder =\n            new InjectionExcluderProcessor.PageInjectionExcluder();\n\n        @Test\n        void excludeTest() {\n            var cases = new String[] {\n                \"login\",\n                \"signup\",\n                \"logout\",\n                \"password-reset/email/reset\",\n                \"error/404\",\n                \"error/500\",\n                \"challenges/totp\"\n            };\n\n            for (String templateName : cases) {\n                assertThat(pageInjectionExcluder.isExcluded(templateName)).isTrue();\n            }\n        }\n\n        @Test\n        void shouldNotExcludeTest() {\n            var cases = new String[] {\n                \"index\",\n                \"post\",\n                \"page\",\n                \"category\",\n                \"tag\",\n                \"archive\",\n                \"search\",\n                \"feed\",\n                \"sitemap\",\n                \"robots\",\n                \"custom\",\n                \"error\",\n                \"login.html\",\n            };\n\n            for (String templateName : cases) {\n                assertThat(pageInjectionExcluder.isExcluded(templateName)).isFalse();\n            }\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/LinkExpressionObjectDialectTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link LinkExpressionObjectDialect}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass LinkExpressionObjectDialectTest {\n\n    private final LinkExpressionObjectDialect linkExpressionObjectDialect =\n        new LinkExpressionObjectDialect();\n\n    @Test\n    void getExpressionObjectFactory() {\n        assertThat(linkExpressionObjectDialect.getName())\n            .isEqualTo(\"themeLink\");\n        assertThat(linkExpressionObjectDialect.getExpressionObjectFactory())\n            .isInstanceOf(DefaultLinkExpressionFactory.class);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java",
    "content": "package run.halo.app.theme.dialect;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.beans.factory.ObjectProvider;\nimport org.springframework.context.ApplicationContext;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.model.IModel;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.IProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.spring6.dialect.SpringStandardDialect;\nimport org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\nimport org.thymeleaf.templateresource.StringTemplateResource;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\n\n/**\n * Tests for {@link TemplateFooterElementTagProcessor}.\n *\n * @author guqing\n * @since 2.17.0\n */\n@ExtendWith(MockitoExtension.class)\nclass TemplateFooterElementTagProcessorTest {\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @Mock\n    ExtensionGetter extensionGetter;\n\n    @Mock\n    private SystemConfigFetcher fetcher;\n\n    private TemplateEngine templateEngine;\n\n    @BeforeEach\n    void setUp() {\n        HaloProcessorDialect haloProcessorDialect = new MockHaloProcessorDialect();\n        templateEngine = new TemplateEngine();\n        templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));\n        templateEngine.addTemplateResolver(new MockTemplateResolver());\n\n        SystemSetting.CodeInjection codeInjection = new SystemSetting.CodeInjection();\n        codeInjection.setFooter(\n            \"<p>Powered by <a href=\\\"https://www.halo.run\\\" target=\\\"_blank\\\">Halo</a></p>\");\n        lenient().when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP),\n            eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection));\n\n        lenient().when(applicationContext.getBeanProvider(ExtensionGetter.class))\n            .thenAnswer(invocation -> {\n                var objectProvider = mock(ObjectProvider.class);\n                when(objectProvider.getIfUnique()).thenReturn(extensionGetter);\n                return objectProvider;\n            });\n        lenient().when(applicationContext.getBean(eq(SystemConfigFetcher.class)))\n            .thenReturn(fetcher);\n    }\n\n    @Test\n    void footerProcessorTest() {\n        when(extensionGetter.getExtensions(TemplateFooterProcessor.class))\n            .thenReturn(Flux.just(new FakeFooterCodeInjection()));\n\n        String result = templateEngine.process(\"fake-template\", getContext());\n        // footer injected code is not processable\n        assertThat(result).isEqualToIgnoringWhitespace(\"\"\"\n            <p>Powered by <a href=\"https://www.halo.run\" target=\"_blank\">Halo</a></p>\n            <div>© 2024 guqing's blog</div>\n            <div th:text=\"${footerText}\"></div>\n            \"\"\");\n    }\n\n    private Context getContext() {\n        Context context = new Context();\n        context.setVariable(\n            ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,\n            new ThymeleafEvaluationContext(applicationContext, null));\n        return context;\n    }\n\n    static class MockTemplateResolver extends StringTemplateResolver {\n        @Override\n        protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,\n            String ownerTemplate, String template,\n            Map<String, Object> templateResolutionAttributes) {\n            return new StringTemplateResource(\"\"\"\n                <halo:footer />\n                \"\"\");\n        }\n    }\n\n    static class MockHaloProcessorDialect extends HaloProcessorDialect {\n        @Override\n        public Set<IProcessor> getProcessors(String dialectPrefix) {\n            var processors = new HashSet<IProcessor>();\n            processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));\n            return processors;\n        }\n    }\n\n    static class FakeFooterCodeInjection implements TemplateFooterProcessor {\n\n        @Override\n        public Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,\n            IElementTagStructureHandler structureHandler, IModel model) {\n            var factory = context.getModelFactory();\n            // regular footer text\n            var copyRight = factory.createText(\"<div>© 2024 guqing's blog</div>\");\n            model.add(copyRight);\n            // variable footer text\n            model.add(factory.createText(\"<div th:text=\\\"${footerText}\\\"></div>\"));\n            return Mono.empty();\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/endpoint/ThemeEndpointTest.java",
    "content": "package run.halo.app.theme.endpoint;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.springframework.web.reactive.function.BodyInserters.fromMultipartData;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.reactivestreams.Publisher;\nimport org.springframework.core.io.FileSystemResource;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.MultipartBodyBuilder;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.util.FileSystemUtils;\nimport org.springframework.util.ResourceUtils;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.core.user.service.SettingConfigService;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.ReactiveUrlDataBufferFetcher;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.theme.TemplateEngineManager;\nimport run.halo.app.theme.service.ThemeService;\n\n/**\n * Tests for {@link ThemeEndpoint}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ThemeEndpointTest {\n\n    @Mock\n    ThemeRootGetter themeRoot;\n\n    @Mock\n    ThemeService themeService;\n\n    @Mock\n    TemplateEngineManager templateEngineManager;\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private SystemConfigFetcher environmentFetcher;\n\n    @Mock\n    private ReactiveUrlDataBufferFetcher urlDataBufferFetcher;\n\n    @Mock\n    private SettingConfigService settingConfigService;\n\n    @InjectMocks\n    ThemeEndpoint themeEndpoint;\n\n    private Path tmpHaloWorkDir;\n\n    WebTestClient webTestClient;\n\n    private File defaultTheme;\n\n    @BeforeEach\n    void setUp() throws IOException {\n        tmpHaloWorkDir = Files.createTempDirectory(\"halo-theme-endpoint-test\");\n        lenient().when(themeRoot.get()).thenReturn(tmpHaloWorkDir);\n        defaultTheme = ResourceUtils.getFile(\"classpath:themes/test-theme.zip\");\n        webTestClient = WebTestClient\n            .bindToRouterFunction(themeEndpoint.endpoint())\n            .build();\n    }\n\n    @AfterEach\n    void tearDown() throws IOException {\n        FileSystemUtils.deleteRecursively(tmpHaloWorkDir);\n    }\n\n    @Nested\n    class UpgradeTest {\n\n        @Test\n        void shouldNotOkIfThemeNotInstalled() {\n            var bodyBuilder = new MultipartBodyBuilder();\n            bodyBuilder.part(\"file\", new FileSystemResource(defaultTheme))\n                .contentType(MediaType.MULTIPART_FORM_DATA);\n\n            when(themeService.upgrade(eq(\"invalid-missing-manifest\"), isA(Publisher.class)))\n                .thenReturn(\n                    Mono.error(() -> new ServerWebInputException(\"Failed to upgrade theme\")));\n\n            webTestClient.post()\n                .uri(\"/themes/invalid-missing-manifest/upgrade\")\n                .body(fromMultipartData(bodyBuilder.build()))\n                .exchange()\n                .expectStatus().isBadRequest();\n\n            verify(themeService).upgrade(eq(\"invalid-missing-manifest\"), isA(Publisher.class));\n        }\n\n        @Test\n        void shouldUpgradeSuccessfullyIfThemeInstalled() {\n            var bodyBuilder = new MultipartBodyBuilder();\n            bodyBuilder.part(\"file\", new FileSystemResource(defaultTheme))\n                .contentType(MediaType.MULTIPART_FORM_DATA);\n\n            var metadata = new Metadata();\n            metadata.setName(\"default\");\n            var newTheme = new Theme();\n            newTheme.setMetadata(metadata);\n\n            when(themeService.upgrade(eq(\"default\"), isA(Publisher.class)))\n                .thenReturn(Mono.just(newTheme));\n\n            when(templateEngineManager.clearCache(eq(\"default\")))\n                .thenReturn(Mono.empty());\n\n            webTestClient.post()\n                .uri(\"/themes/default/upgrade\")\n                .body(fromMultipartData(bodyBuilder.build()))\n                .exchange()\n                .expectStatus().isOk();\n\n            verify(themeService).upgrade(eq(\"default\"), isA(Publisher.class));\n\n            verify(templateEngineManager, times(1)).clearCache(eq(\"default\"));\n        }\n\n        @Test\n        void upgradeFromUri() {\n            var uri = URI.create(\"https://example.com/test-theme.zip\");\n            var metadata = new Metadata();\n            metadata.setName(\"default\");\n            var fakeTheme = new Theme();\n            fakeTheme.setMetadata(metadata);\n            when(themeService.upgrade(eq(\"default\"), any()))\n                .thenReturn(Mono.just(fakeTheme));\n            when(templateEngineManager.clearCache(eq(\"default\")))\n                .thenReturn(Mono.empty());\n            var body = new ThemeEndpoint.UpgradeFromUriRequest(uri);\n            webTestClient.post()\n                .uri(\"/themes/default/upgrade-from-uri\")\n                .bodyValue(body)\n                .exchange()\n                .expectStatus().isOk()\n                .expectBody(Theme.class).isEqualTo(fakeTheme);\n\n            verify(themeService).upgrade(eq(\"default\"), any());\n\n            verify(templateEngineManager, times(1)).clearCache(eq(\"default\"));\n        }\n    }\n\n    @Test\n    void install() {\n        var multipartBodyBuilder = new MultipartBodyBuilder();\n        multipartBodyBuilder.part(\"file\", new FileSystemResource(defaultTheme))\n            .contentType(MediaType.MULTIPART_FORM_DATA);\n\n        var installedTheme = new Theme();\n        var metadata = new Metadata();\n        metadata.setName(\"fake-name\");\n        installedTheme.setMetadata(metadata);\n        when(themeService.install(any())).thenReturn(Mono.just(installedTheme));\n\n        webTestClient.post()\n            .uri(\"/themes/install\")\n            .body(fromMultipartData(multipartBodyBuilder.build()))\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(Theme.class)\n            .isEqualTo(installedTheme);\n\n        verify(themeService).install(any());\n\n\n        when(themeService.install(any())).thenReturn(\n            Mono.error(new RuntimeException(\"Fake exception\")));\n        // Verify the theme is installed.\n        webTestClient.post()\n            .uri(\"/themes/install\")\n            .body(fromMultipartData(multipartBodyBuilder.build()))\n            .exchange()\n            .expectStatus().is5xxServerError();\n    }\n\n    @Test\n    void installFromUri() {\n        final URI uri = URI.create(\"https://example.com/test-theme.zip\");\n        var metadata = new Metadata();\n        metadata.setName(\"fake-theme\");\n        var theme = new Theme();\n        theme.setMetadata(metadata);\n\n        when(themeService.install(any())).thenReturn(Mono.just(theme));\n        var body = new ThemeEndpoint.UpgradeFromUriRequest(uri);\n        webTestClient.post()\n            .uri(\"/themes/-/install-from-uri\")\n            .bodyValue(body)\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(Theme.class).isEqualTo(theme);\n\n        verify(themeService).install(any());\n    }\n\n    @Test\n    void reloadTheme() {\n        when(themeService.reloadTheme(any())).thenReturn(Mono.empty());\n        webTestClient.put()\n            .uri(\"/themes/fake/reload\")\n            .exchange()\n            .expectStatus().isOk();\n    }\n\n    @Test\n    void resetSettingConfig() {\n        when(themeService.resetSettingConfig(any())).thenReturn(Mono.empty());\n        webTestClient.put()\n            .uri(\"/themes/fake/reset-config\")\n            .exchange()\n            .expectStatus().isOk();\n    }\n\n    @Nested\n    class UpdateThemeConfigTest {\n\n        @Test\n        void updateJsonConfigTest() {\n            Theme theme = new Theme();\n            theme.setMetadata(new Metadata());\n            theme.setSpec(new Theme.ThemeSpec());\n            theme.getSpec().setConfigMapName(\"fake-config-map\");\n\n            when(client.fetch(eq(Theme.class), eq(\"fake-theme\"))).thenReturn(Mono.just(theme));\n            when(settingConfigService.upsertConfig(eq(\"fake-config-map\"), any()))\n                .thenReturn(Mono.empty());\n\n            webTestClient.put()\n                .uri(\"/themes/fake-theme/json-config\")\n                .body(Mono.just(Map.of()), Map.class)\n                .exchange()\n                .expectStatus().is2xxSuccessful();\n        }\n    }\n\n    @Test\n    void fetchActivatedTheme() {\n        var theme = new Theme();\n        theme.setMetadata(new Metadata());\n        theme.getMetadata().setName(\"fake-activated\");\n        when(themeService.fetchActivatedTheme()).thenReturn(Mono.just(theme));\n        webTestClient.get()\n            .uri(\"/themes/-/activation\")\n            .exchange()\n            .expectStatus().isOk()\n            .expectBody(Theme.class)\n            .isEqualTo(theme);\n    }\n\n    @Test\n    void fetchThemeSetting() {\n        Theme theme = new Theme();\n        theme.setMetadata(new Metadata());\n        theme.getMetadata().setName(\"fake\");\n        theme.setSpec(new Theme.ThemeSpec());\n        theme.getSpec().setSettingName(\"fake-setting\");\n\n        when(client.fetch(eq(Setting.class), eq(\"fake-setting\")))\n            .thenReturn(Mono.just(new Setting()));\n\n        when(client.fetch(eq(Theme.class), eq(\"fake\"))).thenReturn(Mono.just(theme));\n        webTestClient.get()\n            .uri(\"/themes/fake/setting\")\n            .exchange()\n            .expectStatus().isOk();\n\n        verify(client).fetch(eq(Setting.class), eq(\"fake-setting\"));\n        verify(client).fetch(eq(Theme.class), eq(\"fake\"));\n    }\n\n    @Test\n    void fetchThemeJsonConfigTest() {\n        Theme theme = new Theme();\n        theme.setMetadata(new Metadata());\n        theme.getMetadata().setName(\"fake\");\n        theme.setSpec(new Theme.ThemeSpec());\n        theme.getSpec().setConfigMapName(\"fake-config\");\n\n        when(settingConfigService.fetchConfig(eq(\"fake-config\"))).thenReturn(Mono.empty());\n\n        when(client.fetch(eq(Theme.class), eq(\"fake\"))).thenReturn(Mono.just(theme));\n        webTestClient.get()\n            .uri(\"/themes/fake/json-config\")\n            .exchange()\n            .expectStatus().isOk();\n\n        verify(settingConfigService).fetchConfig(eq(\"fake-config\"));\n        verify(client).fetch(eq(Theme.class), eq(\"fake\"));\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/engine/DefaultThemeTemplateAvailabilityProviderTest.java",
    "content": "package run.halo.app.theme.engine;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\nimport java.io.FileNotFoundException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.boot.thymeleaf.autoconfigure.ThymeleafProperties;\nimport org.springframework.util.ResourceUtils;\nimport run.halo.app.theme.ThemeContext;\n\n@ExtendWith(MockitoExtension.class)\nclass DefaultThemeTemplateAvailabilityProviderTest {\n\n    @InjectMocks\n    DefaultThemeTemplateAvailabilityProvider provider;\n\n    @Mock\n    ThymeleafProperties thymeleafProperties;\n\n    @Test\n    void templateAvailableTest() throws FileNotFoundException, URISyntaxException {\n        var themeUrl = ResourceUtils.getURL(\"classpath:themes/default\");\n        var themePath = Path.of(themeUrl.toURI());\n\n        when(thymeleafProperties.getSuffix()).thenReturn(\".html\");\n        var themeContext = ThemeContext.builder()\n            .name(\"default\")\n            .path(themePath)\n            .build();\n        boolean templateAvailable = provider.isTemplateAvailable(themeContext, \"fake\");\n        assertFalse(templateAvailable);\n\n        templateAvailable = provider.isTemplateAvailable(themeContext, \"index\");\n        assertTrue(templateAvailable);\n\n        templateAvailable = provider.isTemplateAvailable(themeContext, \"timezone\");\n        assertTrue(templateAvailable);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/engine/PluginClassloaderTemplateResolverTest.java",
    "content": "package run.halo.app.theme.engine;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.PluginManager;\n\n/**\n * Tests for {@link PluginClassloaderTemplateResolver}.\n *\n * @author guqing\n * @since 2.11.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PluginClassloaderTemplateResolverTest {\n\n    @Mock\n    private PluginManager haloPluginManager;\n\n    @InjectMocks\n    private PluginClassloaderTemplateResolver templateResolver;\n\n    @Test\n    void matchPluginTemplateWhenOwnerTemplateMatch() {\n        var result =\n            templateResolver.matchPluginTemplate(\"plugin:fake-plugin:doc\", \"modules/layout\");\n        assertThat(result.matches()).isTrue();\n        assertThat(result.pluginName()).isEqualTo(\"fake-plugin\");\n        assertThat(result.templateName()).isEqualTo(\"modules/layout\");\n        assertThat(result.ownerTemplateName()).isEqualTo(\"doc\");\n    }\n\n    @Test\n    void matchPluginTemplateWhenDoesNotMatch() {\n        var result =\n            templateResolver.matchPluginTemplate(\"doc\", \"modules/layout\");\n        assertThat(result.matches()).isFalse();\n    }\n\n    @Test\n    void matchPluginTemplateWhenTemplateMatch() {\n        var result =\n            templateResolver.matchPluginTemplate(\"doc\", \"plugin:fake-plugin:modules/layout\");\n        assertThat(result.matches()).isTrue();\n        assertThat(result.pluginName()).isEqualTo(\"fake-plugin\");\n        assertThat(result.templateName()).isEqualTo(\"modules/layout\");\n        assertThat(result.ownerTemplateName()).isEqualTo(\"doc\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/FinderRegistryTest.java",
    "content": "package run.halo.app.theme.finders;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.ApplicationContext;\n\n/**\n * Tests for {@link FinderRegistry}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass FinderRegistryTest {\n\n    private DefaultFinderRegistry finderRegistry;\n    @Mock\n    private ApplicationContext applicationContext;\n\n    @BeforeEach\n    void setUp() {\n        finderRegistry = new DefaultFinderRegistry(applicationContext);\n    }\n\n    @Test\n    void registerFinder() {\n        assertThatThrownBy(() -> {\n            finderRegistry.putFinder(new Object());\n        }).isInstanceOf(IllegalStateException.class)\n            .hasMessage(\"Finder must be annotated with @Finder\");\n\n        String s = finderRegistry.putFinder(new FakeFinder());\n        assertThat(s).isEqualTo(\"test\");\n    }\n\n    @Test\n    void removeFinder() {\n        String s = finderRegistry.putFinder(new FakeFinder());\n        assertThat(s).isEqualTo(\"test\");\n        Object test = finderRegistry.get(\"test\");\n        assertThat(test).isNotNull();\n        finderRegistry.removeFinder(s);\n\n        test = finderRegistry.get(\"test\");\n        assertThat(test).isNull();\n    }\n\n    @Test\n    void getFinders() {\n        assertThat(finderRegistry.getFinders()).hasSize(0);\n\n        finderRegistry.putFinder(new FakeFinder());\n        Map<String, Object> finders = finderRegistry.getFinders();\n        assertThat(finders).hasSize(1);\n    }\n\n    @Finder(\"test\")\n    static class FakeFinder {\n\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.when;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.data.domain.Sort;\nimport org.springframework.util.ResourceUtils;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.CategoryService;\nimport run.halo.app.core.extension.content.Category;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.theme.finders.vo.CategoryTreeVo;\nimport run.halo.app.theme.finders.vo.CategoryVo;\n\n/**\n * Tests for {@link CategoryFinderImpl}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass CategoryFinderImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private CategoryService categoryService;\n\n    private CategoryFinderImpl categoryFinder;\n\n    @BeforeEach\n    void setUp() {\n        categoryFinder = new CategoryFinderImpl(client, categoryService);\n        lenient().when(categoryService.isCategoryHidden(any())).thenReturn(Mono.just(false));\n    }\n\n    @Test\n    void getByName() throws JSONException {\n        when(client.fetch(eq(Category.class), eq(\"hello\")))\n            .thenReturn(Mono.just(category()));\n        CategoryVo categoryVo = categoryFinder.getByName(\"hello\").block();\n        categoryVo.getMetadata().setCreationTimestamp(null);\n        JSONAssert.assertEquals(\"\"\"\n                {\n                     \"metadata\": {\n                         \"name\": \"hello\",\n                         \"annotations\": {\n                             \"K1\": \"V1\"\n                         }\n                     },\n                     \"spec\": {\n                         \"displayName\": \"displayName-1\",\n                         \"slug\": \"slug-1\",\n                         \"description\": \"description-1\",\n                         \"cover\": \"cover-1\",\n                         \"template\": \"template-1\",\n                         \"priority\": 0,\n                         \"children\": [\n                             \"C1\",\n                             \"C2\"\n                         ],\n                         \"preventParentPostCascadeQuery\": false,\n                         \"hideFromList\": false\n                     }\n                }\n                \"\"\",\n            JsonUtils.objectToJson(categoryVo),\n            true);\n    }\n\n    @Test\n    void list() {\n        ListResult<Category> categories = new ListResult<>(1, 10, 3,\n            categories().stream()\n                .sorted(CategoryFinderImpl.defaultComparator())\n                .toList());\n        when(client.listBy(eq(Category.class), any(ListOptions.class), any(PageRequest.class)))\n            .thenReturn(Mono.just(categories));\n        ListResult<CategoryVo> list = categoryFinder.list(1, 10).block();\n        assertThat(list.getItems()).hasSize(3);\n        assertThat(list.get().map(categoryVo -> categoryVo.getMetadata().getName()).toList())\n            .isEqualTo(List.of(\"c3\", \"c2\", \"hello\"));\n    }\n\n    @Test\n    void listAsTree() {\n        when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.fromIterable(categoriesForTree()));\n        List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();\n        assertThat(treeVos).hasSize(1);\n    }\n\n    @Test\n    void listSubTreeByName() {\n        when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.fromIterable(categoriesForTree()));\n        List<CategoryTreeVo> treeVos = categoryFinder.listAsTree(\"E\").collectList().block();\n        assertThat(treeVos.get(0).getMetadata().getName()).isEqualTo(\"E\");\n        assertThat(treeVos.get(0).getChildren()).hasSize(2);\n        assertThat(treeVos.get(0).getChildren().get(0).getMetadata().getName()).isEqualTo(\"A\");\n        assertThat(treeVos.get(0).getChildren().get(1).getMetadata().getName()).isEqualTo(\"C\");\n    }\n\n    /**\n     * Test for {@link CategoryFinderImpl#listAsTree()}.\n     *\n     * @see <a href=\"https://github.com/halo-dev/halo/issues/2532\">Fix #2532</a>\n     */\n    @Test\n    void listAsTreeMore() {\n        when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.fromIterable(moreCategories()));\n        List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();\n        String s = visualizeTree(treeVos);\n        assertThat(s).isEqualTo(\"\"\"\n            全部 (7)\n            ├── FIT2CLOUD (4)\n            │   ├── DataEase (0)\n            │   ├── Halo (2)\n            │   ├── MeterSphere (0)\n            │   └── JumpServer (0)\n            └── 默认分类 (3)\n            \"\"\");\n    }\n\n    @Nested\n    class CategoryPostCountTest {\n\n        /**\n         * <p>Structure below.</p>\n         * <pre>\n         * 全部 (35)\n         * ├── FIT2CLOUD (15)\n         * │   ├── DataEase (10)\n         * │   │   ├── SubNode1 (4)\n         * │   │   │   ├── Leaf1 (2)\n         * │   │   │   ├── Leaf2 (2)\n         * │   │   ├── SubNode2 (6)  (independent)\n         * │   │       ├── IndependentChild1 (3)\n         * │   │       ├── IndependentChild2 (3)\n         * │   ├── IndependentNode (5)  (independent)\n         * │       ├── IndependentChild3 (2)\n         * │       ├── IndependentChild4 (3)\n         * ├── AnotherRootChild (20)\n         * │   ├── Child1 (8)\n         * │   │   ├── SubChild1 (3)\n         * │   │   │   ├── DeepNode1 (1)\n         * │   │   │   ├── DeepNode2 (1)\n         * │   │   │   │   ├── DeeperNode (1)\n         * │   │   ├── SubChild2 (5)\n         * │   │       ├── DeepNode3 (2)  (independent)\n         * │   │           ├── DeepNode4 (1)\n         * │   │           ├── DeepNode5 (1)\n         * │   ├── Child2 (12)\n         * │       ├── IndependentSubNode (12)  (independent)\n         * │           ├── SubNode3 (6)\n         * │           ├── SubNode4 (6)\n         * </pre>\n         */\n        private List<Category> categories;\n\n        @BeforeEach\n        void setUp() throws IOException {\n            var file = ResourceUtils.getFile(\"classpath:categories/independent-post-count.json\");\n            var json = Files.readString(file.toPath());\n            categories = JsonUtils.jsonToObject(json, new TypeReference<>() {\n            });\n        }\n\n        @Test\n        void computePostCountFromTree() {\n            when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.fromIterable(categories));\n            var treeVos = categoryFinder.listAsTree(\"全部\")\n                .collectList().block();\n            assertThat(treeVos).hasSize(1);\n            String s = visualizeTree(treeVos.get(0).getChildren());\n            assertThat(s).isEqualTo(\"\"\"\n                全部 (84)\n                ├── AnotherRootChild (51)\n                │   ├── Child1 (19)\n                │   │   ├── SubChild1 (6)\n                │   │   │   ├── DeepNode1 (1)\n                │   │   │   └── DeepNode2 (2)\n                │   │   │       └── DeeperNode (1)\n                │   │   └── SubChild2 (5)\n                │   │       └── DeepNode3 (4) (Independent)\n                │   │           ├── DeepNode4 (1)\n                │   │           └── DeepNode5 (1)\n                │   └── Child2 (12)\n                │       └── IndependentSubNode (24) (Independent)\n                │           ├── SubNode3 (6)\n                │           └── SubNode4 (6)\n                └── FIT2CLOUD (33)\n                    ├── DataEase (18)\n                    │   ├── SubNode1 (8)\n                    │   │   ├── Leaf1 (2)\n                    │   │   └── Leaf2 (2)\n                    │   └── SubNode2 (12) (Independent)\n                    │       ├── IndependentChild1 (3)\n                    │       └── IndependentChild2 (3)\n                    └── IndependentNode (10) (Independent)\n                        ├── IndependentChild3 (2)\n                        └── IndependentChild4 (3)\n                \"\"\");\n        }\n\n        @Test\n        void getBreadcrumbsTest() {\n            when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.fromIterable(categories));\n            // first level\n            var breadcrumbs = categoryFinder.getBreadcrumbs(\"全部\").collectList().block();\n            assertThat(toNames(breadcrumbs)).containsSequence(\"全部\");\n\n            // second level\n            breadcrumbs = categoryFinder.getBreadcrumbs(\"AnotherRootChild\").collectList().block();\n            assertThat(toNames(breadcrumbs)).containsSequence(\"全部\", \"AnotherRootChild\");\n\n            // more levels\n            breadcrumbs = categoryFinder.getBreadcrumbs(\"DeepNode5\").collectList().block();\n            assertThat(toNames(breadcrumbs)).containsSequence(\"全部\", \"AnotherRootChild\", \"Child1\",\n                \"SubChild2\", \"DeepNode3\", \"DeepNode5\");\n\n            breadcrumbs = categoryFinder.getBreadcrumbs(\"IndependentChild4\").collectList().block();\n            assertThat(toNames(breadcrumbs)).containsSequence(\"全部\", \"FIT2CLOUD\",\n                \"IndependentNode\",\n                \"IndependentChild4\");\n\n            breadcrumbs = categoryFinder.getBreadcrumbs(\"SubNode4\").collectList().block();\n            assertThat(toNames(breadcrumbs)).containsSequence(\"全部\", \"AnotherRootChild\", \"Child2\",\n                \"IndependentSubNode\", \"SubNode4\");\n\n            // not exist\n            breadcrumbs = categoryFinder.getBreadcrumbs(\"not-exist\").collectList().block();\n            assertThat(toNames(breadcrumbs)).isEmpty();\n        }\n\n        @Test\n        void getBreadcrumbsForHiddenTest() {\n            Map<String, Category> categoryMap = categories.stream()\n                .collect(\n                    Collectors.toMap(item -> item.getMetadata().getName(), Function.identity()));\n            var category = categoryMap.get(\"IndependentNode\");\n            category.getSpec().setHideFromList(true);\n            when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))\n                .thenReturn(Flux.fromIterable(categoryMap.values()));\n\n            when(categoryService.isCategoryHidden(eq(\"IndependentChild4\")))\n                .thenReturn(Mono.just(true));\n\n            var breadcrumbs = categoryFinder.getBreadcrumbs(\"IndependentChild4\")\n                .collectList().block();\n            assertThat(toNames(breadcrumbs)).containsSequence(\"全部\", \"FIT2CLOUD\",\n                \"IndependentNode\",\n                \"IndependentChild4\");\n        }\n\n        static List<String> toNames(List<CategoryVo> categories) {\n            if (categories == null) {\n                return List.of();\n            }\n            return categories.stream()\n                .map(category -> category.getMetadata().getName())\n                .toList();\n        }\n    }\n\n    private List<Category> categoriesForTree() {\n        /*\n         *  D\n         *  ├── E\n         *  │   ├── A\n         *  │   │   └── B\n         *  │   └── C\n         *  └── G\n         *  ├── F\n         *      └── H\n         */\n        Category d = category();\n        d.getMetadata().setName(\"D\");\n        d.getSpec().setChildren(List.of(\"E\", \"G\", \"F\"));\n\n        Category e = category();\n        e.getMetadata().setName(\"E\");\n        e.getSpec().setChildren(List.of(\"A\", \"C\"));\n\n        Category a = category();\n        a.getMetadata().setName(\"A\");\n        a.getSpec().setChildren(List.of(\"B\"));\n\n        Category b = category();\n        b.getMetadata().setName(\"B\");\n        b.getSpec().setChildren(null);\n\n        Category c = category();\n        c.getMetadata().setName(\"C\");\n        c.getSpec().setChildren(null);\n\n        Category g = category();\n        g.getMetadata().setName(\"G\");\n        g.getSpec().setChildren(null);\n\n        Category f = category();\n        f.getMetadata().setName(\"F\");\n        f.getSpec().setChildren(List.of(\"H\"));\n\n        Category h = category();\n        h.getMetadata().setName(\"H\");\n        h.getSpec().setChildren(null);\n        return List.of(d, e, a, b, c, g, f, h);\n    }\n\n    /**\n     * Visualize a tree.\n     */\n    String visualizeTree(List<CategoryTreeVo> categoryTreeVos) {\n        Category.CategorySpec categorySpec = new Category.CategorySpec();\n        categorySpec.setSlug(\"/\");\n        categorySpec.setDisplayName(\"全部\");\n        Integer postCount = categoryTreeVos.stream()\n            .map(CategoryTreeVo::getPostCount)\n            .filter(Objects::nonNull)\n            .reduce(Integer::sum)\n            .orElse(0);\n        CategoryTreeVo root = CategoryTreeVo.builder()\n            .spec(categorySpec)\n            .postCount(postCount)\n            .children(categoryTreeVos)\n            .metadata(new Metadata())\n            .build();\n        StringBuilder stringBuilder = new StringBuilder();\n        root.print(stringBuilder, \"\", \"\");\n        return stringBuilder.toString();\n    }\n\n    private List<Category> categories() {\n        Category category2 = JsonUtils.deepCopy(category());\n        category2.getMetadata().setName(\"c2\");\n        category2.getSpec().setPriority(2);\n\n        Category category3 = JsonUtils.deepCopy(category());\n        category3.getMetadata().setName(\"c3\");\n        category3.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(20));\n        category3.getSpec().setPriority(2);\n        return List.of(category2, category(), category3);\n    }\n\n    private Category category() {\n        final Category category = new Category();\n\n        Metadata metadata = new Metadata();\n        metadata.setName(\"hello\");\n        metadata.setAnnotations(Map.of(\"K1\", \"V1\"));\n        metadata.setCreationTimestamp(Instant.now());\n        category.setMetadata(metadata);\n\n        Category.CategorySpec categorySpec = new Category.CategorySpec();\n        categorySpec.setSlug(\"slug-1\");\n        categorySpec.setDisplayName(\"displayName-1\");\n        categorySpec.setCover(\"cover-1\");\n        categorySpec.setDescription(\"description-1\");\n        categorySpec.setTemplate(\"template-1\");\n        categorySpec.setPriority(0);\n        categorySpec.setChildren(List.of(\"C1\", \"C2\"));\n        category.setSpec(categorySpec);\n        return category;\n    }\n\n    private List<Category> moreCategories() {\n        // see also https://github.com/halo-dev/halo/issues/2643\n        String s = \"\"\"\n            [\n               {\n                  \"spec\":{\n                     \"displayName\":\"默认分类\",\n                     \"slug\":\"default\",\n                     \"description\":\"这是你的默认分类，如不需要，删除即可。\",\n                     \"cover\":\"\",\n                     \"template\":\"\",\n                     \"priority\":1,\n                     \"children\":[\n                     ]\n                  },\n                  \"status\":{\n                     \"permalink\":\"/categories/default\",\n                     \"postCount\":3,\n                     \"visiblePostCount\":3\n                  },\n                  \"apiVersion\":\"content.halo.run/v1alpha1\",\n                  \"kind\":\"Category\",\n                  \"metadata\":{\n                     \"name\":\"76514a40-6ef1-4ed9-b58a-e26945bde3ca\",\n                     \"version\":16,\n                     \"creationTimestamp\":\"2022-10-08T06:17:47.589181Z\"\n                  }\n               },\n               {\n                  \"spec\":{\n                     \"displayName\":\"MeterSphere\",\n                     \"slug\":\"metersphere\",\n                     \"description\":\"\",\n                     \"cover\":\"\",\n                     \"template\":\"\",\n                     \"priority\":2,\n                     \"children\":[\n                     ]\n                  },\n                  \"status\":{\n                     \"permalink\":\"/categories/metersphere\",\n                     \"postCount\":0,\n                     \"visiblePostCount\":0\n                  },\n                  \"apiVersion\":\"content.halo.run/v1alpha1\",\n                  \"kind\":\"Category\",\n                  \"metadata\":{\n                     \"finalizers\":[\n                        \"category-protection\"\n                     ],\n                     \"name\":\"acf09686-d5a7-4227-ba8c-3aeff063f12f\",\n                     \"version\":13,\n                     \"creationTimestamp\":\"2022-10-08T06:32:36.650974Z\"\n                  }\n               },\n               {\n                  \"spec\":{\n                     \"displayName\":\"DataEase\",\n                     \"slug\":\"dataease\",\n                     \"description\":\"\",\n                     \"cover\":\"\",\n                     \"template\":\"\",\n                     \"priority\":0,\n                     \"children\":[\n                     ]\n                  },\n                  \"status\":{\n                     \"permalink\":\"/categories/dataease\",\n                     \"postCount\":0,\n                     \"visiblePostCount\":0\n                  },\n                  \"apiVersion\":\"content.halo.run/v1alpha1\",\n                  \"kind\":\"Category\",\n                  \"metadata\":{\n                     \"finalizers\":[\n                        \"category-protection\"\n                     ],\n                     \"name\":\"bd95f914-22fc-4de5-afcc-a9ffba2f6401\",\n                     \"version\":13,\n                     \"creationTimestamp\":\"2022-10-08T06:32:53.353838Z\"\n                  }\n               },\n               {\n                  \"spec\":{\n                     \"displayName\":\"FIT2CLOUD\",\n                     \"slug\":\"fit2cloud\",\n                     \"description\":\"\",\n                     \"cover\":\"\",\n                     \"template\":\"\",\n                     \"priority\":0,\n                     \"children\":[\n                        \"bd95f914-22fc-4de5-afcc-a9ffba2f6401\",\n                        \"e1150fd9-4512-453c-9186-f8de9c156c3d\",\n                        \"acf09686-d5a7-4227-ba8c-3aeff063f12f\",\n                        \"ed064d5e-2b6f-4123-8114-78d0c6f2c4e2\",\n                        \"non-existent-children-name\"\n                     ]\n                  },\n                  \"status\":{\n                     \"permalink\":\"/categories/fit2cloud\",\n                     \"postCount\":2,\n                     \"visiblePostCount\":2\n                  },\n                  \"apiVersion\":\"content.halo.run/v1alpha1\",\n                  \"kind\":\"Category\",\n                  \"metadata\":{\n                     \"finalizers\":[\n                        \"category-protection\"\n                     ],\n                     \"name\":\"c25c17ae-4a7b-43c5-a424-76950b9622cd\",\n                     \"version\":14,\n                     \"creationTimestamp\":\"2022-10-08T06:32:27.802025Z\"\n                  }\n               },\n               {\n                  \"spec\":{\n                     \"displayName\":\"Halo\",\n                     \"slug\":\"halo\",\n                     \"description\":\"\",\n                     \"cover\":\"\",\n                     \"template\":\"\",\n                     \"priority\":1,\n                     \"children\":[\n                     ]\n                  },\n                  \"status\":{\n                     \"permalink\":\"/categories/halo\",\n                     \"postCount\":2,\n                     \"visiblePostCount\":2\n                  },\n                  \"apiVersion\":\"content.halo.run/v1alpha1\",\n                  \"kind\":\"Category\",\n                  \"metadata\":{\n                     \"finalizers\":[\n                        \"category-protection\"\n                     ],\n                     \"name\":\"e1150fd9-4512-453c-9186-f8de9c156c3d\",\n                     \"version\":15,\n                     \"creationTimestamp\":\"2022-10-08T06:32:42.991788Z\"\n                  }\n               },\n               {\n                  \"spec\":{\n                     \"displayName\":\"JumpServer\",\n                     \"slug\":\"jumpserver\",\n                     \"description\":\"\",\n                     \"cover\":\"\",\n                     \"template\":\"\",\n                     \"priority\":3,\n                     \"children\":[\n                     ]\n                  },\n                  \"status\":{\n                     \"permalink\":\"/categories/jumpserver\",\n                     \"postCount\":0,\n                     \"visiblePostCount\":0\n                  },\n                  \"apiVersion\":\"content.halo.run/v1alpha1\",\n                  \"kind\":\"Category\",\n                  \"metadata\":{\n                     \"finalizers\":[\n                        \"category-protection\"\n                     ],\n                     \"name\":\"ed064d5e-2b6f-4123-8114-78d0c6f2c4e2\",\n                     \"version\":13,\n                     \"creationTimestamp\":\"2022-10-08T06:33:00.557435Z\"\n                  }\n               }\n            ]\n            \"\"\";\n        return JsonUtils.jsonToObject(s, new TypeReference<>() {\n        });\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.mockito.stubbing.Answer;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.extension.Counter;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.user.service.UserService;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link CommentFinderImpl}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith({SpringExtension.class, MockitoExtension.class})\nclass CommentPublicQueryServiceImplTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    UserService userService;\n\n    @Mock\n    CounterService counterService;\n\n    @InjectMocks\n    CommentPublicQueryServiceImpl commentPublicQueryService;\n\n    @BeforeEach\n    void setUp() {\n        when(userService.getUserOrGhost(eq(\"fake-user\"))).thenReturn(Mono.just(createUser()));\n    }\n\n    @Nested\n    class ListCommentTest {\n        @Test\n        void desensitizeComment() throws JSONException {\n            var commentOwner = new Comment.CommentOwner();\n            commentOwner.setName(\"fake-user\");\n            commentOwner.setDisplayName(\"Fake User\");\n            commentOwner.setAnnotations(new HashMap<>() {\n                {\n                    put(Comment.CommentOwner.KIND_EMAIL, \"mail@halo.run\");\n                }\n            });\n            var comment = commentForCompare(\"1\", null, true, 0);\n            comment.getSpec().setIpAddress(\"127.0.0.1\");\n            comment.getSpec().setOwner(commentOwner);\n\n            Counter counter = new Counter();\n            counter.setUpvote(0);\n            when(counterService.getByName(any())).thenReturn(Mono.just(counter));\n\n            var result = commentPublicQueryService.toCommentVo(comment).block();\n            result.getMetadata().setCreationTimestamp(null);\n            result.getSpec().setCreationTime(null);\n            JSONAssert.assertEquals(\"\"\"\n                    {\n                         \"metadata\":{\n                             \"name\":\"1\"\n                         },\n                         \"spec\":{\n                             \"owner\":{\n                                 \"name\":\"\",\n                                 \"displayName\":\"Fake User\",\n                                 \"annotations\":{\n                    \n                                 }\n                             },\n                             \"ipAddress\":\"\",\n                             \"priority\":0,\n                             \"top\":true\n                         },\n                         \"owner\":{\n                             \"kind\":\"User\",\n                             \"displayName\":\"fake-display-name\"\n                         },\n                         \"stats\":{\n                             \"upvote\":0\n                         }\n                     }\n                    \"\"\",\n                JsonUtils.objectToJson(result),\n                true);\n        }\n\n        Comment commentForCompare(String name, Instant creationTime, boolean top, int priority) {\n            Comment comment = new Comment();\n            comment.setMetadata(new Metadata());\n            comment.getMetadata().setName(name);\n            comment.getMetadata().setCreationTimestamp(Instant.now());\n            comment.setSpec(new Comment.CommentSpec());\n            comment.getSpec().setCreationTime(creationTime);\n            comment.getSpec().setTop(top);\n            comment.getSpec().setPriority(priority);\n            return comment;\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private void mockWhenListComment() {\n            // Mock\n            Comment commentNotApproved = createComment();\n            commentNotApproved.getMetadata().setName(\"comment-not-approved\");\n            commentNotApproved.getSpec().setApproved(false);\n\n            Comment commentApproved = createComment();\n            commentApproved.getMetadata().setName(\"comment-approved\");\n            commentApproved.getSpec().setApproved(true);\n\n            Comment notApprovedWithAnonymous = createComment();\n            notApprovedWithAnonymous.getMetadata().setName(\"comment-not-approved-anonymous\");\n            notApprovedWithAnonymous.getSpec().setApproved(false);\n            notApprovedWithAnonymous.getSpec().getOwner().setName(AnonymousUserConst.PRINCIPAL);\n\n            Comment commentApprovedButAnotherOwner = createComment();\n            commentApprovedButAnotherOwner.getMetadata()\n                .setName(\"comment-approved-but-another-owner\");\n            commentApprovedButAnotherOwner.getSpec().setApproved(true);\n            commentApprovedButAnotherOwner.getSpec().getOwner().setName(\"another\");\n\n            Comment commentNotApprovedAndAnotherOwner = createComment();\n            commentNotApprovedAndAnotherOwner.getMetadata()\n                .setName(\"comment-not-approved-and-another\");\n            commentNotApprovedAndAnotherOwner.getSpec().setApproved(false);\n            commentNotApprovedAndAnotherOwner.getSpec().getOwner().setName(\"another\");\n\n            Comment notApprovedAndAnotherRef = createComment();\n            notApprovedAndAnotherRef.getMetadata()\n                .setName(\"comment-not-approved-and-another-ref\");\n            notApprovedAndAnotherRef.getSpec().setApproved(false);\n            Ref anotherRef =\n                Ref.of(\"another-fake-post\", GroupVersionKind.fromExtension(Post.class));\n            notApprovedAndAnotherRef.getSpec().setSubjectRef(anotherRef);\n\n            when(client.list(eq(Comment.class), any(),\n                any(),\n                eq(1),\n                eq(10))\n            ).thenAnswer((Answer<Mono<ListResult<Comment>>>) invocation -> {\n                Predicate<Comment> predicate =\n                    invocation.getArgument(1, Predicate.class);\n                List<Comment> comments = Stream.of(\n                    commentNotApproved,\n                    commentApproved,\n                    commentApprovedButAnotherOwner,\n                    commentNotApprovedAndAnotherOwner,\n                    notApprovedWithAnonymous,\n                    notApprovedAndAnotherRef\n                ).filter(predicate).toList();\n                return Mono.just(new ListResult<>(1, 10, comments.size(), comments));\n            });\n\n            extractedUser();\n            when(client.fetch(eq(User.class), any())).thenReturn(Mono.just(createUser()));\n\n            Counter counter = new Counter();\n            counter.setUpvote(9);\n            when(counterService.getByName(any())).thenReturn(Mono.just(counter));\n        }\n\n        Comment createComment() {\n            Comment comment = new Comment();\n            comment.setMetadata(new Metadata());\n            comment.getMetadata().setName(\"fake-comment\");\n            comment.setSpec(new Comment.CommentSpec());\n            comment.setStatus(new Comment.CommentStatus());\n\n            comment.getSpec().setRaw(\"fake-raw\");\n            comment.getSpec().setContent(\"fake-content\");\n            comment.getSpec().setHidden(false);\n            comment.getSpec()\n                .setSubjectRef(Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class)));\n            Comment.CommentOwner commentOwner = new Comment.CommentOwner();\n            commentOwner.setKind(User.KIND);\n            commentOwner.setName(\"fake-user\");\n            commentOwner.setDisplayName(\"fake-display-name\");\n            comment.getSpec().setOwner(commentOwner);\n            return comment;\n        }\n    }\n\n    private void extractedUser() {\n        User another = createUser();\n        another.getMetadata().setName(\"another\");\n        when(userService.getUserOrGhost(eq(\"another\"))).thenReturn(Mono.just(another));\n\n        User ghost = createUser();\n        ghost.getMetadata().setName(\"ghost\");\n        when(userService.getUserOrGhost(eq(\"ghost\"))).thenReturn(Mono.just(ghost));\n        when(userService.getUserOrGhost(eq(\"fake-user\"))).thenReturn(Mono.just(createUser()));\n        when(userService.getUserOrGhost(any())).thenReturn(Mono.just(ghost));\n    }\n\n    User createUser() {\n        User user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(\"fake-user\");\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setDisplayName(\"fake-display-name\");\n        return user;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceIntegrationTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.annotation.DirtiesContext;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.core.extension.content.Comment;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.Reply;\nimport run.halo.app.extension.Extension;\nimport run.halo.app.extension.ExtensionStoreUtil;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Ref;\nimport run.halo.app.extension.SchemeManager;\nimport run.halo.app.extension.store.ReactiveExtensionStoreClient;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.infra.exception.DuplicateNameException;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@DirtiesContext\n@SpringBootTest\nclass CommentPublicQueryServiceIntegrationTest {\n\n    @Autowired\n    private SchemeManager schemeManager;\n\n    @Autowired\n    private ReactiveExtensionClient client;\n\n    @Autowired\n    private ReactiveExtensionStoreClient storeClient;\n\n    Mono<Extension> deleteImmediately(Extension extension) {\n        var name = extension.getMetadata().getName();\n        var scheme = schemeManager.get(extension.getClass());\n\n        // delete from db\n        var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);\n        return storeClient.delete(storeName, extension.getMetadata().getVersion())\n            .thenReturn(extension);\n    }\n\n    @Nested\n    class CommentListTest {\n        private final List<Comment> storedComments = commentsForStore();\n\n        @Autowired\n        private CommentPublicQueryServiceImpl commentPublicQueryService;\n\n        @BeforeEach\n        void setUp() {\n            Flux.fromIterable(storedComments)\n                .flatMap(comment -> client.create(comment))\n                .as(StepVerifier::create)\n                .expectNextCount(storedComments.size())\n                .verifyComplete();\n        }\n\n        @AfterEach\n        void tearDown() {\n            Flux.fromIterable(storedComments)\n                .flatMap(CommentPublicQueryServiceIntegrationTest.this::deleteImmediately)\n                .as(StepVerifier::create)\n                .expectNextCount(storedComments.size())\n                .verifyComplete();\n        }\n\n        @Test\n        void listWhenUserNotLogin() {\n            Ref ref = Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class));\n            commentPublicQueryService.list(ref, 1, 10)\n                .as(StepVerifier::create)\n                .consumeNextWith(listResult -> {\n                    assertThat(listResult.getTotal()).isEqualTo(2);\n                    assertThat(listResult.getItems().size()).isEqualTo(2);\n                    assertThat(listResult.getItems().get(0).getMetadata().getName())\n                        .isEqualTo(\"comment-approved\");\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        @WithMockUser(username = AnonymousUserConst.PRINCIPAL)\n        void listWhenUserIsAnonymous() {\n            Ref ref = Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class));\n            commentPublicQueryService.list(ref, 1, 10)\n                .as(StepVerifier::create)\n                .consumeNextWith(listResult -> {\n                    assertThat(listResult.getTotal()).isEqualTo(2);\n                    assertThat(listResult.getItems().size()).isEqualTo(2);\n                    assertThat(listResult.getItems().get(0).getMetadata().getName())\n                        .isEqualTo(\"comment-approved\");\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        @WithMockUser(username = \"fake-user\")\n        void listWhenUserLoggedIn() {\n            Ref ref = Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class));\n            commentPublicQueryService.list(ref, 1, 10)\n                .as(StepVerifier::create)\n                .consumeNextWith(listResult -> {\n                    assertThat(listResult.getTotal()).isEqualTo(3);\n                    assertThat(listResult.getItems().size()).isEqualTo(3);\n                    assertThat(listResult.getItems().get(0).getMetadata().getName())\n                        .isEqualTo(\"comment-approved\");\n                    assertThat(listResult.getItems().get(1).getMetadata().getName())\n                        .isEqualTo(\"comment-approved-but-another-owner\");\n                    assertThat(listResult.getItems().get(2).getMetadata().getName())\n                        .isEqualTo(\"comment-not-approved\");\n                })\n                .verifyComplete();\n        }\n\n        List<Comment> commentsForStore() {\n            // Mock\n            Comment commentNotApproved = fakeComment();\n            commentNotApproved.getMetadata().setName(\"comment-not-approved\");\n            commentNotApproved.getSpec().setApproved(false);\n\n            Comment commentApproved = fakeComment();\n            commentApproved.getMetadata().setName(\"comment-approved\");\n            commentApproved.getSpec().setApproved(true);\n\n            Comment notApprovedWithAnonymous = fakeComment();\n            notApprovedWithAnonymous.getMetadata().setName(\"comment-not-approved-anonymous\");\n            notApprovedWithAnonymous.getSpec().setApproved(false);\n            notApprovedWithAnonymous.getSpec().getOwner().setName(AnonymousUserConst.PRINCIPAL);\n\n            Comment commentApprovedButAnotherOwner = fakeComment();\n            commentApprovedButAnotherOwner.getMetadata()\n                .setName(\"comment-approved-but-another-owner\");\n            commentApprovedButAnotherOwner.getSpec().setApproved(true);\n            commentApprovedButAnotherOwner.getSpec().getOwner().setName(\"another\");\n\n            Comment commentNotApprovedAndAnotherOwner = fakeComment();\n            commentNotApprovedAndAnotherOwner.getMetadata()\n                .setName(\"comment-not-approved-and-another\");\n            commentNotApprovedAndAnotherOwner.getSpec().setApproved(false);\n            commentNotApprovedAndAnotherOwner.getSpec().getOwner().setName(\"another\");\n\n            Comment notApprovedAndAnotherRef = fakeComment();\n            notApprovedAndAnotherRef.getMetadata()\n                .setName(\"comment-not-approved-and-another-ref\");\n            notApprovedAndAnotherRef.getSpec().setApproved(false);\n            Ref anotherRef =\n                Ref.of(\"another-fake-post\", GroupVersionKind.fromExtension(Post.class));\n            notApprovedAndAnotherRef.getSpec().setSubjectRef(anotherRef);\n\n            return List.of(\n                commentNotApproved,\n                commentApproved,\n                commentApprovedButAnotherOwner,\n                commentNotApprovedAndAnotherOwner,\n                notApprovedWithAnonymous,\n                notApprovedAndAnotherRef\n            );\n        }\n\n        Comment fakeComment() {\n            Comment comment = createComment();\n            comment.getMetadata().setDeletionTimestamp(null);\n            comment.getMetadata().setName(\"fake-comment\");\n\n            comment.getSpec().setRaw(\"fake-raw\");\n            comment.getSpec().setContent(\"fake-content\");\n            comment.getSpec().setHidden(false);\n            comment.getSpec()\n                .setSubjectRef(Ref.of(\"fake-post\", GroupVersionKind.fromExtension(Post.class)));\n            Comment.CommentOwner commentOwner = new Comment.CommentOwner();\n            commentOwner.setKind(User.KIND);\n            commentOwner.setName(\"fake-user\");\n            commentOwner.setDisplayName(\"fake-display-name\");\n            comment.getSpec().setOwner(commentOwner);\n            return comment;\n        }\n    }\n\n    @Nested\n    class CommentDefaultSortTest {\n        private final List<Comment> commentList = createCommentList();\n\n        @BeforeEach\n        void setUp() {\n            Flux.fromIterable(commentList)\n                .flatMap(comment -> client.create(comment))\n                .as(StepVerifier::create)\n                .expectNextCount(commentList.size())\n                .verifyComplete();\n        }\n\n        @AfterEach\n        void tearDown() {\n            Flux.fromIterable(commentList)\n                .flatMap(CommentPublicQueryServiceIntegrationTest.this::deleteImmediately)\n                .as(StepVerifier::create)\n                .expectNextCount(commentList.size())\n                .verifyComplete();\n        }\n\n        @Test\n        void sortTest() {\n            var comments = client.listAll(Comment.class, new ListOptions(),\n                    CommentPublicQueryServiceImpl.defaultCommentSort())\n                .collectList()\n                .block();\n            assertThat(comments).isNotNull();\n\n            var result = comments.stream()\n                .map(comment -> comment.getMetadata().getName())\n                .collect(Collectors.joining(\", \"));\n            assertThat(result).isEqualTo(\"1, 2, 4, 3, 5, 6, 10, 14, 9, 8, 7, 11, 12, 13\");\n        }\n\n        List<Comment> createCommentList() {\n            // 1, now + 1s, top, 0\n            // 2, now + 2s, top, 1\n            // 3, now + 3s, top, 2\n            // 4, now + 4s, top, 2\n            // 5, now + 4s, top, 3\n            // 6, now + 4s, no, 0\n            // 7, now + 1s, no, 0\n            // 8, now + 2s, no, 0\n            // 9, now + 3s, no, 0\n            // 10, null, no, 0\n            // 11, null, no, 1\n            // 12, null, no, 3\n            // 13, now + 3s, no, 3\n            Instant now = Instant.now();\n            var comment1 = commentForCompare(\"1\", now.plusSeconds(1), true, 0);\n            var comment2 = commentForCompare(\"2\", now.plusSeconds(2), true, 1);\n            var comment3 = commentForCompare(\"3\", now.plusSeconds(3), true, 2);\n            var comment4 = commentForCompare(\"4\", now.plusSeconds(4), true, 2);\n            var comment5 = commentForCompare(\"5\", now.plusSeconds(4), true, 3);\n            var comment6 = commentForCompare(\"6\", now.plusSeconds(4), true, 3);\n            var comment7 = commentForCompare(\"7\", now.plusSeconds(1), false, 0);\n            var comment8 = commentForCompare(\"8\", now.plusSeconds(2), false, 0);\n            var comment9 = commentForCompare(\"9\", now.plusSeconds(3), false, 0);\n            var comment10 = commentForCompare(\"10\", now.plusSeconds(3), false, 0);\n            var comment11 = commentForCompare(\"11\", now, false, 0);\n            var comment12 = commentForCompare(\"12\", now, false, 1);\n            var comment13 = commentForCompare(\"13\", now, false, 3);\n            var comment14 = commentForCompare(\"14\", now.plusSeconds(3), false, 3);\n\n            return List.of(comment1, comment2, comment3, comment4, comment5, comment6, comment7,\n                comment8, comment9, comment10, comment11, comment12, comment13, comment14);\n        }\n\n        Comment commentForCompare(String name, Instant creationTime, boolean top, int priority) {\n            var comment = createComment();\n            comment.getMetadata().setName(name);\n            comment.getMetadata().setCreationTimestamp(creationTime);\n            comment.getSpec().setCreationTime(creationTime);\n            comment.getSpec().setTop(top);\n            comment.getSpec().setPriority(priority);\n            return comment;\n        }\n    }\n\n    @Nested\n    class ListReplyTest {\n        private final List<Reply> storedReplies = mockRelies();\n\n        @Autowired\n        private CommentPublicQueryServiceImpl commentPublicQueryService;\n\n        @BeforeEach\n        void setUp() {\n            // create comment\n            var comment = createComment();\n            client.create(comment)\n                .onErrorResume(DuplicateNameException.class, e -> Mono.just(comment))\n                .as(StepVerifier::create)\n                .expectNextCount(1)\n                .verifyComplete();\n            Flux.fromIterable(storedReplies)\n                .flatMap(reply -> client.create(reply))\n                .as(StepVerifier::create)\n                .expectNextCount(storedReplies.size())\n                .verifyComplete();\n        }\n\n        @AfterEach\n        void tearDown() {\n            Flux.fromIterable(storedReplies)\n                .flatMap(CommentPublicQueryServiceIntegrationTest.this::deleteImmediately)\n                .as(StepVerifier::create)\n                .expectNextCount(storedReplies.size())\n                .verifyComplete();\n        }\n\n        @Test\n        void listWhenUserNotLogin() {\n            commentPublicQueryService.listReply(\"fake-comment\", 1, 10)\n                .as(StepVerifier::create)\n                .consumeNextWith(listResult -> {\n                    assertThat(listResult.getTotal()).isEqualTo(2);\n                    assertThat(listResult.getItems().size()).isEqualTo(2);\n                    assertThat(listResult.getItems().get(0).getMetadata().getName())\n                        .isEqualTo(\"reply-approved\");\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        @WithMockUser(username = AnonymousUserConst.PRINCIPAL)\n        void listWhenUserIsAnonymous() {\n            commentPublicQueryService.listReply(\"fake-comment\", 1, 10)\n                .as(StepVerifier::create)\n                .consumeNextWith(listResult -> {\n                    assertThat(listResult.getTotal()).isEqualTo(2);\n                    assertThat(listResult.getItems().size()).isEqualTo(2);\n                    assertThat(listResult.getItems().get(0).getMetadata().getName())\n                        .isEqualTo(\"reply-approved\");\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        @WithMockUser(username = \"fake-user\")\n        void listWhenUserLoggedIn() {\n            commentPublicQueryService.listReply(\"fake-comment\", 1, 10)\n                .as(StepVerifier::create)\n                .consumeNextWith(listResult -> {\n                    assertThat(listResult.getTotal()).isEqualTo(3);\n                    assertThat(listResult.getItems().size()).isEqualTo(3);\n                    assertThat(listResult.getItems().get(0).getMetadata().getName())\n                        .isEqualTo(\"reply-approved\");\n                    assertThat(listResult.getItems().get(1).getMetadata().getName())\n                        .isEqualTo(\"reply-approved-but-another-owner\");\n                    assertThat(listResult.getItems().get(2).getMetadata().getName())\n                        .isEqualTo(\"reply-not-approved\");\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        void desensitizeReply() throws JSONException {\n            var reply = createReply();\n            reply.getSpec().getOwner()\n                .setAnnotations(new HashMap<>() {\n                    {\n                        put(Comment.CommentOwner.KIND_EMAIL, \"mail@halo.run\");\n                    }\n                });\n            reply.getSpec().setIpAddress(\"127.0.0.1\");\n\n            var result = commentPublicQueryService.toReplyVo(reply).block();\n            result.getMetadata().setCreationTimestamp(null);\n            var jsonObject = JsonUtils.jsonToObject(fakeReplyJson(), JsonNode.class);\n            ((ObjectNode) jsonObject.get(\"owner\"))\n                .put(\"displayName\", \"已删除用户\");\n            JSONAssert.assertEquals(jsonObject.toString(),\n                JsonUtils.objectToJson(result),\n                true);\n        }\n\n        String fakeReplyJson() {\n            return \"\"\"\n                    {\n                        \"metadata\":{\n                            \"name\":\"fake-reply\"\n                        },\n                        \"spec\":{\n                            \"raw\":\"fake-raw\",\n                            \"content\":\"fake-content\",\n                            \"owner\":{\n                                \"kind\":\"User\",\n                                \"name\":\"\",\n                                \"displayName\":\"fake-display-name\",\n                                \"annotations\":{\n                                    \"email-hash\": \\\n                    \"79783106d88279c6c8f94f1f4dec22bdb9f90a8d14c9d6c6628a11430e236cbf\"\n                                }\n                            },\n                            \"creationTime\": \"2024-03-11T06:23:42.923294424Z\",\n                            \"ipAddress\":\"\",\n                            \"hidden\": false,\n                            \"allowNotification\": false,\n                            \"top\": false,\n                            \"priority\": 0,\n                            \"commentName\":\"fake-comment\"\n                        },\n                        \"owner\":{\n                            \"kind\":\"User\",\n                            \"displayName\":\"fake-display-name\"\n                        },\n                        \"stats\":{\n                            \"upvote\":0\n                        }\n                    }\n                \"\"\";\n        }\n\n        private List<Reply> mockRelies() {\n            // Mock\n            Reply notApproved = createReply();\n            notApproved.getMetadata().setName(\"reply-not-approved\");\n            notApproved.getSpec().setApproved(false);\n\n            Reply approved = createReply();\n            approved.getMetadata().setName(\"reply-approved\");\n            approved.getSpec().setApproved(true);\n\n            Reply notApprovedWithAnonymous = createReply();\n            notApprovedWithAnonymous.getMetadata().setName(\"reply-not-approved-anonymous\");\n            notApprovedWithAnonymous.getSpec().setApproved(false);\n            notApprovedWithAnonymous.getSpec().getOwner().setName(AnonymousUserConst.PRINCIPAL);\n\n            Reply approvedButAnotherOwner = createReply();\n            approvedButAnotherOwner.getMetadata()\n                .setName(\"reply-approved-but-another-owner\");\n            approvedButAnotherOwner.getSpec().setApproved(true);\n            approvedButAnotherOwner.getSpec().getOwner().setName(\"another\");\n\n            Reply notApprovedAndAnotherOwner = createReply();\n            notApprovedAndAnotherOwner.getMetadata()\n                .setName(\"reply-not-approved-and-another\");\n            notApprovedAndAnotherOwner.getSpec().setApproved(false);\n            notApprovedAndAnotherOwner.getSpec().getOwner().setName(\"another\");\n\n            Reply notApprovedAndAnotherCommentName = createReply();\n            notApprovedAndAnotherCommentName.getMetadata()\n                .setName(\"reply-approved-and-another-comment-name\");\n            notApprovedAndAnotherCommentName.getSpec().setApproved(false);\n            notApprovedAndAnotherCommentName.getSpec().setCommentName(\"another-fake-comment\");\n\n            return List.of(\n                notApproved,\n                approved,\n                approvedButAnotherOwner,\n                notApprovedAndAnotherOwner,\n                notApprovedWithAnonymous,\n                notApprovedAndAnotherCommentName\n            );\n        }\n\n        Reply createReply() {\n            var reply = JsonUtils.jsonToObject(fakeReplyJson(), Reply.class);\n            reply.getMetadata().setName(\"fake-reply\");\n\n            reply.getSpec().setRaw(\"fake-raw\");\n            reply.getSpec().setContent(\"fake-content\");\n            reply.getSpec().setHidden(false);\n            reply.getSpec().setCommentName(\"fake-comment\");\n            Comment.CommentOwner commentOwner = new Comment.CommentOwner();\n            commentOwner.setKind(User.KIND);\n            commentOwner.setName(\"fake-user\");\n            commentOwner.setDisplayName(\"fake-display-name\");\n            reply.getSpec().setOwner(commentOwner);\n            return reply;\n        }\n    }\n\n    Comment createComment() {\n        return JsonUtils.jsonToObject(\"\"\"\n              {\n                \"spec\": {\n                    \"raw\": \"fake-raw\",\n                    \"content\": \"fake-content\",\n                    \"owner\": {\n                        \"kind\": \"User\",\n                        \"name\": \"fake-user\"\n                    },\n                    \"userAgent\": \"\",\n                    \"ipAddress\": \"\",\n                    \"approvedTime\": \"2024-02-28T09:15:16.095Z\",\n                    \"creationTime\": \"2024-02-28T06:23:42.923294424Z\",\n                    \"priority\": 0,\n                    \"top\": false,\n                    \"allowNotification\": false,\n                    \"approved\": true,\n                    \"hidden\": false,\n                    \"subjectRef\": {\n                        \"group\": \"content.halo.run\",\n                        \"version\": \"v1alpha1\",\n                        \"kind\": \"SinglePage\",\n                        \"name\": \"67\"\n                    },\n                    \"lastReadTime\": \"2024-02-29T03:39:04.230Z\"\n                },\n                \"apiVersion\": \"content.halo.run/v1alpha1\",\n                \"kind\": \"Comment\",\n                \"metadata\": {\n                    \"name\": \"fake-comment\",\n                    \"creationTimestamp\": \"2024-02-28T06:23:42.923439037Z\"\n                }\n            }\n            \"\"\", Comment.class);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/MenuFinderImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\n\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.util.function.Tuple2;\nimport reactor.util.function.Tuples;\nimport run.halo.app.core.extension.Menu;\nimport run.halo.app.core.extension.MenuItem;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.theme.finders.vo.MenuVo;\n\n/**\n * Tests for {@link MenuFinderImpl}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass MenuFinderImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @InjectMocks\n    private MenuFinderImpl menuFinder;\n\n    @Test\n    void listAsTree() {\n        Tuple2<List<Menu>, List<MenuItem>> tuple = testTree();\n        Mockito.when(client.list(eq(Menu.class), eq(null), eq(null)))\n            .thenReturn(Flux.fromIterable(tuple.getT1()));\n        Mockito.when(client.list(eq(MenuItem.class), eq(null), any()))\n            .thenReturn(Flux.fromIterable(tuple.getT2()));\n\n        List<MenuVo> menuVos = menuFinder.listAsTree().collectList().block();\n        assertThat(visualizeTree(menuVos)).isEqualTo(\"\"\"\n            D\n            └── E\n                ├── A\n                │   └── B\n                └── C\n            X\n            └── G\n            Y\n            └── F\n                └── H\n            \"\"\");\n    }\n\n    /**\n     * Visualize a tree.\n     */\n    String visualizeTree(List<MenuVo> menuVos) {\n        StringBuilder stringBuilder = new StringBuilder();\n        for (MenuVo menuVo : menuVos) {\n            menuVo.print(stringBuilder);\n        }\n        return stringBuilder.toString();\n    }\n\n    Tuple2<List<Menu>, List<MenuItem>> testTree() {\n        /*\n         *  D\n         *  ├── E\n         *  │   ├── A\n         *  │   │   └── B\n         *  │   └── C\n         *  X── G\n         *  Y── F\n         *      └── H\n         */\n        Menu menuD = menu(\"D\", of(\"E\"));\n        Menu menuX = menu(\"X\", of(\"G\"));\n        Menu menuY = menu(\"Y\", of(\"F\"));\n\n        MenuItem itemE = menuItem(\"E\", of(\"A\", \"C\", \"non-existent-children-name\"));\n        MenuItem itemG = menuItem(\"G\", null);\n        MenuItem itemF = menuItem(\"F\", of(\"H\"));\n        MenuItem itemA = menuItem(\"A\", of(\"B\"));\n        MenuItem itemB = menuItem(\"B\", null);\n        MenuItem itemC = menuItem(\"C\", null);\n        MenuItem itemH = menuItem(\"H\", null);\n        return Tuples.of(List.of(menuD, menuX, menuY),\n            List.of(itemE, itemG, itemF, itemA, itemB, itemC, itemH));\n    }\n\n    LinkedHashSet<String> of(String... names) {\n        LinkedHashSet<String> list = new LinkedHashSet<>();\n        Collections.addAll(list, names);\n        return list;\n    }\n\n    Menu menu(String name, LinkedHashSet<String> menuItemNames) {\n        Menu menu = new Menu();\n        Metadata metadata = new Metadata();\n        metadata.setName(name);\n        menu.setMetadata(metadata);\n\n        Menu.Spec spec = new Menu.Spec();\n        spec.setDisplayName(name);\n        spec.setMenuItems(menuItemNames);\n        menu.setSpec(spec);\n        return menu;\n    }\n\n    MenuItem menuItem(String name, LinkedHashSet<String> childrenNames) {\n        MenuItem menuItem = new MenuItem();\n        Metadata metadata = new Metadata();\n        metadata.setName(name);\n        menuItem.setMetadata(metadata);\n\n        MenuItem.MenuItemSpec spec = new MenuItem.MenuItemSpec();\n        spec.setPriority(0);\n        spec.setDisplayName(name);\n        spec.setChildren(childrenNames);\n        menuItem.setSpec(spec);\n\n        MenuItem.MenuItemStatus status = new MenuItem.MenuItemStatus();\n        menuItem.setStatus(status);\n        return menuItem;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/PluginFinderImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.pf4j.DefaultVersionManager;\nimport org.pf4j.PluginDescriptor;\nimport org.pf4j.PluginManager;\nimport org.pf4j.PluginState;\nimport org.pf4j.PluginWrapper;\n\n/**\n * Tests for {@link PluginFinderImpl}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PluginFinderImplTest {\n\n    @Mock\n    private PluginManager haloPluginManager;\n\n    @InjectMocks\n    private PluginFinderImpl pluginFinder;\n\n    @Test\n    void available() {\n        assertThat(pluginFinder.available(null)).isFalse();\n\n        boolean available = pluginFinder.available(\"fake-plugin\");\n        assertThat(available).isFalse();\n\n        PluginWrapper mockPluginWrapper = Mockito.mock(PluginWrapper.class);\n        when(haloPluginManager.getPlugin(eq(\"fake-plugin\")))\n            .thenReturn(mockPluginWrapper);\n\n        when(mockPluginWrapper.getPluginState()).thenReturn(PluginState.RESOLVED);\n        available = pluginFinder.available(\"fake-plugin\");\n        assertThat(available).isFalse();\n\n        when(mockPluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);\n        available = pluginFinder.available(\"fake-plugin\");\n        assertThat(available).isTrue();\n    }\n\n    @Test\n    void availableWithVersionTest() {\n        when(haloPluginManager.getVersionManager()).thenReturn(new DefaultVersionManager());\n\n        assertThatThrownBy(() -> pluginFinder.available(\"fake-plugin\", null))\n            .isInstanceOf(IllegalArgumentException.class);\n\n        boolean available = pluginFinder.available(\"fake-plugin\", \"1.0.0\");\n        assertThat(available).isFalse();\n\n        PluginWrapper mockPluginWrapper = Mockito.mock(PluginWrapper.class);\n        when(haloPluginManager.getPlugin(eq(\"fake-plugin\")))\n            .thenReturn(mockPluginWrapper);\n\n        when(mockPluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);\n        var descriptor = mock(PluginDescriptor.class);\n        when(mockPluginWrapper.getDescriptor()).thenReturn(descriptor);\n        when(descriptor.getVersion()).thenReturn(\"1.0.0\");\n\n        available = pluginFinder.available(\"fake-plugin\", \"1.0.0\");\n        assertThat(available).isTrue();\n\n        available = pluginFinder.available(\"fake-plugin\", \">=1.0.0\");\n        assertThat(available).isTrue();\n\n        available = pluginFinder.available(\"fake-plugin\", \"<2.0.0\");\n        assertThat(available).isTrue();\n\n        available = pluginFinder.available(\"fake-plugin\", \"2.0.0\");\n        assertThat(available).isFalse();\n\n        available = pluginFinder.available(\"fake-plugin\", \"<1.0.0\");\n        assertThat(available).isFalse();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplIntegrationTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.thymeleaf.IEngineConfiguration;\nimport org.thymeleaf.TemplateEngine;\nimport org.thymeleaf.context.Context;\nimport org.thymeleaf.spring6.SpringTemplateEngine;\nimport org.thymeleaf.spring6.dialect.SpringStandardDialect;\nimport org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;\nimport org.thymeleaf.templateresolver.StringTemplateResolver;\nimport org.thymeleaf.templateresource.ITemplateResource;\nimport org.thymeleaf.templateresource.StringTemplateResource;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.theme.ReactiveSpelVariableExpressionEvaluator;\nimport run.halo.app.theme.finders.PostPublicQueryService;\n\n/**\n * Tests for {@link PostFinderImpl}.\n *\n * @author guqing\n * @since 2.19.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostFinderImplIntegrationTest {\n\n    private TemplateEngine templateEngine;\n\n    @Mock\n    private PostPublicQueryService postPublicQueryService;\n\n    @InjectMocks\n    private PostFinderImpl postFinder;\n\n    @Mock\n    private TemplateResourceComputer templateResourceComputer;\n\n    @BeforeEach\n    void setUp() {\n        templateEngine = new SpringTemplateEngine();\n        templateEngine.setDialect(new SpringStandardDialect() {\n            @Override\n            public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {\n                return ReactiveSpelVariableExpressionEvaluator.INSTANCE;\n            }\n        });\n        templateEngine.addTemplateResolver(new TestTemplateResolver(templateResourceComputer));\n    }\n\n    @Test\n    void listTest() {\n        var context = new Context();\n        context.setVariable(\"postFinder\", postFinder);\n\n        // empty param\n        when(templateResourceComputer.compute(eq(\"post\"))).thenReturn(new StringTemplateResource(\"\"\"\n            <span th:text=\"${postFinder.list({})}\"></span>\n            \"\"\"));\n\n        when(postPublicQueryService.list(any(), any()))\n            .thenReturn(Mono.just(ListResult.emptyResult()));\n\n        var result = templateEngine.process(\"post\", context);\n        assertThat(result).isEqualToIgnoringWhitespace(\n            \"<span>ListResult(page=0, size=0, total=0, items=[])</span>\");\n\n        when(templateResourceComputer.compute(eq(\"post\"))).thenReturn(new StringTemplateResource(\"\"\"\n            <span\n              th:each=\"post : ${postFinder.list({page: 1, size: 10, tagName: 'fake-tag',\n               ownerName: 'fake-owner', sort: {'spec.publishTime,desc',\n                'metadata.creationTimestamp,asc'}})}\"\n            >\n            </span>\n            \"\"\"));\n        result = templateEngine.process(\"post\", context);\n        assertThat(result).isEqualToIgnoringWhitespace(\"\");\n    }\n\n    static class TestTemplateResolver extends StringTemplateResolver {\n        private final TemplateResourceComputer templateResourceComputer;\n\n        TestTemplateResolver(TemplateResourceComputer templateResourceComputer) {\n            this.templateResourceComputer = templateResourceComputer;\n        }\n\n        @Override\n        protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,\n            String ownerTemplate, String template,\n            Map<String, Object> templateResolutionAttributes) {\n            return templateResourceComputer.compute(template);\n        }\n    }\n\n    interface TemplateResourceComputer {\n        ITemplateResource compute(String template);\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.counter.CounterService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.theme.finders.CategoryFinder;\nimport run.halo.app.theme.finders.ContributorFinder;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.TagFinder;\nimport run.halo.app.theme.finders.vo.ListedPostVo;\nimport run.halo.app.theme.finders.vo.PostArchiveVo;\nimport run.halo.app.theme.finders.vo.PostArchiveYearMonthVo;\nimport run.halo.app.theme.router.DefaultQueryPostPredicateResolver;\n\n/**\n * Tests for {@link PostFinderImpl}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostFinderImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private CounterService counterService;\n\n    @Mock\n    private PostService postService;\n\n    @Mock\n    private CategoryFinder categoryFinder;\n\n    @Mock\n    private TagFinder tagFinder;\n\n    @Mock\n    private ContributorFinder contributorFinder;\n\n    @Mock\n    private PostPublicQueryService publicQueryService;\n\n    @InjectMocks\n    private PostFinderImpl postFinder;\n\n    @Test\n    void predicate() {\n        Predicate<Post> predicate = new DefaultQueryPostPredicateResolver().getPredicate().block();\n        assertThat(predicate).isNotNull();\n\n        List<String> strings = posts().stream().filter(predicate)\n            .map(post -> post.getMetadata().getName())\n            .toList();\n        assertThat(strings).isEqualTo(List.of(\"post-1\", \"post-2\", \"post-6\"));\n    }\n\n    @Test\n    void archives() {\n        List<ListedPostVo> listedPostVos = postsForArchives().stream()\n            .map(ListedPostVo::from)\n            .toList();\n        ListResult<ListedPostVo> listResult = new ListResult<>(1, 10, 3, listedPostVos);\n        when(publicQueryService.list(any(), any(PageRequest.class)))\n            .thenReturn(Mono.just(listResult));\n\n        ListResult<PostArchiveVo> archives = postFinder.archives(1, 10).block();\n        assertThat(archives).isNotNull();\n\n        List<PostArchiveVo> items = archives.getItems();\n        assertThat(items.size()).isEqualTo(2);\n        assertThat(items.get(0).getYear()).isEqualTo(\"2022\");\n        assertThat(items.get(0).getMonths().size()).isEqualTo(1);\n        List<PostArchiveYearMonthVo> months = items.get(0).getMonths();\n        assertThat(months.get(0).getMonth()).isEqualTo(\"12\");\n        assertThat(months.get(0).getPosts()).hasSize(2);\n\n        assertThat(items.get(1).getYear()).isEqualTo(\"2021\");\n        assertThat(items.get(1).getMonths()).hasSize(1);\n        assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo(\"01\");\n    }\n\n    List<Post> postsForArchives() {\n        Post post1 = post(1);\n        post1.getSpec().setPublish(true);\n        post1.getSpec().setPublishTime(Instant.parse(\"2021-01-01T00:00:00Z\"));\n        post1.getMetadata().setCreationTimestamp(Instant.now());\n\n        Post post2 = post(2);\n        post2.getSpec().setPublish(true);\n        post2.getSpec().setPublishTime(Instant.parse(\"2022-12-01T00:00:00Z\"));\n        post2.getMetadata().setCreationTimestamp(Instant.now());\n\n        Post post3 = post(3);\n        post3.getSpec().setPublish(true);\n        post3.getSpec().setPublishTime(Instant.parse(\"2022-12-03T00:00:00Z\"));\n        post3.getMetadata().setCreationTimestamp(Instant.now());\n        return List.of(post1, post2, post3);\n    }\n\n    List<Post> posts() {\n        // 置顶的排前面按 priority 排序\n        // 再根据发布时间排序\n        // 相同再根据名称排序\n        // 6, 2, 1, 5, 4, 3\n        Post post1 = post(1);\n        post1.getSpec().setPinned(false);\n        post1.getSpec().setPublishTime(Instant.now().plusSeconds(20));\n\n        Post post2 = post(2);\n        post2.getSpec().setPinned(true);\n        post2.getSpec().setPriority(2);\n        post2.getSpec().setPublishTime(Instant.now());\n\n        Post post3 = post(3);\n        post3.getSpec().setDeleted(true);\n        post3.getSpec().setPublishTime(Instant.now());\n\n        Post post4 = post(4);\n        post4.getSpec().setVisible(Post.VisibleEnum.PRIVATE);\n        post4.getSpec().setPublishTime(Instant.now());\n\n        Post post5 = post(5);\n        post5.getSpec().setPublish(false);\n        post5.getMetadata().getLabels().clear();\n        post5.getSpec().setPublishTime(Instant.now());\n\n        Post post6 = post(6);\n        post6.getSpec().setPinned(true);\n        post6.getSpec().setPriority(3);\n        post6.getSpec().setPublishTime(Instant.now());\n\n        return List.of(post1, post2, post3, post4, post5, post6);\n    }\n\n    Post post(int i) {\n        final Post post = new Post();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"post-\" + i);\n        metadata.setCreationTimestamp(Instant.now());\n        metadata.setAnnotations(Map.of(\"K1\", \"V1\"));\n        metadata.setLabels(new HashMap<>());\n        metadata.getLabels().put(Post.PUBLISHED_LABEL, \"true\");\n        post.setMetadata(metadata);\n\n        Post.PostSpec postSpec = new Post.PostSpec();\n        postSpec.setDeleted(false);\n        postSpec.setAllowComment(true);\n        postSpec.setPublishTime(Instant.now());\n        postSpec.setPinned(false);\n        postSpec.setPriority(0);\n        postSpec.setPublish(true);\n        postSpec.setVisible(Post.VisibleEnum.PUBLIC);\n        postSpec.setTitle(\"title-\" + i);\n        postSpec.setSlug(\"slug-\" + i);\n        post.setSpec(postSpec);\n\n        Post.PostStatus postStatus = new Post.PostStatus();\n        postStatus.setPermalink(\"/post-\" + i);\n        postStatus.setContributors(List.of(\"contributor-1\", \"contributor-2\"));\n        postStatus.setExcerpt(\"hello world!\");\n        post.setStatus(postStatus);\n        return post;\n    }\n\n    @Nested\n    class PostQueryTest {\n\n        @Test\n        void toPageRequestTest() {\n            var query = new PostFinderImpl.PostQuery();\n            var result = query.toPageRequest();\n            assertThat(result.getSort()).isEqualTo(PostFinderImpl.defaultSort());\n\n            query.setSort(List.of(\"spec.publishTime,desc\"));\n            result = query.toPageRequest();\n            assertThat(result.getSort())\n                .isEqualTo(Sort.by(Sort.Order.desc(\"spec.publishTime\"))\n                    .and(PostFinderImpl.defaultSort()));\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.ReactivePostContentHandler;\n\n/**\n * Tests for {@link PostPublicQueryServiceImpl}.\n *\n * @author guqing\n * @since 2.7.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostPublicQueryServiceImplTest {\n\n    @Mock\n    private ExtensionGetter extensionGetter;\n\n    @InjectMocks\n    private PostPublicQueryServiceImpl postPublicQueryService;\n\n    @Test\n    void extendPostContent() {\n        when(extensionGetter.getEnabledExtensions(\n            eq(ReactivePostContentHandler.class))).thenReturn(\n            Flux.just(new PostContentHandlerB(), new PostContentHandlerA(),\n                new PostContentHandlerC()));\n        Post post = TestPost.postV1();\n        post.getMetadata().setName(\"fake-post\");\n        ContentWrapper contentWrapper =\n            ContentWrapper.builder().content(\"fake-content\").raw(\"fake-raw\").rawType(\"markdown\")\n                .build();\n        postPublicQueryService.extendPostContent(post, contentWrapper)\n            .as(StepVerifier::create).consumeNextWith(contentVo -> {\n                assertThat(contentVo.getContent()).isEqualTo(\"fake-content-B-A-C\");\n            }).verifyComplete();\n    }\n\n    static class PostContentHandlerA implements ReactivePostContentHandler {\n\n        @Override\n        public Mono<PostContentContext> handle(PostContentContext postContent) {\n            postContent.setContent(postContent.getContent() + \"-A\");\n            return Mono.just(postContent);\n        }\n    }\n\n    static class PostContentHandlerB implements ReactivePostContentHandler {\n\n        @Override\n        public Mono<PostContentContext> handle(PostContentContext postContent) {\n            postContent.setContent(postContent.getContent() + \"-B\");\n            return Mono.just(postContent);\n        }\n    }\n\n    static class PostContentHandlerC implements ReactivePostContentHandler {\n\n        @Override\n        public Mono<PostContentContext> handle(PostContentContext postContent) {\n            postContent.setContent(postContent.getContent() + \"-C\");\n            return Mono.just(postContent);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/SinglePageConversionServiceImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.lang.NonNull;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.content.ContentWrapper;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.plugin.extensionpoint.ExtensionGetter;\nimport run.halo.app.theme.ReactiveSinglePageContentHandler;\n\n/**\n * Tests for {@link SinglePageConversionServiceImpl}.\n *\n * @author guqing\n * @since 2.7.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SinglePageConversionServiceImplTest {\n\n    @Mock\n    private ExtensionGetter extensionGetter;\n\n    @InjectMocks\n    private SinglePageConversionServiceImpl pageConversionService;\n\n    @Test\n    void extendPageContent() {\n        when(extensionGetter.getEnabledExtensions(\n            eq(ReactiveSinglePageContentHandler.class)))\n            .thenReturn(\n                Flux.just(new PageContentHandlerB(),\n                    new PageContentHandlerA(),\n                    new PageContentHandlerC())\n            );\n        ContentWrapper contentWrapper = ContentWrapper.builder()\n            .content(\"fake-content\")\n            .raw(\"fake-raw\")\n            .rawType(\"markdown\")\n            .build();\n        SinglePage singlePage = new SinglePage();\n        singlePage.setMetadata(new Metadata());\n        singlePage.getMetadata().setName(\"fake-page\");\n        pageConversionService.extendPageContent(singlePage, contentWrapper)\n            .as(StepVerifier::create)\n            .consumeNextWith(contentVo -> {\n                assertThat(contentVo.getContent()).isEqualTo(\"fake-content-B-A-C\");\n            })\n            .verifyComplete();\n    }\n\n    static class PageContentHandlerA implements ReactiveSinglePageContentHandler {\n\n        @Override\n        public Mono<SinglePageContentContext> handle(\n            @NonNull SinglePageContentContext pageContent) {\n            pageContent.setContent(pageContent.getContent() + \"-A\");\n            return Mono.just(pageContent);\n        }\n    }\n\n    static class PageContentHandlerB implements ReactiveSinglePageContentHandler {\n\n        @Override\n        public Mono<SinglePageContentContext> handle(\n            @NonNull SinglePageContentContext pageContent) {\n            pageContent.setContent(pageContent.getContent() + \"-B\");\n            return Mono.just(pageContent);\n        }\n    }\n\n    static class PageContentHandlerC implements ReactiveSinglePageContentHandler {\n\n        @Override\n        public Mono<SinglePageContentContext> handle(\n            @NonNull SinglePageContentContext pageContent) {\n            pageContent.setContent(pageContent.getContent() + \"-C\");\n            return Mono.just(pageContent);\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/SinglePageFinderImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Map;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.theme.finders.SinglePageConversionService;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * Tests for {@link SinglePageFinderImpl}.\n *\n * @author guqing\n * @since 2.0.1\n */\n@ExtendWith(MockitoExtension.class)\nclass SinglePageFinderImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private SinglePageConversionService singlePageConversionService;\n\n    @InjectMocks\n    private SinglePageFinderImpl singlePageFinder;\n\n    @Test\n    void getByName() {\n        // fix gh-2992\n        String fakePageName = \"fake-page\";\n        SinglePage singlePage = new SinglePage();\n        singlePage.setMetadata(new Metadata());\n        singlePage.getMetadata().setName(fakePageName);\n        singlePage.getMetadata().setLabels(Map.of(SinglePage.PUBLISHED_LABEL, \"true\"));\n        singlePage.setSpec(new SinglePage.SinglePageSpec());\n        singlePage.getSpec().setOwner(\"fake-owner\");\n        singlePage.getSpec().setReleaseSnapshot(\"fake-release\");\n        singlePage.getSpec().setPublish(true);\n        singlePage.getSpec().setDeleted(false);\n        singlePage.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n        singlePage.setStatus(new SinglePage.SinglePageStatus());\n        when(client.get(eq(SinglePage.class), eq(fakePageName)))\n            .thenReturn(Mono.just(singlePage));\n\n        when(singlePageConversionService.convertToVo(eq(singlePage)))\n            .thenReturn(Mono.just(mock(SinglePageVo.class)));\n\n        singlePageFinder.getByName(fakePageName)\n            .as(StepVerifier::create)\n            .consumeNextWith(page -> assertThat(page).isNotNull())\n            .verifyComplete();\n\n        verify(client).get(SinglePage.class, fakePageName);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/TagFinderImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport java.time.Instant;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.data.domain.Sort;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ListOptions;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.utils.JsonUtils;\nimport run.halo.app.theme.finders.vo.TagVo;\n\n/**\n * Tests for {@link TagFinderImpl}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass TagFinderImplTest {\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    private TagFinderImpl tagFinder;\n\n    @BeforeEach\n    void setUp() {\n        tagFinder = new TagFinderImpl(client);\n    }\n\n    @Test\n    void getByName() throws JSONException {\n        when(client.fetch(eq(Tag.class), eq(\"t1\")))\n            .thenReturn(Mono.just(tag(1)));\n        TagVo tagVo = tagFinder.getByName(\"t1\").block();\n        tagVo.getMetadata().setCreationTimestamp(null);\n        JSONAssert.assertEquals(\"\"\"\n                {\n                     \"metadata\": {\n                         \"name\": \"t1\",\n                         \"annotations\": {\n                             \"K1\": \"V1\"\n                         }\n                     },\n                     \"spec\": {\n                         \"displayName\": \"displayName-1\",\n                         \"slug\": \"slug-1\",\n                         \"color\": \"color-1\",\n                         \"cover\": \"cover-1\"\n                     },\n                     \"status\": {\n                         \"permalink\": \"permalink-1\",\n                         \"postCount\": 2,\n                         \"visiblePostCount\": 1\n                     },\n                     \"postCount\": 1\n                }\n                \"\"\",\n            JsonUtils.objectToJson(tagVo),\n            true);\n    }\n\n    @Test\n    void listAll() {\n        when(client.listAll(eq(Tag.class), any(ListOptions.class), any(Sort.class)))\n            .thenReturn(Flux.fromIterable(\n                    tags().stream().sorted(TagFinderImpl.DEFAULT_COMPARATOR.reversed()).toList()\n                )\n            );\n        List<TagVo> tags = tagFinder.listAll().collectList().block();\n        assertThat(tags).hasSize(3);\n        assertThat(tags.stream()\n            .map(tag -> tag.getMetadata().getName())\n            .collect(Collectors.toList()))\n            .isEqualTo(List.of(\"t3\", \"t2\", \"t1\"));\n    }\n\n    List<Tag> tags() {\n        Tag tag1 = tag(1);\n\n        Tag tag2 = tag(2);\n        tag2.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(1));\n\n        Tag tag3 = tag(3);\n        tag3.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(2));\n        // sorted: 3, 2, 1\n        return List.of(tag2, tag1, tag3);\n    }\n\n    Tag tag(int i) {\n        final Tag tag = new Tag();\n        Metadata metadata = new Metadata();\n        metadata.setName(\"t\" + i);\n        metadata.setAnnotations(Map.of(\"K1\", \"V1\"));\n        metadata.setCreationTimestamp(Instant.now());\n        tag.setMetadata(metadata);\n\n        Tag.TagSpec tagSpec = new Tag.TagSpec();\n        tagSpec.setDisplayName(\"displayName-\" + i);\n        tagSpec.setSlug(\"slug-\" + i);\n        tagSpec.setColor(\"color-\" + i);\n        tagSpec.setCover(\"cover-\" + i);\n        tag.setSpec(tagSpec);\n\n        Tag.TagStatus tagStatus = new Tag.TagStatus();\n        tagStatus.setPermalink(\"permalink-\" + i);\n        tagStatus.setPostCount(2);\n        tagStatus.setVisiblePostCount(1);\n        tag.setStatus(tagStatus);\n        return tag;\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/impl/ThumbnailFinderImplTest.java",
    "content": "package run.halo.app.theme.finders.impl;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.attachment.thumbnail.ThumbnailService;\n\n/**\n * Tests for {@link ThumbnailFinderImpl}.\n *\n * @author guqing\n * @since 2.20.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ThumbnailFinderImplTest {\n\n    @Mock\n    ThumbnailService thumbnailService;\n\n    @InjectMocks\n    ThumbnailFinderImpl thumbnailFinder;\n\n    @Test\n    void shouldNotGenWhenUriIsInvalid() {\n        thumbnailFinder.gen(\"invalid uri\", \"l\")\n            .as(StepVerifier::create)\n            .expectNext(\"invalid uri\")\n            .verifyComplete();\n\n        verify(thumbnailService, never()).get(any(), any());\n    }\n\n    @Test\n    void shouldGenWhenUriIsValid() {\n        when(thumbnailService.get(any(), any()))\n            .thenReturn(Mono.just(URI.create(\"/test-thumb.jpg\")));\n        thumbnailFinder.gen(\"/test.jpg\", \"l\")\n            .as(StepVerifier::create)\n            .expectNext(\"/test-thumb.jpg\")\n            .verifyComplete();\n\n        verify(thumbnailService).get(any(), any());\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/finders/vo/UserVoTest.java",
    "content": "package run.halo.app.theme.finders.vo;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.time.Instant;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.Test;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.infra.utils.JsonUtils;\n\n/**\n * Tests for {@link UserVo}.\n *\n * @author guqing\n * @since 2.0.1\n */\nclass UserVoTest {\n\n    @Test\n    void from() throws JSONException {\n        User user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(\"fake-user\");\n        user.setSpec(new User.UserSpec());\n        user.getSpec().setPassword(\"123456\");\n        user.getSpec().setEmail(\"example@example.com\");\n        user.getSpec().setAvatar(\"avatar\");\n        user.getSpec().setDisplayName(\"fake-user-display-name\");\n        user.getSpec().setBio(\"user bio\");\n        user.getSpec().setDisabled(false);\n        user.getSpec().setPhone(\"123456789\");\n        user.getSpec().setRegisteredAt(Instant.parse(\"2022-01-01T00:00:00.00Z\"));\n        user.getSpec().setLoginHistoryLimit(5);\n        user.getSpec().setTwoFactorAuthEnabled(false);\n\n        user.setStatus(new User.UserStatus());\n\n        UserVo userVo = UserVo.from(user);\n        JSONAssert.assertEquals(\"\"\"\n                {\n                    \"metadata\": {\n                        \"name\": \"fake-user\"\n                    },\n                    \"spec\": {\n                        \"displayName\": \"fake-user-display-name\",\n                        \"avatar\": \"avatar\",\n                        \"email\": \"example@example.com\",\n                        \"emailVerified\": false,\n                        \"phone\": \"123456789\",\n                        \"password\": \"[PROTECTED]\",\n                        \"bio\": \"user bio\",\n                        \"registeredAt\": \"2022-01-01T00:00:00Z\",\n                        \"twoFactorAuthEnabled\": false,\n                        \"disabled\": false,\n                        \"loginHistoryLimit\": 5\n                    },\n                    \"status\": {\n                    }\n                }\n                \"\"\",\n            JsonUtils.objectToJson(userVo),\n            true);\n    }\n\n    @Test\n    void fromWhenStatusIsNull() {\n        User user = new User();\n        user.setMetadata(new Metadata());\n        user.getMetadata().setName(\"fake-user\");\n        user.setSpec(new User.UserSpec());\n        UserVo userVo = UserVo.from(user);\n\n        assertThat(userVo).isNotNull();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/message/ThemeMessageResolutionUtilsTest.java",
    "content": "package run.halo.app.theme.message;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.FileNotFoundException;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.util.Locale;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.util.ResourceUtils;\nimport run.halo.app.theme.ThemeContext;\n\n/**\n * @author guqing\n * @since 2.0.0\n */\nclass ThemeMessageResolutionUtilsTest {\n    private URL defaultThemeUrl;\n\n    @BeforeEach\n    void setUp() throws FileNotFoundException {\n        defaultThemeUrl = ResourceUtils.getURL(\"classpath:themes/default\");\n    }\n\n    @Test\n    void resolveMessagesForTemplateForDefault() throws URISyntaxException {\n        Map<String, String> properties =\n            ThemeMessageResolutionUtils.resolveMessagesForTemplate(Locale.CHINESE, getTheme());\n        assertThat(properties).isEqualTo(Map.of(\"index.welcome\", \"欢迎来到首页\",\n            \"title\", \"来自 i18n/zh.properties 的标题\"));\n    }\n\n    @Test\n    void resolveMessagesForTemplateForEnglish() throws URISyntaxException {\n        Map<String, String> properties =\n            ThemeMessageResolutionUtils.resolveMessagesForTemplate(Locale.ENGLISH, getTheme());\n        assertThat(properties).isEqualTo(Map.of(\"index.welcome\", \"Welcome to the index\",\n            \"title\", \"这是来自 i18n/default.properties 的标题\"));\n    }\n\n    ThemeContext getTheme() throws URISyntaxException {\n        return ThemeContext.builder()\n            .name(\"default\")\n            .path(Path.of(defaultThemeUrl.toURI()))\n            .active(true)\n            .build();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java",
    "content": "package run.halo.app.theme.message;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.when;\n\nimport java.io.FileNotFoundException;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.TestConfiguration;\nimport org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.context.bean.override.mockito.MockitoSpyBean;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.util.ResourceUtils;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.InitializationStateGetter;\nimport run.halo.app.theme.ThemeContext;\nimport run.halo.app.theme.ThemeResolver;\n\n/**\n * Tests for {@link ThemeMessageResolver}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@SpringBootTest\n@AutoConfigureWebTestClient\npublic class ThemeMessageResolverIntegrationTest {\n\n    @MockitoSpyBean\n    private ThemeResolver themeResolver;\n\n    private URL defaultThemeUrl;\n\n    private URL otherThemeUrl;\n\n    @MockitoSpyBean\n    private InitializationStateGetter initializationStateGetter;\n\n    @Autowired\n    private WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() throws FileNotFoundException, URISyntaxException {\n        when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));\n        defaultThemeUrl = ResourceUtils.getURL(\"classpath:themes/default\");\n        otherThemeUrl = ResourceUtils.getURL(\"classpath:themes/other\");\n\n        when(themeResolver.getTheme(any(ServerWebExchange.class)))\n            .thenReturn(Mono.just(createDefaultContext()));\n    }\n\n    @Test\n    void messageResolverWhenDefaultTheme() {\n        webTestClient.get()\n            .uri(\"/?language=zh\")\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody()\n            .xpath(\"/html/body/div[1]\").isEqualTo(\"zh\")\n            .xpath(\"/html/body/div[2]\").isEqualTo(\"欢迎来到首页\");\n    }\n\n    @Test\n    void messageResolverForEnLanguageWhenDefaultTheme() {\n        webTestClient.get()\n            .uri(\"/?language=en\")\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody()\n            .xpath(\"/html/body/div[1]\").isEqualTo(\"en\")\n            .xpath(\"/html/body/div[2]\").isEqualTo(\"Welcome to the index\");\n    }\n\n    @Test\n    void shouldUseDefaultWhenLanguageNotSupport() {\n        webTestClient.get()\n            .uri(\"/index?language=foo\")\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody()\n            // make sure the \"templates/index.properties\" file is precedence over the\n            // \"i18n/default.properties\".\n            .xpath(\"/html/head/title\").isEqualTo(\"Title from index.properties\")\n            .xpath(\"/html/body/div[1]\").isEqualTo(\"foo\")\n            .xpath(\"/html/body/div[2]\").isEqualTo(\"欢迎来到首页\");\n    }\n\n    @Test\n    void switchTheme() throws URISyntaxException {\n        webTestClient.get()\n            .uri(\"/index?language=zh\")\n            .exchange()\n            .expectStatus()\n            .isOk()\n            .expectBody()\n            .xpath(\"/html/head/title\").isEqualTo(\"来自 index_zh.properties 的标题\")\n            .xpath(\"/html/body/div[1]\").isEqualTo(\"zh\")\n            .xpath(\"/html/body/div[2]\").isEqualTo(\"欢迎来到首页\")\n        ;\n\n        // For other theme\n        when(themeResolver.getTheme(any(ServerWebExchange.class)))\n            .thenReturn(Mono.just(createOtherContext()));\n        webTestClient.get()\n            .uri(\"/index?language=zh\")\n            .exchange()\n            .expectBody()\n            .xpath(\"/html/head/title\").isEqualTo(\"Other theme title\")\n            .xpath(\"/html/body/p\").isEqualTo(\"Other 首页\");\n\n        webTestClient.get()\n            .uri(\"/index?language=en\")\n            .exchange()\n            .expectBody()\n            .xpath(\"/html/head/title\").isEqualTo(\"Other theme title\")\n            .xpath(\"/html/body/p\").isEqualTo(\"other index\");\n    }\n\n    ThemeContext createDefaultContext() throws URISyntaxException {\n        return ThemeContext.builder()\n            .name(\"default\")\n            .path(Path.of(defaultThemeUrl.toURI()))\n            .active(true)\n            .build();\n    }\n\n    ThemeContext createOtherContext() throws URISyntaxException {\n        return ThemeContext.builder()\n            .name(\"other\")\n            .path(Path.of(otherThemeUrl.toURI()))\n            .active(false)\n            .build();\n    }\n\n    @TestConfiguration\n    static class MessageResolverConfig {\n        @Bean\n        RouterFunction<ServerResponse> routeTestIndex() {\n            return RouterFunctions\n                .route(RequestPredicates.GET(\"/\").or(RequestPredicates.GET(\"/index\"))\n                        .and(RequestPredicates.accept(MediaType.TEXT_HTML)),\n                    request -> ServerResponse.ok().render(\"index\"));\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/EmptyView.java",
    "content": "package run.halo.app.theme.router;\n\nimport java.util.Map;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;\nimport reactor.core.publisher.Mono;\n\n/**\n * Empty view for test.\n *\n * @author guqing\n * @since 2.0.0\n */\npublic class EmptyView extends ThymeleafReactiveView {\n    public EmptyView() {\n    }\n\n    @Override\n    public Mono<Void> render(Map<String, ?> model, MediaType contentType,\n        ServerWebExchange exchange) {\n        return Mono.empty();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/PageUrlUtilsTest.java",
    "content": "package run.halo.app.theme.router;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\n\n/**\n * Tests for {@link PageUrlUtils}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass PageUrlUtilsTest {\n    static String s = \"/tags\";\n    static String s1 = \"/tags/page/1\";\n    static String s2 = \"/tags/page/2\";\n    static String s3 = \"/tags/y/m/page/2\";\n    static String s4 = \"/tags/y/m\";\n    static String s5 = \"/tags/y/m/page/3\";\n\n    @Test\n    void nextPageUrl() {\n        long totalPage = 10;\n        assertThat(PageUrlUtils.nextPageUrl(s, totalPage))\n            .isEqualTo(\"/tags/page/2\");\n        assertThat(PageUrlUtils.nextPageUrl(s2, totalPage))\n            .isEqualTo(\"/tags/page/3\");\n        assertThat(PageUrlUtils.nextPageUrl(s3, totalPage))\n            .isEqualTo(\"/tags/y/m/page/3\");\n        assertThat(PageUrlUtils.nextPageUrl(s4, totalPage))\n            .isEqualTo(\"/tags/y/m/page/2\");\n        assertThat(PageUrlUtils.nextPageUrl(s5, totalPage))\n            .isEqualTo(\"/tags/y/m/page/4\");\n\n        // The number of pages does not exceed the total number of pages\n        totalPage = 1;\n        assertThat(PageUrlUtils.nextPageUrl(\"/tags/page/1\", totalPage))\n            .isEqualTo(\"/tags/page/1\");\n\n        totalPage = 0;\n        assertThat(PageUrlUtils.nextPageUrl(\"/tags\", totalPage))\n            .isEqualTo(\"/tags/page/1\");\n    }\n\n    @Test\n    void prevPageUrl() {\n        assertThat(PageUrlUtils.prevPageUrl(s))\n            .isEqualTo(\"/tags\");\n        assertThat(PageUrlUtils.prevPageUrl(s1))\n            .isEqualTo(\"/tags\");\n        assertThat(PageUrlUtils.prevPageUrl(s2))\n            .isEqualTo(\"/tags\");\n        assertThat(PageUrlUtils.prevPageUrl(s3))\n            .isEqualTo(\"/tags/y/m\");\n        assertThat(PageUrlUtils.prevPageUrl(s4))\n            .isEqualTo(\"/tags/y/m\");\n        assertThat(PageUrlUtils.prevPageUrl(s5))\n            .isEqualTo(\"/tags/y/m/page/2\");\n\n        assertThat(PageUrlUtils.prevPageUrl(\"/page/2\"))\n            .isEqualTo(\"/\");\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/PreviewRouterFunctionTest.java",
    "content": "package run.halo.app.theme.router;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.MediaType;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.HandlerStrategies;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.result.view.ViewResolver;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.PostService;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.infra.AnonymousUserConst;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.finders.PostPublicQueryService;\nimport run.halo.app.theme.finders.SinglePageConversionService;\nimport run.halo.app.theme.finders.vo.ContributorVo;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\n\n/**\n * Tests for {@link PreviewRouterFunction}.\n *\n * @author guqing\n * @since 2.6.x\n */\n@ExtendWith({SpringExtension.class, MockitoExtension.class})\nclass PreviewRouterFunctionTest {\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    PostPublicQueryService postPublicQueryService;\n\n    @Mock\n    ViewNameResolver viewNameResolver;\n\n    @Mock\n    ViewResolver viewResolver;\n\n    @Mock\n    PostService postService;\n\n    @Mock\n    SinglePageConversionService singlePageConversionService;\n\n    @InjectMocks\n    PreviewRouterFunction previewRouterFunction;\n\n    WebTestClient webTestClient;\n\n    @BeforeEach\n    void setUp() {\n        webTestClient = WebTestClient.bindToRouterFunction(previewRouterFunction.previewRouter())\n            .handlerStrategies(HandlerStrategies.builder()\n                .viewResolver(viewResolver)\n                .build())\n            .build();\n    }\n\n    @Test\n    @WithMockUser(username = \"testuser\")\n    void previewPost() {\n        when(viewResolver.resolveViewName(any(), any()))\n            .thenReturn(Mono.just(new EmptyView() {\n                @Override\n                public Mono<Void> render(Map<String, ?> model, MediaType contentType,\n                    ServerWebExchange exchange) {\n                    return super.render(model, contentType, exchange);\n                }\n            }));\n\n        Post post = new Post();\n        post.setMetadata(new Metadata());\n        post.getMetadata().setName(\"post1\");\n        post.setSpec(new Post.PostSpec());\n        post.getSpec().setOwner(\"testuser\");\n        post.getSpec().setHeadSnapshot(\"snapshot1\");\n        post.getSpec().setBaseSnapshot(\"snapshot2\");\n        post.getSpec().setTemplate(\"postTemplate\");\n        when(client.fetch(eq(Post.class), eq(\"post1\"))).thenReturn(Mono.just(post));\n\n        PostVo postVo = PostVo.from(post);\n        postVo.setContributors(contributorVos());\n        when(postPublicQueryService.convertToVo(eq(post), eq(post.getSpec().getHeadSnapshot())))\n            .thenReturn(Mono.just(postVo));\n\n        when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class),\n            eq(\"postTemplate\"), eq(\"post\"))).thenReturn(Mono.just(\"postView\"));\n\n        webTestClient.get().uri(\"/preview/posts/post1\")\n            .exchange()\n            .expectStatus().isOk();\n\n        verify(viewResolver).resolveViewName(any(), any());\n        verify(postPublicQueryService).convertToVo(eq(post), eq(post.getSpec().getHeadSnapshot()));\n        verify(client).fetch(eq(Post.class), eq(\"post1\"));\n    }\n\n    @Test\n    public void previewPostWhenUnAuthenticated() {\n        webTestClient.get().uri(\"/preview/posts/post1\")\n            .exchange()\n            .expectStatus().isEqualTo(404);\n    }\n\n    @Test\n    @WithMockUser(username = \"testuser\")\n    public void previewSinglePage() {\n        when(viewResolver.resolveViewName(any(), any()))\n            .thenReturn(Mono.just(new EmptyView() {\n                @Override\n                public Mono<Void> render(Map<String, ?> model, MediaType contentType,\n                    ServerWebExchange exchange) {\n                    return super.render(model, contentType, exchange);\n                }\n            }));\n\n        SinglePage singlePage = new SinglePage();\n        singlePage.setMetadata(new Metadata());\n        singlePage.getMetadata().setName(\"page1\");\n        singlePage.setSpec(new SinglePage.SinglePageSpec());\n        singlePage.getSpec().setOwner(\"testuser\");\n        singlePage.getSpec().setHeadSnapshot(\"snapshot1\");\n        singlePage.getSpec().setTemplate(\"pageTemplate\");\n        when(client.fetch(SinglePage.class, \"page1\")).thenReturn(Mono.just(singlePage));\n\n        SinglePageVo singlePageVo = SinglePageVo.from(singlePage);\n        singlePageVo.setContributors(contributorVos());\n        when(singlePageConversionService.convertToVo(singlePage, \"snapshot1\"))\n            .thenReturn(Mono.just(singlePageVo));\n\n        when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class),\n            eq(\"pageTemplate\"), eq(\"page\"))).thenReturn(Mono.just(\"pageView\"));\n\n        webTestClient.get().uri(\"/preview/singlepages/page1\")\n            .exchange()\n            .expectStatus().isOk();\n\n        verify(viewResolver).resolveViewName(any(), any());\n        verify(client).fetch(eq(SinglePage.class), eq(\"page1\"));\n    }\n\n    @Test\n    public void previewSinglePageWhenUnAuthenticated() {\n        webTestClient.get().uri(\"/preview/singlepages/page1\")\n            .exchange()\n            .expectStatus().isEqualTo(404);\n    }\n\n    @Test\n    @WithMockUser(username = AnonymousUserConst.PRINCIPAL)\n    public void previewWithAnonymousUser() {\n        webTestClient.get().uri(\"/preview/singlepages/page1\")\n            .exchange()\n            .expectStatus().isEqualTo(404);\n    }\n\n    List<ContributorVo> contributorVos() {\n        ContributorVo contributorA = ContributorVo.builder()\n            .name(\"fake-user\")\n            .build();\n        ContributorVo contributorB = ContributorVo.builder()\n            .name(\"testuser\")\n            .build();\n        return List.of(contributorA, contributorB);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolverTest.java",
    "content": "package run.halo.app.theme.router;\n\nimport static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;\n\nimport java.util.Map;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.springframework.security.test.context.support.WithMockUser;\nimport org.springframework.test.context.junit.jupiter.SpringExtension;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.Metadata;\n\n/**\n * Tests for {@link ReactiveQueryPostPredicateResolver}.\n *\n * @author guqing\n * @since 2.9.0\n */\n@ExtendWith(SpringExtension.class)\nclass ReactiveQueryPostPredicateResolverTest {\n\n    private ReactiveQueryPostPredicateResolver postPredicateResolver;\n\n    @BeforeEach\n    void setUp() {\n        postPredicateResolver = new DefaultQueryPostPredicateResolver();\n    }\n\n    @Test\n    void getPredicateWithoutAuth() {\n        postPredicateResolver.getPredicate()\n            .as(StepVerifier::create)\n            .consumeNextWith(predicate -> {\n                Post post = new Post();\n                post.setMetadata(new Metadata());\n                post.getMetadata().setName(\"fake-post\");\n\n                post.setSpec(new Post.PostSpec());\n                post.getSpec().setDeleted(false);\n                post.getMetadata().setLabels(Map.of(Post.PUBLISHED_LABEL, \"true\"));\n                post.getSpec().setVisible(Post.VisibleEnum.PRIVATE);\n                assertThat(predicate.test(post)).isFalse();\n\n                post.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n                assertThat(predicate.test(post)).isTrue();\n\n                post.getMetadata().setLabels(Map.of(Post.PUBLISHED_LABEL, \"false\"));\n                assertThat(predicate.test(post)).isFalse();\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    @WithMockUser(username = \"halo\")\n    void getPredicateWithAuth() {\n        postPredicateResolver.getPredicate()\n            .as(StepVerifier::create)\n            .consumeNextWith(predicate -> {\n                Post post = new Post();\n                post.setMetadata(new Metadata());\n                post.getMetadata().setName(\"fake-post\");\n\n                post.setSpec(new Post.PostSpec());\n                post.getSpec().setDeleted(false);\n                post.getSpec().setOwner(\"halo\");\n                post.getMetadata().setLabels(Map.of(Post.PUBLISHED_LABEL, \"true\"));\n                post.getSpec().setVisible(Post.VisibleEnum.PRIVATE);\n                assertThat(predicate.test(post)).isTrue();\n\n                post.getSpec().setOwner(\"guqing\");\n                assertThat(predicate.test(post)).isFalse();\n\n                post.getSpec().setOwner(\"halo\");\n                post.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n                assertThat(predicate.test(post)).isTrue();\n\n                post.getSpec().setDeleted(true);\n                assertThat(predicate.test(post)).isFalse();\n\n                post.getSpec().setVisible(Post.VisibleEnum.INTERNAL);\n                assertThat(predicate.test(post)).isFalse();\n            })\n            .verifyComplete();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/SinglePageRouteTest.java",
    "content": "package run.halo.app.theme.router;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.net.URI;\nimport java.time.Instant;\nimport java.util.HashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.i18n.SimpleLocaleContext;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.reactive.function.server.MockServerRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.HandlerFunction;\nimport org.springframework.web.reactive.function.server.HandlerStrategies;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.reactive.result.view.ViewResolver;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport org.springframework.web.util.UriUtils;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.content.SinglePage;\nimport run.halo.app.extension.ExtensionClient;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.controller.Reconciler;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.finders.SinglePageFinder;\nimport run.halo.app.theme.finders.vo.SinglePageVo;\nimport run.halo.app.theme.router.SinglePageRoute.NameSlugPair;\n\n/**\n * Tests for {@link SinglePageRoute}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass SinglePageRouteTest {\n\n    @Mock\n    ViewNameResolver viewNameResolver;\n\n    @Mock\n    SinglePageFinder singlePageFinder;\n\n    @Mock\n    ViewResolver viewResolver;\n\n    @Mock\n    ExtensionClient client;\n\n    @Mock\n    LocaleContextResolver localeContextResolver;\n\n    @Mock\n    TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    @InjectMocks\n    SinglePageRoute singlePageRoute;\n\n    @Test\n    void handlerFunction() {\n        // fix gh-3448\n        when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class), any(), any()))\n            .thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));\n\n        String pageName = \"fake-page\";\n        when(viewResolver.resolveViewName(any(), any()))\n            .thenReturn(Mono.just(new EmptyView() {\n                @Override\n                public Mono<Void> render(Map<String, ?> model, MediaType contentType,\n                    ServerWebExchange exchange) {\n                    assertThat(model).containsKey(ModelConst.TEMPLATE_ID);\n                    assertThat(model.get(ModelConst.TEMPLATE_ID))\n                        .isEqualTo(DefaultTemplateEnum.SINGLE_PAGE.getValue());\n                    assertThat(model.get(\"name\"))\n                        .isEqualTo(pageName);\n                    assertThat(model.get(\"plural\")).isEqualTo(\"singlepages\");\n                    assertThat(model.get(\"singlePage\")).isNotNull();\n                    assertThat(model.get(\"groupVersionKind\"))\n                        .isEqualTo(GroupVersionKind.fromExtension(SinglePage.class));\n                    return super.render(model, contentType, exchange);\n                }\n            }));\n\n        SinglePage singlePage = new SinglePage();\n        singlePage.setMetadata(new Metadata());\n        singlePage.getMetadata().setName(pageName);\n        singlePage.setSpec(new SinglePage.SinglePageSpec());\n        when(singlePageFinder.getByName(eq(pageName)))\n            .thenReturn(Mono.just(SinglePageVo.from(singlePage)));\n\n        HandlerFunction<ServerResponse> handlerFunction =\n            singlePageRoute.handlerFunction(pageName);\n        RouterFunction<ServerResponse> routerFunction =\n            RouterFunctions.route().GET(\"/archives/{name}\", handlerFunction).build();\n\n        WebTestClient webTestClient = WebTestClient.bindToRouterFunction(routerFunction)\n            .handlerStrategies(HandlerStrategies.builder()\n                .viewResolver(viewResolver)\n                .build())\n            .build();\n\n        when(localeContextResolver.resolveLocaleContext(any()))\n            .thenReturn(new SimpleLocaleContext(Locale.getDefault()));\n        webTestClient.get()\n            .uri(\"/archives/fake-name\")\n            .exchange()\n            .expectStatus().isOk();\n    }\n\n    @Test\n    void shouldNotThrowErrorIfSlugNameContainsSpecialChars() {\n        var specialChars = \"/with-special-chars-{}-[]-{{}}-{[]}-[{}]\";\n        var specialCharsUri =\n            URI.create(UriUtils.encodePath(specialChars, UTF_8));\n        var mockHttpRequest = MockServerHttpRequest.get(specialCharsUri.toString())\n            .accept(MediaType.TEXT_HTML)\n            .build();\n        var mockExchange = MockServerWebExchange.from(mockHttpRequest);\n        var request = MockServerRequest.builder()\n            .exchange(mockExchange)\n            .uri(specialCharsUri)\n            .method(HttpMethod.GET)\n            .header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE)\n            .build();\n        var nameSlugPair = new NameSlugPair(\"fake-single-page\", specialChars);\n        singlePageRoute.setQuickRouteMap(Map.of(nameSlugPair, r -> ServerResponse.ok().build()));\n        StepVerifier.create(singlePageRoute.route(request))\n            .expectNextCount(1)\n            .verifyComplete();\n    }\n\n    @Nested\n    class SinglePageReconcilerTest {\n\n        @Test\n        void shouldRemoveRouteIfSinglePageUnpublished() {\n            var name = \"fake-single-page\";\n            var page = newSinglePage(name, false);\n            when(client.fetch(SinglePage.class, name)).thenReturn(\n                Optional.of(page));\n\n            var routeMap = Mockito.<Map<NameSlugPair, HandlerFunction<ServerResponse>>>mock(\n                invocation -> new HashMap<NameSlugPair, HandlerFunction<ServerResponse>>());\n            singlePageRoute.setQuickRouteMap(routeMap);\n            var result = singlePageRoute.reconcile(new Reconciler.Request(name));\n            assertNotNull(result);\n            assertFalse(result.reEnqueue());\n            verify(client).fetch(SinglePage.class, name);\n            verify(routeMap).remove(NameSlugPair.from(page));\n        }\n\n        @Test\n        void shouldAddRouteIfSinglePagePublished() {\n            var name = \"fake-single-page\";\n            var page = newSinglePage(name, true);\n            when(client.fetch(SinglePage.class, name)).thenReturn(\n                Optional.of(page));\n\n            var routeMap = Mockito.<Map<NameSlugPair, HandlerFunction<ServerResponse>>>mock(\n                invocation -> new HashMap<NameSlugPair, HandlerFunction<ServerResponse>>());\n            singlePageRoute.setQuickRouteMap(routeMap);\n            var result = singlePageRoute.reconcile(new Reconciler.Request(name));\n            assertNotNull(result);\n            assertFalse(result.reEnqueue());\n            verify(client).fetch(SinglePage.class, name);\n            verify(routeMap).put(eq(NameSlugPair.from(page)), any());\n        }\n\n        @Test\n        void shouldRemoveRouteIfSinglePageDeleted() {\n            var name = \"fake-single-page\";\n            var page = newDeletedSinglePage(name);\n            when(client.fetch(SinglePage.class, name)).thenReturn(\n                Optional.of(page));\n\n            var routeMap = Mockito.<Map<NameSlugPair, HandlerFunction<ServerResponse>>>mock(\n                invocation -> new HashMap<NameSlugPair, HandlerFunction<ServerResponse>>());\n            singlePageRoute.setQuickRouteMap(routeMap);\n            var result = singlePageRoute.reconcile(new Reconciler.Request(name));\n            assertNotNull(result);\n            assertFalse(result.reEnqueue());\n            verify(client).fetch(SinglePage.class, name);\n            verify(routeMap).remove(NameSlugPair.from(page));\n        }\n\n        @Test\n        void shouldRemoveRouteIfSinglePageRecycled() {\n            var name = \"fake-single-page\";\n            var page = newRecycledSinglePage(name);\n            when(client.fetch(SinglePage.class, name)).thenReturn(\n                Optional.of(page));\n\n            var routeMap = Mockito.<Map<NameSlugPair, HandlerFunction<ServerResponse>>>mock(\n                invocation -> new HashMap<NameSlugPair, HandlerFunction<ServerResponse>>());\n            singlePageRoute.setQuickRouteMap(routeMap);\n            var result = singlePageRoute.reconcile(new Reconciler.Request(name));\n            assertNotNull(result);\n            assertFalse(result.reEnqueue());\n            verify(client).fetch(SinglePage.class, name);\n            verify(routeMap).remove(NameSlugPair.from(page));\n        }\n\n\n        SinglePage newSinglePage(String name, boolean published) {\n            var metadata = new Metadata();\n            metadata.setName(name);\n            var page = new SinglePage();\n            page.setMetadata(metadata);\n            var spec = new SinglePage.SinglePageSpec();\n            spec.setSlug(\"/fake-slug\");\n            page.setSpec(spec);\n            var status = new SinglePage.SinglePageStatus();\n            page.setStatus(status);\n            SinglePage.changePublishedState(page, published);\n            return page;\n        }\n\n        SinglePage newDeletedSinglePage(String name) {\n            var page = newSinglePage(name, true);\n            page.getMetadata().setDeletionTimestamp(Instant.now());\n            return page;\n        }\n\n        SinglePage newRecycledSinglePage(String name) {\n            var page = newSinglePage(name, true);\n            page.getSpec().setDeleted(true);\n            return page;\n        }\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/ArchiveRouteFactoryTest.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.theme.finders.PostFinder;\n\n/**\n * Tests for {@link ArchiveRouteFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass ArchiveRouteFactoryTest extends RouteFactoryTestSuite {\n    @Mock\n    private PostFinder postFinder;\n\n    @InjectMocks\n    private ArchiveRouteFactory archiveRouteFactory;\n\n    @Test\n    void create() {\n        String prefix = \"/new-archives\";\n        RouterFunction<ServerResponse> routerFunction = archiveRouteFactory.create(prefix);\n        WebTestClient client = getWebTestClient(routerFunction);\n\n        client.get()\n            .uri(prefix)\n            .exchange()\n            .expectStatus().isOk();\n\n        client.get()\n            .uri(prefix + \"/page/1\")\n            .exchange()\n            .expectStatus().isOk();\n\n        client.get()\n            .uri(prefix + \"/2022/09\")\n            .exchange()\n            .expectStatus().isOk();\n\n        client.get()\n            .uri(prefix + \"/2022/08/page/1\")\n            .exchange()\n            .expectStatus().isOk();\n\n        client.get()\n            .uri(prefix + \"/2022/8/page/1\")\n            .exchange()\n            .expectStatus()\n            .isEqualTo(HttpStatus.NOT_FOUND);\n\n        client.get()\n            .uri(\"/nothing\")\n            .exchange()\n            .expectStatus()\n            .isEqualTo(HttpStatus.NOT_FOUND);\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactoryTest.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.mockito.ArgumentMatchers.anyString;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.doReturn;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.User;\nimport run.halo.app.extension.ReactiveExtensionClient;\n\n/**\n * Tests for {@link AuthorPostsRouteFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass AuthorPostsRouteFactoryTest extends RouteFactoryTestSuite {\n    @Mock\n    ReactiveExtensionClient client;\n    @InjectMocks\n    AuthorPostsRouteFactory authorPostsRouteFactory;\n\n    @Test\n    void create() {\n        var spyAuthorRoute = spy(authorPostsRouteFactory);\n        doReturn(Mono.just(true)).when(spyAuthorRoute).hasPostManageRole(anyString());\n\n        RouterFunction<ServerResponse> routerFunction = spyAuthorRoute.create(null);\n        WebTestClient webClient = getWebTestClient(routerFunction);\n\n        when(client.fetch(eq(User.class), eq(\"fake-user\")))\n            .thenReturn(Mono.just(new User()));\n        webClient.get()\n            .uri(\"/authors/fake-user\")\n            .exchange()\n            .expectStatus().isOk();\n\n        webClient.get()\n            .uri(\"/authors/fake-user/page/p\")\n            .exchange()\n            .expectStatus().isNotFound();\n\n        webClient.get()\n            .uri(\"/authors/fake-user/page/abc\")\n            .exchange()\n            .expectStatus().isNotFound();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/CategoriesRouteFactoryTest.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Flux;\nimport run.halo.app.theme.finders.CategoryFinder;\n\n/**\n * Tests for {@link CategoriesRouteFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\nclass CategoriesRouteFactoryTest extends RouteFactoryTestSuite {\n\n    @Mock\n    private CategoryFinder categoryFinder;\n\n    @InjectMocks\n    private CategoriesRouteFactory categoriesRouteFactory;\n\n    @Test\n    void create() {\n        String prefix = \"/topics\";\n        RouterFunction<ServerResponse> routerFunction = categoriesRouteFactory.create(prefix);\n        WebTestClient webClient = getWebTestClient(routerFunction);\n\n        when(categoryFinder.listAsTree())\n            .thenReturn(Flux.empty());\n        webClient.get()\n            .uri(prefix)\n            .exchange()\n            .expectStatus().isOk();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/IndexRouteFactoryTest.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport run.halo.app.theme.finders.PostFinder;\n\n/**\n * Tests for {@link IndexRouteFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass IndexRouteFactoryTest extends RouteFactoryTestSuite {\n    @Mock\n    private PostFinder postFinder;\n\n    @InjectMocks\n    private IndexRouteFactory indexRouteFactory;\n\n    @Test\n    void create() {\n        RouterFunction<ServerResponse> routerFunction = indexRouteFactory.create(\"/\");\n        WebTestClient webTestClient = getWebTestClient(routerFunction);\n\n        webTestClient.get()\n            .uri(\"/\")\n            .exchange()\n            .expectStatus().isOk();\n\n        webTestClient.get()\n            .uri(\"/page/1\")\n            .exchange()\n            .expectStatus().isOk();\n\n        webTestClient.get()\n            .uri(\"/page/abc\")\n            .exchange()\n            .expectStatus().isNotFound();\n\n        webTestClient.get()\n            .uri(\"/page/2f\")\n            .exchange()\n            .expectStatus().isNotFound();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/PostRouteFactoryTest.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Locale;\nimport java.util.Map;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.context.i18n.SimpleLocaleContext;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.i18n.LocaleContextResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.content.TestPost;\nimport run.halo.app.core.extension.content.Post;\nimport run.halo.app.extension.GroupVersionKind;\nimport run.halo.app.extension.MetadataUtil;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.theme.DefaultTemplateEnum;\nimport run.halo.app.theme.ViewNameResolver;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.vo.PostVo;\nimport run.halo.app.theme.router.DefaultQueryPostPredicateResolver;\nimport run.halo.app.theme.router.EmptyView;\nimport run.halo.app.theme.router.ModelConst;\nimport run.halo.app.theme.router.ReactiveQueryPostPredicateResolver;\nimport run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;\n\n/**\n * Tests for {@link PostRouteFactory}.\n *\n * @author guqing\n * @since 2.3.0\n */\n@ExtendWith(MockitoExtension.class)\nclass PostRouteFactoryTest extends RouteFactoryTestSuite {\n\n    @Mock\n    private PostFinder postFinder;\n\n    @Mock\n    private ViewNameResolver viewNameResolver;\n\n    @Mock\n    private ReactiveExtensionClient client;\n\n    @Mock\n    private ReactiveQueryPostPredicateResolver predicateResolver;\n\n    @Mock\n    private LocaleContextResolver localeContextResolver;\n\n    @Mock\n    private TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;\n\n    @InjectMocks\n    private PostRouteFactory postRouteFactory;\n\n    @Test\n    void shouldBeSameResultWhenParsePattenMultiply() {\n        var parser = new PostRouteFactory.PatternParser(\"/?p={slug}\");\n        Assertions.assertTrue(parser.isQueryParamPattern());\n        parser = new PostRouteFactory.PatternParser(\"/?p={slug}\");\n        Assertions.assertTrue(parser.isQueryParamPattern());\n    }\n\n    @Test\n    void create() {\n        Post post = TestPost.postV1();\n        Map<String, String> labels = MetadataUtil.nullSafeLabels(post);\n        labels.put(Post.PUBLISHED_LABEL, \"true\");\n        post.getMetadata().setName(\"fake-name\");\n        post.getSpec().setDeleted(false);\n        post.getSpec().setVisible(Post.VisibleEnum.PUBLIC);\n        when(postFinder.getByName(eq(\"fake-name\"))).thenReturn(Mono.just(PostVo.from(post)));\n\n        when(client.fetch(eq(Post.class), eq(\"fake-name\"))).thenReturn(Mono.just(post));\n\n        when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class), any(), any()))\n            .thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));\n        when(predicateResolver.getPredicate())\n            .thenReturn(new DefaultQueryPostPredicateResolver().getPredicate());\n\n        RouterFunction<ServerResponse> routerFunction = postRouteFactory.create(\"/archives/{name}\");\n        WebTestClient webTestClient = getWebTestClient(routerFunction);\n\n        when(localeContextResolver.resolveLocaleContext(any()))\n            .thenReturn(new SimpleLocaleContext(Locale.getDefault()));\n        when(viewResolver.resolveViewName(any(), any()))\n            .thenReturn(Mono.just(new EmptyView() {\n                @Override\n                public Mono<Void> render(Map<String, ?> model, MediaType contentType,\n                    ServerWebExchange exchange) {\n                    assertThat(model).containsKey(ModelConst.TEMPLATE_ID);\n                    assertThat(model.get(ModelConst.TEMPLATE_ID))\n                        .isEqualTo(DefaultTemplateEnum.POST.getValue());\n                    assertThat(model.get(\"name\"))\n                        .isEqualTo(post.getMetadata().getName());\n                    assertThat(model.get(\"plural\")).isEqualTo(\"posts\");\n                    assertThat(model.get(\"post\")).isNotNull();\n                    assertThat(model.get(\"groupVersionKind\"))\n                        .isEqualTo(GroupVersionKind.fromExtension(Post.class));\n                    return super.render(model, contentType, exchange);\n                }\n            }));\n        webTestClient.get()\n            .uri(\"/archives/fake-name\")\n            .exchange()\n            .expectStatus().isOk();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/RouteFactoryTest.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.Mockito.when;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.theme.router.ModelConst;\n\n/**\n * Tests for {@link RouteFactory}.\n *\n * @author guqing\n * @since 2.3.0\n */\n@ExtendWith(MockitoExtension.class)\nclass RouteFactoryTest extends RouteFactoryTestSuite {\n\n    @Test\n    void configuredPageSize() {\n        SystemSetting.Post post = new SystemSetting.Post();\n        post.setPostPageSize(1);\n        post.setArchivePageSize(2);\n        post.setCategoryPageSize(3);\n        post.setTagPageSize(null);\n        post.setAuthorPageSize(4);\n        when(environmentFetcher.fetchPost()).thenReturn(Mono.just(post));\n\n        TestRouteFactory routeFactory = new TestRouteFactory();\n        assertThat(\n            routeFactory.configuredPageSize(environmentFetcher, SystemSetting.Post::getTagPageSize)\n                .block()).isEqualTo(ModelConst.DEFAULT_PAGE_SIZE);\n\n        assertThat(\n            routeFactory.configuredPageSize(environmentFetcher, SystemSetting.Post::getPostPageSize)\n                .block()).isEqualTo(post.getPostPageSize());\n\n        assertThat(\n            routeFactory.configuredPageSize(environmentFetcher,\n                SystemSetting.Post::getCategoryPageSize).block())\n            .isEqualTo(post.getCategoryPageSize());\n\n        assertThat(\n            routeFactory.configuredPageSize(environmentFetcher,\n                SystemSetting.Post::getArchivePageSize).block())\n            .isEqualTo(post.getArchivePageSize());\n        \n        assertThat(\n            routeFactory.configuredPageSize(environmentFetcher,\n                SystemSetting.Post::getAuthorPageSize).block())\n            .isEqualTo(post.getAuthorPageSize());\n    }\n\n    static class TestRouteFactory implements RouteFactory {\n\n        @Override\n        public RouterFunction<ServerResponse> create(String pattern) {\n            return null;\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/RouteFactoryTestSuite.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.lenient;\n\nimport java.net.URISyntaxException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport org.springframework.web.reactive.function.server.HandlerStrategies;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.reactive.result.view.ViewResolver;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.theme.router.EmptyView;\n\n/**\n * Abstract test for {@link RouteFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nabstract class RouteFactoryTestSuite {\n    @Mock\n    protected SystemConfigFetcher environmentFetcher;\n    @Mock\n    protected ViewResolver viewResolver;\n\n    @BeforeEach\n    final void setUpParent() throws URISyntaxException {\n        lenient().when(environmentFetcher.fetchPost())\n            .thenReturn(Mono.just(new SystemSetting.Post()));\n        lenient().when(environmentFetcher.fetch(eq(SystemSetting.ThemeRouteRules.GROUP),\n            eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules()));\n        lenient().when(viewResolver.resolveViewName(any(), any()))\n            .thenReturn(Mono.just(new EmptyView()));\n        setUp();\n    }\n\n    public void setUp() {\n\n    }\n\n    public SystemSetting.ThemeRouteRules getThemeRouteRules() {\n        SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules();\n        themeRouteRules.setArchives(\"archives\");\n        themeRouteRules.setPost(\"/archives/{slug}\");\n        themeRouteRules.setTags(\"tags\");\n        themeRouteRules.setCategories(\"categories\");\n        return themeRouteRules;\n    }\n\n    public WebTestClient getWebTestClient(RouterFunction<ServerResponse> routeFunction) {\n        return WebTestClient.bindToRouterFunction(routeFunction)\n            .handlerStrategies(HandlerStrategies.builder()\n                .viewResolver(viewResolver)\n                .build())\n            .build();\n    }\n}\n"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java",
    "content": "package run.halo.app.theme.router.factories;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.when;\n\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.web.reactive.server.WebTestClient;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.core.extension.content.Tag;\nimport run.halo.app.extension.ListResult;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.PageRequest;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.theme.finders.PostFinder;\nimport run.halo.app.theme.finders.TagFinder;\nimport run.halo.app.theme.finders.vo.TagVo;\n\n/**\n * Tests for @link TagPostRouteFactory}.\n *\n * @author guqing\n * @since 2.0.0\n */\n@ExtendWith(MockitoExtension.class)\nclass TagPostRouteFactoryTest extends RouteFactoryTestSuite {\n    @Mock\n    private ReactiveExtensionClient client;\n    @Mock\n    private TagFinder tagFinder;\n    @Mock\n    private PostFinder postFinder;\n\n    @InjectMocks\n    TagPostRouteFactory tagPostRouteFactory;\n\n    @Test\n    void create() {\n        when(client.listBy(eq(Tag.class), any(), any(PageRequest.class)))\n            .thenReturn(Mono.just(ListResult.emptyResult()));\n        WebTestClient webTestClient = getWebTestClient(tagPostRouteFactory.create(\"/new-tags\"));\n\n        webTestClient.get()\n            .uri(\"/new-tags/tag-slug-1\")\n            .exchange()\n            .expectStatus().isNotFound();\n\n        Tag tag = new Tag();\n        tag.setMetadata(new Metadata());\n        tag.getMetadata().setName(\"fake-tag-name\");\n        tag.setSpec(new Tag.TagSpec());\n        tag.getSpec().setSlug(\"tag-slug-2\");\n        when(client.listBy(eq(Tag.class), any(), any(PageRequest.class)))\n            .thenReturn(Mono.just(new ListResult<>(List.of(tag))));\n        when(tagFinder.getByName(eq(tag.getMetadata().getName())))\n            .thenReturn(Mono.just(TagVo.from(tag)));\n        webTestClient.get()\n            .uri(\"/new-tags/tag-slug-2\")\n            .exchange()\n            .expectStatus().isOk();\n\n        webTestClient.get()\n            .uri(\"/new-tags/tag-slug-2/page/1\")\n            .exchange()\n            .expectStatus().isOk();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/service/ThemeServiceImplTest.java",
    "content": "package run.halo.app.theme.service;\n\nimport static java.nio.file.Files.createTempDirectory;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.ArgumentMatchers.isA;\nimport static org.mockito.Mockito.lenient;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;\nimport static run.halo.app.infra.utils.FileUtils.zip;\n\nimport com.github.zafarkhaja.semver.Version;\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport org.json.JSONException;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.mockito.stubbing.Answer;\nimport org.skyscreamer.jsonassert.JSONAssert;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.core.io.buffer.DefaultDataBufferFactory;\nimport org.springframework.util.ResourceUtils;\nimport org.springframework.util.StreamUtils;\nimport org.springframework.web.server.ServerWebInputException;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\nimport reactor.test.StepVerifier;\nimport run.halo.app.core.extension.AnnotationSetting;\nimport run.halo.app.core.extension.Setting;\nimport run.halo.app.core.extension.Theme;\nimport run.halo.app.extension.ConfigMap;\nimport run.halo.app.extension.Metadata;\nimport run.halo.app.extension.ReactiveExtensionClient;\nimport run.halo.app.extension.Unstructured;\nimport run.halo.app.extension.exception.ExtensionException;\nimport run.halo.app.infra.SystemConfigFetcher;\nimport run.halo.app.infra.SystemSetting;\nimport run.halo.app.infra.SystemVersionSupplier;\nimport run.halo.app.infra.ThemeRootGetter;\nimport run.halo.app.infra.exception.ThemeInstallationException;\nimport run.halo.app.infra.utils.JsonUtils;\n\n@ExtendWith(MockitoExtension.class)\nclass ThemeServiceImplTest {\n\n    @Mock\n    ReactiveExtensionClient client;\n\n    @Mock\n    ThemeRootGetter themeRoot;\n\n    @Mock\n    SystemVersionSupplier systemVersionSupplier;\n\n    @Mock\n    SystemConfigFetcher systemConfigFetcher;\n\n    @InjectMocks\n    ThemeServiceImpl themeService;\n\n    Path tmpDir;\n\n    @BeforeEach\n    void setUp() throws IOException {\n        tmpDir = createTempDirectory(\"halo-theme-service-test-\");\n        lenient().when(themeRoot.get()).thenReturn(tmpDir.resolve(\"themes\"));\n        // init the folder\n        Files.createDirectory(themeRoot.get());\n\n        lenient().when(systemVersionSupplier.get()).thenReturn(Version.parse(\"0.0.0\"));\n    }\n\n    @AfterEach\n    void cleanUp() {\n        deleteRecursivelyAndSilently(tmpDir);\n    }\n\n    Path prepareTheme(String themeFilename) throws IOException, URISyntaxException {\n        var defaultThemeUri = ResourceUtils.getURL(\"classpath:themes/\" + themeFilename).toURI();\n        var defaultThemeZipPath = tmpDir.resolve(\"default.zip\");\n        zip(Path.of(defaultThemeUri), defaultThemeZipPath);\n        return defaultThemeZipPath;\n    }\n\n    Theme createTheme() {\n        return createTheme(theme -> {\n        });\n    }\n\n    Theme createTheme(Consumer<Theme> customizer) {\n        var metadata = new Metadata();\n        metadata.setName(\"default\");\n\n        var spec = new Theme.ThemeSpec();\n        spec.setDisplayName(\"Default\");\n\n        var theme = new Theme();\n        theme.setMetadata(metadata);\n        theme.setSpec(spec);\n        customizer.accept(theme);\n        return theme;\n    }\n\n    Unstructured convert(Theme theme) {\n        return Unstructured.OBJECT_MAPPER.convertValue(theme, Unstructured.class);\n    }\n\n    Flux<DataBuffer> content(Path path) {\n        return DataBufferUtils.read(\n            path,\n            DefaultDataBufferFactory.sharedInstance,\n            StreamUtils.BUFFER_SIZE);\n    }\n\n    @Nested\n    class UpgradeTest {\n\n        @Test\n        void shouldFailIfThemeNotInstalledBefore() throws IOException, URISyntaxException {\n            var themeZipPath = prepareTheme(\"other\");\n            when(client.fetch(Theme.class, \"default\")).thenReturn(Mono.empty());\n            StepVerifier.create(themeService.upgrade(\"default\", content(themeZipPath)))\n                .verifyError(ServerWebInputException.class);\n\n            verify(client).fetch(Theme.class, \"default\");\n        }\n\n        @Test\n        void shouldUpgradeSuccessfully() throws IOException, URISyntaxException {\n            var themeZipPath = prepareTheme(\"other\");\n\n            var oldTheme = createTheme();\n            when(client.fetch(Theme.class, \"default\"))\n                // for old theme check\n                .thenReturn(Mono.just(oldTheme))\n                // for theme deletion\n                .thenReturn(Mono.just(oldTheme))\n                // for theme deleted check\n                .thenReturn(Mono.empty());\n\n            when(client.get(Theme.class, \"default\")).thenReturn(Mono.just(oldTheme));\n            when(client.update(oldTheme)).thenReturn(Mono.just(createTheme(t -> {\n                t.getSpec().setDisplayName(\"New fake theme\");\n            })));\n\n            StepVerifier.create(themeService.upgrade(\"default\", content(themeZipPath)))\n                .consumeNextWith(newTheme -> {\n                    assertEquals(\"default\", newTheme.getMetadata().getName());\n                    assertEquals(\"New fake theme\", newTheme.getSpec().getDisplayName());\n                })\n                .verifyComplete();\n\n            verify(client).fetch(Theme.class, \"default\");\n            verify(client, never()).delete(oldTheme);\n        }\n    }\n\n    @Nested\n    class InstallTest {\n\n\n        @Test\n        void shouldInstallSuccessfully() throws IOException, URISyntaxException {\n            var defaultThemeZipPath = prepareTheme(\"default\");\n            when(client.create(isA(Theme.class))).thenReturn(Mono.just(createTheme()));\n            StepVerifier.create(themeService.install(content(defaultThemeZipPath)))\n                .consumeNextWith(theme -> {\n                    assertEquals(\"default\", theme.getMetadata().getName());\n                    assertEquals(\"Default\", theme.getSpec().getDisplayName());\n                })\n                .verifyComplete();\n        }\n\n        @Test\n        void shouldFailWhenPersistentError() throws IOException, URISyntaxException {\n            var defaultThemeZipPath = prepareTheme(\"default\");\n            when(client.create(isA(Theme.class))).thenReturn(\n                Mono.error(() -> new ExtensionException(\"Failed to create the extension\")));\n            StepVerifier.create(themeService.install(content(defaultThemeZipPath)))\n                .verifyError(ExtensionException.class);\n        }\n\n        @Test\n        void shouldFailWhenThemeManifestIsInvalid() throws IOException, URISyntaxException {\n            var defaultThemeZipPath = prepareTheme(\"invalid-missing-manifest\");\n            StepVerifier.create(themeService.install(content(defaultThemeZipPath)))\n                .verifyError(ThemeInstallationException.class);\n        }\n    }\n\n    @Test\n    void reloadThemeWhenSettingNameSetBeforeThenDeleteSetting() throws IOException {\n        Theme theme = new Theme();\n        theme.setMetadata(new Metadata());\n        theme.getMetadata().setName(\"fake-theme\");\n        theme.setSpec(new Theme.ThemeSpec());\n        theme.getSpec().setDisplayName(\"Hello\");\n        theme.getSpec().setSettingName(\"fake-setting\");\n        when(client.fetch(Theme.class, \"fake-theme\"))\n            .thenReturn(Mono.just(theme));\n        when(client.delete(any(Setting.class))).thenReturn(Mono.empty());\n        Setting setting = new Setting();\n        setting.setMetadata(new Metadata());\n        setting.setSpec(new Setting.SettingSpec());\n        setting.getSpec().setForms(List.of());\n        when(client.fetch(Setting.class, \"fake-setting\"))\n            .thenReturn(Mono.just(setting));\n\n        Path themeWorkDir = themeRoot.get().resolve(theme.getMetadata().getName());\n        if (!Files.exists(themeWorkDir)) {\n            Files.createDirectories(themeWorkDir);\n        }\n        Files.writeString(themeWorkDir.resolve(\"settings.yaml\"), \"\"\"\n            apiVersion: v1alpha1\n            kind: Setting\n            metadata:\n              name: fake-setting\n            spec:\n              forms:\n                - group: sns\n                  label: 社交资料\n                  formSchema:\n                    - $el: h1\n                      children: Register\n            \"\"\");\n\n        Files.writeString(themeWorkDir.resolve(\"theme.yaml\"), \"\"\"\n            apiVersion: v1alpha1\n            kind: Theme\n            metadata:\n              name: fake-theme\n            spec:\n              displayName: Fake Theme\n            \"\"\");\n        when(client.update(any(Theme.class)))\n            .thenAnswer((Answer<Mono<Theme>>) invocation -> {\n                Theme argument = invocation.getArgument(0);\n                return Mono.just(argument);\n            });\n\n        when(client.list(eq(AnnotationSetting.class), any(), eq(null))).thenReturn(Flux.empty());\n\n        themeService.reloadTheme(\"fake-theme\")\n            .as(StepVerifier::create)\n            .consumeNextWith(themeUpdated -> {\n                try {\n                    JSONAssert.assertEquals(\"\"\"\n                            {\n                                \"spec\": {\n                                    \"displayName\": \"Fake Theme\",\n                                    \"version\": \"*\",\n                                    \"requires\": \"*\"\n                                },\n                                \"apiVersion\": \"theme.halo.run/v1alpha1\",\n                                \"kind\": \"Theme\",\n                                \"metadata\": {\n                                    \"name\": \"fake-theme\"\n                                }\n                            }\n                            \"\"\",\n                        JsonUtils.objectToJson(themeUpdated),\n                        true);\n                } catch (JSONException e) {\n                    throw new RuntimeException(e);\n                }\n            })\n            .verifyComplete();\n        // delete fake-setting\n        verify(client, times(1)).delete(any(Setting.class));\n        // Will not be created\n        verify(client, times(0)).create(any(Setting.class));\n    }\n\n    @Test\n    void reloadThemeWhenSettingNameNotSetBefore() throws IOException {\n        Theme theme = new Theme();\n        theme.setMetadata(new Metadata());\n        theme.getMetadata().setName(\"fake-theme\");\n        theme.setSpec(new Theme.ThemeSpec());\n        theme.getSpec().setDisplayName(\"Hello\");\n        when(client.fetch(Theme.class, \"fake-theme\"))\n            .thenReturn(Mono.just(theme));\n        Setting setting = new Setting();\n        setting.setMetadata(new Metadata());\n        setting.setSpec(new Setting.SettingSpec());\n        setting.getSpec().setForms(List.of());\n\n        when(client.fetch(eq(Setting.class), eq(null))).thenReturn(Mono.empty());\n\n        Path themeWorkDir = themeRoot.get().resolve(theme.getMetadata().getName());\n        if (!Files.exists(themeWorkDir)) {\n            Files.createDirectories(themeWorkDir);\n        }\n        Files.writeString(themeWorkDir.resolve(\"settings.yaml\"), \"\"\"\n            apiVersion: v1alpha1\n            kind: Setting\n            metadata:\n              name: fake-setting\n            spec:\n              forms:\n                - group: sns\n                  label: 社交资料\n                  formSchema:\n                    - $el: h1\n                      children: Register\n            \"\"\");\n\n        Files.writeString(themeWorkDir.resolve(\"theme.yaml\"), \"\"\"\n            apiVersion: v1alpha1\n            kind: Theme\n            metadata:\n              name: fake-theme\n            spec:\n              displayName: Fake Theme\n              settingName: fake-setting\n            \"\"\");\n        when(client.update(any(Theme.class)))\n            .thenAnswer((Answer<Mono<Theme>>) invocation -> {\n                Theme argument = invocation.getArgument(0);\n                return Mono.just(argument);\n            });\n\n        when(client.create(any(Unstructured.class)))\n            .thenAnswer((Answer<Mono<Unstructured>>) invocation -> {\n                Unstructured argument = invocation.getArgument(0);\n                JSONAssert.assertEquals(\"\"\"\n                        {\n                           \"spec\": {\n                             \"forms\": [\n                               {\n                                 \"group\": \"sns\",\n                                 \"label\": \"社交资料\",\n                                 \"formSchema\": [\n                                   {\n                                     \"$el\": \"h1\",\n                                     \"children\": \"Register\"\n                                   }\n                                 ]\n                               }\n                             ]\n                           },\n                           \"apiVersion\": \"v1alpha1\",\n                           \"kind\": \"Setting\",\n                           \"metadata\": {\n                              \"name\": \"fake-setting\",\n                              \"labels\": {\n                                  \"theme.halo.run/theme-name\": \"fake-theme\"\n                              }\n                            }\n                         }\n                        \"\"\",\n                    JsonUtils.objectToJson(argument),\n                    true);\n                return Mono.just(invocation.getArgument(0));\n            });\n\n        when(client.list(eq(AnnotationSetting.class), any(), eq(null))).thenReturn(Flux.empty());\n\n        when(client.fetch(eq(Setting.GVK), eq(\"fake-setting\")))\n            .thenReturn(Mono.empty());\n        themeService.reloadTheme(\"fake-theme\")\n            .as(StepVerifier::create)\n            .consumeNextWith(themeUpdated -> {\n                try {\n                    JSONAssert.assertEquals(\"\"\"\n                            {\n                                \"spec\": {\n                                    \"settingName\": \"fake-setting\",\n                                    \"displayName\": \"Fake Theme\",\n                                    \"version\": \"*\",\n                                    \"requires\": \"*\"\n                                },\n                                \"apiVersion\": \"theme.halo.run/v1alpha1\",\n                                \"kind\": \"Theme\",\n                                \"metadata\": {\n                                    \"name\": \"fake-theme\"\n                                }\n                            }\n                            \"\"\",\n                        JsonUtils.objectToJson(themeUpdated),\n                        true);\n                } catch (JSONException e) {\n                    throw new RuntimeException(e);\n                }\n            })\n            .verifyComplete();\n    }\n\n    @Test\n    void resetSettingConfig() {\n        Theme theme = new Theme();\n        theme.setMetadata(new Metadata());\n        theme.getMetadata().setName(\"fake-theme\");\n        theme.setSpec(new Theme.ThemeSpec());\n        theme.getSpec().setSettingName(\"fake-setting\");\n        theme.getSpec().setConfigMapName(\"fake-config\");\n        theme.getSpec().setDisplayName(\"Hello\");\n        when(client.fetch(Theme.class, \"fake-theme\"))\n            .thenReturn(Mono.just(theme));\n\n        Setting setting = new Setting();\n        setting.setMetadata(new Metadata());\n        setting.getMetadata().setName(\"fake-setting\");\n        setting.setSpec(new Setting.SettingSpec());\n        var formSchemaItem = Map.of(\"name\", \"email\", \"value\", \"example@exmple.com\");\n        Setting.SettingForm settingForm = new Setting.SettingForm();\n        settingForm.setGroup(\"basic\");\n        settingForm.setFormSchema(List.of(formSchemaItem));\n        setting.getSpec().setForms(List.of(settingForm));\n        when(client.fetch(eq(Setting.class), eq(\"fake-setting\")))\n            .thenReturn(Mono.just(setting));\n\n        ConfigMap configMap = new ConfigMap();\n        configMap.setMetadata(new Metadata());\n        configMap.getMetadata().setName(\"fake-config\");\n        when(client.fetch(eq(ConfigMap.class), eq(\"fake-config\")))\n            .thenReturn(Mono.just(configMap));\n\n        when(client.update(any(ConfigMap.class)))\n            .thenAnswer((Answer<Mono<ConfigMap>>) invocation -> {\n                ConfigMap argument = invocation.getArgument(0);\n                JSONAssert.assertEquals(\"\"\"\n                        {\n                            \"data\": {\n                                \"basic\": \"{\\\\\"email\\\\\":\\\\\"example@exmple.com\\\\\"}\"\n                            },\n                            \"apiVersion\": \"v1alpha1\",\n                            \"kind\": \"ConfigMap\",\n                            \"metadata\": {\n                                \"name\": \"fake-config\"\n                            }\n                        }\n                        \"\"\",\n                    JsonUtils.objectToJson(argument),\n                    true);\n                return Mono.just(invocation.getArgument(0));\n            });\n\n        themeService.resetSettingConfig(\"fake-theme\")\n            .as(StepVerifier::create)\n            .consumeNextWith(next -> {\n                assertThat(next).isNotNull();\n            })\n            .verifyComplete();\n\n        verify(client, times(1))\n            .fetch(eq(Setting.class), eq(setting.getMetadata().getName()));\n\n        verify(client, times(1)).fetch(eq(ConfigMap.class), eq(\"fake-config\"));\n\n        verify(client, times(1)).update(any(ConfigMap.class));\n    }\n\n    @Test\n    void shouldFetchSystemSetting() {\n        var themeSetting = new SystemSetting.Theme();\n        themeSetting.setActive(\"fake-theme\");\n        when(systemConfigFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class))\n            .thenReturn(Mono.just(themeSetting));\n\n        StepVerifier.create(themeService.fetchSystemSetting())\n            .expectNext(themeSetting)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFetchActivatedTheme() {\n        var themeSetting = new SystemSetting.Theme();\n        themeSetting.setActive(\"fake-theme\");\n        when(systemConfigFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class))\n            .thenReturn(Mono.just(themeSetting));\n\n        var theme = createTheme();\n        when(client.fetch(Theme.class, \"fake-theme\")).thenReturn(Mono.just(theme));\n\n        StepVerifier.create(themeService.fetchActivatedTheme())\n            .expectNext(theme)\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldFetchActivatedThemeName() {\n        var themeSetting = new SystemSetting.Theme();\n        themeSetting.setActive(\"fake-theme\");\n        when(systemConfigFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class))\n            .thenReturn(Mono.just(themeSetting));\n\n        StepVerifier.create(themeService.fetchActivatedThemeName())\n            .expectNext(\"fake-theme\")\n            .verifyComplete();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/theme/utils/PatternUtilsTest.java",
    "content": "package run.halo.app.theme.utils;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.CsvSource;\nimport org.junit.jupiter.params.provider.ValueSource;\nimport run.halo.app.infra.SystemSetting.ThemeRouteRules;\n\nclass PatternUtilsTest {\n\n    @ParameterizedTest\n    @CsvSource({\n        \"test, /test\",\n        \"/test/, /test\",\n        \"/test, /test\",\n        \"test/, /test\",\n        \"path/to/resource/, /path/to/resource\"\n    })\n    void normalizePatternTest(String pattern, String expected) {\n        assertEquals(expected, PatternUtils.normalizePattern(pattern));\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = {\n        \"\",\n        \" \",\n        \"/\",\n        \" /\",\n        \"/ \"\n    })\n    void shouldThrowExceptionWhen(String pattern) {\n        assertThrows(IllegalArgumentException.class, () -> PatternUtils.normalizePattern(pattern));\n    }\n\n    @ParameterizedTest\n    @CsvSource({\n        \"/posts/{slug}, /archives, /categories, /posts/{slug}\",\n        \"/archives/{slug}, /blog/archives, /categories, /blog/archives/{slug}\",\n        \"/categories/{slug}, /archives, /blog/categories, /blog/categories/{slug}\",\n        \"archives/{slug}, blog/archives, /categories, /blog/archives/{slug}\",\n        \"categories/{slug}, /archives, blog/categories, /blog/categories/{slug}\",\n        \"\"\"\n            /archives/{year}/{month}/{slug}/, /blog/archives/, /categories, \\\n            /blog/archives/{year}/{month}/{slug}\"\"\",\n        \"\"\"\n            /categories/{category}/{slug}/, /archives, /blog/categories/, \\\n            /blog/categories/{category}/{slug}\"\"\",\n        \"\"\"\n            /archives/categories/{slug}, /blog/archives, /blog/categories, \\\n            /blog/archives/categories/{slug}\"\"\",\n    })\n    void normalizePostPatternTest(\n        String postPattern, String archivesPattern, String categoriesPattern, String expected\n    ) {\n        var rules = new ThemeRouteRules();\n        rules.setPost(postPattern);\n        rules.setArchives(archivesPattern);\n        rules.setCategories(categoriesPattern);\n        var result = PatternUtils.normalizePostPattern(rules);\n        assertEquals(expected, result);\n    }\n\n\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/ui/WebSocketServerWebExchangeMatcherTest.java",
    "content": "package run.halo.app.ui;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport reactor.test.StepVerifier;\nimport run.halo.app.infra.ui.WebSocketServerWebExchangeMatcher;\n\nclass WebSocketServerWebExchangeMatcherTest {\n\n    @Test\n    void shouldMatchIfWebSocketProtocol() {\n        var httpRequest = MockServerHttpRequest.get(\"\")\n            .header(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE)\n            .header(HttpHeaders.UPGRADE, \"websocket\")\n            .build();\n        var wsExchange = MockServerWebExchange.from(httpRequest);\n        var wsMatcher = new WebSocketServerWebExchangeMatcher();\n        StepVerifier.create(wsMatcher.matches(wsExchange))\n            .consumeNextWith(result -> assertTrue(result.isMatch()))\n            .verifyComplete();\n    }\n\n    @Test\n    void shouldNotMatchIfNotWebSocketProtocol() {\n        var httpRequest = MockServerHttpRequest.get(\"\")\n            .header(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE)\n            .header(HttpHeaders.UPGRADE, \"not-a-websocket\")\n            .build();\n        var wsExchange = MockServerWebExchange.from(httpRequest);\n        var wsMatcher = new WebSocketServerWebExchangeMatcher();\n        StepVerifier.create(wsMatcher.matches(wsExchange))\n            .consumeNextWith(result -> assertFalse(result.isMatch()))\n            .verifyComplete();\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/ui/WebSocketUtilsTest.java",
    "content": "package run.halo.app.ui;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.http.HttpHeaders;\nimport run.halo.app.infra.ui.WebSocketUtils;\n\nclass WebSocketUtilsTest {\n\n    @Nested\n    class IsWebSocketTest {\n\n        @Test\n        void shouldBeWebSocketIfHeadersContaining() {\n            var headers = new HttpHeaders();\n            headers.add(\"Connection\", \"Upgrade\");\n            headers.add(\"Upgrade\", \"websocket\");\n            assertTrue(WebSocketUtils.isWebSocketUpgrade(headers));\n        }\n\n        @Test\n        void shouldNotBeWebSocketIfHeaderValuesAreIncorrect() {\n            var headers = new HttpHeaders();\n            headers.add(\"Connection\", \"keep-alive\");\n            headers.add(\"Upgrade\", \"websocket\");\n            assertFalse(WebSocketUtils.isWebSocketUpgrade(headers));\n        }\n\n        @Test\n        void shouldNotBeWebSocketIfMissingUpgradeHeader() {\n            var headers = new HttpHeaders();\n            headers.add(\"Connection\", \"Upgrade\");\n            assertFalse(WebSocketUtils.isWebSocketUpgrade(headers));\n        }\n\n        @Test\n        void shouldNotBeWebSocketIfMissingConnectionHeader() {\n            var headers = new HttpHeaders();\n            headers.add(\"Connection\", \"Upgrade\");\n            assertFalse(WebSocketUtils.isWebSocketUpgrade(headers));\n        }\n\n        @Test\n        void shouldNotBeWebSocketIfMissingHeaders() {\n            var headers = new HttpHeaders();\n            assertFalse(WebSocketUtils.isWebSocketUpgrade(headers));\n        }\n    }\n}"
  },
  {
    "path": "application/src/test/java/run/halo/app/webfilter/LocaleChangeWebFilterTest.java",
    "content": "package run.halo.app.webfilter;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport java.util.stream.Stream;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.http.MediaType;\nimport org.springframework.mock.http.server.reactive.MockServerHttpRequest;\nimport org.springframework.mock.web.server.MockServerWebExchange;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.infra.webfilter.LocaleChangeWebFilter;\nimport run.halo.app.theme.ThemeLocaleContextResolver;\n\nclass LocaleChangeWebFilterTest {\n\n    LocaleChangeWebFilter filter;\n\n    @BeforeEach\n    void setUp() {\n        var themeLocaleContextResolver = new ThemeLocaleContextResolver();\n        filter = new LocaleChangeWebFilter(themeLocaleContextResolver);\n    }\n\n    @Test\n    void shouldRespondLanguageCookie() {\n        WebFilterChain webFilterChain = filterExchange -> {\n            var languageCookie = filterExchange.getResponse().getCookies().getFirst(\"language\");\n            assertNotNull(languageCookie);\n            assertEquals(\"zh-CN\", languageCookie.getValue());\n            return Mono.empty();\n        };\n        var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/home\")\n            .accept(MediaType.TEXT_HTML)\n            .queryParam(\"language\", \"zh-CN\")\n            .build()\n        );\n        this.filter.filter(exchange, webFilterChain).block();\n    }\n\n    @Test\n    void shouldNotRespondLanguageCookieIfChanged() {\n        WebFilterChain webFilterChain = filterExchange -> {\n            var languageCookie = filterExchange.getResponse().getCookies().getFirst(\"language\");\n            assertNotNull(languageCookie);\n            assertEquals(\"zh-CN\", languageCookie.getValue());\n            return Mono.empty();\n        };\n        var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/home\")\n            .accept(MediaType.TEXT_HTML)\n            .cookie(new HttpCookie(\"language\", \"zh-HK\"))\n            .queryParam(\"language\", \"zh-CN\")\n            .build()\n        );\n        this.filter.filter(exchange, webFilterChain).block();\n    }\n\n    @Test\n    void shouldNotRespondLanguageCookieIfNotChanged() {\n        WebFilterChain webFilterChain = filterExchange -> {\n            var languageCookie = filterExchange.getResponse().getCookies().getFirst(\"language\");\n            assertNull(languageCookie);\n            return Mono.empty();\n        };\n        var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/home\")\n            .accept(MediaType.TEXT_HTML)\n            .cookie(new HttpCookie(\"language\", \"zh-CN\"))\n            .queryParam(\"language\", \"zh-CN\")\n            .build()\n        );\n        this.filter.filter(exchange, webFilterChain).block();\n    }\n\n    @Test\n    void shouldNotRespondLanguageCookieWithUndeterminedLanguageTag() {\n        WebFilterChain webFilterChain = filterExchange -> {\n            var languageCookie = filterExchange.getResponse().getCookies().getFirst(\"language\");\n            assertNull(languageCookie);\n            return Mono.empty();\n        };\n        var exchange = MockServerWebExchange.from(MockServerHttpRequest.get(\"/home\")\n            .accept(MediaType.TEXT_HTML)\n            .queryParam(\"language\", \"invalid_language_tag\")\n            .build()\n        );\n        this.filter.filter(exchange, webFilterChain).block();\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"provideInvalidRequest\")\n    void shouldNotRespondLanguageCookieIfRequestNotMatch(MockServerHttpRequest mockRequest) {\n        WebFilterChain webFilterChain = filterExchange -> {\n            var languageCookie = filterExchange.getResponse().getCookies().getFirst(\"language\");\n            assertNull(languageCookie);\n            return Mono.empty();\n        };\n        var exchange = MockServerWebExchange.from(mockRequest);\n        this.filter.filter(exchange, webFilterChain).block();\n    }\n\n    static Stream<MockServerHttpRequest> provideInvalidRequest() {\n        return Stream.of(\n            MockServerHttpRequest.get(\"/home\")\n                .accept(MediaType.ALL)\n                .queryParam(\"language\", \"zh-CN\")\n                .build(),\n            MockServerHttpRequest.get(\"/home\")\n                .accept(MediaType.APPLICATION_JSON)\n                .queryParam(\"language\", \"zh-CN\")\n                .build(),\n            MockServerHttpRequest.post(\"/home\")\n                .accept(MediaType.TEXT_HTML)\n                .queryParam(\"language\", \"zh-CN\")\n                .build(),\n            MockServerHttpRequest.get(\"/home\")\n                .accept(MediaType.TEXT_HTML)\n                .build()\n        );\n    }\n}"
  },
  {
    "path": "application/src/test/resources/apiToken.salt",
    "content": "bySwF9ZxJ2JpQYs830+eA3Fw6O7F/ULDvyYGEPaZKwY="
  },
  {
    "path": "application/src/test/resources/app.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOjnDY1K1lrOrK\nETfKfDlVGVbPCiy+TDmTaXg4SWjdHUpXfqbXMkSX/j2dJ/ECqb/FtsvVxiSwRieG\n3MWDKWlNRz0C0QKrsoDYbcvLf68uc7L5eKFZhu0AkXP4T5BIbdMXH8V0+5e+6R+n\neHahFhMyaiYoHVrPMrW2Jn9iWIXuNTDpg9VFhejN4jG1wQqIu1puKeGYPQvtfNO5\nEf5cQdEFCvFfuDQvNhLgI1f798qY6EVFfRo2S3LLCut3wfDzRZiUN4Kz8qYz42Zv\n97GS1gW/lfcEsmBApov9xiIaUzUECN35XbZYMK5Y4gfhAseZ+tlj+YarEiPjAtL1\nJPUehCmRAgMBAAECggEAKmaI+ammsoFtbO9d4X3gkvxxmmx/RM0G4KC84ekH0qPp\nl85S10flVsIEydbiHWbVC/P7IbXb4Cd2g7OcA9GjYQ6nkoVvI+mvkz3uoKZkQofT\njGxbyrHswroY8Tb76jJJK60E7n+a5cCbE9ihmW2boTSzAncMJg5FyM9cRMbhL0Vz\nh90/gE2U8awQ8Ug47BN1Dk/awxB9f5zVqI+LCGC0Py0/oQudjSaqPihydTsuqkhV\nxNu3NMcL/POt9WxmYyJFDJRW3+EYraPumdUsIWw8p4JJDt1jkyNpSbjGhu8vzRYX\n0QSo1pa3VrDY4guEMk4RdJsKJDqQPTvCTTgDYBzFlQKBgQDq98CRLTwqHSEWyVKN\n0KRujhVAVEmLDvPxZ2tVaMM37RanCHYSfHLiYCD54rUv7BFWjQ+hfq3iHUpgrefN\nKRS9e01mT0f24sAsWfhrFzrhlHaQStFgOw4uvwIDCfzrBeQQsqcAvWSjNr8CqSMX\nUIGz9oB6EP39PT3QxT3oYf3ItwKBgQDhC6WN78+0sf2zlptQ7V02eWaRfePtQfmb\now3c9aF8V7sSwDzjInqV5Bva4RyftYRTYZttBiANjGZ1pSNPi/2p7b+0hxJ1pPf8\n6VcFDJBGLbFYNDWOux13KRJToMY0ckzSeBXgkWLVFSfESuoXzy+8bj5eMavJLg6L\n2Ek6q6mH9wKBgBZmmE0+6sV5EXaCqwQqKAMCOLRxVLGVM1yIZ4s0+aeTSt2RyO/q\nPWmnkH1CR9PRxbVirWLQGPO9pyGgcsD0ca2+25otZMb8xyVzTmOnS03GQadv+pYa\nCzgZra9sfFhLr3qIDbPcWoPU7FDsnxPR8QufLJB2nkBOXl5Q753/+ZnxAoGBAI47\nGisWwaNmSv3R1d/T5PGk0Jprgj5VUDh5WS2pYKKBoA49yT2UcP2C6cfwNnMJ+dPp\nAJ5rHJ7zeV4pPKPtyig3xs2GALixxrnlj8X1Jsnz3v3sIV1QDVNedeK83ggPpVXv\n54PC3z/k2vlIj6L0oyroUiqeIgBIR5FC5SVbkQ4JAoGBAOEGQkqw1xR3fd27J6/R\ns9hOhItPnjExf5yqeg0nbZYIGd+6PiaVBBWUefZDDS79KUwTiqiHGP7iEVghJr9C\nxJI9odzY8WQJ+Q9ZQy1VQfP5mkRUTTkABhykXfWsHckO7yP6c3kwNIOOki8QPrmY\n3GKNb5HtQVpazCvrB5PFh65g\n-----END PRIVATE KEY-----"
  },
  {
    "path": "application/src/test/resources/app.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzo5w2NStZazqyhE3ynw5\nVRlWzwosvkw5k2l4OElo3R1KV36m1zJEl/49nSfxAqm/xbbL1cYksEYnhtzFgylp\nTUc9AtECq7KA2G3Ly3+vLnOy+XihWYbtAJFz+E+QSG3TFx/FdPuXvukfp3h2oRYT\nMmomKB1azzK1tiZ/YliF7jUw6YPVRYXozeIxtcEKiLtabinhmD0L7XzTuRH+XEHR\nBQrxX7g0LzYS4CNX+/fKmOhFRX0aNktyywrrd8Hw80WYlDeCs/KmM+Nmb/exktYF\nv5X3BLJgQKaL/cYiGlM1BAjd+V22WDCuWOIH4QLHmfrZY/mGqxIj4wLS9ST1HoQp\nkQIDAQAB\n-----END PUBLIC KEY-----"
  },
  {
    "path": "application/src/test/resources/application.yaml",
    "content": "server:\n  port: 8090\nspring:\n  output:\n    ansi:\n      enabled: detect\n  r2dbc:\n    name: halo-test\n    generate-unique-name: true\n  sql:\n    init:\n      mode: always\n      platform: h2\n  messages:\n    basename: config.i18n.messages\n\nhalo:\n  work-dir: ${user.home}/halo-next-test\n  external-url: \"http://${server.address:localhost}:${server.port}\"\n  security:\n    initializer:\n      disabled: true\n    oauth2:\n      jwt:\n        public-key-location: classpath:app.pub\n        private-key-location: classpath:app.key\n  extension:\n    controller:\n      disabled: true\n  search-engine:\n    lucene:\n      enabled: false\n\nspringdoc:\n  api-docs:\n    enabled: false\nlogging:\n  level:\n    run.halo.app: debug\n    org.springframework.r2dbc: DEBUG\n"
  },
  {
    "path": "application/src/test/resources/backups/backup-for-restoration/extensions.data",
    "content": "[{\"name\":\"fake-extension-store\",\"data\":\"ZmFrZS1kYXRh\",\"version\":1024}]"
  },
  {
    "path": "application/src/test/resources/backups/backup-for-restoration/workdir/fake-file",
    "content": "halo"
  },
  {
    "path": "application/src/test/resources/categories/independent-post-count.json",
    "content": "[\n    {\n        \"spec\": {\n            \"displayName\": \"全部\",\n            \"children\": [\"FIT2CLOUD\", \"AnotherRootChild\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 35\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"全部\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"FIT2CLOUD\",\n            \"children\": [\"DataEase\", \"IndependentNode\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 15\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"FIT2CLOUD\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"DataEase\",\n            \"children\": [\"SubNode1\", \"SubNode2\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 10\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"DataEase\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"SubNode1\",\n            \"children\": [\"Leaf1\", \"Leaf2\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 4\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"SubNode1\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"Leaf1\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 2\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"Leaf1\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"Leaf2\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 2\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"Leaf2\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"SubNode2\",\n            \"preventParentPostCascadeQuery\": true,\n            \"children\": [\"IndependentChild1\", \"IndependentChild2\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 6\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"SubNode2\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"IndependentChild1\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 3\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"IndependentChild1\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"IndependentChild2\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 3\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"IndependentChild2\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"IndependentNode\",\n            \"preventParentPostCascadeQuery\": true,\n            \"children\": [\"IndependentChild3\", \"IndependentChild4\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 5\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"IndependentNode\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"IndependentChild3\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 2\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"IndependentChild3\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"IndependentChild4\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 3\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"IndependentChild4\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"AnotherRootChild\",\n            \"children\": [\"Child1\", \"Child2\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 20\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"AnotherRootChild\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"Child1\",\n            \"children\": [\"SubChild1\", \"SubChild2\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 8\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"Child1\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"SubChild1\",\n            \"children\": [\"DeepNode1\", \"DeepNode2\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 3\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"SubChild1\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"DeepNode1\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 1\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"DeepNode1\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"DeepNode2\",\n            \"children\": [\"DeeperNode\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 1\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"DeepNode2\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"DeeperNode\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 1\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"DeeperNode\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"SubChild2\",\n            \"children\": [\"DeepNode3\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 5\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"SubChild2\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"DeepNode3\",\n            \"preventParentPostCascadeQuery\": true,\n            \"children\": [\"DeepNode4\", \"DeepNode5\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 2\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"DeepNode3\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"DeepNode4\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 1\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"DeepNode4\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"DeepNode5\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 1\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"DeepNode5\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"Child2\",\n            \"children\": [\"IndependentSubNode\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 12\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"Child2\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"IndependentSubNode\",\n            \"preventParentPostCascadeQuery\": true,\n            \"children\": [\"SubNode3\", \"SubNode4\"]\n        },\n        \"status\": {\n            \"visiblePostCount\": 12\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"IndependentSubNode\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"SubNode3\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 6\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"SubNode3\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    },\n    {\n        \"spec\": {\n            \"displayName\": \"SubNode4\",\n            \"children\": []\n        },\n        \"status\": {\n            \"visiblePostCount\": 6\n        },\n        \"apiVersion\": \"content.halo.run/v1alpha1\",\n        \"kind\": \"Category\",\n        \"metadata\": {\n            \"name\": \"SubNode4\",\n            \"version\": 0,\n            \"creationTimestamp\": \"2024-06-14T06:17:47.589181Z\"\n        }\n    }\n]\n"
  },
  {
    "path": "application/src/test/resources/config/i18n/messages.properties",
    "content": "problemDetail.title.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Error Response\nproblemDetail.internalServerError=Something went wrong, please try again later.\n\nproblemDetail.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Message argument is {0}.\nerror.somethingWentWrong=Something went wrong, argument is {0}.\nproblemDetail.title.internalServerError=Internal Server Error\n\nproblemDetail.title.conflict=Conflict\nproblemDetail.conflict=Conflict detected.\n"
  },
  {
    "path": "application/src/test/resources/config/i18n/messages_zh.properties",
    "content": "problemDetail.title.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=发生错误\nproblemDetail.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=参数：{0}。\nerror.somethingWentWrong=发生了一些错误，参数：{0}。\n"
  },
  {
    "path": "application/src/test/resources/console/assets/fake.txt",
    "content": "fake.\n"
  },
  {
    "path": "application/src/test/resources/console/index.html",
    "content": "console index\n"
  },
  {
    "path": "application/src/test/resources/file-type-detect/index.html",
    "content": ""
  },
  {
    "path": "application/src/test/resources/file-type-detect/index.js",
    "content": ""
  },
  {
    "path": "application/src/test/resources/file-type-detect/test.json",
    "content": ""
  },
  {
    "path": "application/src/test/resources/folder-to-zip/examplefile",
    "content": "Here is an example file.\n"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-0.0.1/extensions/reverseProxy.yaml",
    "content": "apiVersion: plugin.halo.run/v1alpha1\nkind: ReverseProxy\nmetadata:\n  name: reverse-proxy-template\n  labels:\n    plugin.halo.run/pluginName: io.github.guqing.apples\nrules:\n  - path: /static/**\n    file:\n      directory: static\n  - path: /admin/**\n    file:\n      directory: admin\n"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-0.0.1/extensions/roles.yaml",
    "content": "apiVersion: v1alpha1\nkind: Role\nmetadata:\n  name: role-template-view-apples\n  labels:\n    halo.run/role-template: \"true\"\n  annotations:\n    halo.run/module: \"Apples Management\"\n    halo.run/alias-name: \"Apples View\"\nrules:\n  - apiGroups: [ \"apples.guqing.github.com\" ]\n    resources: [ \"apples\" ]\n    verbs: [ \"get\", \"list\" ]\n"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-0.0.1/extensions/setting.yaml",
    "content": "apiVersion: v1alpha1\nkind: Setting\nmetadata:\n  name: fake-setting\nspec:\n  forms: [ ]"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-0.0.1/extensions/test.yml",
    "content": "apiVersion: plugin.halo.run/v1alpha1\nkind: ReverseProxy\nmetadata:\n  name: reverse-proxy-template\n  labels:\n    plugin.halo.run/pluginName: io.github.guqing.apples\nrules:\n  - path: /static/**\n    file:\n      directory: static\n  - path: /admin/**\n    file:\n      directory: admin\n"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-0.0.1/plugin.yaml",
    "content": "apiVersion: v1\nkind: Plugin\nmetadata:\n  name: plugin-1\nspec:\n  # 'version' is a valid semantic version string (see semver.org).\n  version: 0.0.1\n  requires: \">=2.0.0\"\n  author:\n    name: guqing\n  logo: https://guqing.xyz/avatar\n  pluginDependencies:\n    \"banana\": \"0.0.1\"\n  homepage: https://github.com/guqing/halo-plugin-1\n  displayName: \"a name to show\"\n  description: \"Tell me more about this plugin.\"\n  license:\n    - name: MIT"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-0.0.2/plugin.yaml",
    "content": "apiVersion: v1\nkind: Plugin\nmetadata:\n  name: fake-plugin\nspec:\n  # 'version' is a valid semantic version string (see semver.org).\n  version: 0.0.2\n  requires: \">=2.0.0\"\n  author:\n    name: johnniang\n  logo: https://halo.run/avatar\n  homepage: https://github.com/halo-sigs/halo-plugin-1\n  displayName: \"Fake Display Name\"\n  description: \"Fake description\"\n  license:\n    - name: GPLv3\n"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-for-finder/META-INF/plugin-components.idx",
    "content": "# Generated by Halo\nrun.halo.fake.FakePlugin\n"
  },
  {
    "path": "application/src/test/resources/plugin/plugin-for-reverseproxy/static/test.txt",
    "content": "Fake content."
  },
  {
    "path": "application/src/test/resources/plugin/plugin.yaml",
    "content": "apiVersion: v1\nkind: Plugin\nmetadata:\n  name: plugin-1\nspec:\n  # 'version' is a valid semantic version string (see semver.org).\n  version: 0.0.1\n  requires: \">=2.0.0\"\n  author:\n    name: guqing\n  logo: https://guqing.xyz/avatar\n  pluginDependencies:\n    \"banana\": \"0.0.1\"\n  homepage: https://github.com/guqing/halo-plugin-1\n  displayName: \"a name to show\"\n  description: \"Tell me more about this plugin.\"\n  license:\n    - name: MIT"
  },
  {
    "path": "application/src/test/resources/themes/default/i18n/default.properties",
    "content": "index.welcome=欢迎来到首页\ntitle=这是来自 i18n/default.properties 的标题\n"
  },
  {
    "path": "application/src/test/resources/themes/default/i18n/en.properties",
    "content": "index.welcome=Welcome to the index"
  },
  {
    "path": "application/src/test/resources/themes/default/i18n/zh.properties",
    "content": "title=来自 i18n/zh.properties 的标题\n"
  },
  {
    "path": "application/src/test/resources/themes/default/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <title th:text=\"#{title}\">Title</title>\n</head>\n<body>\nindex\n<div th:text=\"${#locale}\"></div>\n<div th:text=\"#{index.welcome}\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "application/src/test/resources/themes/default/templates/index.properties",
    "content": "title=Title from index.properties\n"
  },
  {
    "path": "application/src/test/resources/themes/default/templates/index_zh.properties",
    "content": "title=来自 index_zh.properties 的标题\n"
  },
  {
    "path": "application/src/test/resources/themes/default/templates/timezone.html",
    "content": "<p th:text=\"${#temporals.format(instants, 'yyyy-MM-dd HH:mm:ss')}\"></p>\n"
  },
  {
    "path": "application/src/test/resources/themes/default/theme.yaml",
    "content": "apiVersion: theme.halo.run/v1alpha1\nkind: Theme\nmetadata:\n  name: default\nspec:\n  displayName: Default\n  author:\n    name: halo-dev\n    website: https://halo.run\n  description: Default theme for Halo 2.0\n  logo: https://halo.run/logo\n  website: https://github.com/halo-sigs/theme-default\n  repo: https://github.com/halo-sigs/theme-default.git\n  version: 1.0.0\n  require: 2.0.0\n"
  },
  {
    "path": "application/src/test/resources/themes/invalid-missing-manifest/i18n/default.properties",
    "content": "index.welcome=\\u6B22\\u8FCE\\u6765\\u5230\\u9996\\u9875"
  },
  {
    "path": "application/src/test/resources/themes/invalid-missing-manifest/i18n/en.properties",
    "content": "index.welcome=Welcome to the index"
  },
  {
    "path": "application/src/test/resources/themes/invalid-missing-manifest/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Title</title>\n</head>\n<body>\nindex\n<div th:text=\"${#locale}\"></div>\n<div th:text=\"#{index.welcome}\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "application/src/test/resources/themes/invalid-missing-manifest/templates/timezone.html",
    "content": "<p th:text=\"${#temporals.format(instants, 'yyyy-MM-dd HH:mm:ss')}\"></p>\n"
  },
  {
    "path": "application/src/test/resources/themes/other/i18n/default.properties",
    "content": "index.welcome=Other \\u9996\\u9875"
  },
  {
    "path": "application/src/test/resources/themes/other/i18n/en.properties",
    "content": "index.welcome=other index"
  },
  {
    "path": "application/src/test/resources/themes/other/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\"/>\n    <title>Other theme title</title>\n</head>\n<body>\n<p th:text=\"#{index.welcome}\"></p>\n</body>\n</html>\n"
  },
  {
    "path": "application/src/test/resources/themes/other/theme.yaml",
    "content": "apiVersion: theme.halo.run/v1alpha1\nkind: Theme\nmetadata:\n  name: default\nspec:\n  displayName: Default\n  author:\n    name: halo-dev\n    website: https://halo.run\n  description: Default theme for Halo 2.0\n  logo: https://halo.run/logo\n  website: https://github.com/halo-sigs/theme-default\n  repo: https://github.com/halo-sigs/theme-default.git\n  version: 1.0.1\n  require: 2.0.0\n"
  },
  {
    "path": "application/src/test/resources/ui/console.html",
    "content": "console index\n"
  },
  {
    "path": "application/src/test/resources/ui/uc.html",
    "content": "uc index\n"
  },
  {
    "path": "application/src/test/resources/ui/ui-assets/fake.txt",
    "content": "fake.\n"
  },
  {
    "path": "buildSrc/build.gradle",
    "content": "plugins {\n    id 'groovy-gradle-plugin'\n}\n"
  },
  {
    "path": "buildSrc/src/main/groovy/UploadBundleTask.groovy",
    "content": "import groovy.json.JsonSlurper\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.artifacts.repositories.PasswordCredentials\nimport org.gradle.api.file.RegularFileProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.*\n\nimport java.net.http.HttpClient\nimport java.net.http.HttpRequest\nimport java.net.http.HttpResponse\nimport java.time.Duration\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipInputStream\n\nimport static java.net.http.HttpRequest.BodyPublishers.ofFile\nimport static java.net.http.HttpRequest.BodyPublishers.ofString\nimport static java.nio.charset.StandardCharsets.UTF_8\n\n/**\n * Task to upload a bundle to Sonatype Central Repository.\n * @author JohnNiang\n */\nabstract class UploadBundleTask extends DefaultTask {\n\n    @InputFile\n    abstract RegularFileProperty getBundleFile();\n\n    @Input\n    @Optional\n    abstract Property<PublishingType> getPublishingType();\n\n    @Input\n    abstract Property<PasswordCredentials> getCredentials();\n\n    UploadBundleTask() {\n        getPublishingType().convention(PublishingType.AUTOMATIC)\n    }\n\n    @TaskAction\n    void upload() {\n        if (project.version.toString().endsWith(\"-SNAPSHOT\")) {\n            logger.lifecycle(\"Detected SNAPSHOT version, uploading to maven-snapshots repository...\")\n            uploadSnapshotBundle()\n            logger.lifecycle(\"Snapshot bundle uploaded successfully.\")\n            return\n        }\n        if (checkIfPublished()) {\n            logger.lifecycle(\"The component ${project.group}:${project.name}:${project.version} is already published, skipping upload.\")\n            return\n        }\n\n        def deploymentId = uploadReleaseBundle()\n        def maxWait = Duration.ofMinutes(12)\n        def pollingInterval = Duration.ofSeconds(10)\n        def maxRetry = (int) (maxWait.toMillis() / pollingInterval.toMillis())\n        def retry = 0\n        logger.lifecycle(\"Preparing to check deployment state for deployment ID: ${deploymentId}, max retries: ${maxRetry}, polling interval: ${pollingInterval.toSeconds()} seconds\")\n        while (retry++ <= maxRetry) {\n            def state = checkDeploymentState(deploymentId)\n            if (state == null) {\n                throw new RuntimeException(\"Failed to check deployment state for deployment ID: ${deploymentId}\")\n            }\n            if (state == DeploymentState.PUBLISHED) {\n                println(\"Bundle published successfully with deployment ID: ${deploymentId}\")\n                break\n            }\n            if (state == DeploymentState.VALIDATED) {\n                println(\"Bundle validated successfully with deployment ID: ${deploymentId}\")\n                break\n            }\n            if (state == DeploymentState.FAILED) {\n                throw new RuntimeException(\"Bundle deployment failed with deployment ID: ${deploymentId}\")\n            }\n            if (state == DeploymentState.VALIDATING) {\n                logger.lifecycle('Bundle is being validated, please wait...')\n            } else if (state == DeploymentState.PENDING) {\n                logger.lifecycle('Bundle is pending, please wait...')\n            }\n            println(\"Deployment state: ${state}, retrying(${retry}) in ${pollingInterval.toSeconds()} seconds...\")\n            sleep(pollingInterval.toMillis())\n        }\n    }\n\n    boolean checkIfPublished() {\n        createHttpClient().withCloseable { client ->\n            def endpoint = \"https://central.sonatype.com/api/v1/publisher/published?namespace=${project.group}&name=${project.name}&version=${project.version}\";\n            logger.debug(\"Checking if published at endpoint: ${endpoint}\")\n            def request = HttpRequest.newBuilder(URI.create(endpoint))\n                    .header(\"Authorization\", bearerAuthorizationHeader)\n                    .GET()\n                    .build()\n            def response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8))\n            if (response.statusCode() != 200) {\n                throw new RuntimeException(\"Failed to check if published: status: ${response.statusCode()} - body: ${response.body()}\")\n            }\n            logger.debug(\"Response body: ${response.body()}\")\n            def result = new JsonSlurper().parseText(response.body())\n            logger.debug(\"Check if published result: ${result}\")\n            return result.published as boolean\n        }\n    }\n\n    DeploymentState checkDeploymentState(String deploymentId) {\n        createHttpClient().withCloseable { client ->\n            def endpoint = \"https://central.sonatype.com/api/v1/publisher/status?id=${deploymentId}\"\n\n            def request = HttpRequest.newBuilder(URI.create(endpoint))\n                    .header(\"Authorization\", bearerAuthorizationHeader)\n                    .POST(HttpRequest.BodyPublishers.noBody())\n                    .build()\n            def response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8))\n            if (response.statusCode() != 200) {\n                throw new RuntimeException(\"Failed to check deployment status: status: ${response.statusCode()} - body: ${response.body()}\")\n            }\n            logger.debug(\"Response body: ${response.body()}\")\n            def status = new JsonSlurper().parseText(response.body())\n            logger.debug(\"Deployment status: ${status}\")\n            return status.deploymentState as DeploymentState\n        }\n    }\n\n    void uploadSnapshotBundle() {\n        logger.lifecycle('Starting to upload snapshot bundle: {}', bundleFile.asFile.get())\n        createHttpClient().withCloseable { client ->\n            new ZipInputStream(bundleFile.asFile.get().newInputStream()).withCloseable { zis ->\n                ZipEntry entry\n                while ((entry = zis.nextEntry) != null) {\n                    if (entry.directory) {\n                        continue\n                    }\n                    def relativePath = entry.name\n                    def endpoint = \"https://central.sonatype.com/repository/maven-snapshots/$relativePath\"\n                    def request = HttpRequest.newBuilder(URI.create(endpoint))\n                            .header(\"Authorization\", basicAuthorizationHeader)\n                            .header(\"Content-Type\", \"application/octet-stream\")\n                            .PUT(HttpRequest.BodyPublishers.ofByteArray(zis.readAllBytes()))\n                            .build()\n                    def response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8))\n                    logger.debug('Response status code: {}, body: {}', response.statusCode(), response.body())\n                    if (response.statusCode() != 200 && response.statusCode() != 201) {\n                        throw new RuntimeException(\"Failed to upload snapshot bundle: status: ${response.statusCode()} - entry: ${relativePath} - body : ${response.body()}\")\n                    }\n                    logger.lifecycle('Uploaded snapshot entry: {}, status: {}', relativePath, response.statusCode())\n                }\n            }\n        }\n    }\n\n    String uploadReleaseBundle() {\n        logger.lifecycle('Starting to upload release bundle: {}', bundleFile.asFile.get())\n        createHttpClient().withCloseable { client ->\n            // See https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html for more\n            def boundary = \"------haloformboundary${UUID.randomUUID().toString().replace('-', '')}\"\n            def crlf = \"\\r\\n\"\n            def delimiter = \"${crlf}--${boundary}\"\n            def endpoint = \"https://central.sonatype.com/api/v1/publisher/upload?publishingType=${publishingType.get()}\"\n\n            def publishers = new ArrayList<HttpRequest.BodyPublisher>()\n            publishers.add(ofString(\"${delimiter}${crlf}\"))\n            publishers.add(ofString(\"\"\"\\\nContent-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.get().asFile.name}\"\\\n${crlf}\\\n\"\"\"))\n            publishers.add(ofString(\"Content-Type: application/octet-stream${crlf}${crlf}\"))\n            publishers.add(ofFile(bundleFile.get().asFile.toPath()))\n            publishers.add(ofString(\"${delimiter}--${crlf}\"))\n\n            def request = HttpRequest.newBuilder(URI.create(endpoint))\n                    .header(\"Authorization\", bearerAuthorizationHeader)\n                    .header(\"Content-Type\", \"multipart/form-data; boundary=${boundary}\")\n                    .POST(HttpRequest.BodyPublishers.concat(publishers.toArray(HttpRequest.BodyPublisher[]::new)))\n                    .build()\n            def response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8))\n            if (logger.debugEnabled) {\n                logger.debug('Response status code: {}, body: {}', response.statusCode(), response.body())\n            }\n            if (response.statusCode() != 201 && response.statusCode() != 200) {\n                throw new RuntimeException(\"Failed to upload bundle: status: ${response.statusCode()} - body: ${response.body()}\")\n            }\n            def deploymentId = response.body().trim()\n            logger.lifecycle('Uploaded release bundle successfully, deployment ID: {}', deploymentId)\n            return deploymentId\n        }\n    }\n\n    @Internal\n    String getBearerAuthorizationHeader() {\n        def encoded = Base64.encoder.encodeToString(\"${credentials.get().username}:${credentials.get().password}\".getBytes(UTF_8))\n        return \"Bearer ${encoded}\"\n    }\n\n    @Internal\n    String getBasicAuthorizationHeader() {\n        def encoded = Base64.encoder.encodeToString(\"${credentials.get().username}:${credentials.get().password}\".getBytes(UTF_8))\n        return \"Basic ${encoded}\"\n    }\n\n    static HttpClient createHttpClient() {\n        return HttpClient.newBuilder()\n                .connectTimeout(Duration.ofMinutes(1))\n                .build()\n    }\n\n    enum PublishingType {\n        AUTOMATIC,\n        USER_MANAGED,\n        ;\n    }\n\n    enum DeploymentState {\n        PENDING,\n        VALIDATING,\n        VALIDATED,\n        PUBLISHING,\n        PUBLISHED,\n        FAILED,\n        ;\n    }\n\n}\n"
  },
  {
    "path": "buildSrc/src/main/groovy/halo.publish.gradle",
    "content": "plugins {\n    id 'maven-publish'\n    id 'signing'\n}\n\ndef internalRepo = layout.buildDirectory.dir('repos/internal')\n\npublishing {\n    publications.register('mavenJava', MavenPublication) {\n        pom {\n            url = 'https://github.com/halo-dev/halo'\n            licenses {\n                license {\n                    name = 'The GNU General Public License v3.0'\n                    url = 'https://www.gnu.org/licenses/gpl-3.0.en.html'\n                }\n            }\n            developers {\n                developer {\n                    id = 'johnniang'\n                    name = 'JohnNiang'\n                    email = 'johnniang@foxmail.com'\n                }\n            }\n            scm {\n                connection = 'scm:git:https://github.com/halo-dev/halo.git'\n                developerConnection = 'scm:git:ssh://git@github.com:halo-dev/halo.git'\n                url = 'https://github.com/halo-dev/halo'\n            }\n        }\n    }\n\n    repositories {\n        maven {\n            name = 'internal'\n            url = internalRepo\n        }\n    }\n}\n\nsigning {\n    sign publishing.publications.mavenJava\n}\n\ntasks.register('createBundle', Zip) {\n    group = PublishingPlugin.PUBLISH_TASK_GROUP\n    dependsOn tasks.named('publishAllPublicationsToInternalRepository')\n    from(internalRepo)\n    archiveBaseName = \"${project.group}.${project.name}\"\n}\n\ntasks.register('uploadBundle', UploadBundleTask) {\n    group = PublishingPlugin.PUBLISH_TASK_GROUP\n    credentials = providers.credentials(PasswordCredentials, \"portal\")\n    bundleFile = tasks.named('createBundle', Zip).map { it.archiveFile }.get()\n}\n\ntasks.register('cleanInternalRepo', Delete) {\n    group = PublishingPlugin.PUBLISH_TASK_GROUP\n    delete internalRepo\n}\n\ntasks.named('publishAllPublicationsToInternalRepository') {\n    dependsOn tasks.named('cleanInternalRepo')\n}\n\ntasks.named('publish') {\n    dependsOn tasks.named('uploadBundle')\n}\n"
  },
  {
    "path": "config/checkstyle/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n        \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n    Checkstyle configuration that checks the Google coding conventions from Google Java Style\n    that can be found at https://google.github.io/styleguide/javaguide.html\n\n    Checkstyle is very configurable. Be sure to read the documentation at\n    http://checkstyle.org (or in your downloaded distribution).\n\n    To completely disable a check, just comment it out or delete it from the file.\n    To suppress certain violations please review suppression filters.\n\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n -->\n\n<module name=\"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n\n    <property name=\"severity\" value=\"error\"/>\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\"/>\n    <!-- Excludes all 'module-info.java' files              -->\n    <!-- See https://checkstyle.org/config_filefilters.html -->\n    <module name=\"BeforeExecutionExclusionFileFilter\">\n        <property name=\"fileNamePattern\" value=\"module\\-info\\.java$\"/>\n    </module>\n    <!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${org.checkstyle.google.suppressionfilter.config}\"\n                  default=\"checkstyle-suppressions.xml\"/>\n        <property name=\"optional\" value=\"true\"/>\n    </module>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.org/config_whitespace.html -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\"/>\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"fileExtensions\" value=\"java\"/>\n        <property name=\"max\" value=\"100\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n        <module name=\"SuppressionCommentFilter\"/>\n        <module name=\"OuterTypeFilename\"/>\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\"/>\n            <property name=\"format\"\n                      value=\"\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\"/>\n            <property name=\"message\"\n                      value=\"Consider using special escape sequence instead of octal value or Unicode escaped value.\"/>\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\"/>\n            <property name=\"allowByTailComment\" value=\"true\"/>\n            <property name=\"allowNonPrintableEscapes\" value=\"true\"/>\n        </module>\n        <module name=\"UnusedImports\"/>\n        <module name=\"AvoidStarImport\"/>\n        <module name=\"OneTopLevelClass\"/>\n        <module name=\"NoLineWrap\">\n            <property name=\"tokens\" value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT\"/>\n        </module>\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH\"/>\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"tokens\"\n                      value=\"LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE\"/>\n        </module>\n        <module name=\"LeftCurly\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,\n                    INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,\n                    LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,\n                    OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlySame\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_DO\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"option\" value=\"alone\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,\n                    INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,\n                    COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"query\" value=\"//RCURLY[parent::SLIST[count(./*)=1]\n                                     or preceding-sibling::*[last()][self::LCURLY]]\"/>\n        </module>\n        <module name=\"WhitespaceAfter\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE\"/>\n        </module>\n        <module name=\"WhitespaceAround\">\n            <property name=\"allowEmptyConstructors\" value=\"true\"/>\n            <property name=\"allowEmptyLambdas\" value=\"true\"/>\n            <property name=\"allowEmptyMethods\" value=\"true\"/>\n            <property name=\"allowEmptyTypes\" value=\"true\"/>\n            <property name=\"allowEmptyLoops\" value=\"true\"/>\n            <property name=\"ignoreEnhancedForColon\" value=\"false\"/>\n            <property name=\"tokens\"\n                      value=\"ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,\n                    BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,\n                    LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,\n                    LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,\n                    LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,\n                    NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,\n                    SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND\"/>\n            <message key=\"ws.notFollowed\"\n                     value=\"WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"WhitespaceAround: ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"OneStatementPerLine\"/>\n        <module name=\"MultipleVariableDeclarations\"/>\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"MissingSwitchDefault\"/>\n        <module name=\"FallThrough\"/>\n        <module name=\"UpperEll\"/>\n        <module name=\"ModifierOrder\"/>\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"tokens\"\n                      value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,\n                    COMPACT_CTOR_DEF\"/>\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapDot\"/>\n            <property name=\"tokens\" value=\"DOT\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapComma\"/>\n            <property name=\"tokens\" value=\"COMMA\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->\n            <property name=\"id\" value=\"SeparatorWrapEllipsis\"/>\n            <property name=\"tokens\" value=\"ELLIPSIS\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->\n            <property name=\"id\" value=\"SeparatorWrapArrayDeclarator\"/>\n            <property name=\"tokens\" value=\"ARRAY_DECLARATOR\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapMethodRef\"/>\n            <property name=\"tokens\" value=\"METHOD_REF\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^run\\.halo(\\.[a-z][a-z0-9]*)*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Package name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"TypeName\">\n            <property name=\"tokens\" value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    ANNOTATION_DEF, RECORD_DEF\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MemberName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Member name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LambdaParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Lambda parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"CatchParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Catch parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LocalVariableName\">\n            <property name=\"severity\" value=\"warning\"/>\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Local variable name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"PatternVariableName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Pattern variable name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"severity\" value=\"warning\"/>\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Class type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"RecordTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Record type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"InterfaceTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Interface type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"NoFinalizer\"/>\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n                     value=\"GenericWhitespace ''{0}'' is followed by whitespace.\"/>\n            <message key=\"ws.preceded\"\n                     value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\"/>\n            <message key=\"ws.illegalFollow\"\n                     value=\"GenericWhitespace ''{0}'' should followed by whitespace.\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"Indentation\">\n            <property name=\"basicOffset\" value=\"4\"/>\n            <property name=\"braceAdjustment\" value=\"0\"/>\n            <property name=\"caseIndent\" value=\"4\"/>\n            <property name=\"throwsIndent\" value=\"4\"/>\n            <property name=\"lineWrappingIndentation\" value=\"4\"/>\n            <property name=\"arrayInitIndent\" value=\"4\"/>\n        </module>\n        <module name=\"AbbreviationAsWordInName\">\n            <property name=\"severity\" value=\"warning\"/>\n            <property name=\"ignoreFinal\" value=\"false\"/>\n            <property name=\"allowedAbbreviationLength\" value=\"0\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,\n                    PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,\n                    RECORD_COMPONENT_DEF\"/>\n        </module>\n        <module name=\"OverloadMethodsDeclarationOrder\"/>\n        <module name=\"VariableDeclarationUsageDistance\">\n            <property name=\"severity\" value=\"warning\"/>\n        </module>\n        <module name=\"CustomImportOrder\">\n            <property name=\"sortImportsInGroupAlphabetically\" value=\"true\"/>\n            <property name=\"separateLineBetweenGroups\" value=\"true\"/>\n            <property name=\"customImportOrderRules\" value=\"STATIC###THIRD_PARTY_PACKAGE\"/>\n            <property name=\"tokens\" value=\"IMPORT, STATIC_IMPORT, PACKAGE_DEF\"/>\n        </module>\n        <module name=\"MethodParamPad\">\n            <property name=\"tokens\"\n                      value=\"CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,\n                    SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF\"/>\n        </module>\n        <module name=\"NoWhitespaceBefore\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, POST_INC, POST_DEC, DOT,\n                    LABELED_STAT, METHOD_REF\"/>\n            <property name=\"allowLineBreaks\" value=\"true\"/>\n        </module>\n        <module name=\"ParenPad\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,\n                    EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,\n                    METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,\n                    RECORD_DEF\"/>\n        </module>\n        <module name=\"OperatorWrap\">\n            <property name=\"option\" value=\"NL\"/>\n            <property name=\"tokens\"\n                      value=\"BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,\n                    LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF \"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationMostCases\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,\n                      RECORD_DEF, COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationVariables\"/>\n            <property name=\"tokens\" value=\"VARIABLE_DEF\"/>\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\"/>\n        </module>\n        <module name=\"NonEmptyAtclauseDescription\"/>\n        <module name=\"InvalidJavadocPosition\"/>\n        <module name=\"JavadocTagContinuationIndentation\">\n            <property name=\"severity\" value=\"warning\"/>\n        </module>\n        <module name=\"SummaryJavadoc\">\n            <property name=\"severity\" value=\"warning\"/>\n            <property name=\"forbiddenSummaryFragments\"\n                      value=\"^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )\"/>\n        </module>\n        <module name=\"JavadocParagraph\">\n            <property name=\"severity\" value=\"warning\"/>\n        </module>\n        <module name=\"RequireEmptyLineBeforeBlockTagGroup\"/>\n        <module name=\"AtclauseOrder\">\n            <property name=\"tagOrder\" value=\"@param, @return, @throws, @deprecated\"/>\n            <property name=\"target\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\"/>\n        </module>\n        <module name=\"JavadocStyle\"/>\n        <module name=\"JavadocMethod\">\n            <property name=\"allowMissingParamTags\" value=\"true\"/>\n            <property name=\"allowMissingReturnTag\" value=\"true\"/>\n            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>\n            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"MissingJavadocMethod\">\n            <property name=\"severity\" value=\"warning\"/>\n            <property name=\"scope\" value=\"public\"/>\n            <property name=\"minLineCount\" value=\"2\"/>\n            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>\n            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,\n                                   COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9_]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"SingleLineJavadoc\">\n            <property name=\"ignoreInlineTags\" value=\"false\"/>\n        </module>\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"expected\"/>\n        </module>\n        <module name=\"CommentsIndentation\">\n            <property name=\"tokens\" value=\"SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN\"/>\n        </module>\n        <!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->\n        <module name=\"SuppressionXpathFilter\">\n            <property name=\"file\" value=\"${org.checkstyle.google.suppressionxpathfilter.config}\"\n                      default=\"checkstyle-xpath-suppressions.xml\"/>\n            <property name=\"optional\" value=\"true\"/>\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "docs/authentication/README.md",
    "content": "# Halo 认证方式\n\n目前 Halo 支持的认证方式有：\n\n- 基本认证（Basic Auth）\n- 表单登录（Form Login）\n\n计划支持的认证方式有：\n\n- [个人令牌认证（Personal Access Token）](https://github.com/halo-dev/halo/issues/1309)\n- [OAuth2](https://oauth.net/2/)\n\n## 基本认证\n\n这是最简单的一种认证方式，通过简单设置 HTTP 请求头 `Authorization: Basic xxxyyyzzz==` 即可实现认证，访问 Halo API，例如：\n\n```bash\n╰─❯ curl -u \"admin:P@88w0rd\" -H \"Accept: application/json\" http://localhost:8090/api/v1alpha1/users\n\n或者\n╰─❯ echo -n \"admin:P@88w0rd\" | base64\nYWRtaW46UEA4OHcwcmQ=\n╰─❯ curl -H \"Authorization: Basic YWRtaW46UEA4OHcwcmQ=\" -H \"Accept: application/json\" http://localhost:8090/api/v1alpha1/users\n```\n\n## 表单认证\n\n这是一种比较常用的认证方式，只需提供用户名和密码以及 `CSRF 令牌`（用于防止重复提交和跨站请求伪造）。\n\n- 表单参数\n\n  | 参数名     | 类型   | 说明                                  |\n                                                                                                            | ---------- | ------ | ------------------------------------- |\n  | username   | form   | 用户名                                |\n  | password   | form   | 密码                                  |\n  | _csrf      | form   | `CSRF` 令牌。由客户端随机生成。       |\n  | XSRF-TOKEN | cookie | 跨站请求伪造令牌，和 `_csrf` 的值一致 |\n\n- HTTP 200 响应\n\n  仅在请求头 `Accept` 中包含 `application/json` 时发生，响应示例如下所示：\n\n    ```bash\n    ╰─❯ curl 'http://localhost:8090/login' \\\n      -H 'Accept: application/json' \\\n      -H 'Cookie: XSRF-TOKEN=1ff67e0c-6f2c-4cf9-afb5-81bc1015b8e5' \\\n      -H 'Content-Type: application/x-www-form-urlencoded' \\\n      --data-raw '_csrf=1ff67e0c-6f2c-4cf9-afb5-81bc1015b8e5&username=admin&password=P@88w0rd'\n    ```\n\n    ```bash\n    < HTTP/1.1 200 OK\n    < Vary: Origin\n    < Vary: Access-Control-Request-Method\n    < Vary: Access-Control-Request-Headers\n    < Content-Type: application/json\n    < Content-Length: 161\n    < Cache-Control: no-cache, no-store, max-age=0, must-revalidate\n    < Pragma: no-cache\n    < Expires: 0\n    < X-Content-Type-Options: nosniff\n    < X-Frame-Options: DENY\n    < X-XSS-Protection: 1 ; mode=block\n    < Referrer-Policy: no-referrer\n    < Set-Cookie: SESSION=d04db9f7-d2a6-4b7c-9845-ef790eb4a980; Path=/; HttpOnly; SameSite=Lax\n    ```\n\n    ```json\n    {\n      \"username\": \"admin\",\n      \"authorities\": [\n        {\n          \"authority\": \"ROLE_super-role\"\n        }\n      ],\n      \"accountNonExpired\": true,\n      \"accountNonLocked\": true,\n      \"credentialsNonExpired\": true,\n      \"enabled\": true\n    }\n    ```\n\n- HTTP 302 响应\n\n  仅在请求头 `Accept` 中不包含 `application/json`才会发生，响应示例如下所示：\n\n  ```bash\n  ╰─❯ curl 'http://localhost:8090/login' \\\n    -H 'Accept: */*' \\\n    -H 'Cookie: XSRF-TOKEN=1ff67e0c-6f2c-4cf9-afb5-81bc1015b8e5' \\\n    -H 'Content-Type: application/x-www-form-urlencoded' \\\n    --data-raw '_csrf=1ff67e0c-6f2c-4cf9-afb5-81bc1015b8e5&username=admin&password=P@88w0rd'\n  ```\n\n  ```bash\n  < HTTP/1.1 302 Found\n  < Vary: Origin\n  < Vary: Access-Control-Request-Method\n  < Vary: Access-Control-Request-Headers\n  < Location: /console/\n  < Cache-Control: no-cache, no-store, max-age=0, must-revalidate\n  < Pragma: no-cache\n  < Expires: 0\n  < X-Content-Type-Options: nosniff\n  < X-Frame-Options: DENY\n  < X-XSS-Protection: 1 ; mode=block\n  < Referrer-Policy: no-referrer\n  < Set-Cookie: SESSION=9ce6ad3f-7eba-4de5-abca-650b4721c7ac; Path=/; HttpOnly; SameSite=Lax\n  < content-length: 0\n  ```\n\n未来计划支持“记住我（Remember Me）”功能。\n\n## Personal Access Token\n\n### 背景\n\nHalo 是一款现代化的开源 CMS / 建站系统，为了便于开发者和用户利用 API 访问网站数据，Halo 支持了 Personal Access Token（以下简称\nPAT）功能。\n用户可以在 Halo 的后台生成 PAT，它是一个随机字符串，用于在 API 请求头里提供验证身份用。Halo 后端在接收请求时会校验 PAT\n的值，如果匹配就会允许访问相应的 API 数据。\n这种 PAT 机制避免了直接使用用户名密码的安全隐患，开发者可以为每个 PAT 设置访问范围、过期时间等。同时使用随机 PAT\n也增加了安全性。这为开发 Halo 插件和应用提供了更安全简便的认证方式。\n相比直接暴露服务端 API，这种 PAT 机制也更标准化和安全可控。Halo 在参考业内主流做法的基础上，引入了 PAT，以便于生态系统的开放与丰富。\n\n### 设计\n\nPAT 以 `pat_` 开头，剩余部分为随机字符串，随机字符串可以是 [JWT](https://datatracker.ietf.org/doc/html/rfc7519)、UUID\n或其他经过加密的随机字符串。目前，Halo 的实现是 `pat_` + `JWT` 的形式，例如：\n\n```text\npat_eyJraWQiOiJabUNtcWhJX2FuaFlWQW5aRlVTS0lOckxXRFhqaEp1Nk9ZRGRtcW13Rno4IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInJvbGVzIjpbInN1cGVyLXJvbGUiXSwicGF0X25hbWUiOiJwYXQtYWRtaW4tSVdvbFEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwOTAvIiwiZXhwIjoxNjk0NjcyMDc5LCJpYXQiOjE2OTQ1ODU3MjAsImp0aSI6IjE3ZWFkNzlkLTRkMjctYjg4NS02YjAzLTM4Y2JlYzQxMmFlMyJ9.xiq36NZIM3_ynBx-l0scGdfX-89aJi6uV7HJz_kNnuT78CFmxD-XTpncK1E-hqPdQSrSwyG4gT1pVO17UmUCoyoAkZKKKVk_seFwxdbygIueo2UJA5kVw1Naf_6iLtNkAXxAiYUpd8ihIwvVedhmOMQ9UUfd4QKZDR1XnTW4EAteWBi7b0pWqSa4h5lv7TpmAECY_KDAGrBRGGhc9AxsrGYPNZo68n2QGJ5BjH29vfdQaZz4vwsgKxG1WJ9Y7c8cQI9JN8EyQD_n560NWAaoFnRi1qL3nexvhjq8EVyGVyM48aKA02UcyvI9cxZFk6ZgnzmUsMjyA6ZL7wuexkujVqmc3iO5plBDCjW7oMe1zPQq-gEJXJU6gdr_SHcGG1BjamoekCkOeNT3CPzA_-5j3AVlj7FTFQkbn_h-kV07mfNO45BVVKsMb08HrN6iEk7TOX7SxN0s2gFc3xYVcXBMveLtftOfXs04SvSFCfTDeJH_Jy-3lYb_GLOji7xSc6FgRbuAwmzHLlsgBT4NJhR_0dZ-jNsCDIQCIC3iDc0qbcNTJYYocT77YaQzIkleFIXyPiV0RsNPmSTEDGiDlctsZ-AmcGCDQ-UmW8SIFBrA93OHncvb47o0-uBwZLdF_we4S90hJlNiAPVhhrBMtCoTJotyrODMEzwbLIukvewFXp8\n```\n\n示例 Token 中 JWT 部分所对应的 Header 如下：\n\n```json\n{\n  \"kid\": \"ZmCmqhI_anhYVAnZFUSKINrLWDXjhJu6OYDdmqmwFz8\",\n  \"alg\": \"RS256\"\n}\n```\n\nPayload 如下：\n\n```json\n{\n  \"sub\": \"admin\",\n  \"roles\": [\n    \"super-role\"\n  ],\n  \"pat_name\": \"pat-admin-IWolQ\",\n  \"iss\": \"http://localhost:8090/\",\n  \"exp\": 1694672079,\n  \"iat\": 1694585720,\n  \"jti\": \"17ead79d-4d27-b885-6b03-38cbec412ae3\"\n}\n```\n\n### 使用方式\n\n#### 生成 PAT\n\nHalo 专门提供了生成 PAT 的端口：`/apis/api.console.security.halo.run/v1alpha1/users/-/personalaccesstokens`。创建 PAT\n请求示例如下：\n\n```shell\ncurl -u admin:admin -X 'POST' \\\n  'http://localhost:8090/apis/api.console.security.halo.run/v1alpha1/users/-/personalaccesstokens' \\\n  -H 'accept: application/json' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n  \"spec\": {\n    \"name\": \"My PAT\",\n    \"description\": \"This is my first PAT.\",\n    \"expiresAt\": \"2023-09-15T02:42:35.136Z\"\n    \"roles\": [\"\"]\n  }\n}'\n```\n\n```json\n{\n  \"spec\": {\n    \"description\": \"This is my first PAT.\",\n    \"expiresAt\": \"2023-09-16T02:42:35.136Z\",\n    \"roles\": [],\n    \"username\": \"admin\",\n    \"revoked\": false,\n    \"tokenId\": \"0b897d9c-56d7-5541-2662-110b70e3f9fd\"\n  },\n  \"apiVersion\": \"security.halo.run/v1alpha1\",\n  \"kind\": \"PersonalAccessToken\",\n  \"metadata\": {\n    \"generateName\": \"pat-admin-\",\n    \"name\": \"pat-admin-lobkm\",\n    \"annotations\": {\n      \"security.halo.run/access-token\": \"pat_eyJraWQiOiJabUNtcWhJX2FuaFlWQW5aRlVTS0lOckxXRFhqaEp1Nk9ZRGRtcW13Rno4IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInJvbGVzIjpbXSwicGF0X25hbWUiOiJwYXQtYWRtaW4tbG9ia20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwOTAvIiwiZXhwIjoxNjk0ODMyMTU1LCJpYXQiOjE2OTQ3NDcyOTgsImp0aSI6IjBiODk3ZDljLTU2ZDctNTU0MS0yNjYyLTExMGI3MGUzZjlmZCJ9.UVFYzKmz3bUk7fV6xh_CpuNJA-BR8bci-DIJ7o0fk-hayHXFHr_-7HMrVn7iZcphryqmk0RLv7Zsu_AjY9Qn9iCYybBJBycU0tUJzhDexRtj1ViJtlsraoYxLNSYpJK1hcPngeJuiMa9FZrYGp0k_7GX1NddoXLUBI9orN9DbdKmmJXtvigaxPCp52Mu7fBtVsTmO5fk_y2CglqRl_tkLRpFSgUbERKOqKItctDFRg-WUALBYEpXbhZIXBMuTCsJwhniBMpc1Uu_a1Dqa3K5hDgfHTeUADY2BuhEdYJCODPCzmdfWMNqxYSKQT5JFYoDv-ed6cRqNjKeNvd1IPT3RDkVt_fbo8KPrzvkgIjIzni-Wlwe-pXXQbj_n8iax-jkeK526iu8q2CLptxYxLGD0j8htKZramrov4UkK_eIsotEZZfqig9sYVU5_b442WhOWatdB_pbKj7h-YK1Cb2ueg5kl73bcbBu63b8edJZClp6xr72az343SfBZdwrT_JJ5HR0hJmckAMR_U4qvGWrJ-dobXDgY9Oz-qObfiyglzn0Wrz4HRPlmqDFr2o6TMV7UVjQiV77tDzaNbaXVevXGPS5MaZr313dia7XLpIV3QopXma7rDR6Xnqg7ftDQb5vAvsjwN-JsVabAsdFeCo6ejE1slAD9ZQrD88kgfAIuX4\"\n    },\n    \"version\": 0,\n    \"creationTimestamp\": \"2023-09-15T03:08:18.875350Z\"\n  }\n}\n```\n\n请求体说明如下表所示：\n\n| 属性名         | 描述                                                                                                 |\n|-------------|----------------------------------------------------------------------------------------------------|\n| name        | PAT 名称。必填。                                                                                         |\n| description | PAT 描述。非必填。                                                                                        |\n| expiresAt   | PAT 过期时间，一旦创建不可修改，或修改无效。如果不填写，则表示 PAT 无过期时间。                                                       |\n| roles       | 授权给 PAT 的角色，必须包含在当前用户所拥有的角色内。如果设置为 `null` 或者 `[]`，则表示当前 PAT 仅会拥有 `anonymous` 和 `authenticated` 角色。 |\n\n响应体说明如下所示：\n\n| 属性路径                                                | 描述                                           |\n|-----------------------------------------------------|----------------------------------------------|\n| security.halo.run/access-token | 生成好的 PAT。需要注意的是，这个 PAT 不会保存在数据库中，所以仅有一次保存机会。 |\n\n#### 使用 PAT\n\n向 Halo 发送请求时，携带 Header：`Authorization: Bearer $PAT` 即可。示例如下：\n\n```shell\ncurl http://localhost:8090/apis/api.console.halo.run/v1alpha1/users/- \\\n  -H \"Authorization: Bearer pat_eyJraWQiOiJabUNtcWhJX2FuaFlWQW5aRlVTS0lOckxXRFhqaEp1Nk9ZRGRtcW13Rno4IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInJvbGVzIjpbXSwicGF0X25hbWUiOiJwYXQtYWRtaW4tbG9ia20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwOTAvIiwiZXhwIjoxNjk0ODMyMTU1LCJpYXQiOjE2OTQ3NDcyOTgsImp0aSI6IjBiODk3ZDljLTU2ZDctNTU0MS0yNjYyLTExMGI3MGUzZjlmZCJ9.UVFYzKmz3bUk7fV6xh_CpuNJA-BR8bci-DIJ7o0fk-hayHXFHr_-7HMrVn7iZcphryqmk0RLv7Zsu_AjY9Qn9iCYybBJBycU0tUJzhDexRtj1ViJtlsraoYxLNSYpJK1hcPngeJuiMa9FZrYGp0k_7GX1NddoXLUBI9orN9DbdKmmJXtvigaxPCp52Mu7fBtVsTmO5fk_y2CglqRl_tkLRpFSgUbERKOqKItctDFRg-WUALBYEpXbhZIXBMuTCsJwhniBMpc1Uu_a1Dqa3K5hDgfHTeUADY2BuhEdYJCODPCzmdfWMNqxYSKQT5JFYoDv-ed6cRqNjKeNvd1IPT3RDkVt_fbo8KPrzvkgIjIzni-Wlwe-pXXQbj_n8iax-jkeK526iu8q2CLptxYxLGD0j8htKZramrov4UkK_eIsotEZZfqig9sYVU5_b442WhOWatdB_pbKj7h-YK1Cb2ueg5kl73bcbBu63b8edJZClp6xr72az343SfBZdwrT_JJ5HR0hJmckAMR_U4qvGWrJ-dobXDgY9Oz-qObfiyglzn0Wrz4HRPlmqDFr2o6TMV7UVjQiV77tDzaNbaXVevXGPS5MaZr313dia7XLpIV3QopXma7rDR6Xnqg7ftDQb5vAvsjwN-JsVabAsdFeCo6ejE1slAD9ZQrD88kgfAIuX4\"\n```\n"
  },
  {
    "path": "docs/backup-and-restore.md",
    "content": "# 备份和恢复 Proposal\n\n## Motivation\n\n目前，Halo 2.x 支持多种数据库：H2、MySQL、MariaDB、Microsoft SQL Server、Oracle 和\nPostgreSQL，虽然数据库有备份和恢复的功能，但是仍然缺少应用级别的备份和恢复功能。Halo\n的数据不仅限于数据库中的数据，还包含工作目录下的数据，例如主题、插件和日志等。\n\n## Goals\n\n- 全站备份，包括数据库中的数据和工作目录的数据。\n- 全站恢复，包括恢复数据库中的数据和工作目录的数据。\n- 用户可控制备份文件存储的时间。\n- 对于工作目录的数据，用户可选择性备份和恢复。\n- 用户可指定备份权限到任意用户。\n\n## Non-Goals\n\n- 仅备份部分自定义资源。\n- 仅备份和恢复文章 Markdown。\n- 定时备份。\n- 加密备份文件。\n- 备份文件自动上传至对象存储。\n\n## Use Cases\n\n- 从某种数据库（例如：H2）迁移至另外的数据库（例如：MySQL），不会因为 SQL 的兼容性而影响迁移。\n- 定时完整备份 Halo，并存储至对象存储，一旦发生意外可随时恢复。\n\n## Requirements\n\n- 仅支持 2.8.x 及以上的 Halo。\n- 恢复的数据的 creationTimestamp 可能会被当前时间覆盖。\n\n## Draft\n\n恢复数据之前需要完整备份当前 Halo，以便恢复过程中发生错误导致无法回滚。\n\n备份文件将存储在 `${halo.work-dir}/backups/halo-full-backup-2023.07.03-17:52:59.zip`。\n\n备份整站可能需要大量的时间，所以我们需要创建自定义模型（Backup）用于保存用户创建备份的请求，并异步执行备份操作，最终将结果反馈至自定义模型数据中。\n\nBackup 模型样例如下：\n\n- 备份成功样例\n\n```yaml\napiVersion: migration.halo.run/v1alpha1\nkind: Backup\nmetadata:\n  name: halo-full-backup-xyz\n  creationTimestamp: 2023.07.04-10:25:30\nspec:\n  format: zip\n  autoDeleteWhen: 2023.07.10-00:00:00Z\nstatus:\n  phase: Succeeded\n  startTimestamp: 2023.07.04-10:25:31\n  completionTimestamp: 2023.07.04-10:26:30\n  filename: halo-full-backup-2023-07-04-10-25-30.zip\n  size: 1024 # data unit: bytes\n```\n\n- 备份失败样例\n\n```yaml\napiVersion: migration.halo.run/v1alpha1\nkind: Backup\nmetadata:\n  name: halo-full-backup-xyz\n  creationTimestamp: 2023.07.04-10:25:30\nspec:\n  compressionFormat: zip | 7z | tar | tar.gz # 压缩格式\nstatus:\n  startTimestamp: 2023.07.04-10:25:31\n  # Pending: 刚刚创建好 Backup 资源，等待 Reconciler reconcile。\n  # Running: Reconciler 正在备份 Halo。\n  # Succeeded: Reconciler 成功执行备份 Halo 操作。\n  # Failed: 备份 Halo 失败。\n  phase: Failed\n  failureReason: DatabaseConnectionReset | UnsupportedCompression # 机器可识别的信息\n  failureMessage: The database connection reset. # 人类可阅读的信息\n```\n\n同时，BackupReconciler 将负责备份操作，并更新 Backup 数据。\n\n请求示例如下：\n\n```text\nPOST /apis/migration.halo.run/v1alpha1/backups\nContent-Type: application/json\n```\n\n### 备份\n\n准备好所有的备份内容后，需要计算摘要并保存，以便后期恢复校验备份文件完整性使用。\n\n#### 数据库备份和恢复\n\n因为 Halo 的 [Extension 设计](https://github.com/halo-dev/rfcs/tree/main/extension)，所以 Halo 的在数据库中的数据备份相对比较简单，只需要简单备份\nExtensionStore 即可。恢复同理。\n\n#### 工作目录备份和恢复\n\nHalo 工作目录样例如下所示：\n\n```text\n├── application.yaml\n├── attachments\n│   └── upload\n│       └── image_2023-06-09_16-24-41.png\n├── db\n│   └── halo-next.mv.db\n├── indices\n│   └── posts\n│       ├── _a.cfe\n│       ├── _a.cfs\n│       ├── _a.si\n│       ├── segments_h\n│       └── write.lock\n├── keys\n│   ├── id_rsa\n│   └── id_rsa.pub\n├── logs\n│   ├── halo.log\n│   ├── halo.log.2023-06-01.0.gz\n│   ├── halo.log.2023-06-02.0.gz\n│   ├── halo.log.2023-06-05.0.gz\n│   └── halo.log.2023-06-26.0.gz\n├── plugins\n│   ├── PluginCommentWidget-1.5.0.jar\n│   ├── PluginFeed-1.1.1.jar\n│   ├── PluginSearchWidget-1.0.0.jar\n│   ├── PluginSitemap-1.0.2.jar\n│   └── configs\n└── themes\n    ├── theme-earth\n    │   ├── README.md\n    │   ├── settings.yaml\n    │   ├── templates\n    │   │   ├── archives.html\n    │   │   ├── assets\n    │   │   │   ├── dist\n    │   │   │   │   ├── main.iife.js\n    │   │   │   │   └── style.css\n    │   │   │   └── images\n    │   │   │       ├── default-avatar.svg\n    │   │   │       └── default-background.png\n    │   │   ├── author.html\n    │   │   ├── category.html\n    │   │   ├── error\n    │   │   │   └── error.html\n    │   │   ├── index.html\n    │   │   ├── links.html\n    │   │   ├── modules\n    │   │   │   ├── category-filter.html\n    │   │   │   ├── category-tree.html\n    │   │   │   ├── featured-post-card.html\n    │   │   │   ├── footer.html\n    │   │   │   ├── header.html\n    │   │   │   ├── hero.html\n    │   │   │   ├── layout.html\n    │   │   │   ├── post-card.html\n    │   │   │   ├── sidebar.html\n    │   │   │   ├── tag-filter.html\n    │   │   │   └── widgets\n    │   │   │       ├── categories.html\n    │   │   │       ├── latest-comments.html\n    │   │   │       ├── popular-posts.html\n    │   │   │       ├── profile.html\n    │   │   │       └── tags.html\n    │   │   ├── page.html\n    │   │   ├── post.html\n    │   │   ├── tag.html\n    │   │   └── tags.html\n    │   └── theme.yaml\n```\n\n备份时需要过滤 `db`、backups` 和 `indices` 目录。\n\n#### 备份文件结构\n\n备份文件主要包含自定义资源（`extensions.data`）和工作目录（`workdir.data`）的数据。\n\n- `extensions.data`\n\n前期可考虑使用 JSON 来存储所有的 ExtensionStore 数据。\n\n- `workdir.data`\n\n对工作目录进行 `ZIP` 压缩。\n\n- config.yaml（备份配置）\n\n主要用于描述 `extensions.data` 和 `workdir.data` 压缩格式，后续可扩展备份与恢复相关的配置。例如：\n\n```yaml\ncompressions:\n  extensions: json | others\n  workdir: zip | others\n```\n\n前期可不实现该功能。\n\n### 恢复\n\n用户通过上传备份文件的方式进行恢复。当且仅当博客未初始化阶段才能进行恢复操作，否则可能会造成数据不一致。\n\n请求示例如下：\n\n```text\nPOST /apis/migration.halo.run/v1alpha1/restorations\nContent-Type: multipart/form-data; boundary=\"boundary\"\n\n'''\n--boundary\nContent-Disposition: form-data; name=\"backupfile\"; filename=\"halo-full-backup.zip\"\nContent-Type: application/zip\n'''\n```\n\n恢复步骤如下：\n\n1. 解压缩备份文件。\n2. 校验备份文件的完整性。\n2. 恢复所有 ExtensionStore。\n3. 覆盖当前工作目录。\n4. 备份完成。\n\n> 需要注意内存占用问题。\n\n## TBDs\n\n- 数据备份期间可能会存在数据的创建、更新和删除。\n\n我们将忽略这些数据变化。\n\n- 是否支持在初始化博客后恢复数据？\n\n支持。不过可能会覆盖掉已有的数据。\n"
  },
  {
    "path": "docs/cache/page.md",
    "content": "# 缓存\n\n缓存在各个领域用得非常广泛，例如 CPU 的三级缓存，可加速从主内存中获取数据到处理器。Halo 的主要应用以博客为主，页面更新不会特别频繁，大多数情况下，实时渲染的结果都是没有变化的。如果能够缓存这些不经常变更的页面，可减少数据库访问，加快访问速度。\n\nHalo 采用由 Spring 框架提供的 Caching 作为缓存框架。该缓存框架面对各种缓存实现，提供了统一的访问入口，后续更换缓存仅需修改少量代码和配置。\n\nHalo 默认提供了 CacheProperties 用于启用/禁用缓存，示例如下：\n\n```yaml\nhalo:\n  caches:\n    page:\n      disabled: true\n    others:\n      disabled: false\n```\n\n# 页面缓存\n\n页面缓存包括缓存响应体、响应头和响应状态。页面缓存规则如下：\n\n1. 仅缓存模板引擎所渲染的页面。\n2. 仅缓存 `Content-Type` 为 `text/html` 的页面。\n3. 仅缓存响应状态为 `HTTP 200（OK）`。\n4. 请求访问为 `GET`。\n\n缓存详情见下表：\n\n| 术语   | 值              |\n|------|----------------|\n| 名称   | `page`         |\n| 失效时间 | 距最近一次访问 `1` 小时 |\n| 缓存数量 | `10,000` 个     |\n"
  },
  {
    "path": "docs/developer-guide/custom-endpoint.md",
    "content": "# 系统自定义 API\n\n系统自定义 API 是一组特殊的 API，因为自定义模型 API 无法满足要求，需要开发者自己实现。\n\n但是系统自定义 API 有一个统一的前缀：`/apis/api.console.halo.run/v1alpha1/`，剩余的部分可随意定义。\n\n## 如何在系统中创建一个系统自定义 API\n\n1. 实现 `run.halo.app.core.extension.endpoint.CustomEndpoint` 接口\n2. 将实现类设置为 Spring Bean\n\n关于用户的自定义 API 实现类如下：\n\n```java\n\n@Component\npublic class UserEndpoint implements CustomEndpoint {\n\n    private final ExtensionClient client;\n\n    public UserEndpoint(ExtensionClient client) {\n        this.client = client;\n    }\n\n    Mono<ServerResponse> me(ServerRequest request) {\n        return ReactiveSecurityContextHolder.getContext()\n                .map(ctx -> {\n                    var name = ctx.getAuthentication().getName();\n                    return client.fetch(User.class, name)\n                            .orElseThrow(() -> new ExtensionNotFoundException(name));\n                })\n                .flatMap(user -> ServerResponse.ok()\n                        .contentType(MediaType.APPLICATION_JSON)\n                        .bodyValue(user));\n    }\n\n    @Override\n    public RouterFunction<ServerResponse> endpoint() {\n        return SpringdocRouteBuilder.route()\n                .GET(\"/users/-\", this::me, builder -> builder.operationId(\"GetCurrentUserDetail\")\n                        .description(\"Get current user detail\")\n                        .tag(\"api.console.halo.run/v1alpha1/User\")\n                        .response(responseBuilder().implementation(User.class)))\n                // 这里可添加其他自定义 API\n                .build();\n    }\n}\n```\n\n这样我们就可以启动 Halo，访问 Swagger UI 文档地址，并进行测试。\n"
  },
  {
    "path": "docs/developer-guide/plugin-configuration-properties.md",
    "content": "# 插件外部配置\n\n插件外部配置功能允许用户在特定目录添加插件相关的配置，插件启动的时候能够自动读取到该配置。\n\n## 配置优先级\n\n> 优先级从上到下由高到低。\n\n1. `${halo.work-dir}/plugins/configs/${plugin-id}.{yaml|yml}`\n2. `classpath:/config.{yaml|yml}`\n\n插件开发者可在 `Class Path` 下 添加 `config.{yaml|yml}` 作为默认配置。当 `.yaml` 和 `.yml` 同时出现时，以 `.yml` 的配置将会被忽略。\n\n## 插件中定义配置并使用\n\n- `src/main/java/my/plugin/MyPluginProperties.java`\n\n    ```java\n    @Data\n    @ConfigurationProperties\n    public class MyPluginProperties {\n    \n        private String encryptKey;\n    \n        private String certPath;\n    }\n    ```\n\n- `src/main/java/my/plugin/MyPluginConfiguration.java`\n\n    ```java\n    @EnableConfigurationProperties(MyPluginProperties.class)\n    @Configuration\n    public class MyPluginConfiguration {\n        \n    }\n    ```\n\n- `src/main/java/my/plugin/MyPlugin.java`\n\n    ```java\n    @Component\n    @Slf4j\n    public class MyPlugin extends BasePlugin {\n    \n        private final MyPluginProperties storeProperties;\n    \n        public MyPlugin(PluginWrapper wrapper, MyPluginProperties storeProperties) {\n            super(wrapper);\n            this.storeProperties = storeProperties;\n        }\n    \n        @Override\n        public void start() {\n            log.info(\"My plugin properties: {}\", storeProperties);\n        }\n    }\n    ```\n\n- `src/main/resources/config.yaml`\n\n    ```yaml\n    encryptKey: encrytkey==\n    certPath: /path/to/cert\n    ```\n\n## 插件使用者配置\n\n- `${halo.work-dir}/plugins/configs/${plugin-id}.{yaml|yml}`\n\n    ```yaml\n    encryptKey: override encrytkey==\n    certPath: /another/path/to/cert\n    ```\n\n## 可能存在的问题\n\n- 增加未来实现\"集群\"架构的难度。\n"
  },
  {
    "path": "docs/email-verification/README.md",
    "content": "## 背景\n\n在 Halo 中，邮箱作为用户主要的身份识别和通信方式，不仅有助于确保用户提供的邮箱地址的有效性和所有权，还对于减少滥用行为、提高账户安全性以及确保用户可以接收重要通知（如密码重置、注册新账户、确认重要操作等）至关重要。\n\n邮箱验证是用户管理过程中的一个关键组成部分，可以帮助维护了一个健康、可靠的用户基础，并且为系统管理员提供了一个额外的安全和管理手段，因此实现一个高效、安全且用户友好的邮箱验证功能至关重要。\n\n## 需求\n\n1. **用户注册验证**：确保新用户在注册过程中提供有效的邮箱地址。邮箱验证作为新用户激活其账户的必要步骤，有助于减少虚假账户和提升用户的整体质量。\n2. **密码重置和安全操作**：在用户忘记密码或需要重置密码时，向已验证的邮箱地址发送密码重置链接来确保安全性。\n3. **用户通知**：验证邮箱地址有助于确保用户可以接收到重要通知，如文章被评论、有新回复等。\n\n## 目标\n\n- 支持用户在修改邮箱后支持重新进行邮箱验证。\n- 允许用户在未收到邮件或邮件过期时重新请求发送验证邮件。\n- 避免邮件通知被滥用，如频繁发送验证邮件，需要添加限制。\n- 验证码过期机制，以确保验证邮件的有效性和安全性。\n\n## 非目标\n\n- 不考虑用户多邮箱地址的验证。\n\n## 方案\n\n### EmailVerificationManager\n\n通过使用 guava 提供的 Cache 来实现一个 EmailVerificationManager 来管理邮箱验证的缓存。\n\n```java\nclass EmailVerificationManager {\n  private final Cache<UsernameEmail, Verification> emailVerificationCodeCache =\n          CacheBuilder.newBuilder()\n                  .expireAfterWrite(CODE_EXPIRATION_MINUTES, TimeUnit.MINUTES)\n                  .maximumSize(10000)\n                  .build();\n\n  private final Cache<UsernameEmail, Boolean> blackListCache = CacheBuilder.newBuilder()\n          .expireAfterWrite(Duration.ofHours(1))\n          .maximumSize(1000)\n          .build();\n\n  record UsernameEmail(String username, String email) {\n  }\n\n  @Data\n  @Accessors(chain = true)\n  static class Verification {\n    private String code;\n    private AtomicInteger attempts;\n  }\n}\n```\n\n当用户请求发送验证邮件时，会生成一个随机的验证码，并将其存储在缓存中，默认有效期为 10 分钟，当十分钟内用户未验证成功，验证码会自动过期被缓存清除。\n\n用户可以在十分钟内重新请求发送验证邮件，此时会生成一个新的验证码有效期依然为 10 分钟。但会限制用户发送频率，同一个用户的邮箱发送验证邮件的时间间隔不得小于\n1 分钟，以防止滥用。\n\n当用户请求验证邮箱时，会从缓存中获取验证码，如果验证码不存在或已过期，会提示验证码无效或已过期，如果验证码存在且未过期，会进行验证码的比对，如果验证码不正确，会提示验证码无效，如果验证码正确，会将用户邮箱地址标记为已验证，并从缓存中清除验证码。\n\n如果用户反复使用 code 验证邮箱，会记录失败次数，如果达到了默认的最大尝试次数（默认为 5 次），将被加入黑名单，需要 1\n小时后才能重新验证邮件。\n\n根据上述规则：\n\n- 每个验证码有10分钟的有效期。\n- 在这10分钟内，如果失败次数超过5次，用户会被加入黑名单，禁止验证1小时。\n- 如果在10分钟内尝试了5次且失败，然后请求重新发送验证码，可以再次尝试5次。\n\n那么：\n\n- 在不触发黑名单的情况下，每10分钟可以尝试5次。\n- 一小时内，可以尝试 (60/10) * 5 = 30 次，前提是每10分钟都请求一次新的验证码。\n- 但是，如果在任何10分钟内尝试超过5次，则会被禁止1小时。\n\n因此，为了最大化尝试次数而不触发黑名单，每小时可以尝试 30 次，预计一天内（24h）最多可以尝试 720 次验证码。\n验证码的组成为随机的 6 为数字，可能组合总数：一个 6 位数字的验证码可以从 000000 到 999999，总共有 10 <sup>6</sup> 种可能的组合。\n10 <sup>6</sup> / 720 = 1388，因此，预计最坏情况下需要 1388 天可以破解验证码。这个时间足够长，可以认为非常安全的。\n\n### 提供 APIs 用于处理验证请求\n\n- `POST /apis/v1alpha1/users/-/send-verification-email`：用于请求发送验证邮件来验证邮箱地址。\n- `POST /apis/v1alpha1/users/-/verify-email`：用于根据邮箱验证码来验证邮箱地址。\n\n以上两个 APIs 认证用户都可以访问，但会对请求进行限制，请求间隔不得小于 1 分钟，以防止滥用。\n\n并且会在用户个人资料 API 中添加 emailVerified 字段，用于标识用户邮箱是否已验证。\n\n### 验证码邮件通知\n\n只会通过用户请求验证的邮箱地址发送验证邮件，并且提供了以下变量用户自定义通知模板：\n\n- **username**: 请求验证邮件地址的用户名。\n- **code**: 验证码。\n- **expirationAtMinutes**: 验证码过期时间（分钟）。\n\n验证邮件默认模板示例内容如下:\n\n```markdown\nguqing 你好：\n\n使用下面的动态验证码（OTP）验证您的电子邮件地址。\n\n277436\n\n动态验证码的有效期为 10 分钟。如果您没有尝试验证您的电子邮件地址，请忽略此电子邮件。\n\nguqing's blog\n```\n\n### 安全和异常处理\n\n- 确保所有敏感数据安全传输，当验证码不正确或过期时，只应该提示一个通用的错误信息防止用户猜测或爆破验证码。\n- 异常提示多语言支持。\n\n## 结论\n\n通过实施上述方案，考虑到了以下情况：\n\n1. 新邮箱验证请求\n2. 用户邮箱地址更新\n3. 用户请求重新发送验证邮件\n4. 邮件发送失败\n5. 验证码有效期\n6. 发送频率限制\n7. 验证状态的指示和反馈\n\n我们将能够提供一个安全、可靠且用户友好的邮箱验证功能。\n"
  },
  {
    "path": "docs/extension-points/authentication.md",
    "content": "# Halo 认证扩展点\n\n此前，Halo 提供了 AdditionalWebFilter 作为扩展点供插件扩展认证相关的功能。但是近期我们明确了 AdditionalWebFilter\n的使用用途，故不再作为认证的扩展点。\n\n目前，Halo 提供了三种认证扩展点：表单登录认证、普通认证和匿名认证。\n\n## 表单登录（FormLogin）\n\n示例如下：\n\n```java\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.FormLoginSecurityWebFilter;\n\n@Component\npublic class MyFormLoginSecurityWebFilter implements FormLoginSecurityWebFilter {\n\n  @Override\n  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n    // Do your logic here\n    return chain.filter(exchange);\n  }\n}\n\n```\n\n## 普通认证（Authentication）\n\n示例如下：\n\n```java\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.AuthenticationSecurityWebFilter;\n\n@Component\npublic class MyAuthenticationSecurityWebFilter implements AuthenticationSecurityWebFilter {\n\n  @Override\n  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n    // Do your logic here\n    return chain.filter(exchange);\n  }\n}\n```\n\n## 匿名认证（Anonymous Authentication\n\n示例如下：\n\n```java\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport run.halo.app.security.AnonymousAuthenticationSecurityWebFilter;\n\n@Component\npublic class MyAnonymousAuthenticationSecurityWebFilter\n        implements AnonymousAuthenticationSecurityWebFilter {\n\n  @Override\n  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n    // Do your logic here\n    return chain.filter(exchange);\n  }\n}\n```\n\n## 前置过滤器（BeforeSecurityWebFilter）\n\n主要用于在进行认证之前的一些处理。需要注意的是，当前过滤器中无法直接通过 ReactiveSecurityContextHolder 获取\nSecurityContext。示例如下：\n\n```java\npublic class MyBeforeSecurityWebFilter implements BeforeSecurityWebFilter {\n\n  @Override\n  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n    // Do your logic here\n    return chain.filter(exchange);\n  }\n}\n```\n\n## 后置过滤器（AfterSecurityWebFilter）\n\n主要用于进行认证之后的一些处理。在当前过滤器中，可以通过 ReactiveSecurityContextHolder 获取 SecurityContext。示例如下：\n\n```java\npublic class MyAfterSecurityWebFilter implements AfterSecurityWebFilter {\n\n  @Override\n  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n    return ReactiveSecurityContextHolder.getContext()\n            .switchIfEmpty(Mono.defer(() -> {\n              // do something...\n              return chain.filter(exchange).then(Mono.empty());\n            }))\n            .flatMap(securityContext -> {\n              // do something...\n              return chain.filter(exchange);\n            });\n  }\n}\n```\n\n---\n\n我们在实现扩展点的时候需要注意：如果当前请求不满足认证条件，请一定要调用 `chain.filter(exchange)`，给其他 filter 留下机会。\n\n后续会根据需求实现其他认证相关的扩展点。"
  },
  {
    "path": "docs/extension-points/content.md",
    "content": "# 内容扩展点\n\n## 文章内容扩展点\n\n文章内容扩展点用于在主题端文章内容渲染之前对文章内容进行修改，比如添加广告、添加版权声明、插入脚本等。\n\n## 使用方式\n\n在插件中通过实现 `run.halo.app.theme.ReactivePostContentHandler` 接口来实现文章内容扩展。\n\n以下是一个扩展文章内容支持 Katex 的示例：\n\n```javascript\nString katexScript=\"\"\"\n    <link rel=\"stylesheet\" href=\"/plugins/plugin-katex/assets/static/katex.min.css\">\n    <script defer src=\"/plugins/plugin-katex/assets/static/katex.min.js\"></script>\n    <script defer src=\"/plugins/plugin-katex/assets/static/contrib/auto-render.min.js\"></script>\n    <script>\n        document.addEventListener(\"DOMContentLoaded\", function() {\n            renderMathInElement(document.body, {\n              // customised options\n              // • auto-render specific keys, e.g.:\n              delimiters: [\n                  {left: '$$', right: '$$', display: true},\n                  {left: '$', right: '$', display: false},\n                  {left: '\\\\(', right: '\\\\)', display: false},\n                  {left: '\\\\[', right: '\\\\]', display: true}\n              ],\n              // • rendering keys, e.g.:\n              throwOnError : false\n            });\n        });\n    </script>\n    \"\"\";\n```\n\n然后在 `handle` 方法中将 Katex 的脚本字符串插入到内容前面：\n\n```java\n\n@Component\npublic class KatexPostContentHandler implements ReactivePostContentHandler {\n\n    @Override\n    public Mono<PostContentContext> handle(PostContentContext postContent) {\n        postContent.setContent(katexScript + \"\\n\" + postContent.getContent());\n        return Mono.just(postContent);\n    }\n}\n```\n\n定义了扩展点实现（扩展），还需要在插件的 `resources/extensions` 目录下添加对扩展的声明：\n\n```yaml\n# resources/extensions/extension-definitions.yml\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: ext-def-katex-post-content\nspec:\n  className: run.halo.katex.KatexPostContentHandler\n  # 文章内容扩展点的名称，固定值\n  extensionPointName: reactive-post-content-handler\n  displayName: \"KatexPostContentHandler\"\n  description: \"Katex support for post content.\"\n```\n\n## 自定义页面内容扩展点\n\n自定义页面（SinglePage）内容扩展点用于在主题端自定义页面内容渲染之前对内容进行修改，比如添加广告、添加版权声明、插入脚本等。\n\n## 使用方式\n\n在插件中通过实现 `run.halo.app.theme.ReactiveSinglePageContentHandler` 接口来实现内容扩展。\n\n以下是一个扩展内容支持 Katex 的示例：\n\n```java\n\n@Component\npublic class KatexSinglePageContentHandler implements ReactiveSinglePageContentHandler {\n\n    @Override\n    public Mono<SinglePageContentContext> handle(SinglePageContentContext pageContent) {\n\n        String katexScript = \"\"; // 参考文章内容扩展点的示例脚本块\n        pageContent.setContent(katexScript + \"\\n\" + pageContent.getContent());\n        return Mono.just(pageContent);\n    }\n}\n```\n\n在插件的 `resources/extensions` 目录下添加对自定义页面内容扩展的声明：\n\n```yaml\n# resources/extensions/extension-definitions.yml\napiVersion: plugin.halo.run/v1alpha1\nkind: ExtensionDefinition\nmetadata:\n  name: ext-def-katex-singlepage-content\nspec:\n  className: run.halo.katex.KatexSinglePageContentHandler\n  # 自定义页面内容扩展点的名称，固定值\n  extensionPointName: reactive-post-content-handler\n  displayName: \"KatexSinglePageContentHandler\"\n  description: \"Katex support for single page content.\"\n```\n"
  },
  {
    "path": "docs/extension-points/search-engine.md",
    "content": "# 搜索引擎扩展点\n\n随着 Halo 的不断发展，搜索引擎模块也逐渐完善。搜索引擎模块是 Halo 的核心模块之一，它负责为 Halo\n提供全文搜索功能。搜索引擎模块目前仅支持本地全文搜索引擎 [Lucene](https://lucene.apache.org/)，其他搜索引擎的支持，如\nSolr、MeiliSearch 或 ElasticSearch，需要通过插件来实现。\n\n搜索引擎模块包含两个扩展点，分别是搜索引擎扩展和搜索文档扩展。搜索引擎扩展主要负责索引文档的添加、更新、删除和重建，搜索文档扩展则主要用于扩展文档类型，不仅限于文章类型。\n\n从 Halo 2.17 开始，Halo 利用事件机制收集来自核心和插件中所发布的文档，其中也包含了文档类型用于区分。所以插件中可以通过发布事件的方式来控制文档的添加、更新、删除和重建，重建操作所需要的数据则由搜索文档扩展提供。\n\n## 搜索引擎扩展（`run.halo.app.search.SearchEngine`）\n\n如果插件想要扩展搜索引擎，如 Solr、MeiliSearch 或者 ElasticSearch，可以通过实现 `SearchEngine` 接口来实现。\n\n具体实现可参考 Halo 的 Lucene 搜索引擎实现：`run.halo.app.search.LuceneSearchEngine`。\n\n## 搜索文档扩展（`run.halo.app.search.HaloDocumentsProvider`）\n\n如果插件想要扩展搜索文档类型，可以通过实现 `HaloDocumentsProvider` 接口来实现。具体实现可参考 Halo\n的默认实现：`run.halo.app.search.post.PostHaloDocumentsProvider`。\n\n- 添加文档示例如下所示\n\n  ```java\n  class HaloDocumentAddExample {\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    void addDocuments() {\n      // concrete Halo documents\n      List<HaloDocument> documents = ...;\n      eventPublisher.publishEvent(new HaloDocumentAddRequestEvent(this, documents));\n    }\n  }\n  ```\n\n- 删除文档示例如下所示\n\n  ```java\n  class HaloDocumentDeleteExample {\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    void deleteDocuments() {\n      Set<String> docIds = ...;\n      eventPublisher.publishEvent(new HaloDocumentDeleteRequestEvent(this, docIds));\n    }\n  }\n  ```\n\n- 重建索引示例如下所示：\n\n  ```java\n  class HaloDocumentRebuildExample {\n\n    private final ApplicationEventPublisher eventPublisher;\n\n    void rebuildDocument() {\n      eventPublisher.publishEvent(new HaloDocumentRebuildRequestEvent(this));\n    }\n\n  }\n  ```\n  "
  },
  {
    "path": "docs/full-text-search/README.md",
    "content": "# 在 Halo 中实践全文搜索\n\n主题端需全文搜索接口用于模糊搜索文章，且对效率要求极高。已经有对应的 Issue\n提出，可参考：<https://github.com/halo-dev/halo/issues/2637>。\n\n实现全文搜索的本地方案最好的就是 Apache 旗下开源的 [Lucene](https://lucene.apache.org/)\n，不过 [Hibernate Search](https://hibernate.org/search/) 也基于 Lucene 实现了全文搜索。Halo 2.0 的自定义模型并不是直接在\nHibernate 上构建的，也就是说 Hibernate 在 Halo 2.0 只是一个可选项，故我们最终可能并不会采用 Hibernate Search，即使它有很多优势。\n\nHalo 也可以学习 Hibernate 适配多种搜索引擎，如 Lucene、ElasticSearch、MeiliSearch 等。默认实现为 Lucene，对于用户来说，这种实现方式部署成本最低。\n\n## 搜索接口设计\n\n### 搜索参数\n\n字段如下所示：\n\n- keyword: string. 关键字\n- sort: string[]. 搜索字段和排序方式\n- offset: number. 本次查询结果偏移数\n- limit: number. 本次查询的结果最大条数\n\n例如：\n\n```bash\nhttp://localhost:8090/apis/api.halo.run/v1alpha1/posts?keyword=halo&sort=title.asc&sort=publishTimestamp,desc&offset=20&limit=10\n```\n\n### 搜索结果\n\n```yaml\nhits:\n  - name: halo01\n    title: Halo 01\n    permalink: /posts/halo01\n    categories:\n      - a\n      - b\n    tags:\n      - c\n      - d\n  - name: halo02\n    title: Halo 02\n    permalink: /posts/halo02\n    categories:\n      - a\n      - b\n    tags:\n      - c\n      - d\nquery: \"halo\"\ntotal: 100\nlimit: 20\noffset: 10\nprocessingTimeMills: 2\n```\n\n#### 搜索结果分页问题\n\n目前，大多数搜索引擎为了性能问题，并没有直接提供分页功能，或者不推荐分页。\n\n请参考：\n\n- <https://solr.apache.org/guide/solr/latest/query-guide/pagination-of-results.html>\n- <https://docs.meilisearch.com/learn/advanced/pagination.html>\n- <https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html>\n- <https://discourse.algolia.com/t/pagination-limit/10585>\n\n综合以上讨论，我们暂定不支持分页。不过允许设置单次查询的记录数（limit <= max_limit）。\n\n#### 中文搜索优化\n\nLucene 默认的分析器，对中文的分词不够友好，我们需要借助外部依赖或者外部整理好的词库帮助我们更好的对中文句子分词，以便优化中文搜索结果。\n\n以下是关于中文分析器的 Java 库：\n\n- <https://gitee.com/lionsoul/jcseg>\n- <https://code.google.com/archive/p/ik-analyzer>\n- <https://github.com/huaban/jieba-analysis>\n- <https://github.com/medcl/elasticsearch-analysis-ik>\n- <https://github.com/blueshen/ik-analyzer>\n\n### 搜索引擎样例\n\n#### MeiliSearch\n\n```bash\ncurl 'http://localhost:7700/indexes/movies/search' \\\n  -H 'Accept: */*' \\\n  -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5' \\\n  -H 'Authorization: Bearer MASTER_KEY' \\\n  -H 'Connection: keep-alive' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Cookie: logged_in=yes; adminer_permanent=; XSRF-TOKEN=75995791-980a-4f3e-81fb-2e199d8f3934' \\\n  -H 'Origin: http://localhost:7700' \\\n  -H 'Referer: http://localhost:7700/' \\\n  -H 'Sec-Fetch-Dest: empty' \\\n  -H 'Sec-Fetch-Mode: cors' \\\n  -H 'Sec-Fetch-Site: same-origin' \\\n  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26' \\\n  -H 'X-Meilisearch-Client: Meilisearch mini-dashboard (v0.2.2) ; Meilisearch instant-meilisearch (v0.8.2) ; Meilisearch JavaScript (v0.27.0)' \\\n  -H 'sec-ch-ua: \"Microsoft Edge\";v=\"107\", \"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"' \\\n  -H 'sec-ch-ua-mobile: ?0' \\\n  -H 'sec-ch-ua-platform: \"Windows\"' \\\n  --data-raw '{\"q\":\"halo\",\"attributesToHighlight\":[\"*\"],\"highlightPreTag\":\"<ais-highlight-0000000000>\",\"highlightPostTag\":\"</ais-highlight-0000000000>\",\"limit\":21}' \\\n  --compressed\n```\n\n```json\n{\n    \"hits\": [\n        {\n            \"id\": 108761,\n            \"title\": \"I Am... Yours: An Intimate Performance at Wynn Las Vegas\",\n            \"overview\": \"Filmed at the Encore Theater at Wynn Las Vegas, this extraordinary concert features performances of over 30 songs from Beyoncé’s three multi-platinum solo releases, Destiny’s Child catalog and a few surprises. This amazing concert includes the #1 hits, “Single Ladies (Put A Ring On It),” “If I Were A Boy,” “Halo,” “Sweet Dreams” and showcases a gut-wrenching performance of “That’s Why You’re Beautiful.”  Included on \\\"I AM... YOURS An Intimate Performance At Wynn Las Vegas,\\\" is a biographical storytelling woven between many songs and exclusive behind-the-scenes footage.\",\n            \"genres\": [\"Music\", \"Documentary\"],\n            \"poster\": \"https://image.tmdb.org/t/p/w500/j8n1XQNfw874Ka7SS3HQLCVNBxb.jpg\",\n            \"release_date\": 1258934400,\n            \"_formatted\": {\n                \"id\": \"108761\",\n                \"title\": \"I Am... Yours: An Intimate Performance at Wynn Las Vegas\",\n                \"overview\": \"Filmed at the Encore Theater at Wynn Las Vegas, this extraordinary concert features performances of over 30 songs from Beyoncé’s three multi-platinum solo releases, Destiny’s Child catalog and a few surprises. This amazing concert includes the #1 hits, “Single Ladies (Put A Ring On It),” “If I Were A Boy,” “<ais-highlight-0000000000>Halo</ais-highlight-0000000000>,” “Sweet Dreams” and showcases a gut-wrenching performance of “That’s Why You’re Beautiful.”  Included on \\\"I AM... YOURS An Intimate Performance At Wynn Las Vegas,\\\" is a biographical storytelling woven between many songs and exclusive behind-the-scenes footage.\",\n                \"genres\": [\"Music\", \"Documentary\"],\n                \"poster\": \"https://image.tmdb.org/t/p/w500/j8n1XQNfw874Ka7SS3HQLCVNBxb.jpg\",\n                \"release_date\": \"1258934400\"\n            }\n        }\n    ],\n    \"estimatedTotalHits\": 10,\n    \"query\": \"halo\",\n    \"limit\": 21,\n    \"offset\": 0,\n    \"processingTimeMs\": 2\n}\n```\n\n![MeiliSearch UI](./meilisearch.jpg)\n\n#### Algolia\n\n```bash\ncurl 'https://og53ly1oqh-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(4.14.2)%3B%20Browser%20(lite)%3B%20docsearch%20(3.2.1)%3B%20docsearch-react%20(3.2.1)%3B%20docusaurus%20(2.1.0)&x-algolia-api-key=739f2a55c6d13d93af146c22a4885669&x-algolia-application-id=OG53LY1OQH' \\\n  -H 'Accept: */*' \\\n  -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5' \\\n  -H 'Connection: keep-alive' \\\n  -H 'Origin: https://docs.halo.run' \\\n  -H 'Referer: https://docs.halo.run/' \\\n  -H 'Sec-Fetch-Dest: empty' \\\n  -H 'Sec-Fetch-Mode: cors' \\\n  -H 'Sec-Fetch-Site: cross-site' \\\n  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26' \\\n  -H 'content-type: application/x-www-form-urlencoded' \\\n  -H 'sec-ch-ua: \"Microsoft Edge\";v=\"107\", \"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"' \\\n  -H 'sec-ch-ua-mobile: ?0' \\\n  -H 'sec-ch-ua-platform: \"Windows\"' \\\n  --data-raw '{\"requests\":[{\"query\":\"halo\",\"indexName\":\"docs\",\"params\":\"attributesToRetrieve=%5B%22hierarchy.lvl0%22%2C%22hierarchy.lvl1%22%2C%22hierarchy.lvl2%22%2C%22hierarchy.lvl3%22%2C%22hierarchy.lvl4%22%2C%22hierarchy.lvl5%22%2C%22hierarchy.lvl6%22%2C%22content%22%2C%22type%22%2C%22url%22%5D&attributesToSnippet=%5B%22hierarchy.lvl1%3A5%22%2C%22hierarchy.lvl2%3A5%22%2C%22hierarchy.lvl3%3A5%22%2C%22hierarchy.lvl4%3A5%22%2C%22hierarchy.lvl5%3A5%22%2C%22hierarchy.lvl6%3A5%22%2C%22content%3A5%22%5D&snippetEllipsisText=%E2%80%A6&highlightPreTag=%3Cmark%3E&highlightPostTag=%3C%2Fmark%3E&hitsPerPage=20&facetFilters=%5B%22language%3Azh-Hans%22%2C%5B%22docusaurus_tag%3Adefault%22%2C%22docusaurus_tag%3Adocs-default-1.6%22%5D%5D\"}]}' \\\n  --compressed\n```\n\n```json\n{\n    \"results\": [\n        {\n            \"hits\": [\n                {\n                    \"content\": null,\n                    \"hierarchy\": {\n                        \"lvl0\": \"Documentation\",\n                        \"lvl1\": \"使用 Docker Compose 部署 Halo\",\n                        \"lvl2\": \"更新容器组 ​\",\n                        \"lvl3\": null,\n                        \"lvl4\": null,\n                        \"lvl5\": null,\n                        \"lvl6\": null\n                    },\n                    \"type\": \"lvl2\",\n                    \"url\": \"https://docs.halo.run/getting-started/install/other/docker-compose/#更新容器组\",\n                    \"objectID\": \"4ccfa93009143feb6e423274a4944496267beea8\",\n                    \"_snippetResult\": {\n                        \"hierarchy\": {\n                            \"lvl1\": {\n                                \"value\": \"… Docker Compose 部署 <mark>Halo</mark>\",\n                                \"matchLevel\": \"full\"\n                            },\n                            \"lvl2\": {\n                                \"value\": \"更新容器组 ​\",\n                                \"matchLevel\": \"none\"\n                            }\n                        }\n                    },\n                    \"_highlightResult\": {\n                        \"hierarchy\": {\n                            \"lvl0\": {\n                                \"value\": \"Documentation\",\n                                \"matchLevel\": \"none\",\n                                \"matchedWords\": []\n                            },\n                            \"lvl1\": {\n                                \"value\": \"使用 Docker Compose 部署 <mark>Halo</mark>\",\n                                \"matchLevel\": \"full\",\n                                \"fullyHighlighted\": false,\n                                \"matchedWords\": [\"halo\"]\n                            },\n                            \"lvl2\": {\n                                \"value\": \"更新容器组 ​\",\n                                \"matchLevel\": \"none\",\n                                \"matchedWords\": []\n                            }\n                        },\n                        \"hierarchy_camel\": [\n                            {\n                                \"lvl0\": {\n                                    \"value\": \"Documentation\",\n                                    \"matchLevel\": \"none\",\n                                    \"matchedWords\": []\n                                },\n                                \"lvl1\": {\n                                    \"value\": \"使用 Docker Compose 部署 <mark>Halo</mark>\",\n                                    \"matchLevel\": \"full\",\n                                    \"fullyHighlighted\": false,\n                                    \"matchedWords\": [\"halo\"]\n                                },\n                                \"lvl2\": {\n                                    \"value\": \"更新容器组 ​\",\n                                    \"matchLevel\": \"none\",\n                                    \"matchedWords\": []\n                                }\n                            }\n                        ]\n                    }\n                }\n            ],\n            \"nbHits\": 113,\n            \"page\": 0,\n            \"nbPages\": 6,\n            \"hitsPerPage\": 20,\n            \"exhaustiveNbHits\": true,\n            \"exhaustiveTypo\": true,\n            \"exhaustive\": {\n                \"nbHits\": true,\n                \"typo\": true\n            },\n            \"query\": \"halo\",\n            \"params\": \"query=halo&attributesToRetrieve=%5B%22hierarchy.lvl0%22%2C%22hierarchy.lvl1%22%2C%22hierarchy.lvl2%22%2C%22hierarchy.lvl3%22%2C%22hierarchy.lvl4%22%2C%22hierarchy.lvl5%22%2C%22hierarchy.lvl6%22%2C%22content%22%2C%22type%22%2C%22url%22%5D&attributesToSnippet=%5B%22hierarchy.lvl1%3A5%22%2C%22hierarchy.lvl2%3A5%22%2C%22hierarchy.lvl3%3A5%22%2C%22hierarchy.lvl4%3A5%22%2C%22hierarchy.lvl5%3A5%22%2C%22hierarchy.lvl6%3A5%22%2C%22content%3A5%22%5D&snippetEllipsisText=%E2%80%A6&highlightPreTag=%3Cmark%3E&highlightPostTag=%3C%2Fmark%3E&hitsPerPage=20&facetFilters=%5B%22language%3Azh-Hans%22%2C%5B%22docusaurus_tag%3Adefault%22%2C%22docusaurus_tag%3Adocs-default-1.6%22%5D%5D\",\n            \"index\": \"docs\",\n            \"renderingContent\": {},\n            \"processingTimeMS\": 1,\n            \"processingTimingsMS\": {\n                \"total\": 1\n            }\n        }\n    ]\n}\n```\n\n![Algolia UI](./algolia.png)\n\n#### Wiki\n\n```bash\ncurl 'https://wiki.fit2cloud.com/rest/api/search?cql=siteSearch%20~%20%22halo%22%20AND%20type%20in%20(%22space%22%2C%22user%22%2C%22com.atlassian.confluence.extra.team-calendars%3Acalendar-content-type%22%2C%22attachment%22%2C%22page%22%2C%22com.atlassian.confluence.extra.team-calendars%3Aspace-calendars-view-content-type%22%2C%22blogpost%22)&start=20&limit=20&excerpt=highlight&expand=space.icon&includeArchivedSpaces=false&src=next.ui.search' \\\n  -H 'authority: wiki.fit2cloud.com' \\\n  -H 'accept: */*' \\\n  -H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5' \\\n  -H 'cache-control: no-cache, no-store, must-revalidate' \\\n  -H 'cookie: _ga=GA1.2.1720479041.1657188862; seraph.confluence=89915546%3A6fc1394f8d537ffa08fb679e6e4dd64993448051; mywork.tab.tasks=false; JSESSIONID=5347D8618AC5883DE9B702E77152170D' \\\n  -H 'expires: 0' \\\n  -H 'pragma: no-cache' \\\n  -H 'referer: https://wiki.fit2cloud.com/' \\\n  -H 'sec-ch-ua: \"Microsoft Edge\";v=\"107\", \"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"' \\\n  -H 'sec-ch-ua-mobile: ?0' \\\n  -H 'sec-ch-ua-platform: \"Windows\"' \\\n  -H 'sec-fetch-dest: empty' \\\n  -H 'sec-fetch-mode: cors' \\\n  -H 'sec-fetch-site: same-origin' \\\n  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26' \\\n  --compressed\n```\n\n```json\n{\n    \"results\": [\n        {\n            \"content\": {\n                \"id\": \"76722\",\n                \"type\": \"page\",\n                \"status\": \"current\",\n                \"title\": \"2.3 测试 - 接口\",\n                \"restrictions\": {},\n                \"_links\": {\n                    \"webui\": \"/pages/viewpage.action?pageId=721\",\n                    \"tinyui\": \"/x/8K_SB\",\n                    \"self\": \"https://wiki.halo.run/rest/api/content/76720\"\n                },\n                \"_expandable\": {\n                    \"container\": \"\",\n                    \"metadata\": \"\",\n                    \"extensions\": \"\",\n                    \"operations\": \"\",\n                    \"children\": \"\",\n                    \"history\": \"/rest/api/content/7670/history\",\n                    \"ancestors\": \"\",\n                    \"body\": \"\",\n                    \"version\": \"\",\n                    \"descendants\": \"\",\n                    \"space\": \"/rest/api/space/IT\"\n                }\n            },\n            \"title\": \"2.3 接口 - 接口\",\n            \"excerpt\": \"另存为新用例\",\n            \"url\": \"/pages/viewpage.action?pageId=7672\",\n            \"resultGlobalContainer\": {\n                \"title\": \"IT 客户\",\n                \"displayUrl\": \"/display/IT\"\n            },\n            \"entityType\": \"content\",\n            \"iconCssClass\": \"aui-icon content-type-page\",\n            \"lastModified\": \"2022-05-11T22:40:53.000+08:00\",\n            \"friendlyLastModified\": \"五月 11, 2022\",\n            \"timestamp\": 1652280053000\n        }\n    ],\n    \"start\": 20,\n    \"limit\": 20,\n    \"size\": 20,\n    \"totalSize\": 70,\n    \"cqlQuery\": \"siteSearch ~ \\\"halo\\\" AND type in (\\\"space\\\",\\\"user\\\",\\\"com.atlassian.confluence.extra.team-calendars:calendar-content-type\\\",\\\"attachment\\\",\\\"page\\\",\\\"com.atlassian.confluence.extra.team-calendars:space-calendars-view-content-type\\\",\\\"blogpost\\\")\",\n    \"searchDuration\": 36,\n    \"_links\": {\n        \"base\": \"https://wiki.halo.run\",\n        \"context\": \"\"\n    }\n}\n```\n\n### FAQ\n\n#### 是否需要统一参数和响应体结构？\n\n以下是关于统一参数和响应体结构的优缺点分析：\n\n优点：\n\n- 主题端搜索结果 UI 更加一致，不会因为使用不同搜索引擎导致 UI 上的变动\n\n缺点：\n\n- 无法完全发挥出对应的搜索引擎的实力。比如某个搜索引擎有很实用的功能，而某些搜索引擎没有。\n- Halo Core 需要适配不同的搜索引擎，比较繁琐\n\n#### 是否需要提供扩展点集成其他搜索引擎？\n\n既然 Lucene 非常强大，且暂时已经能够满足我们的要求，我们为什么还需要集成其他搜索引擎呢？\n\n- Lucene 目前是作为 Halo 的依赖使用的，也就意味着只支持 Halo 单实例部署，阻碍未来 Halo 无状态化的趋势。\n- 相反，其他搜索引擎（例如 Solr、MeiliSearch、ElasticSearch 等）都可以独立部署，Halo 只需要利用对应的 SDK 和搜索引擎沟通即可，无论 Halo 是否是多实例部署。\n"
  },
  {
    "path": "docs/index/README.md",
    "content": "# 索引机制 RFC\n\n## 背景\n\n目前 Halo 使用 Extension 机制来存储和获取数据以便支持更好的扩展性，所以设计之初就存在查询数据时会将对应 Extension 的所有数据查询到内存中处理的问题，这会导致当分页查询和条件查询时也会有大批量无效数据被加载到内存中，随着 Halo 用户的数据量的增长，如果没有一个方案来解决这样的数据查询问题会对 Halo 用户的服务器内存资源有较高的要求，因此本篇提出使用索引机制来解决数据查询问题，以便提高查询效率和减少内存开销。\n\n## 目标\n\n- **提高查询效率**：加快数据检索速度。通过使用索引，数据库可以快速定位到数据行的位置，从而减少必须读取的数据量。\n- **减少网络和内存开销：** 没有索引前查询数据会将 Extension 的所有数据都传输到应用对网络和内存开销都很大，通过索引定位确切的数据来减少不必要的消耗。\n- **优化排序操作**：通过索引加速排序操作，因此需要索引本身有序。\n- **索引存储可扩展**：索引虽然能提高查询效率，但它会占用额外的存储空间，如果过大可以考虑在磁盘上读写等方式来减少对内存的占用。\n\n## 非目标\n\n- 索引的持久化存储，前期只考虑在内存中存储索引，如果后续索引过大可以考虑在磁盘上读写等方式来减少对内存的占用。\n- 索引的自动维护，索引的维护需要考虑到索引的数据是否改变，如果改变则需要更新索引，这个改变的判断不好界定，所以先不考虑索引的自动维护。\n- 索引的前置验证，比如在启动时验证索引的完整性和正确性，但目前每次启动都会重新构建索引，所以先不考虑索引的前置验证。\n- 多线程构建索引，目前索引的构建是单线程的，如果后续索引过大可以考虑多线程构建索引。\n\n## 方案\n\n索引是一种存储数据结构，可提供对数据集中字段的高效查找。索引将 Extension 中的字段映射到 Extension 的名称，以便在查询特定字段时不需要完整的扫描。\n\n### 索引结构\n\n每个 Extension 声明的索引都会被创建为一个 keySpace 与索引信息的映射，\n类如对附件分组的一个对名称的索引示例如下：\n\n```javascript\n{\n \"/registry/storage.halo.run/groups\": [{\n  name: \"specName\",\n  spec: {\n    // a function that returns the value of the index key\n    indexFunc: function(doc) {\n      return doc.spec.name;\n    },\n    order: 'asc',\n    unique: false\n  },\n  v: 1,\n  ready: false\n },\n {\n  name: \"metadata.labels\",\n  spec: {\n    indexFunc: function(doc) {\n       var labels = obj.getMetadata().getLabels();\n        if (labels == null) {\n          return Set.of();\n        }\n        return labels.entrySet()\n          .stream()\n          .map(entry -> entry.getKey() + \"=\" + entry.getValue())\n          .collect(Collectors.toSet());\n    },\n    order: 'asc',\n    unique: false\n  },\n  v: 1,\n  ready: true,\n }]\n}\n```\n\n- `name: specName` 表示索引的名称，每个 Extension 声明的索引名称不能重复，通常为字段路径如 `metadata.name`。\n- `spec.indexFunc` 用于生成索引键，索引键是一个字符串数组，每个字符串都是一个索引键值，索引键值是一个字符串。\n- `spec.order` 用于记录索引键的排序方式，可选值为 `asc` 或 `desc`，`asc` 表示升序，`desc` 表示降序。\n- `spec.unique` 用于标识是否为唯一索引以在添加索引时进行唯一性检查。\n- `v` 用于记录索引结构的版本以防止后续为优化导致索引结构改变时便于检查重建索引。\n- `ready` 用于记录该索引是否构建完成，当开始构建该索引键索引记录时为 false，如果构建完成则修改为 true，如果因为断电等导致索引构建不完整则 ready 会是 false，下次启动时需要重新开始构建。\n\n对于每个 Extension 都有一个默认的唯一索引 `metadata.name` 其 entries 与 Extension 每一条记录唯一对应。\n\n### 索引构建\n\n索引是通过对 Extension 数据执行完整扫描来构建的。\n\n1. **针对特定 Extension 数据集的操作**: 当构建索引时，操作是针对特定的 Extension 数据进行的。将 `ready` 置为 `false`\n2. **扫描 Extension 数据集**: 构建索引的关键步骤是扫描 Extension 数据集中的每一条记录。这个扫描过程并不是基于数据库中所有数据的顺序，而是专注于该 Extension 数据集内的数据。当构建索引时会锁定对该 Extension 的写操作。\n3. **生成索引键（KeyString键）**:对于 Extension 数据集中的每个 Extension，会根据其索引字段生成 KeyString 键。String 为 Extension 的 `metadata.name` 用于定位 Extension 在数据库中的位置。\n4. **排序和插入操作**: 生成的键会被插入到一个外部排序器中，以确保它们的顺序。排序后，这些键按顺序批量加载到索引中。\n5. 释放对该 Extension 写操作的锁定完成了索引构建。\n\n对于后续 Extension 和索引的更新需要在同一个事务中以确保一致性。\n\n```json\n{\n  \"metadata.name\": {\n    \"group-1\": []\n  },\n  \"specName\": {\n    \"zhangsan\": [\n        \"metadata-name-1\"\n    ],\n    \"lisi\": [\n        \"metadata-name-2\"\n    ]\n  },\n  \"halo.run/hidden\": {\n    \"true\": [\n        \"metadata-name-3\"\n    ],\n    \"false\": [\n        \"metadata-name-4\"\n    ]\n  }\n}\n```\n\n### 索引前置验证\n\n1. 每次启动后先检查索引是否 ready\n2. `metadata.name` 索引条目的数量始终与数据库中记录的 Extension 数量一致\n3. 如果排序顺序为升序，则索引条目按递增顺序排列。\n4. 如果排序顺序为降序，则索引条目按降序排列。\n5. 每个索引的索引条目数量不超过数据库中记录的对应 Extension 数量。\n\n### 索引在 Extension 的声明\n\n手动注册索引\n\n```java\npublic class IndexSpec {\n    private String name;\n\n    private IndexAttribute indexFunc;\n\n    private OrderType order;\n\n    private boolean unique;\n\n    public enum OrderType {\n        ASC,\n        DESC\n    }\n\n    // Getters and other methods...\n}\n\nIndexSpecs indexSpecs = indexSpecRegistry.indexFor(Person.class);\nindexSpecs.add(new IndexSpec()\n    .setName(\"spec.name\")\n    .setOrder(IndexSpec.OrderType.DESC)\n    .setIndexFunc(IndexAttributeFactory.simpleAttribute(Person.class,\n        e -> e.getSpec().getName())\n    )\n    .setUnique(false));\n```\n\n用于普通索引的注解\n\n```java\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) // 用于类和注解的注解\npublic @interface Index {\n  String name(); // 索引名称\n  String field(); // 需要索引的字段\n}\n```\n\nIndexes 注解用于声明多个索引\n\n```java\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface Indexes {\n    Index[] value() default {};  // Index注解数组\n}\n```\n\n```java\n@Data\n@Indexes({\n  @Index(name = \"specName\", field = \"spec.name\"),\n  @Index(name = \"creationTimestamp\", field = \"metadata.creationTimestamp\"),\n})\n@EqualsAndHashCode(callSuper = true)\n@ToString(callSuper = true)\n@GVK(group = \"my-plugin.guqing.io\",\n        version = \"v1alpha1\",\n        kind = \"Person\",\n        plural = \"persons\",\n        singular = \"person\")\npublic class Person extends Extension {\n\n    @Schema(description = \"The description on name field\", maxLength = 100)\n    private String name;\n\n    @Schema(description = \"The description on age field\", maximum = \"150\", minimum = \"0\")\n    private Integer age;\n\n    @Schema(description = \"The description on gender field\")\n    private Gender gender;\n\n    public enum Gender {\n        MALE, FEMALE,\n    }\n}\n```\n\n不论是手动注册索引还是通过注解注册索引都由 IndexSpecRegistry 管理。\n\n```java\npublic interface IndexSpecRegistry {\n    /**\n     * <p>Create a new {@link IndexSpecs} for the given {@link Extension} type.</p>\n     * <p>The returned {@link IndexSpecs} is always includes some default {@link IndexSpec} that\n     * does not need to be registered again:</p>\n     * <ul>\n     *     <li>{@link Metadata#getName()} for unique primary index spec named metadata_name</li>\n     *     <li>{@link Metadata#getCreationTimestamp()} for creation_timestamp index spec</li>\n     *     <li>{@link Metadata#getDeletionTimestamp()} for deletion_timestamp index spec</li>\n     *     <li>{@link Metadata#getLabels()} for labels index spec</li>\n     * </ul>\n     *\n     * @param extensionType must not be {@literal null}.\n     * @param <E> the extension type\n     * @return the {@link IndexSpecs} for the given {@link Extension} type.\n     */\n    <E extends Extension> IndexSpecs indexFor(Class<E> extensionType);\n\n    /**\n     * Get {@link IndexSpecs} for the given {@link Extension} type registered before.\n     *\n     * @param extensionType must not be {@literal null}.\n     * @param <E> the extension type\n     * @return the {@link IndexSpecs} for the given {@link Extension} type.\n     * @throws IllegalArgumentException if no {@link IndexSpecs} found for the given\n     *                                  {@link Extension} type.\n     */\n    <E extends Extension> IndexSpecs getIndexSpecs(Class<E> extensionType);\n\n    boolean contains(Class<? extends Extension> extensionType);\n\n    void removeIndexSpecs(Class<? extends Extension> extensionType);\n\n    /**\n     * Get key space for an extension type.\n     *\n     * @param scheme is a scheme of an Extension.\n     * @return key space(never null)\n     */\n    @NonNull\n    String getKeySpace(Scheme scheme);\n}\n```\n\n对于添加了索引的 Extension 可以使用 `IndexedQueryEngine` 来查询数据：\n\n```java\npublic interface IndexedQueryEngine {\n   /**\n     * Page retrieve the object records by the given {@link GroupVersionKind} and\n     * {@link ListOptions}.\n     *\n     * @param type the type of the object must exist in\n     * {@link run.halo.app.extension.SchemeManager}.\n     * @param options the list options to use for retrieving the object records.\n     * @param page which page to retrieve and how large the page should be.\n     * @return a collection of {@link Metadata#getName()} for the given page.\n     */\n    ListResult<String> retrieve(GroupVersionKind type, ListOptions options, PageRequest page);\n\n    /**\n     * Retrieve all the object records by the given {@link GroupVersionKind} and\n     * {@link ListOptions}.\n     *\n     * @param type the type of the object must exist in {@link run.halo.app.extension.SchemeManager}\n     * @param options the list options to use for retrieving the object records\n     * @return a collection of {@link Metadata#getName()}\n     */\n    List<String> retrieveAll(GroupVersionKind type, ListOptions options);\n}\n```\n\n但为了简便起见，会在 ReactiveExtensionClient 中提供一个 `listBy` 方法来查询数据：\n\n```java\npublic interface ReactiveExtensionClient {\n  //...\n  <E extends Extension> Mono<ListResult<E>> listBy(Class<E> type, ListOptions options,\n        PageRequest pageable);\n}\n```\n\n其中 `ListOptions` 包含两部分，`LabelSelector` 和 `FieldSelector`，一个常见的手动构建的 `ListOptions` 示例：\n\n```java\nvar listOptions = new ListOptions();\nlistOptions.setLabelSelector(LabelSelector.builder()\n  .eq(\"key1\", \"value1\").build());\nlistOptions.setFieldSelector(FieldSelector.builder()\n  .eq(\"slug\", \"slug1\").build());\n```\n\n为了兼容以前的写法，对于 APIs 中可以继续使用 `run.halo.app.extension.router.IListRequest`，然后使用工具类转换即可得到 `ListOptions` 和 `PageRequest`。\n\n```java\nclass QueryListRequest implements IListRequest {\n    public ListOptions toListOptions() {\n        return labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());\n    }\n\n    public PageRequest toPageRequest() {\n        return PageRequestImpl.of(getPage(), getSize(), getSort());\n    }\n}\n```\n\n### Reconciler\n\n对于 Reconciler 来说，之前每次由 DefaultController 启动对于需要 `syncAllOnStart` 的 Reconciler 都是获取所有对应的 Extension 数据，然后再进行 Reconcile，这样会导致每次都将所有的 Extension 数据加载到内存中，随着数据量的增加导致内存占用过大，当有了索引后只获取所有 Extension 的 `metadata.name` 来触发 reconcile 即可。\n\nGcReconciler 也从索引中获取 `metadata.deletionTimestamp` 不为空的 Extension 的 `metadata.name` 来触发 reconcile 以减少全量加载数据的操作。\n"
  },
  {
    "path": "docs/notification/README.md",
    "content": "## 背景\n\n在 Halo 系统中，具有用户协作属性，如当用户发布文章后被访客评论而访客希望在作者回复评论时被提醒以此完成进一步互动，而在没有通知功能的情况下无法满足诸如以下描述的使用场景：\n\n1. 访客只能在评论后一段时间内访问被评论的文章查看是否被回复。\n2. Halo 的用户注册功能无法让用户验证邮箱地址让恶意注册变的更容易。\n\n在这些场景下，为了让用户收到通知或验证消息以及管理和处理这些通知，我们需要设计一个通知功能，以实现根据用户的订阅和偏好推送通知并管理通知。\n\n## 已有需求\n\n- 访客评论文章后希望收到被回复的通知，而文章作者也希望收到文章被评论的通知。\n- 用户注册功能希望验证注册者填写的邮箱实现一个邮箱只能注册一个账号，防止占用别人邮箱，在一定程度上减少恶意注册问题。\n- 关于应用市场插件，管理员希望在用户下单后能收到新订单通知。\n- 付费订阅插件场景，希望给付费订阅用户推送付费文章的浏览链接。\n\n## 目标\n\n设计一个通知功能，可以根据以下目标，实现订阅和推送通知：\n\n- 支持扩展多种通知方式，例如邮件、短信、Slack 等。\n- 支持通知条件并可扩展，例如 Halo\n  有新文章发布事件如果用户订阅了新文章发布事件但付费订阅插件决定了此文章只有付费用户才可收到通知、按照付费等级不同决定是否发送新文章通知给对应用户等需要通过实现通知条件的扩展点来满足对应需求。\n- 支持定制化选项，例如是否开启通知、通知时段等。\n- 支持通知流程，例如通知的发送、接收、查看、标记等。\n- 通知内容支持多语言。\n- 事件类型可扩展，插件可能需要定义自己的事件以通知到订阅事件的用户，如应用市场插件。\n\n## 非目标\n\n- Halo 只会实现站内消息和邮件通知，更多通知方式需要插件去扩展。\n- 定时通知、通知频率或摘要通知功能属于非必要功能，可由插件去扩展。\n- 多语言支持，目前只会支持中文和英文两种，更多语言支持不是此阶段的目标。\n- 可定制的通知模板：通知默认模板由事件定义者提供，如需修改可考虑使用特定的 Notifier 去适配事件。\n\n## 方案\n\n为了实现上述目标，我们设计了以下方案：\n\n### 通知数据模型\n\n#### 通知事件类别和事件\n\n首先通过定义事件来声明此通知事件包含的数据和发送此事件时默认使用的模板。\n\n`ReasonType` 是一个自定义模型，用于定义事件类别，一个事件类别由多个事件表示。\n\n```yaml\napiVersion: notification.halo.run/v1alpha1\nkind: ReasonType\nmetadata:\n  name: comment\nspec:\n  displayName: \"Comment Received\"\n  description: \"The user has received a comment on an post.\"\n  properties:\n    - name: postName\n      type: string\n      description: \"The name of the post.\"\n      optional: false\n    - name: postTitle\n      type: string\n      optional: true\n    - name: commenter\n      type: string\n      description: \"The email address of the user who has left the comment.\"\n      optional: false\n    - name: comment\n      type: string\n      description: \"The content of the comment.\"\n      optional: false\n```\n\n`Reason` 是一个自定义模型，用于定义通知原因，它属于 `ReasonType` 的实例。\n\n当有事件触发时，创建 `Reason` 资源来触发通知，如当文章收到一个新评论时：\n\n```yaml\napiVersion: notification.halo.run/v1alpha1\nkind: Reason\nmetadata:\n  name: comment-axgu\nspec:\n  # a name of ReasonType\n  reasonType: comment\n  author: 'guqing'\n  subject:\n    apiVersion: 'content.halo.run/v1alpha1'\n    kind: Post\n    name: 'post-axgu'\n    title: 'Hello World'\n    url: 'https://guqing.xyz/archives/1'\n  attributes:\n    postName: \"post-fadp\"\n    commenter: \"guqing\"\n    comment: \"Hello! This is your first notification.\"\n```\n\n#### Subscription\n\n`Subscription` 自定义模型，定义了特定事件时与要被通知的订阅者之间的关系, 其中 `subscriber`\n表示订阅者用户, `unsubscribeToken` 表示退订时的身份验证 token, `reason` 订阅者感兴趣的事件。\n\n用户可以通过 `Subscription` 来订阅自己感兴趣的事件，当事件触发时会收到通知：\n\n```yaml\napiVersion: notification.halo.run/v1alpha1\nkind: Subscription\nmetadata:\n  name: user-a-sub\nspec:\n  subscriber:\n    name: guqing\n  unsubscribeToken: xxxxxxxxxxxx\n  reason:\n    reasonType: new-comment-on-post\n    subject:\n      apiVersion: content.halo.run/v1alpha1\n      kind: Post\n      name: 'post-axgu'\n    # expression: 'props.owner == \"guqing\"'\n```\n\n- `spec.reason.subject`：用于根据事件的主体的匹配感兴趣的事件，如果不指定 name 则表示匹配主体与 kind 和 apiVersion\n  相同的一类事件。\n- `spec.expression`：根据表达式匹配感兴趣的事件，例如 `props.owner == \"guqing\"` 表示只有当事件的属性（reason attributes）的\n  owner 等于 guqing 时才会触发通知。表达式符合 SpEL\n  表达式语法，但结果只能是布尔值。参考：[增强 Subscription 模型以支持表达式匹配](https://github.com/halo-dev/halo/issues/5632)\n\n> 当 `spec.expression` 和 `spec.reason.subject` 同时存在时，以 `spec.reason.subject` 的结果为准，不建议同时使用。\n\n订阅退订链接 API\n规则：`/apis/api.notification.halo.run/v1alpha1/subscriptions/{name}/unsubscribe?token={unsubscribeToken}`。\n\n#### 用户通知偏好设置\n\n通过在用户偏好设置的 ConfigMap 中存储一个 `notification` key 用于保存事件类型与通知方式的关系设置，当用户订阅了如 '\nnew-comment-on-post' 事件时会获取对应的通知方式来给用户发送通知。\n\n```yaml\napiVersion: v1alpha1\nkind: ConfigMap\nmetadata:\n  name: user-preferences-guqing\ndata:\n  notification: |\n    {\n      reasonTypeNotification: {\n        'new-comment-on-post': {\n          enabled: true,\n          notifiers: [\n            email-notifier,\n            sms-notifier\n          ]\n        },\n        new-post: {\n          enabled: true,\n          notifiers: [\n            email-notifier,\n            webhook-router-notifier\n          ]\n        }\n      },\n    }\n```\n\n#### Notification 站内通知\n\n当用户订阅到事件后会创建 `Notification`, 它与通知方式（notifier）无关，`recipient` 为用户名，类似站内通知，如用户 `guqing`\n订阅了评论事件那么当监听到评论事件时会创建一条记录可以在个人中心的通知列表看到一条通知消息。\n\n```yaml\napiVersion: notification.halo.run/v1alpha1\nkind: Notification\nmetadata:\n  name: notification-abc\nspec:\n  # username\n  recipient: \"guqing\"\n  reason: 'comment-axgu'\n  title: 'notification-title'\n  rawContent: 'notification-raw-body'\n  htmlContent: 'notification-html'\n  unread: true\n  lastReadAt: '2023-08-04T17:01:45Z'\n```\n\n个人中心通知自定义 APIs:\n\n1. 获取个人中心获取用户通知列表的 APIs 规则：\n   `GET /apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications`\n2. 将通知标记为已读：`PUT /apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/mark-as-read`\n3.\n\n批量将通知标记为已读：`PUT /apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/mark-specified-as-read`\n\n#### 通知模板\n\n`NotificationTemplate` 自定义模型用于定义事件的通知模板，当事件触发时会根据事件的通知模板来渲染通知内容。\n它通过定义 `reasonSelector` 来引用事件类别，当事件触发时会根据用户的语言偏好和触发事件的类别来选择一个最佳的通知模板。\n选择通知模板的规则为：\n\n1. 根据用户设置的语言，选择从通知模板中定义的 `spec.reasonSelector.language` 的值从更具体到不太具体的顺序（例如，gl_ES 的值将比\n   gl 的值具有更高的优先级）。\n2. 当通过语言成功匹配到模板时，匹配到的结果可能不止一个，如 `language` 为 `zh_CN`\n   的模板有三个那么会根据 `NotificationTemplate` 的 `metadata.creationTimestamp` 字段来选择一个最新的模板。\n\n这样的规则有助于用户可以个性化定制某些事件的模板内容。\n\n模板语法使用 ThymeleafEngine 渲染，纯文本模板使用 `textual`\n模板模式，语法参考: [usingthymeleaf.html#textual-syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#textual-syntax)\n\n`HTML`\n则使用标准表达式语法在标签属性中取值，语法参考：[standard-expression-syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-expression-syntax)\n\n在通知中心渲染模板时会在 `ReasonAttributes` 中提供额外属性包括：\n\n- site.title: 站点标题\n- site.subtitle: 站点副标题\n- site.logo: 站点 LOGO\n- site.url: 站点访问地址\n- subscriber.id: 如果是用户则为用户名, 如果是匿名用户则为 `annoymousUser#email`\n- subscriber.displayName: 邮箱地址或`@username`\n- unsubscribeUrl: 退订链接，用于取消订阅\n\n因此，任何模板都可以使用这几个属性，但事件定义者需要注意避免使用这些保留属性。\n\n```yaml\napiVersion: notification.halo.run/v1alpha1\nkind: NotificationTemplate\nmetadata:\n  name: template-new-comment-on-post\nspec:\n  reasonSelector:\n    reasonType: new-comment-on-post\n    language: zh_CN\n  template:\n    title: \"你的文章 [(${postTitle})] 收到了一条新评论\"\n    body: |\n      [(${commenter})] 评论了你的文章 [(${postTitle})]，内容如下：\n      [(${comment})]\n```\n\n#### 通知器声明及扩展\n\n`NotifierDescriptor` 自定义模型用于声明通知器，通过它来描述通知器的名称、描述和关联的 `ExtensionDefinition`\n名称，让用户可以在用户界面知道通知器是什么以及它可以做什么,\n还让 NotificationCenter 知道如何加载通知器和准备通知器需要的设置以发送通知。\n\n```yaml\napiVersion: notification.halo.run/v1alpha1\nkind: NotifierDescriptor\nmetadata:\n  name: email-notifier\nspec:\n  displayName: '邮件通知器'\n  description: '支持通过邮件的方式发送通知。'\n  notifierExtName: '通知对应的扩展名称'\n  senderSettingRef:\n    name: 'email-notifier'\n    group: 'sender'\n  receiverSettingRef:\n    name: 'email-notifier'\n    group: 'receiver'\n```\n\n通知器声明了 senderSettingRef 和 receiverSettingRef 后，对应用户端可以通过以下 APIs 获取和保存配置：\n\n管理员获取和保存通知器发送配置的 APIs:\n\n1. 获取通知器发送方配置：`GET /apis/api.console.halo.run/v1alpha1/notifiers/{name}/sender-config`\n2. 保存通知器发送方配置：`POST /apis/api.console.halo.run/v1alpha1/notifiers/{name}/sender-config`\n\n个人中心用户获取和保存对应通知器接收消息配置的 APIs:\n\n1. 获取通知器接收消息配置：`GET /apis/api.notification.halo.run/v1alpha1/notifiers/{name}/receiver-config`\n2. 获取通知器接收消息配置：`POST /apis/api.notification.halo.run/v1alpha1/notifiers/{name}/receiver-config`\n\n通知器扩展点用于实现发送通知的方式：\n\n```java\npublic interface ReactiveNotifier extends ExtensionPoint {\n\n  /**\n   * Notify user.\n   *\n   * @param context notification context must not be null\n   */\n  Mono<Void> notify(NotificationContext context);\n}\n\n@Data\npublic class NotificationContext {\n  private Message message;\n\n  private ObjectNode receiverConfig;\n\n  private ObjectNode senderConfig;\n\n  @Data\n  static class Message {\n    private MessagePayload payload;\n\n    private Subject subject;\n\n    private String recipient;\n\n    private Instant timestamp;\n  }\n\n  @Data\n  public static class Subject {\n    private String apiVersion;\n    private String kind;\n    private String name;\n    private String title;\n    private String url;\n  }\n\n  @Data\n  static class MessagePayload {\n    private String title;\n\n    private String rawBody;\n\n    private String htmlBody;\n\n    private ReasonAttributes attributes;\n  }\n}\n```\n\n通知数据结构交互图\n\n![Notification datastructures interaction](./image-knhw.png)\n\n通知功能 UI 设计\n![notification-ui.png](notification-ui.png)\n\n### 通知模块功能\n\n- 发送通知：当触发通知事件时，系统会根据 subscriber 的偏好设置获取到事件对应的通知方式再根据偏好设置自动发送通知。\n- 接收通知：用户可以选择接收通知的方式，例如邮件、短信、自定义路由通知等。\n- 查看通知：用户可以在 Halo 中查看所有的通知，包括已读和未读的通知。\n- 标记通知：用户可以标记通知为已读或未读状态，以便更好地管理和处理通知。\n\n### 通知管理列表条件筛选\n\n我们支持以下通知条件筛选策略：\n\n- 按事件类型：列出特定类型的事件通知，例如新文章，新评论、状态更新等。\n- 按已读状态：根据通知是否已读列出，方便用户查看未读通知。\n- 按关键词：列出通知中包含特定关键词的事件通知，例如包含用户名称、标题等关键词的通知。\n- 按时间：列出在特定时间段内发生的事件通知，例如最近一周、最近一个月等时间段内的通知。\n\n### 定制化选项\n\n如果后续有足够的使用场景，可以考虑支持以下定制化选项：\n\n- 通知时间段：用户可以设置通知的时间段，例如只在工作时间内推送通知。\n- 通知频率：用户可以设置通知的频率，例如每天、每周、每月等。\n- 摘要通知：用户可以设置接收每周摘要，总结一周内的通知合并为一条通知并通过如邮件等方式接收。\n\n## 结论\n\n通过以上方案和实现，我们设计了一个通知功能，可以根据用户的需求和偏好，自动筛选和推送通知。同时，为了支持更多的事件类型、通知方式和通知条件筛选策略，系统具有良好的可扩展性。\n"
  },
  {
    "path": "docs/plugin/shared-event.md",
    "content": "# 插件中如何发送共享事件（SharedEvent）\n\n在插件中，可以通过共享事件（SharedEvent）来发送消息。 共享事件是一种特殊的事件，它可以被核心和所有插件订阅。\n\n## 订阅共享事件\n\n目前，核心中已经提供了不少的共享事件，例如 `run.halo.app.event.post.PostPublishedEvent`、`run.halo.app.event.post.PostUpdatedEvent`\n，这些事件由核心发布，核心和插件均可订阅。请看下面的示例：\n\n```java\n\n@Component\npublic class PostPublishedEventListener implements ApplicationListener<PostPublishedEvent> {\n\n  @Override\n  public void onApplicationEvent(PostPublishedEvent event) {\n    // Do something\n  }\n\n}\n```\n\n或者通过 `@EventListener` 注解实现，\n\n```java\n\n@Component\npublic class PostPublishedEventListener {\n\n  @EventListener\n  // @Async // 如果需要异步处理，可以添加此注解\n  public void onPostPublished(PostPublishedEvent event) {\n    // Do something\n  }\n\n}\n```\n\n> 需要注意的是，只有被 `@SharedEvent` 注解标记的事件才能够被其他插件或者核心订阅。\n\n## 发送共享事件\n\n在插件中，我们可以通过 `ApplicationEventPublisher` 来发送共享事件，请看下面的示例：\n\n```java\n\n@Service\npublic class PostService {\n\n  private final ApplicationEventPublisher eventPublisher;\n\n  public PostService(ApplicationEventPublisher eventPublisher) {\n    this.eventPublisher = eventPublisher;\n  }\n\n  public void publishPost(Post post) {\n    // Do something\n    eventPublisher.publishEvent(new PostPublishedEvent(post));\n  }\n\n}\n```\n\n## 创建共享事件\n\n在插件中，我们可以创建自定义的共享事件，供其他插件订阅，示例如下：\n\n```java\n\n@SharedEvent\npublic class MySharedEvent extends ApplicationEvent {\n\n  public MySharedEvent(Object source) {\n    super(source);\n  }\n\n}\n```\n\n> 需要注意的是：\n> 1. 共享事件必须继承 `ApplicationEvent`。\n> 2. 共享事件必须被 `@SharedEvent` 注解标记。\n> 3. 如果想要被其他插件订阅，则需要将该事件类发布到 Maven 仓库中，供其他插件引用。\n"
  },
  {
    "path": "docs/plugin/websocket.md",
    "content": "# 插件中如何实现 WebSocket\n\n## 背景\n\n> https://github.com/halo-dev/halo/issues/5285\n\n越来越多的开发者在开发插件过程中需要及时高效获取某些资源的最新状态，但是因为在插件中不支持 WebSocket，故只能选择定时轮训的方式来解决。\n\n在插件中支持 WebSocket 的功能需要 Halo Core 来适配并制定规则以方便插件实现 WebSocket。\n\n## 实现\n\n插件中实现 WebSocket 的代码样例如下所示：\n\n```java\n@Component\npublic class MyWebSocketEndpoint implements WebSocketEndpoint {\n\n  @Override\n  public GroupVersion groupVersion() {\n    return GroupVersion.parseApiVersion(\"my-plugin.halowrite.com/v1alpha1\");\n  }\n\n  @Override\n  public String urlPath() {\n    return \"/resources\";\n  }\n\n  @Override\n  public WebSocketHandler handler() {\n    return session -> {\n      var messages = session.receive()\n              .map(message -> {\n                var payload = message.getPayloadAsText();\n                return session.textMessage(payload.toUpperCase());\n              });\n      return session.send(messages);\n    };\n  }\n}\n```\n\n插件安装成功后，可以通过 `/apis/my-plugin.halowrite.com/v1alpha1/resources` 进行访问。 示例如下所示：\n\n```bash\nwebsocat --basic-auth admin:admin ws://127.0.0.1:8090/apis/my-plugin.halowrite.com/v1alpha1/resources\n```\n\n同样地，WebSocket 相关的 API 仍然受当前权限系统管理。\n"
  },
  {
    "path": "e2e/Dockerfile",
    "content": "FROM ghcr.io/linuxsuren/api-testing:v0.0.17\nWORKDIR /workspace\nCOPY testsuite.yaml .\nCMD [ \"atest\", \"run\", \"-p\", \"testsuite.yaml\", \"--level=trace\", \"--request-ignore-error\", \"--report=md\" ]\n"
  },
  {
    "path": "e2e/Makefile",
    "content": "all:\n\t./start.sh\n\t./start.sh compose-postgres.yaml\n\t./start.sh compose-mysql.yaml\ndemo:\n\tdocker-compose up halo\n"
  },
  {
    "path": "e2e/README.md",
    "content": "Please add the corresponding e2e (aka end-to-end) test cases if you add or update APIs.\n\n## How to work\n* Start and watch the [docker-compose](https://docs.docker.com/compose/) via [the script](start.sh)\n  * It has three containers: database, Halo, and testing\n* Run the e2e testing via [api-testing](https://github.com/LinuxSuRen/api-testing)\n  * It will run the test cases from top to bottom\n  * You can add the necessary asserts to it\n\n## Run locally\nPlease follow these steps if you want to run the e2e testing locally.\n\n> Please make sure you have installed docker-compose v2\n\n* Build project via `./gradlew clean build -x check` in root directory of this repository\n* Build image via `docker build . -t ghcr.io/halo-dev/halo-dev:main`\n* Change the directory to `e2e`, then execute `./start.sh`\n\n## Run Halo only\nPlease run the following command if you only want to run Halo.\n\n```shell\ndocker-compose up halo\n```\n"
  },
  {
    "path": "e2e/compose-mysql.yaml",
    "content": "version: '3.1'\nservices:\n  testing:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    links:\n      - halo\n    depends_on:\n      halo:\n        condition: service_healthy\n  halo:\n    image: ghcr.io/halo-dev/halo-dev:${TAG:-main}\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8090/actuator/health/readiness\"]\n      interval: 30s\n      timeout: 5s\n      retries: 5\n      start_period: 30s\n    command:\n      - --spring.r2dbc.url=r2dbc:pool:mysql://mysql:3306/halo\n      - --spring.r2dbc.username=root\n      - --spring.r2dbc.password=halo\n      - --spring.sql.init.platform=mysql\n    links:\n      - mysql\n    depends_on:\n      mysql:\n        condition: service_healthy\n  mysql:\n    image: mysql:8.1.0\n    container_name: mysql\n    restart: on-failure:3\n    command: \n      - --default-authentication-plugin=caching_sha2_password\n      - --character-set-server=utf8mb4\n      - --collation-server=utf8mb4_general_ci\n      - --explicit_defaults_for_timestamp=true\n    healthcheck:\n      test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"127.0.0.1\", \"--silent\"]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    environment:\n      - MYSQL_ROOT_PASSWORD=halo\n      - MYSQL_DATABASE=halo\n"
  },
  {
    "path": "e2e/compose-postgres.yaml",
    "content": "version: '3.1'\nservices:\n  testing:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    links:\n      - halo\n    depends_on:\n      halo:\n        condition: service_healthy\n  halo:\n    image: ghcr.io/halo-dev/halo-dev:${TAG:-main}\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8090/actuator/health/readiness\"]\n      interval: 30s\n      timeout: 5s\n      retries: 5\n      start_period: 30s\n    command:\n      - --spring.r2dbc.url=r2dbc:pool:postgresql://postgres/halo\n      - --spring.r2dbc.username=halo\n      # PostgreSQL 的密码，请保证与下方 POSTGRES_PASSWORD 的变量值一致。\n      - --spring.r2dbc.password=openpostgresql\n      - --spring.sql.init.platform=postgresql\n      # 外部访问地址，请根据实际需要修改\n      # - --halo.external-url=http://localhost:8090/\n    ports:\n      - 8090:8090\n    links:\n      - postgres\n    depends_on:\n      postgres:\n        condition: service_healthy\n  postgres:\n    image: postgres:15.4\n    container_name: postgres\n    restart: on-failure:3\n    healthcheck:\n      test: [ \"CMD\", \"pg_isready\" ]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    environment:\n      - POSTGRES_PASSWORD=openpostgresql\n      - POSTGRES_USER=halo\n      - POSTGRES_DB=halo\n      - PGUSER=halo\n"
  },
  {
    "path": "e2e/compose.yaml",
    "content": "version: '3.1'\nservices:\n  testing:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    links:\n      - halo\n    depends_on:\n      halo:\n        condition: service_healthy\n  halo:\n    image: ghcr.io/halo-dev/halo-dev:${TAG:-main}\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8090/actuator/health/readiness\"]\n      interval: 30s\n      timeout: 5s\n      retries: 5\n      start_period: 30s\n    ports:\n      - 8090:8090\n"
  },
  {
    "path": "e2e/start.sh",
    "content": "#!/bin/bash\n\nfile=$1\nif [ \"$file\" == \"\" ]\nthen\n    file=compose.yaml\nfi\n\ndocker-compose -f \"$file\" down\ndocker-compose -f \"$file\" up --build testing --exit-code-from testing --remove-orphans\n"
  },
  {
    "path": "e2e/testsuite.yaml",
    "content": "name: halo\napi: |\n  {{default \"http://halo:8090\" (env \"SERVER\")}}/apis\nparam:\n  postName: \"{{randAlpha 6}}\"\n  userName: \"{{randAlpha 6}}\"\n  roleName: \"{{randAlpha 6}}\"\n  notificationName: \"{{randAlpha 6}}\"\n  auth: \"Basic YWRtaW46MTIzNDU2\"\nitems:\n- name: setup\n  request:\n    api: |\n      {{default \"http://halo:8090\" (env \"SERVER\")}}/system/setup\n    method: POST\n    header:\n      Content-Type: application/x-www-form-urlencoded\n      Accept: application/json\n    body: |\n      siteTitle=testing&username={{.param.userName}}&password=123456&email=testing@halo.run\n  expect:\n    statusCode: 204\n- name: createPost\n  request:\n    api: /api.console.halo.run/v1alpha1/posts\n    method: POST\n    header:\n      Authorization: \"{{.param.auth}}\"\n      Content-Type: application/json\n    body: |\n      {\n        \"post\": {\n          \"spec\": {\n            \"title\": \"{{.param.postName}}\",\n            \"slug\": \"{{.param.postName}}\",\n            \"template\": \"\",\n            \"cover\": \"\",\n            \"deleted\": false,\n            \"publish\": false,\n            \"pinned\": false,\n            \"allowComment\": true,\n            \"visible\": \"PUBLIC\",\n            \"priority\": 0,\n            \"excerpt\": {\n              \"autoGenerate\": true,\n              \"raw\": \"\"\n            },\n            \"categories\": [],\n            \"tags\": [],\n            \"htmlMetas\": []\n          },\n          \"apiVersion\": \"content.halo.run/v1alpha1\",\n          \"kind\": \"Post\",\n          \"metadata\": {\n            \"name\": \"c31f2192-c992-47b9-86b4-f3fc0605360e\",\n            \"annotations\": {\n              \"content.halo.run/preferred-editor\": \"default\"\n            }\n          }\n        },\n        \"content\": {\n          \"raw\": \"<p>{{.param.postName}}</p>\",\n          \"content\": \"<p>{{.param.postName}}</p>\",\n          \"rawType\": \"HTML\"\n        }\n      }\n- name: listPosts\n  request:\n    api: /api.console.halo.run/v1alpha1/posts?keyword={{.param.postName}}\n  expect:\n    verify:\n      - data.total == 1\n- name: recyclePost\n  request:\n    api: /api.console.halo.run/v1alpha1/posts/{{(index .listPosts.items 0).post.metadata.name}}/recycle\n    method: PUT\n- name: recover\n  request:\n    api: /content.halo.run/v1alpha1/posts/{{(index .listPosts.items 0).post.metadata.name}}\n    method: DELETE\n\n## Users\n- name: createUser\n  request:\n    api: /api.console.halo.run/v1alpha1/users\n    method: POST\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"avatar\": \"\",\n        \"bio\": \"{{randAlpha 6}}\",\n        \"displayName\": \"{{randAlpha 6}}\",\n        \"email\": \"test@halo.com\",\n        \"name\": \"{{.param.userName}}\",\n        \"password\": \"{{randAlpha 6}}\",\n        \"phone\": \"\",\n        \"roles\": []\n      }\n- name: updateUserPass\n  request:\n    api: /api.console.halo.run/v1alpha1/users/{{.param.userName}}/password\n    method: PUT\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"password\": \"{{randAlpha 3}}\"\n      }\n- name: grantPermission\n  request:\n    api: /api.console.halo.run/v1alpha1/users/{{.param.userName}}/permissions\n    method: POST\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"roles\": [\n          \"guest\"\n        ]\n      }\n- name: sendPasswordResetEmail\n  request:\n    api: |\n      /api.halo.run/v1alpha1/users/-/send-password-reset-email\n    method: POST\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"username\": \"{{.param.userName}}\",\n        \"email\": \"{{.param.email}}\"\n      }\n  expect:\n    statusCode: 204\n- name: resetPasswordByToken\n  request:\n    api: |\n      /api.halo.run/v1alpha1/users/{{.param.userName}}/reset-password\n    method: PUT\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"newPassword\": \"{{randAlpha 6}}\",\n        \"token\": \"{{randAlpha 6}}\"\n      }\n  expect:\n    statusCode: 403\n## Roles\n- name: createRole\n  request:\n    api: |\n      {{default \"http://halo:8090\" (env \"SERVER\")}}/api/v1alpha1/roles\n    method: POST\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"kind\": \"Role\",\n        \"metadata\": {\n          \"name\": \"\",\n          \"generateName\": \"role-\",\n          \"labels\": {},\n          \"annotations\": {\n            \"rbac.authorization.halo.run/dependencies\": \"[\\\"role-template-manage-appstore\\\"]\",\n            \"rbac.authorization.halo.run/display-name\": \"{{.param.roleName}}\"\n          }\n        },\n        \"rules\": []\n      }\n  expect:\n    statusCode: 201\n- name: listRoles\n  request:\n    api: |\n      {{default \"http://halo:8090\" (env \"SERVER\")}}/api/v1alpha1/roles\n  expect:\n    verify:\n      - data.total >= 3\n- name: deleteRole\n  request:\n    api: |\n      {{default \"http://halo:8090\" (env \"SERVER\")}}/api/v1alpha1/roles/{{(index .listRoles.items 0).metadata.name}}\n    method: DELETE\n\n## Plugins\n- name: installPlugin\n  request:\n    api: /api.console.halo.run/v1alpha1/plugins/-/install-from-uri\n    method: POST\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"uri\": \"https://github.com/Stonewuu/halo-plugin-sitepush/releases/download/1.3.1/halo-plugin-sitepush-1.3.1.jar\"\n      }\n- name: pluginList\n  request:\n    api: /api.console.halo.run/v1alpha1/plugins\n  expect:\n    verify:\n      - data.total >= 1\n- name: inActivePlugins\n  request:\n    api: /api.console.halo.run/v1alpha1/plugins?enabled=false&keyword=&page=0&size=0\n  expect:\n    verify:\n      - data.total == 1\n- name: disablePlugin\n  request:\n    api: /api.console.halo.run/v1alpha1/plugins/PluginSitePush/plugin-state\n    method: PUT\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"enable\": false\n      }\n- name: enablePlugin\n  request:\n    api: /api.console.halo.run/v1alpha1/plugins/PluginSitePush/plugin-state\n    method: PUT\n    header:\n      Content-Type: application/json\n    body: |\n      {\n        \"enable\": true\n      }\n- name: resetPlugin\n  request:\n    api: /api.console.halo.run/v1alpha1/plugins/PluginSitePush/reset-config\n    method: PUT\n    header:\n      Content-Type: application/json\n- name: uninstallPlugin\n  request:\n    api: /plugin.halo.run/v1alpha1/plugins/PluginSitePush\n    method: DELETE\n\n  # Notifications\n- name: createNotification\n  request:\n    api: /notification.halo.run/v1alpha1/notifications\n    method: POST\n    body: |\n      {\n        \"spec\": {\n          \"recipient\": \"admin\",\n          \"reason\": \"fake-reason\",\n          \"title\": \"test 评论了你的页面《关于我》\",\n          \"rawContent\": \"Fake raw content\",\n          \"htmlContent\": \"<p>Fake html content</p>\",\n          \"unread\": true\n        },\n        \"apiVersion\": \"notification.halo.run/v1alpha1\",\n        \"kind\": \"Notification\",\n        \"metadata\": {\n          \"name\": \"{{.param.notificationName}}\"\n        }\n      }\n    header:\n      Content-Type: application/json\n  expect:\n    statusCode: 201\n- name: getNotificationByName\n  request:\n    api: /notification.halo.run/v1alpha1/notifications/{{.param.notificationName}}\n    method: GET\n  expect:\n    statusCode: 200\n    verify:\n      - data.spec.reason == \"fake-reason\"\n      - data.spec.title == \"test 评论了你的页面《关于我》\"\n- name: deleteUserNotification\n  request:\n    api: |\n      /api.notification.halo.run/v1alpha1/userspaces/admin/notifications/{{.param.notificationName}}\n    method: DELETE\n\n- name: deleteUser\n  request:\n    api: |\n      {{default \"http://halo:8090\" (env \"SERVER\")}}/api/v1alpha1/users/{{.param.userName}}\n    method: DELETE\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nlucene = '10.3.2'\nresilience4j = '2.3.0'\ntherapi = '0.15.0'\ncheckstyle = \"12.1.0\"\n\n[libraries]\nlucene-core = { module = 'org.apache.lucene:lucene-core', version.ref = 'lucene' }\nlucene-queryparser = { module = 'org.apache.lucene:lucene-queryparser', version.ref = 'lucene' }\nlucene-highlighter = { module = 'org.apache.lucene:lucene-highlighter', version.ref = 'lucene' }\nlucene-backward-codecs = { module = 'org.apache.lucene:lucene-backward-codecs', version.ref = 'lucene' }\nlucene-analyzers-common = { module = 'org.apache.lucene:lucene-analysis-common', version.ref = 'lucene' }\n\ntherapi-runtime-javadoc = { module = 'com.github.therapi:therapi-runtime-javadoc', version.ref = 'therapi' }\ntherapi-runtime-javadoc-scribe = { module = 'com.github.therapi:therapi-runtime-javadoc-scribe', version.ref = 'therapi' }\n\nresilience4j-springboot3 = { module = 'io.github.resilience4j:resilience4j-spring-boot3', version.ref = 'resilience4j' }\nresilience4j-reactor = { module = 'io.github.resilience4j:resilience4j-reactor', version.ref = 'resilience4j' }\n\napache-commons-lang3 = 'org.apache.commons:commons-lang3:3.20.0'\napache-tika-core = 'org.apache.tika:tika-core:3.2.3'\n\nencoding-base62 = 'io.seruco.encoding:base62:0.1.3'\npf4j = 'org.pf4j:pf4j:3.15.0'\nguava = 'com.google.guava:guava:33.5.0-jre'\njava-diff-utils = 'io.github.java-diff-utils:java-diff-utils:4.16'\njsoup = 'org.jsoup:jsoup:1.22.1'\njson-patch = 'com.github.java-json-tools:json-patch:1.13'\nspringdoc-openapi = 'org.springdoc:springdoc-openapi-starter-webflux-ui:3.0.1'\nopenapi-schema-validator = 'org.openapi4j:openapi-schema-validator:1.0.7'\nbouncycastle-bcpkix = 'org.bouncycastle:bcpkix-jdk18on:1.83'\ntwofactor-auth = 'com.j256.two-factor-auth:two-factor-auth:1.3'\nthumbnailator = 'net.coobird:thumbnailator:0.4.21'\nr2dbc-migrate-starter = 'name.nkonev.r2dbc-migrate:r2dbc-migrate-spring-boot-starter:4.0.1'\n\n[bundles]\nlucene = ['lucene-core', 'lucene-queryparser', 'lucene-highlighter', 'lucene-backward-codecs', 'lucene-analyzers-common']\nresilience4j = ['resilience4j-springboot3', 'resilience4j-reactor']\napache = ['apache-commons-lang3', 'apache-tika-core']\ntherapi = ['therapi-runtime-javadoc', 'therapi-runtime-javadoc-scribe']\n\n[plugins]\nspring-boot = 'org.springframework.boot:4.0.3'\nspring-dependency-management = 'io.spring.dependency-management:1.1.7'\ngit-properties = 'com.gorylenko.gradle-git-properties:2.5.2'\nundercouch-download = 'de.undercouch.download:5.6.0'\nlombok = 'io.freefair.lombok:9.2.0'\nchecksum = 'org.gradle.crypto.checksum:1.4.0'\nnode = 'com.github.node-gradle.node:7.1.0'\nopenapi-generator = 'org.openapi.generator:7.12.0'\nspringdoc-openapi = 'org.springdoc.openapi-gradle-plugin:1.9.0'\nversions = 'com.github.ben-manes.versions:0.53.0'\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.0-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "version=2.23.0-SNAPSHOT\njsonassert.version=2.0-rc1\nr2dbc-mariadb.version=1.4.0\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "hack/cherry_pick_pull.sh",
    "content": "#!/usr/bin/env bash\n\n# Copyright 2015 The Kubernetes Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Usage Instructions: https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md\n\n# Checkout a PR from GitHub. (Yes, this is sitting in a Git tree. How\n# meta.) Assumes you care about pulls from remote \"upstream\" and\n# checks them out to a branch named:\n#  automated-cherry-pick-of-<pr>-<target branch>-<timestamp>\n\nset -o errexit\nset -o nounset\nset -o pipefail\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel)\"\ndeclare -r REPO_ROOT\ncd \"${REPO_ROOT}\"\n\nSTARTINGBRANCH=$(git symbolic-ref --short HEAD)\ndeclare -r STARTINGBRANCH\ndeclare -r REBASEMAGIC=\"${REPO_ROOT}/.git/rebase-apply\"\nDRY_RUN=${DRY_RUN:-\"\"}\nREGENERATE_DOCS=${REGENERATE_DOCS:-\"\"}\nUPSTREAM_REMOTE=${UPSTREAM_REMOTE:-upstream}\nFORK_REMOTE=${FORK_REMOTE:-origin}\nMAIN_REPO_ORG=${MAIN_REPO_ORG:-$(git remote get-url \"$UPSTREAM_REMOTE\" | awk '{gsub(/http[s]:\\/\\/|git@/,\"\")}1' | awk -F'[@:./]' 'NR==1{print $3}')}\nMAIN_REPO_NAME=${MAIN_REPO_NAME:-$(git remote get-url \"$UPSTREAM_REMOTE\" | awk '{gsub(/http[s]:\\/\\/|git@/,\"\")}1' | awk -F'[@:./]' 'NR==1{print $4}')}\n\nif [[ -z ${GITHUB_USER:-} ]]; then\n  echo \"Please export GITHUB_USER=<your-user> (or GH organization, if that's where your fork lives)\"\n  exit 1\nfi\n\nif ! command -v gh > /dev/null; then\n  echo \"Can't find 'gh' tool in PATH, please install from https://github.com/cli/cli\"\n  exit 1\nfi\n\nif [[ \"$#\" -lt 2 ]]; then\n  echo \"${0} <remote branch> <pr-number>...: cherry pick one or more <pr> onto <remote branch> and leave instructions for proposing pull request\"\n  echo\n  echo \"  Checks out <remote branch> and handles the cherry-pick of <pr> (possibly multiple) for you.\"\n  echo \"  Examples:\"\n  echo \"    $0 upstream/release-3.14 12345        # Cherry-picks PR 12345 onto upstream/release-3.14 and proposes that as a PR.\"\n  echo \"    $0 upstream/release-3.14 12345 56789  # Cherry-picks PR 12345, then 56789 and proposes the combination as a single PR.\"\n  echo\n  echo \"  Set the DRY_RUN environment var to skip git push and creating PR.\"\n  echo \"  This is useful for creating patches to a release branch without making a PR.\"\n  echo \"  When DRY_RUN is set the script will leave you in a branch containing the commits you cherry-picked.\"\n  echo\n  echo \"  Set the REGENERATE_DOCS environment var to regenerate documentation for the target branch after picking the specified commits.\"\n  echo \"  This is useful when picking commits containing changes to API documentation.\"\n  echo\n  echo \"  Set UPSTREAM_REMOTE (default: upstream) and FORK_REMOTE (default: origin)\"\n  echo \"  to override the default remote names to what you have locally.\"\n  echo\n  echo \"  For merge process info, see https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md\"\n  exit 2\nfi\n\n# Checks if you are logged in. Will error/bail if you are not.\ngh auth status\n\nif git_status=$(git status --porcelain --untracked=no 2>/dev/null) && [[ -n \"${git_status}\" ]]; then\n  echo \"!!! Dirty tree. Clean up and try again.\"\n  exit 1\nfi\n\nif [[ -e \"${REBASEMAGIC}\" ]]; then\n  echo \"!!! 'git rebase' or 'git am' in progress. Clean up and try again.\"\n  exit 1\nfi\n\ndeclare -r BRANCH=\"$1\"\nshift 1\ndeclare -r PULLS=( \"$@\" )\n\nfunction join { local IFS=\"$1\"; shift; echo \"$*\"; }\nPULLDASH=$(join - \"${PULLS[@]/#/#}\") # Generates something like \"#12345-#56789\"\ndeclare -r PULLDASH\nPULLSUBJ=$(join \" \" \"${PULLS[@]/#/#}\") # Generates something like \"#12345 #56789\"\ndeclare -r PULLSUBJ\n\necho \"+++ Updating remotes...\"\ngit remote update \"${UPSTREAM_REMOTE}\" \"${FORK_REMOTE}\"\n\nif ! git log -n1 --format=%H \"${BRANCH}\" >/dev/null 2>&1; then\n  echo \"!!! '${BRANCH}' not found. The second argument should be something like ${UPSTREAM_REMOTE}/release-0.21.\"\n  echo \"    (In particular, it needs to be a valid, existing remote branch that I can 'git checkout'.)\"\n  exit 1\nfi\n\nNEWBRANCHREQ=\"automated-cherry-pick-of-${PULLDASH}\" # \"Required\" portion for tools.\ndeclare -r NEWBRANCHREQ\nNEWBRANCH=\"$(echo \"${NEWBRANCHREQ}-${BRANCH}\" | sed 's/\\//-/g')\"\ndeclare -r NEWBRANCH\nNEWBRANCHUNIQ=\"${NEWBRANCH}-$(date +%s)\"\ndeclare -r NEWBRANCHUNIQ\necho \"+++ Creating local branch ${NEWBRANCHUNIQ}\"\n\ncleanbranch=\"\"\ngitamcleanup=false\nfunction return_to_kansas {\n  if [[ \"${gitamcleanup}\" == \"true\" ]]; then\n    echo\n    echo \"+++ Aborting in-progress git am.\"\n    git am --abort >/dev/null 2>&1 || true\n  fi\n\n  # return to the starting branch and delete the PR text file\n  if [[ -z \"${DRY_RUN}\" ]]; then\n    echo\n    echo \"+++ Returning you to the ${STARTINGBRANCH} branch and cleaning up.\"\n    git checkout -f \"${STARTINGBRANCH}\" >/dev/null 2>&1 || true\n    if [[ -n \"${cleanbranch}\" ]]; then\n      git branch -D \"${cleanbranch}\" >/dev/null 2>&1 || true\n    fi\n  fi\n}\ntrap return_to_kansas EXIT\n\nSUBJECTS=()\nfunction make-a-pr() {\n  local rel\n  rel=\"$(basename \"${BRANCH}\")\"\n  echo\n  echo \"+++ Creating a pull request on GitHub at ${GITHUB_USER}:${NEWBRANCH}\"\n\n  local numandtitle\n  numandtitle=$(printf '%s\\n' \"${SUBJECTS[@]}\")\n  prtext=$(cat <<EOF\nCherry pick of ${PULLSUBJ} on ${rel}.\n\n${numandtitle}\n\nFor details on the cherry pick process, see the [cherry pick requests](https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md) page.\n\n\\`\\`\\`release-note\n\n\\`\\`\\`\nEOF\n)\n\n  gh pr create --title=\"Automated cherry pick of ${numandtitle}\" --body=\"${prtext}\" --head \"${GITHUB_USER}:${NEWBRANCH}\" --base \"${rel}\" --repo=\"${MAIN_REPO_ORG}/${MAIN_REPO_NAME}\"\n}\n\ngit checkout -b \"${NEWBRANCHUNIQ}\" \"${BRANCH}\"\ncleanbranch=\"${NEWBRANCHUNIQ}\"\n\ngitamcleanup=true\nfor pull in \"${PULLS[@]}\"; do\n  echo \"+++ Downloading patch to /tmp/${pull}.patch (in case you need to do this again)\"\n\n  curl -o \"/tmp/${pull}.patch\" -sSL \"https://github.com/${MAIN_REPO_ORG}/${MAIN_REPO_NAME}/pull/${pull}.patch\"\n  echo\n  echo \"+++ About to attempt cherry pick of PR. To reattempt:\"\n  echo \"  $ git am -3 /tmp/${pull}.patch\"\n  echo\n  git am -3 \"/tmp/${pull}.patch\" || {\n    conflicts=false\n    while unmerged=$(git status --porcelain | grep ^U) && [[ -n ${unmerged} ]] \\\n      || [[ -e \"${REBASEMAGIC}\" ]]; do\n      conflicts=true # <-- We should have detected conflicts once\n      echo\n      echo \"+++ Conflicts detected:\"\n      echo\n      (git status --porcelain | grep ^U) || echo \"!!! None. Did you git am --continue?\"\n      echo\n      echo \"+++ Please resolve the conflicts in another window (and remember to 'git add / git am --continue')\"\n      read -p \"+++ Proceed (anything but 'y' aborts the cherry-pick)? [y/n] \" -r\n      echo\n      if ! [[ \"${REPLY}\" =~ ^[yY]$ ]]; then\n        echo \"Aborting.\" >&2\n        exit 1\n      fi\n    done\n\n    if [[ \"${conflicts}\" != \"true\" ]]; then\n      echo \"!!! git am failed, likely because of an in-progress 'git am' or 'git rebase'\"\n      exit 1\n    fi\n  }\n\n  # set the subject\n  subject=$(grep -m 1 \"^Subject\" \"/tmp/${pull}.patch\" | sed -e 's/Subject: \\[PATCH//g' | sed 's/.*] //')\n  SUBJECTS+=(\"#${pull}: ${subject}\")\n\n  # remove the patch file from /tmp\n  rm -f \"/tmp/${pull}.patch\"\ndone\ngitamcleanup=false\n\n# Re-generate docs (if needed)\nif [[ -n \"${REGENERATE_DOCS}\" ]]; then\n  echo\n  echo \"Regenerating docs...\"\n  if ! hack/generate-docs.sh; then\n    echo\n    echo \"hack/generate-docs.sh FAILED to complete.\"\n    exit 1\n  fi\nfi\n\nif [[ -n \"${DRY_RUN}\" ]]; then\n  echo \"!!! Skipping git push and PR creation because you set DRY_RUN.\"\n  echo \"To return to the branch you were in when you invoked this script:\"\n  echo\n  echo \"  git checkout ${STARTINGBRANCH}\"\n  echo\n  echo \"To delete this branch:\"\n  echo\n  echo \"  git branch -D ${NEWBRANCHUNIQ}\"\n  exit 0\nfi\n\nif git remote -v | grep ^\"${FORK_REMOTE}\" | grep \"${MAIN_REPO_ORG}/${MAIN_REPO_NAME}.git\"; then\n  echo \"!!! You have ${FORK_REMOTE} configured as your ${MAIN_REPO_ORG}/${MAIN_REPO_NAME}.git\"\n  echo \"This isn't normal. Leaving you with push instructions:\"\n  echo\n  echo \"+++ First manually push the branch this script created:\"\n  echo\n  echo \"  git push REMOTE ${NEWBRANCHUNIQ}:${NEWBRANCH}\"\n  echo\n  echo \"where REMOTE is your personal fork (maybe ${UPSTREAM_REMOTE}? Consider swapping those.).\"\n  echo \"OR consider setting UPSTREAM_REMOTE and FORK_REMOTE to different values.\"\n  echo\n  make-a-pr\n  cleanbranch=\"\"\n  exit 0\nfi\n\necho\necho \"+++ I'm about to do the following to push to GitHub (and I'm assuming ${FORK_REMOTE} is your personal fork):\"\necho\necho \"  git push ${FORK_REMOTE} ${NEWBRANCHUNIQ}:${NEWBRANCH}\"\necho\nread -p \"+++ Proceed (anything but 'y' aborts the cherry-pick)? [y/n] \" -r\nif ! [[ \"${REPLY}\" =~ ^[yY]$ ]]; then\n  echo \"Aborting.\" >&2\n  exit 1\nfi\n\ngit push \"${FORK_REMOTE}\" -f \"${NEWBRANCHUNIQ}:${NEWBRANCH}\"\nmake-a-pr\n"
  },
  {
    "path": "platform/application/build.gradle",
    "content": "import org.springframework.boot.gradle.plugin.SpringBootPlugin\n\nplugins {\n    id 'java-platform'\n    id 'halo.publish'\n    alias(libs.plugins.spring.boot) apply false\n}\n\ngroup = 'run.halo.tools.platform'\ndescription = 'Platform of application.'\n\njavaPlatform {\n    allowDependencies()\n}\n\ndependencies {\n    api platform(SpringBootPlugin.BOM_COORDINATES)\n\n    constraints {\n        api libs.bundles.lucene\n        api libs.bundles.apache\n        api libs.bundles.therapi\n        api libs.springdoc.openapi\n        api libs.openapi.schema.validator\n        api libs.bouncycastle.bcpkix\n        api libs.encoding.base62\n        api libs.pf4j\n        api libs.guava\n        api libs.java.diff.utils\n        api libs.jsoup\n        api libs.json.patch\n        api libs.bundles.resilience4j\n        api libs.twofactor.auth\n        api libs.thumbnailator\n        api \"org.springframework.integration:spring-integration-core\"\n        api \"org.thymeleaf.extras:thymeleaf-extras-springsecurity6\"\n    }\n\n}\n\npublishing {\n    publications.named('mavenJava', MavenPublication) {\n        from components.javaPlatform\n        pom {\n            name = 'Application platform.'\n            description = \"$project.description\"\n        }\n    }\n}\n"
  },
  {
    "path": "platform/plugin/build.gradle",
    "content": "plugins {\n    id 'java-platform'\n    id 'halo.publish'\n}\n\ngroup = 'run.halo.tools.platform'\ndescription = 'This is the platform that other plugins depend on. ' +\n        'We can put the plugin API as a dependency at here.'\n\njavaPlatform {\n    allowDependencies()\n}\n\ndependencies {\n    api platform(project(':platform:application'))\n    constraints {\n        api project(':api')\n        // TODO other plugin API dependencies\n        // e.g.: api 'halo.run.plugin:links-api:1.1.0'\n    }\n}\n\npublishing {\n    publications.named('mavenJava', MavenPublication) {\n        from components.javaPlatform\n        pom {\n            name = 'Plugin platform'\n            description = \"$project.description\"\n        }\n    }\n}\n"
  },
  {
    "path": "settings.gradle",
    "content": "pluginManagement {\n    repositories {\n        maven { url = 'https://repo.spring.io/milestone' }\n        gradlePluginPortal()\n    }\n}\n\nrootProject.name = 'halo'\ninclude 'api', 'application', 'platform:application', 'platform:plugin', 'ui'\n"
  },
  {
    "path": "ui/.editorconfig",
    "content": "root=true\n\n[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = false\ninsert_final_newline = true\n"
  },
  {
    "path": "ui/.husky/pre-commit",
    "content": "cd ui && pnpm exec lint-staged\n"
  },
  {
    "path": "ui/.npmrc",
    "content": "strict-peer-dependencies=false\nauto-install-peers=true\n"
  },
  {
    "path": "ui/.oxfmtrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n  \"sortTailwindcss\": {},\n  \"sortImports\": {\n    \"newlinesBetween\": false\n  },\n  \"trailingComma\": \"es5\",\n  \"printWidth\": 80,\n  \"sortPackageJson\": true,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"insertFinalNewline\": true,\n  \"ignorePatterns\": [\n    \"packages/api-client/src\",\n    \"docs\",\n    \"**/dist/**\",\n    \"build/**\",\n    \"storybook-static\",\n    \".idea\"\n  ],\n  \"overrides\": [\n    {\n      \"files\": [\"**/*.html\"],\n      \"options\": {\n        \"printWidth\": 120\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "ui/Makefile",
    "content": "SHELL := /usr/bin/env bash -o errexit -o pipefail -o nounset\n\ninstall: ## Install console\n\tpnpm install\n\nbuild-packages: install ## Build packages of console\n\tpnpm build:packages\n\nbuild: build-packages ## Build console\n\tpnpm build\n\nlint: build-packages ## Lint console\n\tpnpm lint\n\tpnpm typecheck\n\ntest: build-packages ## Test console\n\tpnpm test:unit\n\ncheck: lint test ## Check console\n\ndev: build-packages ## Run console with development environment\n\tpnpm dev\n\napi-client-gen: install ## Generate API client\n\tpnpm api-client:gen\n\nhelp: ## print this help\n\t@awk 'BEGIN {FS = \":.*?## \"} /^[a-zA-Z0-9_-]+:.*?## / {gsub(\"\\\\\\\\n\",sprintf(\"\\n%22c\",\"\"), $$2);printf \"\\033[36m%-20s\\033[0m %s\\n\", $$1, $$2}' $(MAKEFILE_LIST)\n"
  },
  {
    "path": "ui/build.gradle",
    "content": "plugins {\n  id 'idea'\n  id 'base'\n  alias(libs.plugins.node)\n  alias(libs.plugins.openapi.generator)\n}\n\nidea {\n  module {\n    excludeDirs += file('node_modules/')\n    excludeDirs += file('packages').listFiles().collect {\n      file(it.path + '/node_modules/')\n    }\n    excludeDirs += file('packages').listFiles().collect {\n      file(it.path + '/dist/')\n    }\n  }\n}\n\npnpmInstall {\n  nodeModulesOutputFilter {\n    exclude('**')\n  }\n}\n\ntasks.named('clean') {\n  dependsOn tasks.named('doClean')\n}\n\ntasks.register('doClean', Delete) {\n  delete layout.buildDirectory\n  delete fileTree('packages') {\n    include '*/dist/**'\n  }\n}\n\ntasks.named('assemble') {\n  dependsOn tasks.named('doBuild')\n}\n\ntasks.register('doBuild', PnpmTask) {\n  dependsOn tasks.named('buildPackages')\n  pnpmCommand = ['run', 'build']\n  inputs.files(fileTree(layout.projectDirectory) {\n    include 'console-src/**', 'uc-src/**', 'src/**', 'public/**', '*.js', '*.json', '*.yaml', '*.html'\n    exclude '**/node_modules/**', '**/build/**', '**/dist/**'\n  })\n  outputs.dir(layout.buildDirectory.dir('dist'))\n}\n\ntasks.register('buildPackages', PnpmTask) {\n  dependsOn tasks.named('pnpmInstall')\n  inputs.files(fileTree('packages') {\n    exclude '**/node_modules/**', '**/dist/**'\n  })\n  inputs.file('package.json')\n  pnpmCommand = ['run', 'build:packages']\n  outputs.files(fileTree('packages') {\n    include '*/dist/**'\n  })\n}\n\ntasks.register('test', PnpmTask) {\n  dependsOn tasks.named('buildPackages')\n  pnpmCommand = ['run', 'test:unit']\n  shouldRunAfter tasks.named('lint'), tasks.named('typecheck')\n}\n\ntasks.register('lint', PnpmTask) {\n  dependsOn tasks.named('buildPackages')\n  pnpmCommand = ['run', 'lint']\n}\n\ntasks.register('typecheck', PnpmTask) {\n  dependsOn tasks.named('buildPackages')\n  pnpmCommand = ['run', 'typecheck']\n}\n\ntasks.named('check') {\n  dependsOn tasks.named('lint'), tasks.named('typecheck'), tasks.named('test')\n}\n\ntasks.register('dev', PnpmTask) {\n  dependsOn tasks.named('buildPackages')\n  pnpmCommand = ['run', 'dev']\n}\n"
  },
  {
    "path": "ui/console-src/App.vue",
    "content": "<script lang=\"ts\" setup>\nimport BaseApp from \"@/components/base-app/BaseApp.vue\";\n</script>\n\n<template>\n  <BaseApp />\n</template>\n"
  },
  {
    "path": "ui/console-src/components/snapshots/BaseSnapshots.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  type ContentWrapper,\n  type ListedSnapshotDto,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconHistoryLine,\n  Toast,\n  VButton,\n  VCard,\n  VLoading,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { computed, toRefs, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { SNAPSHOTS_QUERY_KEY } from \"./query-keys\";\nimport SnapshotContent from \"./SnapshotContent.vue\";\nimport SnapshotDiffContent from \"./SnapshotDiffContent.vue\";\nimport SnapshotListItem from \"./SnapshotListItem.vue\";\n\nconst props = defineProps<{\n  title: string;\n  name: string;\n  cacheKey: string;\n  releaseSnapshot?: string;\n  baseSnapshot?: string;\n  headSnapshot?: string;\n  listApi: () => Promise<ListedSnapshotDto[]>;\n  getApi: (snapshotName: string) => Promise<ContentWrapper>;\n  deleteApi: (snapshotName: string) => Promise<void>;\n  revertApi: (snapshotName: string) => Promise<void>;\n}>();\n\nconst { cacheKey, name } = toRefs(props);\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst { data: snapshots, isLoading } = useQuery({\n  queryKey: SNAPSHOTS_QUERY_KEY(cacheKey, name),\n  queryFn: async () => {\n    return await props.listApi();\n  },\n  refetchInterval(data) {\n    const hasDeletingData = data?.some(\n      (item) => !!item.metadata.deletionTimestamp\n    );\n    return hasDeletingData ? 1000 : false;\n  },\n  enabled: computed(() => !!name.value),\n});\n\nconst selectedSnapshotNames = useRouteQuery<string[] | undefined>(\n  \"snapshot-names\",\n  [],\n  {\n    transform: (value: string[] | string | undefined) => {\n      if (Array.isArray(value)) {\n        return value;\n      }\n      return value ? [value] : [];\n    },\n  }\n);\n\nconst diffModeQuery = useRouteQuery<string | undefined>(\"diff-mode\");\n\nconst diffMode = computed(() => diffModeQuery.value === \"true\");\n\nfunction handleToggleDiffMode() {\n  if (diffMode.value) {\n    selectedSnapshotNames.value = [selectedSnapshotNames.value?.[0]].filter(\n      Boolean\n    ) as string[];\n  }\n  diffModeQuery.value = !diffMode.value ? \"true\" : undefined;\n}\n\nfunction handleSelectSnapshot(snapshotName: string) {\n  if (!diffMode.value) {\n    selectedSnapshotNames.value = [snapshotName];\n    return;\n  }\n\n  // Diff mode\n  if (selectedSnapshotNames.value?.includes(snapshotName)) {\n    selectedSnapshotNames.value = selectedSnapshotNames.value?.filter(\n      (name) => name !== snapshotName\n    );\n    return;\n  }\n\n  if (selectedSnapshotNames.value?.length === 2) {\n    selectedSnapshotNames.value = [snapshotName];\n    return;\n  }\n\n  selectedSnapshotNames.value = [\n    ...(selectedSnapshotNames.value || []),\n    snapshotName,\n  ];\n\n  selectedSnapshotNames.value = selectedSnapshotNames.value?.sort((a, b) => {\n    const aIndex = snapshots.value?.findIndex(\n      (snapshot) => snapshot.metadata.name === a\n    );\n    const bIndex = snapshots.value?.findIndex(\n      (snapshot) => snapshot.metadata.name === b\n    );\n    return (aIndex || 0) - (bIndex || 0);\n  });\n}\n\nwatch(\n  () => snapshots.value,\n  (value) => {\n    if (!value) {\n      return;\n    }\n\n    if (!selectedSnapshotNames.value?.length) {\n      selectedSnapshotNames.value = [value[0].metadata.name];\n    }\n\n    // Reset selectedSnapshotName if the selected snapshot is deleted\n    if (\n      !value.some((snapshot) =>\n        selectedSnapshotNames.value?.includes(snapshot.metadata.name)\n      )\n    ) {\n      selectedSnapshotNames.value = [value?.[0].metadata.name].filter(\n        Boolean\n      ) as string[];\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nfunction handleCleanup() {\n  Dialog.warning({\n    title: t(\"core.snapshots.operations.cleanup.title\"),\n    description: t(\"core.snapshots.operations.cleanup.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    confirmType: \"danger\",\n    async onConfirm() {\n      const snapshotsToDelete = snapshots.value?.filter((snapshot) => {\n        const { name } = snapshot.metadata;\n        return ![props.releaseSnapshot, props.baseSnapshot, props.headSnapshot]\n          .filter(Boolean)\n          .includes(name);\n      });\n\n      if (!snapshotsToDelete?.length) {\n        Toast.info(t(\"core.snapshots.operations.cleanup.toast_empty\"));\n        return;\n      }\n\n      for (let i = 0; i < snapshotsToDelete?.length; i++) {\n        await props.deleteApi(snapshotsToDelete[i].metadata.name);\n      }\n\n      await queryClient.invalidateQueries({\n        queryKey: SNAPSHOTS_QUERY_KEY(cacheKey, name),\n      });\n\n      Toast.success(t(\"core.snapshots.operations.cleanup.toast_success\"));\n    },\n  });\n}\n</script>\n\n<template>\n  <VPageHeader :title=\"title\">\n    <template #icon>\n      <IconHistoryLine />\n    </template>\n    <template #actions>\n      <VButton size=\"sm\" ghost @click=\"$router.back()\">\n        {{ $t(\"core.common.buttons.back\") }}\n      </VButton>\n      <VButton size=\"sm\" @click=\"handleToggleDiffMode\">\n        {{\n          diffMode\n            ? $t(\"core.snapshots.diff_mode.toggle.disable\")\n            : $t(\"core.snapshots.diff_mode.toggle.enable\")\n        }}\n      </VButton>\n      <VButton size=\"sm\" type=\"danger\" @click=\"handleCleanup\">\n        {{ $t(\"core.snapshots.operations.cleanup.button\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard\n      style=\"height: calc(100vh - 5.5rem)\"\n      :body-class=\"['h-full', '!p-0']\"\n    >\n      <div class=\"grid h-full grid-cols-12 divide-y sm:divide-x sm:divide-y-0\">\n        <div\n          class=\"relative col-span-12 h-full overflow-auto sm:col-span-3 xl:col-span-2\"\n        >\n          <OverlayScrollbarsComponent\n            element=\"div\"\n            :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n            class=\"h-full w-full\"\n            defer\n          >\n            <VLoading v-if=\"isLoading\" />\n            <Transition v-else appear name=\"fade\">\n              <ul\n                class=\"box-border h-full w-full divide-y divide-gray-100\"\n                role=\"list\"\n              >\n                <li\n                  v-for=\"snapshot in snapshots\"\n                  :key=\"snapshot.metadata.name\"\n                  @click=\"handleSelectSnapshot(snapshot.metadata.name)\"\n                >\n                  <SnapshotListItem\n                    :snapshot=\"snapshot\"\n                    :cache-key=\"cacheKey\"\n                    :name=\"name\"\n                    :delete-api=\"deleteApi\"\n                    :revert-api=\"revertApi\"\n                    :release-snapshot=\"releaseSnapshot\"\n                    :base-snapshot=\"baseSnapshot\"\n                    :head-snapshot=\"headSnapshot\"\n                    :selected-snapshot-names=\"selectedSnapshotNames\"\n                  />\n                </li>\n              </ul>\n            </Transition>\n          </OverlayScrollbarsComponent>\n        </div>\n        <div\n          class=\"col-span-12 h-full overflow-auto sm:col-span-9 xl:col-span-10\"\n        >\n          <SnapshotContent\n            v-if=\"!diffMode\"\n            :cache-key=\"cacheKey\"\n            :name=\"name\"\n            :get-api=\"getApi\"\n            :snapshot-names=\"selectedSnapshotNames\"\n          />\n          <SnapshotDiffContent\n            v-else\n            :cache-key=\"cacheKey\"\n            :name=\"name\"\n            :get-api=\"getApi\"\n            :snapshot-names=\"selectedSnapshotNames\"\n          />\n        </div>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/components/snapshots/SnapshotContent.vue",
    "content": "<script setup lang=\"ts\">\nimport { type ContentWrapper } from \"@halo-dev/api-client\";\nimport { Toast, VLoading } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport DOMPurify from \"dompurify\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { computed, toRefs } from \"vue\";\nimport { SNAPSHOT_QUERY_KEY } from \"./query-keys\";\n\nconst props = withDefaults(\n  defineProps<{\n    cacheKey: string;\n    name: string;\n    snapshotNames?: string[];\n    getApi: (snapshotName: string) => Promise<ContentWrapper>;\n  }>(),\n  {\n    snapshotNames: () => [],\n  }\n);\n\nconst { name, snapshotNames, cacheKey } = toRefs(props);\n\nconst { data: snapshot, isLoading } = useQuery({\n  queryKey: SNAPSHOT_QUERY_KEY(cacheKey, name, snapshotNames),\n  queryFn: async () => {\n    if (!snapshotNames.value?.length) {\n      throw new Error(\"Please select a snapshot\");\n    }\n\n    return await props.getApi(snapshotNames.value[0]);\n  },\n  onError(err) {\n    if (err instanceof Error) {\n      Toast.error(err.message);\n    }\n  },\n  enabled: computed(() => !!name.value && !!snapshotNames.value?.length),\n});\n\nconst sanitizedContent = computed(() => {\n  return DOMPurify.sanitize(snapshot.value?.content || \"\");\n});\n</script>\n<template>\n  <OverlayScrollbarsComponent\n    element=\"div\"\n    :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n    class=\"h-full w-full\"\n    defer\n  >\n    <VLoading v-if=\"isLoading\" />\n    <div\n      v-else\n      class=\"snapshot-content markdown-body h-full w-full p-4\"\n      v-html=\"sanitizedContent\"\n    ></div>\n  </OverlayScrollbarsComponent>\n</template>\n\n<style scoped lang=\"scss\">\n::v-deep(.snapshot-content) {\n  p {\n    margin-top: 0.75em;\n    margin-bottom: 0;\n  }\n\n  pre {\n    background: #0d0d0d;\n    padding: 0.75rem 1rem;\n    margin: 0;\n\n    code {\n      color: #ccc;\n      background: none;\n      font-size: 0.8rem;\n      padding: 0 !important;\n      border-radius: 0;\n    }\n  }\n\n  ul[data-type=\"taskList\"] {\n    list-style: none;\n    padding: 0;\n\n    p {\n      margin: 0;\n    }\n\n    li {\n      display: flex;\n\n      > label {\n        flex: 0 0 auto;\n        margin-right: 0.5rem;\n        user-select: none;\n      }\n\n      > div {\n        flex: 1 1 auto;\n      }\n    }\n  }\n\n  ul {\n    list-style: disc !important;\n  }\n\n  ol {\n    list-style: decimal !important;\n  }\n\n  code br {\n    display: initial;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/components/snapshots/SnapshotDiffContent.vue",
    "content": "<script setup lang=\"ts\">\nimport { type ContentWrapper } from \"@halo-dev/api-client\";\nimport {\n  IconInformation,\n  Toast,\n  VDropdown,\n  VLoading,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport DOMPurify from \"dompurify\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { visualDomDiff } from \"visual-dom-diff\";\nimport { computed, nextTick, toRefs, useTemplateRef, watch } from \"vue\";\nimport { SNAPSHOT_DIFF_QUERY_KEY } from \"./query-keys\";\n\nconst props = withDefaults(\n  defineProps<{\n    cacheKey: string;\n    name: string;\n    snapshotNames?: string[];\n    getApi: (snapshotName: string) => Promise<ContentWrapper>;\n  }>(),\n  {\n    snapshotNames: () => [],\n  }\n);\n\nconst { name, snapshotNames, cacheKey } = toRefs(props);\n\nconst { data: snapshot, isLoading } = useQuery({\n  queryKey: SNAPSHOT_DIFF_QUERY_KEY(cacheKey, name, snapshotNames),\n  queryFn: async () => {\n    if (snapshotNames.value?.length !== 2) {\n      throw new Error(\"Please select two snapshots to compare\");\n    }\n\n    const newSnapshot = await props.getApi(snapshotNames.value[0]);\n\n    const oldSnapshot = await props.getApi(snapshotNames.value[1]);\n\n    return {\n      old: oldSnapshot,\n      new: newSnapshot,\n    };\n  },\n  onError(err) {\n    if (err instanceof Error) {\n      Toast.error(err.message);\n    }\n  },\n  enabled: computed(() => !!name.value && !!snapshotNames.value?.length),\n});\n\n// Sanitized content for old snapshot\nconst sanitizedOldContent = computed(() => {\n  return DOMPurify.sanitize(snapshot.value?.old.content || \"\");\n});\n\n// Sanitized content for new snapshot\nconst sanitizedNewContent = computed(() => {\n  return DOMPurify.sanitize(snapshot.value?.new.content || \"\");\n});\n\nconst diffContent = computed(() => {\n  if (!snapshot.value) {\n    return null;\n  }\n  const oldContent = document.createElement(\"div\");\n  // Sanitize HTML before setting innerHTML to prevent XSS\n  oldContent.innerHTML = sanitizedOldContent.value;\n  const newContent = document.createElement(\"div\");\n  // Sanitize HTML before setting innerHTML to prevent XSS\n  newContent.innerHTML = sanitizedNewContent.value;\n\n  const diffDocument = visualDomDiff(oldContent, newContent, {\n    skipModified: true,\n  });\n\n  const diffNode = document.createElement(\"div\");\n  diffNode.append(diffDocument.cloneNode(true));\n\n  const html = diffNode.innerHTML;\n  // Sanitize the final diff HTML as well\n  const sanitizedHtml = DOMPurify.sanitize(html);\n\n  oldContent.remove();\n  newContent.remove();\n  diffNode.remove();\n\n  return sanitizedHtml;\n});\n\nconst onlyDiff = useLocalStorage(\"snapshot-diff-only-diff\", false);\nconst enableSyncScroll = useLocalStorage(\"snapshot-diff-sync-scroll\", true);\n\nconst oldScrollRef =\n  useTemplateRef<InstanceType<typeof OverlayScrollbarsComponent>>(\n    \"oldScrollRef\"\n  );\nconst newScrollRef =\n  useTemplateRef<InstanceType<typeof OverlayScrollbarsComponent>>(\n    \"newScrollRef\"\n  );\nconst diffScrollRef =\n  useTemplateRef<InstanceType<typeof OverlayScrollbarsComponent>>(\n    \"diffScrollRef\"\n  );\n\nlet isSyncing = false;\n\nconst syncScroll = (\n  sourceRef: typeof oldScrollRef,\n  targetRefs: (typeof oldScrollRef)[]\n) => {\n  if (isSyncing || !enableSyncScroll.value || onlyDiff.value) return;\n\n  const sourceInstance = sourceRef.value?.osInstance();\n  if (!sourceInstance) return;\n\n  const sourceViewport = sourceInstance.elements().viewport;\n  const scrollTop = sourceViewport.scrollTop;\n  const scrollHeight = sourceViewport.scrollHeight;\n  const clientHeight = sourceViewport.clientHeight;\n\n  if (scrollHeight <= clientHeight) return;\n\n  const scrollPercentage = scrollTop / (scrollHeight - clientHeight);\n\n  isSyncing = true;\n\n  targetRefs.forEach((targetRef) => {\n    const targetInstance = targetRef.value?.osInstance();\n    if (!targetInstance) return;\n\n    const targetViewport = targetInstance.elements().viewport;\n    const targetScrollHeight = targetViewport.scrollHeight;\n    const targetClientHeight = targetViewport.clientHeight;\n\n    if (targetScrollHeight <= targetClientHeight) return;\n\n    const targetScrollTop =\n      scrollPercentage * (targetScrollHeight - targetClientHeight);\n    targetViewport.scrollTop = targetScrollTop;\n  });\n\n  setTimeout(() => {\n    isSyncing = false;\n  }, 10);\n};\n\nwatch([oldScrollRef, newScrollRef, diffScrollRef, onlyDiff, snapshot], () => {\n  nextTick(() => {\n    setTimeout(() => {\n      const oldInstance = oldScrollRef.value?.osInstance();\n      const newInstance = newScrollRef.value?.osInstance();\n      const diffInstance = diffScrollRef.value?.osInstance();\n\n      if (oldInstance) {\n        const viewport = oldInstance.elements().viewport;\n        viewport.removeEventListener(\"scroll\", handleOldScroll);\n        viewport.addEventListener(\"scroll\", handleOldScroll);\n      }\n\n      if (newInstance) {\n        const viewport = newInstance.elements().viewport;\n        viewport.removeEventListener(\"scroll\", handleNewScroll);\n        viewport.addEventListener(\"scroll\", handleNewScroll);\n      }\n\n      if (diffInstance) {\n        const viewport = diffInstance.elements().viewport;\n        viewport.removeEventListener(\"scroll\", handleDiffScroll);\n        viewport.addEventListener(\"scroll\", handleDiffScroll);\n      }\n    }, 100);\n  });\n});\n\nconst handleOldScroll = () => {\n  const targets = onlyDiff.value\n    ? [diffScrollRef]\n    : [newScrollRef, diffScrollRef];\n  syncScroll(oldScrollRef, targets);\n};\n\nconst handleNewScroll = () => {\n  const targets = onlyDiff.value\n    ? [diffScrollRef]\n    : [oldScrollRef, diffScrollRef];\n  syncScroll(newScrollRef, targets);\n};\n\nconst handleDiffScroll = () => {\n  const targets = onlyDiff.value ? [] : [oldScrollRef, newScrollRef];\n  syncScroll(diffScrollRef, targets);\n};\n</script>\n<template>\n  <div class=\"flex h-full flex-col\">\n    <div class=\"flex flex-none items-center justify-between border-b px-4 py-3\">\n      <div class=\"font-semibold\">\n        {{ $t(\"core.snapshots.diff_mode.title\") }}\n      </div>\n      <div class=\"flex items-center gap-4\">\n        <FormKit\n          v-model=\"onlyDiff\"\n          type=\"checkbox\"\n          :label=\"$t('core.snapshots.diff_mode.show_diff_only')\"\n          :classes=\"{\n            outer: '!py-0',\n            wrapper: '!mb-0',\n          }\"\n        ></FormKit>\n        <FormKit\n          v-model=\"enableSyncScroll\"\n          type=\"checkbox\"\n          :label=\"$t('core.snapshots.diff_mode.sync_scroll')\"\n          :disabled=\"onlyDiff\"\n          :classes=\"{\n            outer: '!py-0',\n            wrapper: '!mb-0',\n          }\"\n        ></FormKit>\n      </div>\n    </div>\n\n    <div v-if=\"snapshotNames?.length !== 2\" class=\"flex justify-center py-10\">\n      <span class=\"text-gray-600\">\n        {{ $t(\"core.snapshots.diff_mode.select_two_tip\") }}\n      </span>\n    </div>\n\n    <VLoading v-else-if=\"isLoading\" />\n\n    <div\n      v-else\n      class=\"grid h-full min-h-0 flex-1 shrink grid-cols-1 divide-x\"\n      :class=\"{\n        'grid-cols-1': onlyDiff,\n        'md:grid-cols-3': !onlyDiff,\n      }\"\n    >\n      <OverlayScrollbarsComponent\n        v-if=\"!onlyDiff\"\n        ref=\"oldScrollRef\"\n        element=\"div\"\n        :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n        class=\"h-full w-full\"\n        defer\n      >\n        <div class=\"sticky top-0 border-b bg-white px-4 py-3\">\n          {{ $t(\"core.snapshots.diff_mode.old_version\") }}\n        </div>\n        <div\n          class=\"snapshot-content markdown-body\"\n          v-html=\"sanitizedOldContent\"\n        ></div>\n      </OverlayScrollbarsComponent>\n      <OverlayScrollbarsComponent\n        v-if=\"!onlyDiff\"\n        ref=\"newScrollRef\"\n        element=\"div\"\n        :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n        class=\"h-full w-full\"\n        defer\n      >\n        <div class=\"sticky top-0 border-b bg-white px-4 py-3\">\n          {{ $t(\"core.snapshots.diff_mode.new_version\") }}\n        </div>\n        <div\n          class=\"snapshot-content markdown-body\"\n          v-html=\"sanitizedNewContent\"\n        ></div>\n      </OverlayScrollbarsComponent>\n      <OverlayScrollbarsComponent\n        ref=\"diffScrollRef\"\n        element=\"div\"\n        :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n        class=\"h-full w-full\"\n        defer\n      >\n        <div\n          class=\"sticky top-0 flex items-center gap-2 border-b bg-white px-4 py-3\"\n        >\n          <span>{{ $t(\"core.snapshots.diff_mode.diff\") }}</span>\n          <VDropdown :triggers=\"['hover']\">\n            <IconInformation class=\"size-4\" />\n            <template #popper>\n              <div class=\"w-52\">\n                <ul class=\"flex flex-col gap-2\">\n                  <li class=\"rounded bg-[#ffe6e6] px-1 py-0.5 line-through\">\n                    {{ $t(\"core.snapshots.diff_mode.legend.removed\") }}\n                  </li>\n                  <li class=\"rounded bg-[#e6ffe6] px-1 py-0.5\">\n                    {{ $t(\"core.snapshots.diff_mode.legend.added\") }}\n                  </li>\n                  <li class=\"rounded bg-[#e6f2ff] px-1 py-0.5\">\n                    {{ $t(\"core.snapshots.diff_mode.legend.modified\") }}\n                  </li>\n                </ul>\n              </div>\n            </template>\n          </VDropdown>\n        </div>\n        <div class=\"snapshot-content markdown-body\" v-html=\"diffContent\"></div>\n      </OverlayScrollbarsComponent>\n    </div>\n  </div>\n</template>\n\n<style scoped lang=\"scss\">\n::v-deep(.snapshot-content) {\n  height: 100%;\n  width: 100%;\n  padding: 1rem;\n\n  p {\n    margin-top: 0.75em;\n    margin-bottom: 0;\n  }\n\n  pre {\n    background: #0d0d0d;\n    padding: 0.75rem 1rem;\n    margin: 0;\n\n    code {\n      color: #ccc;\n      background: none;\n      font-size: 0.8rem;\n      padding: 0 !important;\n      border-radius: 0;\n    }\n  }\n\n  ul[data-type=\"taskList\"] {\n    list-style: none;\n    padding: 0;\n\n    p {\n      margin: 0;\n    }\n\n    li {\n      display: flex;\n\n      > label {\n        flex: 0 0 auto;\n        margin-right: 0.5rem;\n        user-select: none;\n      }\n\n      > div {\n        flex: 1 1 auto;\n      }\n    }\n  }\n\n  ul {\n    list-style: disc !important;\n  }\n\n  ol {\n    list-style: decimal !important;\n  }\n\n  code br {\n    display: initial;\n  }\n\n  div > :first-child {\n    margin-top: 0 !important;\n  }\n\n  .vdd-removed {\n    background-color: #ffe6e6;\n    text-decoration: line-through;\n  }\n\n  .vdd-added {\n    background-color: #e6ffe6;\n  }\n\n  .vdd-modified {\n    background-color: #e6f2ff;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/components/snapshots/SnapshotListItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ListedSnapshotDto } from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VButton, VStatusDot, VTag } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { SNAPSHOTS_QUERY_KEY } from \"./query-keys\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    snapshot: ListedSnapshotDto;\n    selectedSnapshotNames?: string[];\n    cacheKey: string;\n    name: string;\n    releaseSnapshot?: string;\n    baseSnapshot?: string;\n    headSnapshot?: string;\n    deleteApi: (snapshotName: string) => Promise<void>;\n    revertApi: (snapshotName: string) => Promise<void>;\n  }>(),\n  {\n    selectedSnapshotNames: undefined,\n    releaseSnapshot: undefined,\n    baseSnapshot: undefined,\n    headSnapshot: undefined,\n  }\n);\n\nconst { cacheKey, name } = toRefs(props);\n\nasync function handleRestore() {\n  Dialog.warning({\n    title: t(\"core.snapshots.operations.revert.title\"),\n    description: t(\"core.snapshots.operations.revert.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      await props.revertApi(props.snapshot.metadata.name);\n      await queryClient.invalidateQueries({\n        queryKey: SNAPSHOTS_QUERY_KEY(cacheKey, name),\n      });\n      Toast.success(t(\"core.snapshots.operations.revert.toast_success\"));\n    },\n  });\n}\n\nfunction handleDelete() {\n  Dialog.warning({\n    title: t(\"core.snapshots.operations.delete.title\"),\n    description: t(\"core.snapshots.operations.delete.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    confirmType: \"danger\",\n    async onConfirm() {\n      await props.deleteApi(props.snapshot.metadata.name);\n      await queryClient.invalidateQueries({\n        queryKey: SNAPSHOTS_QUERY_KEY(cacheKey, name),\n      });\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n}\n\nconst isSelected = computed(() => {\n  return props.selectedSnapshotNames?.includes(props.snapshot.metadata.name);\n});\n\nconst isReleased = computed(() => {\n  return props.releaseSnapshot === props.snapshot.metadata.name;\n});\n\nconst isHead = computed(() => {\n  return (\n    props.headSnapshot !== props.releaseSnapshot &&\n    props.headSnapshot === props.snapshot.metadata.name\n  );\n});\n\nconst isBase = computed(() => {\n  return props.baseSnapshot === props.snapshot.metadata.name;\n});\n</script>\n<template>\n  <div\n    class=\"group relative flex cursor-pointer flex-col gap-5 p-4\"\n    :class=\"{ 'bg-gray-50': isSelected }\"\n  >\n    <div\n      v-if=\"isSelected\"\n      class=\"absolute inset-y-0 left-0 w-0.5 bg-primary\"\n    ></div>\n    <div class=\"flex items-center justify-between\">\n      <div\n        class=\"truncate text-sm\"\n        :class=\"{\n          'font-semibold': isSelected,\n        }\"\n      >\n        {{ utils.date.timeAgo(snapshot.metadata.creationTimestamp) }}\n      </div>\n      <div class=\"inline-flex flex-none items-center space-x-3\">\n        <VTag v-if=\"isReleased\" theme=\"primary\">\n          {{ $t(\"core.snapshots.status.released\") }}\n        </VTag>\n        <VTag v-if=\"isHead\">\n          {{ $t(\"core.snapshots.status.draft\") }}\n        </VTag>\n        <VTag v-if=\"isBase\">\n          {{ $t(\"core.snapshots.status.base\") }}\n        </VTag>\n        <VStatusDot\n          v-if=\"snapshot.metadata.deletionTimestamp\"\n          v-tooltip=\"$t('core.common.status.deleting')\"\n          state=\"warning\"\n          animate\n        />\n      </div>\n    </div>\n    <div class=\"flex h-6 items-end justify-between gap-2\">\n      <div class=\"flex-1 truncate text-xs text-gray-600\">\n        {{ snapshot.spec.owner }}\n      </div>\n      <div\n        v-if=\"!isReleased\"\n        class=\"hidden flex-none space-x-2 group-hover:block\"\n      >\n        <VButton v-if=\"!isHead\" size=\"xs\" @click=\"handleRestore()\">\n          {{ $t(\"core.snapshots.operations.revert.button\") }}\n        </VButton>\n        <VButton v-if=\"!isBase\" size=\"xs\" type=\"danger\" @click=\"handleDelete\">\n          {{ $t(\"core.common.buttons.delete\") }}\n        </VButton>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/components/snapshots/query-keys.ts",
    "content": "import type { Ref } from \"vue\";\n\nexport const SNAPSHOTS_QUERY_KEY = (\n  cacheKey: Ref<string>,\n  name?: Ref<string>\n) => [`core:${cacheKey}:snapshots`, name];\n\nexport const SNAPSHOT_QUERY_KEY = (\n  cacheKey: Ref<string>,\n  name?: Ref<string>,\n  snapshotNames?: Ref<string[]>\n) => [`core:${cacheKey}:snapshot`, name, snapshotNames];\n\nexport const SNAPSHOT_DIFF_QUERY_KEY = (\n  cacheKey: Ref<string>,\n  name?: Ref<string>,\n  snapshotNames?: Ref<string[]>\n) => [`core:${cacheKey}:snapshot-diff`, name, snapshotNames];\n"
  },
  {
    "path": "ui/console-src/composables/use-content-snapshot.ts",
    "content": "import { coreApiClient } from \"@halo-dev/api-client\";\nimport { nextTick, ref, watch, type Ref } from \"vue\";\n\ninterface SnapshotContent {\n  version: Ref<number>;\n  handleFetchSnapshot: () => Promise<void>;\n}\n\nexport function useContentSnapshot(\n  snapshotName: Ref<string | undefined>\n): SnapshotContent {\n  const version = ref(0);\n  watch(snapshotName, () => {\n    nextTick(() => {\n      handleFetchSnapshot();\n    });\n  });\n\n  const handleFetchSnapshot = async () => {\n    if (!snapshotName.value) {\n      return;\n    }\n    const { data } = await coreApiClient.content.snapshot.getSnapshot({\n      name: snapshotName.value,\n    });\n    version.value = data.metadata.version || 0;\n  };\n\n  return {\n    version,\n    handleFetchSnapshot,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/composables/use-dashboard-stats.ts",
    "content": "import { consoleApiClient } from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\n\nexport function useDashboardStats() {\n  const { data } = useQuery({\n    queryKey: [\"dashboard-stats\"],\n    queryFn: async () => {\n      const { data } = await consoleApiClient.system.getStats();\n      return data;\n    },\n  });\n  return { data };\n}\n"
  },
  {
    "path": "ui/console-src/composables/use-entity-extension-points.ts",
    "content": "import type { EntityFieldItem } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed, toValue, type ComputedRef, type Ref } from \"vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nexport function useEntityFieldItemExtensionPoint<T>(\n  extensionPointName: string,\n  entity: Ref<T>,\n  presets: ComputedRef<EntityFieldItem[]>\n) {\n  const { pluginModules } = usePluginModuleStore();\n\n  return useQuery({\n    queryKey: computed(() => [\n      \"core:extension-points:list-item:fields\",\n      extensionPointName,\n      toValue(entity),\n    ]),\n    queryFn: async () => {\n      const itemsFromPlugins: EntityFieldItem[] = [];\n\n      for (const pluginModule of pluginModules) {\n        const { extensionPoints } = pluginModule;\n        if (!extensionPoints?.[extensionPointName]) {\n          continue;\n        }\n        const items = extensionPoints[extensionPointName](\n          entity\n        ) as EntityFieldItem[];\n        itemsFromPlugins.push(...items);\n      }\n\n      const allItems = [...presets.value, ...itemsFromPlugins].sort(\n        (a, b) => a.priority - b.priority\n      );\n\n      const start: EntityFieldItem[] = [];\n      const end: EntityFieldItem[] = [];\n\n      for (const item of allItems) {\n        if (item.position === \"start\") {\n          start.push(item);\n        } else if (item.position === \"end\") {\n          end.push(item);\n        }\n      }\n\n      return {\n        start,\n        end,\n      };\n    },\n    enabled: computed(() => !!presets.value && !!entity.value),\n  });\n}\n"
  },
  {
    "path": "ui/console-src/composables/use-operation-extension-points.ts",
    "content": "import type { OperationItem } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed, toValue, type ComputedRef, type Ref } from \"vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nexport function useOperationItemExtensionPoint<T>(\n  extensionPointName: string,\n  entity: Ref<T>,\n  presets: ComputedRef<OperationItem<T>[]>\n) {\n  const { pluginModules } = usePluginModuleStore();\n\n  return useQuery({\n    queryKey: computed(() => [\n      \"core:extension-points:operation-items\",\n      extensionPointName,\n      toValue(entity),\n    ]),\n    queryFn: async () => {\n      const itemsFromPlugins: OperationItem<T>[] = [];\n      for (const pluginModule of pluginModules) {\n        const { extensionPoints } = pluginModule;\n        if (!extensionPoints?.[extensionPointName]) {\n          continue;\n        }\n\n        const items = extensionPoints[extensionPointName](\n          entity\n        ) as OperationItem<T>[];\n\n        itemsFromPlugins.push(...items);\n      }\n\n      return [...presets.value, ...itemsFromPlugins].sort(\n        (a, b) => a.priority - b.priority\n      );\n    },\n    enabled: computed(() => !!presets.value && !!entity.value),\n  });\n}\n"
  },
  {
    "path": "ui/console-src/composables/use-save-keybinding.ts",
    "content": "import { useEventListener } from \"@vueuse/core\";\nimport { useDebounceFn } from \"@vueuse/shared\";\nimport { nextTick } from \"vue\";\nimport { isMac } from \"@/utils/device\";\n\nexport function useSaveKeybinding(fn: () => void) {\n  const debouncedFn = useDebounceFn(() => {\n    fn();\n  }, 300);\n\n  useEventListener(window, \"keydown\", (e: KeyboardEvent) => {\n    if (isMac ? e.metaKey : e.ctrlKey) {\n      if (e.key === \"s\") {\n        e.preventDefault();\n        nextTick(() => {\n          debouncedFn();\n        });\n      }\n    }\n  });\n}\n"
  },
  {
    "path": "ui/console-src/composables/use-slugify.ts",
    "content": "import { FormType, stores, utils } from \"@halo-dev/ui-shared\";\nimport ShortUniqueId from \"short-unique-id\";\nimport { slugify } from \"transliteration\";\nimport { computed, watch, type Ref } from \"vue\";\n\nconst uid = new ShortUniqueId();\n\ntype SlugStrategy = (value?: string) => string;\n\nconst strategies: Record<string, SlugStrategy> = {\n  generateByTitle: (value?: string) => slugify(value || \"\", { trim: true }),\n  shortUUID: () => uid.randomUUID(8),\n  UUID: () => utils.id.uuid(),\n  timestamp: () => new Date().getTime().toString(),\n};\n\nconst onceStrategies = new Set([\"shortUUID\", \"UUID\", \"timestamp\"]);\n\nexport default function useSlugify(\n  source: Ref<string>,\n  target: Ref<string>,\n  auto: Ref<boolean>,\n  formType: FormType\n) {\n  const globalInfoStore = stores.globalInfo();\n\n  const currentStrategy = computed(\n    () =>\n      globalInfoStore.globalInfo?.postSlugGenerationStrategy ||\n      \"generateByTitle\"\n  );\n\n  const generateSlug = (value: string): string => {\n    const strategy =\n      formType === FormType.POST\n        ? strategies[currentStrategy.value]\n        : strategies.generateByTitle;\n\n    return strategy(value);\n  };\n\n  const handleGenerateSlug = (forceUpdate = false) => {\n    if (\n      !forceUpdate &&\n      onceStrategies.has(currentStrategy.value) &&\n      target.value\n    ) {\n      return;\n    }\n\n    target.value = generateSlug(source.value);\n  };\n\n  watch(\n    source,\n    () => {\n      if (auto.value) {\n        handleGenerateSlug(true);\n      }\n    },\n    { immediate: true }\n  );\n\n  return {\n    handleGenerateSlug,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/layouts/BasicLayout.vue",
    "content": "<script lang=\"ts\" setup>\nimport { coreMenuGroups } from \"@console/router/constant\";\nimport { IconSearch } from \"@halo-dev/components\";\nimport { useEventListener } from \"@vueuse/core\";\nimport {\n  useOverlayScrollbars,\n  type UseOverlayScrollbarsParams,\n} from \"overlayscrollbars-vue\";\nimport { defineStore } from \"pinia\";\nimport { defineAsyncComponent, onMounted, reactive, ref } from \"vue\";\nimport { RouterView, useRoute } from \"vue-router\";\nimport IconLogo from \"~icons/core/logo?width=5rem&height=2rem\";\nimport MenuLoading from \"@/components/menu/MenuLoading.vue\";\nimport { RoutesMenu } from \"@/components/menu/RoutesMenu\";\nimport { useRouteMenuGenerator } from \"@/composables/use-route-menu-generator\";\nimport MobileMenu from \"@/layouts/MobileMenu.vue\";\nimport UserProfileBanner from \"@/layouts/UserProfileBanner.vue\";\nimport { isMac } from \"@/utils/device\";\n\nconst route = useRoute();\n\n// Global Search\nconst globalSearchVisible = ref(false);\n\nconst GlobalSearchModal = defineAsyncComponent(\n  () => import(\"@/components/global-search/GlobalSearchModal.vue\")\n);\n\nuseEventListener(document, \"keydown\", (e: KeyboardEvent) => {\n  const { key, ctrlKey, metaKey } = e;\n  if (key === \"k\" && ((ctrlKey && !isMac) || metaKey)) {\n    globalSearchVisible.value = true;\n    e.preventDefault();\n  }\n});\n\nconst { data, isLoading } = useRouteMenuGenerator(coreMenuGroups);\n\n// aside scroll\nconst navbarScroller = ref();\n\nconst useNavbarScrollStore = defineStore(\"navbar\", {\n  state: () => ({\n    y: 0,\n  }),\n});\n\nconst navbarScrollStore = useNavbarScrollStore();\n\nconst reactiveParams = reactive<UseOverlayScrollbarsParams>({\n  options: {\n    scrollbars: {\n      autoHide: \"scroll\",\n      autoHideDelay: 600,\n    },\n  },\n  events: {\n    scroll: (_, onScrollArgs) => {\n      const target = onScrollArgs.target as HTMLElement;\n      navbarScrollStore.y = target.scrollTop;\n    },\n    updated: (instance) => {\n      const { viewport } = instance.elements();\n      if (!viewport) return;\n      viewport.scrollTo({ top: navbarScrollStore.y });\n    },\n  },\n});\nconst [initialize] = useOverlayScrollbars(reactiveParams);\nonMounted(() => {\n  if (navbarScroller.value) {\n    initialize({ target: navbarScroller.value });\n  }\n});\n</script>\n\n<template>\n  <div class=\"layout\">\n    <aside class=\"sidebar\">\n      <div class=\"sidebar__logo-container\">\n        <a\n          href=\"/\"\n          target=\"_blank\"\n          :title=\"$t('core.sidebar.operations.visit_homepage.title')\"\n        >\n          <IconLogo class=\"sidebar__logo\" />\n        </a>\n      </div>\n      <div ref=\"navbarScroller\" class=\"sidebar__content\">\n        <div class=\"sidebar__search-wrapper\">\n          <div class=\"sidebar__search\" @click=\"globalSearchVisible = true\">\n            <span class=\"sidebar__search-icon\">\n              <IconSearch />\n            </span>\n            <span class=\"sidebar__search-text\">\n              {{ $t(\"core.sidebar.search.placeholder\") }}\n            </span>\n            <div class=\"sidebar__search-shortcut\">\n              {{ `${isMac ? \"⌘\" : \"Ctrl\"}+K` }}\n            </div>\n          </div>\n        </div>\n        <MenuLoading v-if=\"isLoading\" />\n        <RoutesMenu v-else :menus=\"data?.menus || []\" />\n      </div>\n      <div class=\"sidebar__profile\">\n        <UserProfileBanner platform=\"console\" />\n      </div>\n    </aside>\n\n    <main class=\"main-content\">\n      <slot v-if=\"$slots.default\" />\n      <RouterView v-else />\n      <footer v-if=\"!route.meta.hideFooter\" class=\"main-content__footer\">\n        <span class=\"main-content__footer-text\">Powered by </span>\n        <RouterLink to=\"/overview\" class=\"main-content__footer-link\">\n          Halo\n        </RouterLink>\n      </footer>\n    </main>\n    <MobileMenu\n      :menus=\"data?.menus || []\"\n      :minimenus=\"data?.minimenus || []\"\n      platform=\"console\"\n    />\n  </div>\n  <GlobalSearchModal\n    v-if=\"globalSearchVisible\"\n    @close=\"globalSearchVisible = false\"\n  />\n</template>\n\n<style lang=\"scss\">\n.layout {\n  display: flex;\n  min-height: 100vh;\n}\n\n.sidebar {\n  position: fixed;\n  width: theme(\"width.64\");\n  height: 100%;\n  background-color: theme(\"colors.white\");\n  box-shadow: theme(\"boxShadow.DEFAULT\");\n  overflow-y: auto;\n  display: none;\n  flex-direction: column;\n\n  @media (min-width: theme(\"screens.md\")) {\n    display: flex;\n  }\n\n  &__logo-container {\n    display: flex;\n    justify-content: center;\n    padding-top: 1.25rem;\n    padding-bottom: 1.25rem;\n  }\n\n  &__logo {\n    cursor: pointer;\n    user-select: none;\n    transition: all;\n\n    &:hover {\n      filter: brightness(1.25);\n    }\n  }\n\n  &__content {\n    flex: 1;\n    overflow-y: hidden;\n  }\n\n  &__search-wrapper {\n    padding-left: 0.75rem;\n    padding-right: 0.75rem;\n  }\n\n  &__search {\n    display: flex;\n    cursor: pointer;\n    align-items: center;\n    border-radius: 0.25rem;\n    background-color: theme(\"colors.gray.100\");\n    padding: 0.5rem;\n    color: theme(\"colors.gray.400\");\n    transition: all;\n\n    &:hover {\n      color: theme(\"colors.gray.900\");\n    }\n  }\n\n  &__search-icon {\n    margin-right: 0.75rem;\n  }\n\n  &__search-text {\n    flex: 1;\n    user-select: none;\n    font-size: 1rem;\n    font-weight: normal;\n  }\n\n  &__search-shortcut {\n    font-size: 0.875rem;\n  }\n\n  &__profile {\n    flex: none;\n  }\n}\n\n.main-content {\n  width: 100%;\n  padding-bottom: 3rem;\n  margin-bottom: env(safe-area-inset-bottom);\n  display: flex;\n  flex: auto;\n  flex-direction: column;\n\n  @media (min-width: theme(\"screens.md\")) {\n    width: calc(100% - 16rem);\n    margin-left: theme(\"width.64\");\n    padding-bottom: 0;\n  }\n\n  &__footer {\n    margin-top: auto;\n    padding: 1rem;\n    text-align: center;\n    font-size: 0.875rem;\n  }\n\n  &__footer-text {\n    color: theme(\"colors.gray.600\");\n  }\n\n  &__footer-link {\n    &:hover {\n      color: theme(\"colors.gray.600\");\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/layouts/BlankLayout.vue",
    "content": "<template>\n  <RouterView />\n</template>\n\n<script lang=\"ts\" setup>\nimport { RouterView } from \"vue-router\";\n</script>\n"
  },
  {
    "path": "ui/console-src/main.ts",
    "content": "import modules from \"@console/modules\";\nimport { useThemeStore } from \"@console/stores/theme\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { createPinia } from \"pinia\";\nimport \"@/setup/setupStyles\";\nimport { createApp } from \"vue\";\nimport { setLanguage, setupI18n } from \"@/locales\";\nimport { setupApiClient } from \"@/setup/setupApiClient\";\nimport { setupComponents } from \"@/setup/setupComponents\";\nimport { setupCoreModules, setupPluginModules } from \"@/setup/setupModules\";\nimport \"core-js/es/object/has-own\";\nimport { setupUserPermissions } from \"@/setup/setupUserPermissions\";\nimport { setupVueQuery } from \"@/setup/setupVueQuery\";\nimport App from \"./App.vue\";\nimport router from \"./router\";\n\nconst app = createApp(App);\n\nsetupComponents(app);\nsetupI18n(app);\nsetupVueQuery(app);\nsetupApiClient();\n\napp.use(createPinia());\n\nasync function loadActivatedTheme() {\n  const themeStore = useThemeStore();\n  await themeStore.fetchActivatedTheme();\n}\n\nawait initApp();\n\nasync function initApp() {\n  try {\n    setupCoreModules({ app, router, platform: \"console\", modules });\n\n    const currentUserStore = stores.currentUser();\n    await currentUserStore.fetchCurrentUser();\n\n    const globalInfoStore = stores.globalInfo();\n    await globalInfoStore.fetchGlobalInfo();\n\n    await setLanguage();\n\n    if (currentUserStore.isAnonymous) {\n      return;\n    }\n\n    await setupUserPermissions(app);\n\n    try {\n      await setupPluginModules({ app, router, platform: \"console\" });\n    } catch (e) {\n      console.error(\"Failed to load plugins\", e);\n    }\n\n    await loadActivatedTheme();\n  } catch (e) {\n    console.error(e);\n  } finally {\n    app.use(router);\n    app.mount(\"#app\");\n  }\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/AttachmentList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Attachment, Group } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconArrowLeft,\n  IconArrowRight,\n  IconDatabase2Line,\n  IconFolder,\n  IconGrid,\n  IconList,\n  IconRefreshLine,\n  IconUpload,\n  Toast,\n  VButton,\n  VCard,\n  VDropdown,\n  VDropdownItem,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { Ref } from \"vue\";\nimport { computed, onMounted, provide, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport AttachmentGridListItem from \"@/components/attachment/AttachmentGridListItem.vue\";\nimport UserFilterDropdown from \"@/components/filter/UserFilterDropdown.vue\";\nimport AttachmentDetailModal from \"./components/AttachmentDetailModal.vue\";\nimport AttachmentGroupList from \"./components/AttachmentGroupList.vue\";\nimport AttachmentListItem from \"./components/AttachmentListItem.vue\";\nimport AttachmentPoliciesModal from \"./components/AttachmentPoliciesModal.vue\";\nimport AttachmentUploadModal from \"./components/AttachmentUploadModal.vue\";\nimport { useAttachmentControl } from \"./composables/use-attachment\";\nimport { useFetchAttachmentGroup } from \"./composables/use-attachment-group\";\nimport { useFetchAttachmentPolicy } from \"./composables/use-attachment-policy\";\n\nconst { t } = useI18n();\n\nconst policyVisible = ref(false);\nconst uploadVisible = ref(false);\nconst detailVisible = ref(false);\n\nconst { data: policies } = useFetchAttachmentPolicy();\nconst { groups } = useFetchAttachmentGroup();\n\nconst selectedGroup = useRouteQuery<string | undefined>(\"group\");\n\n// Filter\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\nconst page = useRouteQuery<number>(\"page\", 1, {\n  transform: Number,\n});\nconst size = useRouteQuery<number>(\"size\", 60, {\n  transform: Number,\n});\nconst selectedPolicy = useRouteQuery<string | undefined>(\"policy\");\nconst selectedUser = useRouteQuery<string | undefined>(\"user\");\nconst selectedSort = useRouteQuery<string | undefined>(\"sort\");\nconst selectedAccepts = useRouteQuery<string | undefined>(\"accepts\");\n\nwatch(\n  () => [\n    selectedPolicy.value,\n    selectedUser.value,\n    selectedSort.value,\n    selectedAccepts.value,\n    keyword.value,\n  ],\n  () => {\n    page.value = 1;\n  }\n);\n\nconst hasFilters = computed(() => {\n  return (\n    selectedPolicy.value ||\n    selectedUser.value ||\n    selectedSort.value ||\n    selectedAccepts.value\n  );\n});\n\nfunction handleClearFilters() {\n  selectedPolicy.value = undefined;\n  selectedUser.value = undefined;\n  selectedSort.value = undefined;\n  selectedAccepts.value = undefined;\n}\n\nconst {\n  attachments,\n  selectedAttachment,\n  selectedAttachmentNames,\n  checkedAll,\n  isLoading,\n  isFetching,\n  total,\n  handleFetchAttachments,\n  handleSelectNext,\n  handleSelectPrevious,\n  handleDeleteInBatch,\n  handleCheckAll,\n  handleSelect,\n  isChecked,\n  handleReset,\n} = useAttachmentControl({\n  groupName: selectedGroup,\n  policyName: selectedPolicy,\n  user: selectedUser,\n  accepts: computed(() => {\n    if (!selectedAccepts.value) {\n      return [];\n    }\n    return selectedAccepts.value.split(\",\");\n  }),\n  keyword: keyword,\n  sort: selectedSort,\n  page: page,\n  size: size,\n});\n\nprovide<Ref<Set<string>>>(\"selectedAttachmentNames\", selectedAttachmentNames);\n\nconst handleMove = async (group: Group) => {\n  try {\n    const promises = Array.from(selectedAttachmentNames.value).map((name) => {\n      return coreApiClient.storage.attachment.patchAttachment({\n        name,\n        jsonPatchInner: [\n          {\n            op: \"add\",\n            path: \"/spec/groupName\",\n            value: group.metadata.name,\n          },\n        ],\n      });\n    });\n\n    await Promise.all(promises);\n    selectedAttachmentNames.value.clear();\n\n    Toast.success(t(\"core.attachment.operations.move.toast_success\"));\n  } catch (e) {\n    console.error(e);\n  } finally {\n    handleFetchAttachments();\n  }\n};\n\nconst handleClickItem = (attachment: Attachment) => {\n  if (attachment.metadata.deletionTimestamp) {\n    return;\n  }\n\n  if (selectedAttachmentNames.value.size > 0) {\n    handleSelect(attachment);\n    return;\n  }\n\n  selectedAttachment.value = attachment;\n  selectedAttachmentNames.value.clear();\n  detailVisible.value = true;\n};\n\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n  handleCheckAll(checked);\n};\n\nconst onDetailModalClose = () => {\n  selectedAttachment.value = undefined;\n  nameQuery.value = undefined;\n  detailVisible.value = false;\n  handleFetchAttachments();\n};\n\nconst onUploadModalClose = () => {\n  routeQueryAction.value = undefined;\n  handleFetchAttachments();\n  uploadVisible.value = false;\n};\n\n// View type\nconst viewTypes = [\n  {\n    name: \"list\",\n    tooltip: t(\"core.attachment.filters.view_type.items.list\"),\n    icon: IconList,\n  },\n  {\n    name: \"grid\",\n    tooltip: t(\"core.attachment.filters.view_type.items.grid\"),\n    icon: IconGrid,\n  },\n];\n\nconst viewType = useLocalStorage(\"attachment-view-type\", \"list\");\n\n// Route query action\nconst routeQueryAction = useRouteQuery<string | undefined>(\"action\");\n\nonMounted(() => {\n  if (routeQueryAction.value === \"upload\") {\n    uploadVisible.value = true;\n  }\n  if (nameQuery.value) {\n    detailVisible.value = true;\n  }\n});\n\nconst nameQuery = useRouteQuery<string | undefined>(\"name\");\n\nwatch(\n  () => selectedAttachment.value,\n  () => {\n    if (selectedAttachment.value) {\n      nameQuery.value = selectedAttachment.value.metadata.name;\n    }\n  }\n);\n</script>\n<template>\n  <AttachmentDetailModal\n    v-if=\"detailVisible\"\n    :name=\"selectedAttachment?.metadata.name || nameQuery\"\n    @close=\"onDetailModalClose\"\n  >\n    <template #actions>\n      <span @click=\"handleSelectPrevious\">\n        <IconArrowLeft />\n      </span>\n      <span @click=\"handleSelectNext\">\n        <IconArrowRight />\n      </span>\n    </template>\n  </AttachmentDetailModal>\n  <AttachmentUploadModal\n    v-if=\"uploadVisible\"\n    :initial-group-name=\"\n      selectedGroup === 'ungrouped' ? undefined : selectedGroup\n    \"\n    :initial-policy-name=\"selectedPolicy\"\n    @close=\"onUploadModalClose\"\n  />\n  <AttachmentPoliciesModal\n    v-if=\"policyVisible\"\n    @close=\"policyVisible = false\"\n  />\n  <VPageHeader :title=\"$t('core.attachment.title')\">\n    <template #icon>\n      <IconFolder />\n    </template>\n    <template #actions>\n      <VButton\n        v-permission=\"['system:attachments:manage']\"\n        size=\"sm\"\n        @click=\"policyVisible = true\"\n      >\n        <template #icon>\n          <IconDatabase2Line />\n        </template>\n        {{ $t(\"core.attachment.actions.storage_policies\") }}\n      </VButton>\n      <VButton\n        v-permission=\"['system:attachments:manage']\"\n        type=\"secondary\"\n        @click=\"uploadVisible = true\"\n      >\n        <template #icon>\n          <IconUpload />\n        </template>\n        {{ $t(\"core.common.buttons.upload\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <div class=\"flex flex-col gap-2 sm:flex-row\">\n      <div class=\"w-full\">\n        <VCard :body-class=\"[viewType === 'list' ? '!p-0' : '']\">\n          <template #header>\n            <div class=\"block w-full bg-gray-50 px-4 py-3\">\n              <div\n                class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n              >\n                <div\n                  v-permission=\"['system:attachments:manage']\"\n                  class=\"hidden items-center sm:flex\"\n                >\n                  <input\n                    v-model=\"checkedAll\"\n                    type=\"checkbox\"\n                    @change=\"handleCheckAllChange\"\n                  />\n                </div>\n                <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n                  <SearchInput\n                    v-if=\"!selectedAttachmentNames.size\"\n                    v-model=\"keyword\"\n                  />\n                  <VSpace v-else>\n                    <VButton type=\"danger\" @click=\"handleDeleteInBatch\">\n                      {{ $t(\"core.common.buttons.delete\") }}\n                    </VButton>\n                    <VButton @click=\"selectedAttachmentNames.clear()\">\n                      {{\n                        $t(\"core.attachment.operations.deselect_items.button\")\n                      }}\n                    </VButton>\n                    <VDropdown v-if=\"groups?.length\">\n                      <VButton>\n                        {{ $t(\"core.attachment.operations.move.button\") }}\n                      </VButton>\n                      <template #popper>\n                        <VDropdownItem\n                          v-for=\"group in groups\"\n                          :key=\"group.metadata.name\"\n                          @click=\"handleMove(group)\"\n                        >\n                          {{ group.spec.displayName }}\n                        </VDropdownItem>\n                      </template>\n                    </VDropdown>\n                  </VSpace>\n                </div>\n                <VSpace spacing=\"lg\" class=\"flex-wrap\">\n                  <FilterCleanButton\n                    v-if=\"hasFilters\"\n                    @click=\"handleClearFilters\"\n                  />\n                  <FilterDropdown\n                    v-model=\"selectedPolicy\"\n                    :label=\"$t('core.attachment.filters.storage_policy.label')\"\n                    :items=\"[\n                      {\n                        label: t('core.common.filters.item_labels.all'),\n                      },\n                      ...(policies?.map((policy) => {\n                        return {\n                          label: policy.spec.displayName,\n                          value: policy.metadata.name,\n                        };\n                      }) || []),\n                    ]\"\n                  />\n                  <FilterDropdown\n                    v-model=\"selectedAccepts\"\n                    :label=\"$t('core.attachment.filters.accept.label')\"\n                    :items=\"[\n                      {\n                        label: t('core.common.filters.item_labels.all'),\n                      },\n                      {\n                        label: t('core.attachment.filters.accept.items.image'),\n                        value: 'image/*',\n                      },\n                      {\n                        label: t('core.attachment.filters.accept.items.audio'),\n                        value: 'audio/*',\n                      },\n                      {\n                        label: t('core.attachment.filters.accept.items.video'),\n                        value: 'video/*',\n                      },\n                      {\n                        label: t('core.attachment.filters.accept.items.file'),\n                        value: 'text/*,application/*',\n                      },\n                    ]\"\n                  />\n                  <HasPermission :permissions=\"['system:users:view']\">\n                    <UserFilterDropdown\n                      v-model=\"selectedUser\"\n                      :label=\"$t('core.attachment.filters.owner.label')\"\n                    />\n                  </HasPermission>\n                  <FilterDropdown\n                    v-model=\"selectedSort\"\n                    :label=\"$t('core.common.filters.labels.sort')\"\n                    :items=\"[\n                      {\n                        label: t('core.common.filters.item_labels.default'),\n                      },\n                      {\n                        label: t(\n                          'core.attachment.filters.sort.items.create_time_desc'\n                        ),\n                        value: 'metadata.creationTimestamp,desc',\n                      },\n                      {\n                        label: t(\n                          'core.attachment.filters.sort.items.create_time_asc'\n                        ),\n                        value: 'metadata.creationTimestamp,asc',\n                      },\n                      {\n                        label: t(\n                          'core.attachment.filters.sort.items.display_name_desc'\n                        ),\n                        value: 'spec.displayName,desc',\n                      },\n                      {\n                        label: t(\n                          'core.attachment.filters.sort.items.display_name_asc'\n                        ),\n                        value: 'spec.displayName,asc',\n                      },\n                      {\n                        label: t(\n                          'core.attachment.filters.sort.items.size_desc'\n                        ),\n                        value: 'spec.size,desc',\n                      },\n                      {\n                        label: t('core.attachment.filters.sort.items.size_asc'),\n                        value: 'spec.size,asc',\n                      },\n                    ]\"\n                  />\n                  <div class=\"flex flex-row gap-2\">\n                    <div\n                      v-for=\"(item, index) in viewTypes\"\n                      :key=\"index\"\n                      v-tooltip=\"`${item.tooltip}`\"\n                      :class=\"{\n                        'bg-gray-200 font-bold text-black':\n                          viewType === item.name,\n                      }\"\n                      class=\"cursor-pointer rounded p-1 hover:bg-gray-200\"\n                      @click=\"viewType = item.name\"\n                    >\n                      <component :is=\"item.icon\" class=\"h-4 w-4\" />\n                    </div>\n                  </div>\n                  <div class=\"flex flex-row gap-2\">\n                    <div\n                      class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                      @click=\"handleFetchAttachments()\"\n                    >\n                      <IconRefreshLine\n                        v-tooltip=\"$t('core.common.buttons.refresh')\"\n                        :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                        class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                      />\n                    </div>\n                  </div>\n                </VSpace>\n              </div>\n            </div>\n          </template>\n\n          <div :style=\"`${viewType === 'list' ? 'padding:12px 16px 0' : ''}`\">\n            <AttachmentGroupList @select=\"handleReset\" />\n          </div>\n\n          <VLoading v-if=\"isLoading\" />\n\n          <Transition v-else-if=\"!attachments?.length\" appear name=\"fade\">\n            <VEmpty\n              :message=\"$t('core.attachment.empty.message')\"\n              :title=\"$t('core.attachment.empty.title')\"\n            >\n              <template #actions>\n                <VSpace>\n                  <VButton @click=\"handleFetchAttachments\">\n                    {{ $t(\"core.common.buttons.refresh\") }}\n                  </VButton>\n                  <VButton\n                    v-permission=\"['system:attachments:manage']\"\n                    type=\"secondary\"\n                    @click=\"uploadVisible = true\"\n                  >\n                    <template #icon>\n                      <IconUpload />\n                    </template>\n                    {{ $t(\"core.attachment.empty.actions.upload\") }}\n                  </VButton>\n                </VSpace>\n              </template>\n            </VEmpty>\n          </Transition>\n\n          <div v-else>\n            <Transition v-if=\"viewType === 'grid'\" appear name=\"fade\">\n              <div\n                class=\"mt-2 grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12\"\n                role=\"list\"\n              >\n                <AttachmentGridListItem\n                  v-for=\"attachment in attachments\"\n                  :key=\"attachment.metadata.name\"\n                  :attachment=\"attachment\"\n                  :is-selected=\"isChecked(attachment)\"\n                  @click=\"handleClickItem(attachment)\"\n                  @select=\"handleSelect(attachment)\"\n                />\n              </div>\n            </Transition>\n            <Transition v-if=\"viewType === 'list'\" appear name=\"fade\">\n              <VEntityContainer>\n                <AttachmentListItem\n                  v-for=\"attachment in attachments\"\n                  :key=\"attachment.metadata.name\"\n                  :attachment=\"attachment\"\n                  :is-selected=\"isChecked(attachment)\"\n                  @select=\"handleSelect\"\n                  @open-detail=\"handleClickItem\"\n                />\n              </VEntityContainer>\n            </Transition>\n          </div>\n\n          <template #footer>\n            <VPagination\n              v-model:page=\"page\"\n              v-model:size=\"size\"\n              :page-label=\"$t('core.components.pagination.page_label')\"\n              :size-label=\"$t('core.components.pagination.size_label')\"\n              :total-label=\"\n                $t('core.components.pagination.total_label', { total: total })\n              \"\n              :total=\"total\"\n              :size-options=\"[60, 120, 200]\"\n            />\n          </template>\n        </VCard>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentDetailModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconRiPencilFill,\n  VButton,\n  VDescription,\n  VDescriptionItem,\n  VLoading,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport prettyBytes from \"pretty-bytes\";\nimport { computed, ref, toRefs, useTemplateRef } from \"vue\";\nimport AttachmentImagePreview from \"@/components/attachment/AttachmentImagePreview.vue\";\nimport AttachmentPermalinkList from \"@/components/attachment/AttachmentPermalinkList.vue\";\nimport { isImage } from \"@/utils/image\";\nimport DisplayNameEditForm from \"./DisplayNameEditForm.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    name?: string;\n    mountToBody?: boolean;\n  }>(),\n  {\n    name: undefined,\n    mountToBody: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst { name } = toRefs(props);\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n\nconst { data: attachment, isLoading } = useQuery({\n  queryKey: [\"core:attachment-by-name\", name],\n  queryFn: async () => {\n    const { data } = await coreApiClient.storage.attachment.getAttachment({\n      name: name.value as string,\n    });\n    return data;\n  },\n  enabled: computed(() => !!name.value),\n});\n\nconst policyName = computed(() => {\n  return attachment.value?.spec.policyName;\n});\n\nconst groupName = computed(() => {\n  return attachment.value?.spec.groupName;\n});\n\nconst { data: policy } = useQuery({\n  queryKey: [\"core:attachment-policy-by-name\", policyName],\n  queryFn: async () => {\n    if (!policyName.value) {\n      return;\n    }\n\n    const { data } = await coreApiClient.storage.policy.getPolicy({\n      name: policyName.value,\n    });\n\n    return data;\n  },\n  enabled: computed(() => !!policyName.value),\n});\n\nconst { data: group } = useQuery({\n  queryKey: [\"core:attachment-group-by-name\", groupName],\n  queryFn: async () => {\n    if (!groupName.value) {\n      return;\n    }\n\n    const { data } = await coreApiClient.storage.group.getGroup({\n      name: groupName.value,\n    });\n\n    return data;\n  },\n  enabled: computed(() => !!groupName.value),\n});\n\nconst showDisplayNameForm = ref(false);\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"\n      $t('core.attachment.detail_modal.title', {\n        display_name: attachment?.spec.displayName || '',\n      })\n    \"\n    :width=\"1200\"\n    :mount-to-body=\"mountToBody\"\n    :layer-closable=\"true\"\n    height=\"calc(100vh - 20px)\"\n    :body-class=\"['!p-0']\"\n    @close=\"emit('close')\"\n  >\n    <template #actions>\n      <slot name=\"actions\"></slot>\n    </template>\n    <div>\n      <VLoading v-if=\"isLoading\" />\n      <div v-else class=\"overflow-hidden bg-white\">\n        <VDescription>\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.preview')\"\n          >\n            <AttachmentImagePreview\n              v-if=\"isImage(attachment?.spec.mediaType)\"\n              :attachment=\"attachment\"\n            />\n            <div v-else-if=\"attachment?.spec.mediaType?.startsWith('video/')\">\n              <video\n                :src=\"attachment.status?.permalink\"\n                controls\n                class=\"max-w-full rounded sm:max-w-[50%]\"\n              >\n                {{\n                  $t(\"core.attachment.detail_modal.preview.video_not_support\")\n                }}\n              </video>\n            </div>\n            <div v-else-if=\"attachment?.spec.mediaType?.startsWith('audio/')\">\n              <audio :src=\"attachment.status?.permalink\" controls>\n                {{\n                  $t(\"core.attachment.detail_modal.preview.audio_not_support\")\n                }}\n              </audio>\n            </div>\n            <span v-else>\n              {{ $t(\"core.attachment.detail_modal.preview.not_support\") }}\n            </span>\n          </VDescriptionItem>\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.storage_policy')\"\n            :content=\"policy?.spec.displayName\"\n          >\n          </VDescriptionItem>\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.group')\"\n            :content=\"\n              group?.spec.displayName ||\n              $t('core.attachment.common.text.ungrouped')\n            \"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.display_name')\"\n          >\n            <DisplayNameEditForm\n              v-if=\"showDisplayNameForm && attachment\"\n              :attachment=\"attachment\"\n              @close=\"showDisplayNameForm = false\"\n            />\n            <div v-else class=\"flex items-center gap-3\">\n              <span>\n                {{ attachment?.spec.displayName }}\n              </span>\n              <HasPermission :permissions=\"['system:attachments:manage']\">\n                <IconRiPencilFill\n                  class=\"cursor-pointer text-sm text-gray-600 hover:text-gray-900\"\n                  @click=\"showDisplayNameForm = true\"\n                />\n              </HasPermission>\n            </div>\n          </VDescriptionItem>\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.media_type')\"\n            :content=\"attachment?.spec.mediaType\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.size')\"\n            :content=\"prettyBytes(attachment?.spec.size || 0)\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.owner')\"\n            :content=\"attachment?.spec.ownerName\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.creation_time')\"\n            :content=\"utils.date.format(attachment?.metadata.creationTimestamp)\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.attachment.detail_modal.fields.permalink')\"\n          >\n            <AttachmentPermalinkList :attachment=\"attachment\" />\n          </VDescriptionItem>\n        </VDescription>\n      </div>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <VButton type=\"default\" @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close_and_shortcut\") }}\n        </VButton>\n        <slot name=\"footer\" />\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentError.vue",
    "content": "<template>\n  <div class=\"flex h-full items-center justify-center object-cover\">\n    <span class=\"text-xs text-red-400\">\n      {{ $t(\"core.common.status.loading_error\") }}\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentGroupBadge.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  Attachment,\n  AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest,\n  Group,\n} from \"@halo-dev/api-client\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  paginate,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconCheckboxCircle,\n  IconMore,\n  Toast,\n  VDropdown,\n  VDropdownItem,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { chunk } from \"es-toolkit\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport AttachmentGroupEditingModal from \"./AttachmentGroupEditingModal.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    group?: Group;\n    isSelected?: boolean;\n    features?: { actions: boolean; checkIcon?: boolean };\n  }>(),\n  {\n    group: undefined,\n    isSelected: false,\n    features: () => {\n      return {\n        actions: true,\n        checkIcon: false,\n      };\n    },\n  }\n);\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst handleDelete = () => {\n  Dialog.warning({\n    title: t(\"core.attachment.group_list.operations.delete.title\"),\n    description: t(\"core.attachment.group_list.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      if (!props.group) {\n        return;\n      }\n\n      // TODO: 后续将修改为在后端进行批量操作处理\n      const attachments = await fetchAllAttachmentsByGroupName(\n        props.group.metadata.name\n      );\n\n      await coreApiClient.storage.group.deleteGroup({\n        name: props.group.metadata.name,\n      });\n\n      const attachmentChunks = chunk(attachments, 10);\n\n      for (const attachmentChunk of attachmentChunks) {\n        const moveToUnGroupRequests = attachmentChunk.map((attachment) => {\n          return coreApiClient.storage.attachment.patchAttachment({\n            name: attachment.metadata.name,\n            jsonPatchInner: [\n              {\n                op: \"remove\",\n                path: \"/spec/groupName\",\n              },\n            ],\n          });\n        });\n\n        await Promise.all(moveToUnGroupRequests);\n      }\n\n      queryClient.invalidateQueries({ queryKey: [\"attachment-groups\"] });\n      queryClient.invalidateQueries({ queryKey: [\"attachments\"] });\n\n      Toast.success(\n        t(\"core.attachment.group_list.operations.delete.toast_success\", {\n          total: attachments.length,\n        })\n      );\n    },\n  });\n};\n\nasync function fetchAllAttachmentsByGroupName(groupName: string) {\n  return await paginate<\n    AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest,\n    Attachment\n  >((params) => consoleApiClient.storage.attachment.searchAttachments(params), {\n    fieldSelector: [`spec.groupName=${groupName}`],\n    size: 1000,\n  });\n}\n\nconst handleDeleteWithAttachments = () => {\n  Dialog.warning({\n    title: t(\n      \"core.attachment.group_list.operations.delete_with_attachments.title\"\n    ),\n    description: t(\n      \"core.attachment.group_list.operations.delete_with_attachments.description\"\n    ),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      if (!props.group) {\n        return;\n      }\n\n      // TODO: 后续将修改为在后端进行批量操作处理\n      const attachments = await fetchAllAttachmentsByGroupName(\n        props.group.metadata.name\n      );\n\n      await coreApiClient.storage.group.deleteGroup({\n        name: props.group.metadata.name,\n      });\n\n      const attachmentChunks = chunk(attachments, 10);\n\n      for (const attachmentChunk of attachmentChunks) {\n        const deleteAttachmentRequests = attachmentChunk.map((attachment) => {\n          return coreApiClient.storage.attachment.deleteAttachment({\n            name: attachment.metadata.name,\n          });\n        });\n        await Promise.all(deleteAttachmentRequests);\n      }\n\n      queryClient.invalidateQueries({ queryKey: [\"attachment-groups\"] });\n      queryClient.invalidateQueries({ queryKey: [\"attachments\"] });\n\n      Toast.success(\n        t(\n          \"core.attachment.group_list.operations.delete_with_attachments.toast_success\",\n          { total: attachments.length }\n        )\n      );\n    },\n  });\n};\n\n// Editing\nconst editingModalVisible = ref(false);\n\nconst onEditingModalClose = () => {\n  queryClient.invalidateQueries({ queryKey: [\"attachment-groups\"] });\n  editingModalVisible.value = false;\n};\n</script>\n\n<template>\n  <button\n    type=\"button\"\n    class=\"inline-flex h-full w-full items-center gap-2 rounded-md border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50 hover:shadow-sm\"\n    :class=\"{ '!bg-gray-100 shadow-sm': isSelected }\"\n  >\n    <div class=\"inline-flex w-full flex-1 gap-x-2 break-all text-left\">\n      <slot name=\"text\">\n        {{ group?.spec.displayName }}\n      </slot>\n      <VStatusDot\n        v-if=\"group?.metadata.deletionTimestamp\"\n        v-tooltip=\"$t('core.common.status.deleting')\"\n        state=\"warning\"\n        animate\n      />\n    </div>\n    <div class=\"flex-none\">\n      <HasPermission\n        v-if=\"features.actions\"\n        :permissions=\"['system:attachments:manage']\"\n      >\n        <VDropdown>\n          <IconMore @click.stop />\n          <template #popper>\n            <VDropdownItem @click=\"editingModalVisible = true\">\n              {{ $t(\"core.attachment.group_list.operations.rename.button\") }}\n            </VDropdownItem>\n            <VDropdown placement=\"right\" :triggers=\"['click']\">\n              <VDropdownItem type=\"danger\">\n                {{ $t(\"core.common.buttons.delete\") }}\n              </VDropdownItem>\n              <template #popper>\n                <VDropdownItem type=\"danger\" @click=\"handleDelete()\">\n                  {{\n                    $t(\"core.attachment.group_list.operations.delete.button\")\n                  }}\n                </VDropdownItem>\n                <VDropdownItem\n                  type=\"danger\"\n                  @click=\"handleDeleteWithAttachments()\"\n                >\n                  {{\n                    $t(\n                      \"core.attachment.group_list.operations.delete_with_attachments.button\"\n                    )\n                  }}\n                </VDropdownItem>\n              </template>\n            </VDropdown>\n          </template>\n        </VDropdown>\n      </HasPermission>\n\n      <IconCheckboxCircle\n        v-if=\"isSelected && features.checkIcon\"\n        class=\"text-primary\"\n      />\n\n      <slot name=\"actions\" />\n    </div>\n\n    <AttachmentGroupEditingModal\n      v-if=\"editingModalVisible\"\n      :group=\"group\"\n      @close=\"onEditingModalClose\"\n    />\n  </button>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentGroupEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  Group,\n  GroupV1alpha1ApiListGroupRequest,\n} from \"@halo-dev/api-client\";\nimport { coreApiClient, paginate } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    group?: Group;\n  }>(),\n  {\n    group: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst formState = ref<Group>({\n  spec: {\n    displayName: \"\",\n  },\n  apiVersion: \"storage.halo.run/v1alpha1\",\n  kind: \"Group\",\n  metadata: {\n    name: \"\",\n    generateName: \"attachment-group-\",\n  },\n});\nconst isSubmitting = ref(false);\n\nconst modalTitle = props.group\n  ? t(\"core.attachment.group_editing_modal.titles.update\")\n  : t(\"core.attachment.group_editing_modal.titles.create\");\n\nconst handleSave = async () => {\n  try {\n    isSubmitting.value = true;\n    if (props.group) {\n      await coreApiClient.storage.group.updateGroup({\n        name: formState.value.metadata.name,\n        group: formState.value,\n      });\n    } else {\n      const groups = await paginate<GroupV1alpha1ApiListGroupRequest, Group>(\n        (params) => coreApiClient.storage.group.listGroup(params),\n        {\n          size: 1000,\n        }\n      );\n      const hasDisplayNameDuplicate = groups.some(\n        (group) => group.spec.displayName === formState.value.spec.displayName\n      );\n      if (hasDisplayNameDuplicate) {\n        Toast.error(\n          t(\"core.attachment.group_editing_modal.toast.group_name_exists\")\n        );\n        return;\n      }\n      await coreApiClient.storage.group.createGroup({\n        group: formState.value,\n      });\n    }\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n    modal.value?.close();\n  } catch (e) {\n    console.error(\"Failed to save attachment group\", e);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n\nonMounted(() => {\n  setFocus(\"displayNameInput\");\n\n  if (props.group) {\n    formState.value = cloneDeep(props.group);\n  }\n});\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    mount-to-body\n    :title=\"modalTitle\"\n    :width=\"500\"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      id=\"attachment-group-form\"\n      name=\"attachment-group-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      type=\"form\"\n      :actions=\"false\"\n      @submit=\"handleSave\"\n    >\n      <FormKit\n        id=\"displayNameInput\"\n        v-model=\"formState.spec.displayName\"\n        :label=\"\n          $t('core.attachment.group_editing_modal.fields.display_name.label')\n        \"\n        type=\"text\"\n        name=\"displayName\"\n        validation=\"required|length:0,50\"\n      ></FormKit>\n    </FormKit>\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('attachment-group-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentGroupList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Group } from \"@halo-dev/api-client\";\nimport { IconAddCircle } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useFetchAttachmentGroup } from \"../composables/use-attachment-group\";\nimport AttachmentGroupBadge from \"./AttachmentGroupBadge.vue\";\nimport AttachmentGroupEditingModal from \"./AttachmentGroupEditingModal.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    readonly?: boolean;\n  }>(),\n  {\n    readonly: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"select\", group: Group): void;\n}>();\n\nconst queryClient = useQueryClient();\nconst defaultGroups: Group[] = [\n  {\n    spec: {\n      displayName: t(\"core.attachment.group_list.internal_groups.all\"),\n    },\n    apiVersion: \"\",\n    kind: \"\",\n    metadata: {\n      name: \"\",\n    },\n  },\n  {\n    spec: {\n      displayName: t(\"core.attachment.common.text.ungrouped\"),\n    },\n    apiVersion: \"\",\n    kind: \"\",\n    metadata: {\n      name: \"ungrouped\",\n    },\n  },\n];\n\nconst { groups } = useFetchAttachmentGroup();\n\nconst loading = ref<boolean>(false);\nconst creationModalVisible = ref(false);\n\nconst selectedGroup = props.readonly\n  ? ref(\"\")\n  : useRouteQuery<string>(\"group\", \"\");\n\nconst handleSelectGroup = (group: Group) => {\n  emit(\"select\", group);\n  selectedGroup.value = group.metadata.name;\n};\n\nconst onCreationModalClose = () => {\n  queryClient.invalidateQueries({ queryKey: [\"attachment-groups\"] });\n  creationModalVisible.value = false;\n};\n</script>\n<template>\n  <AttachmentGroupEditingModal\n    v-if=\"!readonly && creationModalVisible\"\n    @close=\"onCreationModalClose\"\n  />\n  <div\n    class=\"mb-5 grid grid-cols-2 gap-x-2 gap-y-3 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6\"\n  >\n    <AttachmentGroupBadge\n      v-for=\"defaultGroup in defaultGroups\"\n      :key=\"defaultGroup.metadata.name\"\n      :group=\"defaultGroup\"\n      :is-selected=\"defaultGroup.metadata.name === selectedGroup\"\n      :features=\"{ actions: false, checkIcon: readonly }\"\n      @click=\"handleSelectGroup(defaultGroup)\"\n    />\n\n    <AttachmentGroupBadge\n      v-for=\"group in groups\"\n      :key=\"group.metadata.name\"\n      :group=\"group\"\n      :is-selected=\"group.metadata.name === selectedGroup\"\n      :features=\"{ actions: !readonly, checkIcon: readonly }\"\n      @click=\"handleSelectGroup(group)\"\n    />\n\n    <HasPermission\n      v-if=\"!loading && !readonly\"\n      :permissions=\"['system:attachments:manage']\"\n    >\n      <AttachmentGroupBadge\n        :features=\"{ actions: false }\"\n        @click=\"creationModalVisible = true\"\n      >\n        <template #text>\n          <span>{{ $t(\"core.common.buttons.new\") }}</span>\n        </template>\n        <template #actions>\n          <IconAddCircle />\n        </template>\n      </AttachmentGroupBadge>\n    </HasPermission>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils, type OperationItem } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport prettyBytes from \"pretty-bytes\";\nimport type { Ref } from \"vue\";\nimport { computed, inject, markRaw, ref, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport EntityDropdownItems from \"@/components/entity/EntityDropdownItems.vue\";\nimport { useFetchAttachmentPolicy } from \"../composables/use-attachment-policy\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    attachment: Attachment;\n    isSelected?: boolean;\n  }>(),\n  { isSelected: false }\n);\n\nconst { attachment } = toRefs(props);\n\nconst { data: policies } = useFetchAttachmentPolicy();\n\nconst emit = defineEmits<{\n  (event: \"select\", attachment?: Attachment): void;\n  (event: \"open-detail\", attachment: Attachment): void;\n}>();\n\nconst selectedAttachmentNames = inject<Ref<Set<string>>>(\n  \"selectedAttachmentNames\",\n  ref<Set<string>>(new Set())\n);\n\nconst policyDisplayName = computed(() => {\n  const policy = policies.value?.find(\n    (p) => p.metadata.name === props.attachment.spec.policyName\n  );\n  return policy?.spec.displayName;\n});\n\nconst handleDelete = () => {\n  Dialog.warning({\n    title: t(\"core.attachment.operations.delete.title\"),\n    description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.storage.attachment.deleteAttachment({\n          name: props.attachment.metadata.name,\n        });\n\n        selectedAttachmentNames.value.delete(props.attachment.metadata.name);\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (e) {\n        console.error(\"Failed to delete attachment\", e);\n      } finally {\n        queryClient.invalidateQueries({ queryKey: [\"attachments\"] });\n      }\n    },\n  });\n};\n\nconst { data: operationItems } = useOperationItemExtensionPoint<Attachment>(\n  \"attachment:list-item:operation:create\",\n  attachment,\n  computed((): OperationItem<Attachment>[] => [\n    {\n      priority: 10,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.detail\"),\n      permissions: [],\n      action: () => {\n        emit(\"open-detail\", attachment.value);\n      },\n    },\n    {\n      priority: 20,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.download\"),\n      action: () => {\n        const { permalink } = attachment.value.status || {};\n\n        if (!permalink) {\n          throw new Error(\"Attachment has no permalink\");\n        }\n\n        const a = document.createElement(\"a\");\n        a.href = permalink;\n        a.download = attachment.value.spec.displayName || \"unknown\";\n        document.body.appendChild(a);\n        a.click();\n        document.body.removeChild(a);\n      },\n    },\n    {\n      priority: 30,\n      component: markRaw(VDropdownDivider),\n    },\n    {\n      priority: 40,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.delete\"),\n      permissions: [\"system:attachments:manage\"],\n      action: () => {\n        handleDelete();\n      },\n    },\n  ])\n);\n</script>\n\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template\n      v-if=\"utils.permission.has(['system:attachments:manage'])\"\n      #checkbox\n    >\n      <input\n        :checked=\"selectedAttachmentNames.has(attachment.metadata.name)\"\n        type=\"checkbox\"\n        @click=\"emit('select', attachment)\"\n      />\n    </template>\n    <template #start>\n      <VEntityField>\n        <template #description>\n          <div class=\"h-10 w-10 rounded border bg-white p-1 hover:shadow-sm\">\n            <AttachmentFileTypeIcon\n              :display-ext=\"false\"\n              :file-name=\"attachment.spec.displayName\"\n              :width=\"8\"\n              :height=\"8\"\n            />\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"attachment.spec.displayName\"\n        @click=\"emit('open-detail', attachment)\"\n      >\n        <template #description>\n          <VSpace>\n            <span class=\"text-xs text-gray-500\">\n              {{ attachment.spec.mediaType }}\n            </span>\n            <span class=\"text-xs text-gray-500\">\n              {{ prettyBytes(attachment.spec.size || 0) }}\n            </span>\n          </VSpace>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField :description=\"policyDisplayName\" />\n      <VEntityField>\n        <template #description>\n          <RouterLink\n            :to=\"{\n              name: 'UserDetail',\n              params: {\n                name: attachment.spec.ownerName,\n              },\n            }\"\n            class=\"text-xs text-gray-500\"\n            :class=\"{\n              'pointer-events-none': !utils.permission.has([\n                'system:users:view',\n              ]),\n            }\"\n          >\n            {{ attachment.spec.ownerName }}\n          </RouterLink>\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"attachment.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{ utils.date.format(attachment.metadata.creationTimestamp) }}\n          </span>\n        </template>\n      </VEntityField>\n    </template>\n    <template #dropdownItems>\n      <EntityDropdownItems\n        :dropdown-items=\"operationItems || []\"\n        :item=\"attachment\"\n      />\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentLoading.vue",
    "content": "<template>\n  <div class=\"flex h-full items-center justify-center object-cover\">\n    <span class=\"text-xs text-gray-400\">\n      {{ $t(\"core.common.status.loading\") }}...\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentPoliciesListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  consoleApiClient,\n  coreApiClient,\n  type Policy,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconEyeOff,\n  Toast,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { SYSTEM_PROTECTION } from \"@/constants/finalizers\";\nimport { attachmentPolicyLabels } from \"@/constants/labels\";\nimport { useFetchAttachmentPolicyTemplate } from \"../composables/use-attachment-policy\";\nimport AttachmentPolicyEditingModal from \"./AttachmentPolicyEditingModal.vue\";\n\nconst queryClient = useQueryClient();\n\nconst props = defineProps<{\n  policy: Policy;\n}>();\n\nconst { t } = useI18n();\n\nconst { data: policyTemplates } = useFetchAttachmentPolicyTemplate();\n\nconst templateDisplayName = computed(() => {\n  const policyTemplate = policyTemplates.value?.find(\n    (template) => template.metadata.name === props.policy.spec.templateName\n  );\n  return policyTemplate?.spec?.displayName || \"--\";\n});\n\nconst editingModalVisible = ref(false);\n\nconst handleDelete = async () => {\n  const { data } = await consoleApiClient.storage.attachment.searchAttachments({\n    fieldSelector: [`spec.policyName=${props.policy.metadata.name}`],\n    page: 1,\n    size: 1,\n  });\n\n  if (data.total > 0) {\n    Dialog.warning({\n      title: t(\n        \"core.attachment.policies_modal.operations.can_not_delete.title\"\n      ),\n      description: t(\n        \"core.attachment.policies_modal.operations.can_not_delete.description\"\n      ),\n      confirmText: t(\"core.common.buttons.confirm\"),\n      showCancel: false,\n    });\n    return;\n  }\n\n  Dialog.warning({\n    title: t(\"core.attachment.policies_modal.operations.delete.title\"),\n    description: t(\n      \"core.attachment.policies_modal.operations.delete.description\"\n    ),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await coreApiClient.storage.policy.deletePolicy({\n        name: props.policy.metadata.name,\n      });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n      queryClient.invalidateQueries({ queryKey: [\"attachment-policies\"] });\n    },\n  });\n};\n\nconst isHidden = computed(() => {\n  return (\n    props.policy.metadata.labels?.[attachmentPolicyLabels.HIDDEN] === \"true\"\n  );\n});\n\nconst isSystemProtection = computed(() => {\n  return props.policy.metadata.finalizers?.includes(SYSTEM_PROTECTION);\n});\n</script>\n<template>\n  <VEntity :key=\"policy.metadata.name\">\n    <template #start>\n      <VEntityField\n        :title=\"policy.spec.displayName\"\n        :description=\"templateDisplayName\"\n      ></VEntityField>\n    </template>\n    <template #end>\n      <VEntityField>\n        <template v-if=\"isSystemProtection\" #description>\n          <VTag>{{ $t(\"core.common.text.system_protection\") }}</VTag>\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"isHidden\">\n        <template #description>\n          <IconEyeOff class=\"text-sm transition-all hover:text-blue-600\" />\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"policy.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{ utils.date.format(policy.metadata.creationTimestamp) }}\n          </span>\n        </template>\n      </VEntityField>\n    </template>\n    <template #dropdownItems>\n      <VDropdownItem @click=\"editingModalVisible = true\">\n        {{ $t(\"core.common.buttons.edit\") }}\n      </VDropdownItem>\n      <VDropdownItem\n        :disabled=\"isSystemProtection\"\n        type=\"danger\"\n        @click=\"handleDelete\"\n      >\n        {{ $t(\"core.common.buttons.delete\") }}\n      </VDropdownItem>\n    </template>\n  </VEntity>\n\n  <AttachmentPolicyEditingModal\n    v-if=\"editingModalVisible\"\n    :policy=\"policy\"\n    @close=\"editingModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentPoliciesModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Policy, PolicyTemplate } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  VButton,\n  VDropdown,\n  VDropdownItem,\n  VEmpty,\n  VEntityContainer,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { ref } from \"vue\";\nimport {\n  useFetchAttachmentPolicy,\n  useFetchAttachmentPolicyTemplate,\n} from \"../composables/use-attachment-policy\";\nimport AttachmentPoliciesListItem from \"./AttachmentPoliciesListItem.vue\";\nimport AttachmentPolicyEditingModal from \"./AttachmentPolicyEditingModal.vue\";\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst {\n  data: policies,\n  isLoading,\n  refetch: handleFetchPolicies,\n} = useFetchAttachmentPolicy();\nconst { data: policyTemplates } = useFetchAttachmentPolicyTemplate();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst selectedPolicy = ref<Policy>();\nconst selectedTemplateName = ref();\n\nconst creationModalVisible = ref(false);\n\nconst handleOpenCreateNewPolicyModal = (policyTemplate: PolicyTemplate) => {\n  selectedTemplateName.value = policyTemplate.metadata.name;\n  creationModalVisible.value = true;\n};\n\nconst onCreationModalClose = () => {\n  selectedPolicy.value = undefined;\n  selectedTemplateName.value = undefined;\n  creationModalVisible.value = false;\n};\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"750\"\n    :title=\"$t('core.attachment.policies_modal.title')\"\n    :body-class=\"['!p-0']\"\n    :layer-closable=\"true\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <template #actions>\n      <VDropdown>\n        <span v-tooltip=\"$t('core.common.buttons.new')\">\n          <IconAddCircle />\n        </span>\n        <template #popper>\n          <VDropdownItem\n            v-for=\"policyTemplate in policyTemplates\"\n            :key=\"policyTemplate.metadata.name\"\n            @click=\"handleOpenCreateNewPolicyModal(policyTemplate)\"\n          >\n            {{ policyTemplate.spec?.displayName }}\n          </VDropdownItem>\n        </template>\n      </VDropdown>\n    </template>\n    <VEmpty\n      v-if=\"!policies?.length && !isLoading\"\n      :message=\"$t('core.attachment.policies_modal.empty.message')\"\n      :title=\"$t('core.attachment.policies_modal.empty.title')\"\n    >\n      <template #actions>\n        <VSpace>\n          <VButton @click=\"handleFetchPolicies\">\n            {{ $t(\"core.common.buttons.refresh\") }}\n          </VButton>\n          <VDropdown>\n            <VButton type=\"secondary\">\n              <template #icon>\n                <IconAddCircle />\n              </template>\n              {{ $t(\"core.common.buttons.new\") }}\n            </VButton>\n            <template #popper>\n              <VDropdownItem\n                v-for=\"policyTemplate in policyTemplates\"\n                :key=\"policyTemplate.metadata.name\"\n                @click=\"handleOpenCreateNewPolicyModal(policyTemplate)\"\n              >\n                {{ policyTemplate.spec?.displayName }}\n              </VDropdownItem>\n            </template>\n          </VDropdown>\n        </VSpace>\n      </template>\n    </VEmpty>\n    <VEntityContainer v-else>\n      <AttachmentPoliciesListItem\n        v-for=\"policy in policies\"\n        :key=\"policy.metadata.name\"\n        :policy=\"policy\"\n      />\n    </VEntityContainer>\n    <template #footer>\n      <VButton @click=\"modal?.close()\">\n        {{ $t(\"core.common.buttons.close_and_shortcut\") }}\n      </VButton>\n    </template>\n  </VModal>\n\n  <AttachmentPolicyEditingModal\n    v-if=\"creationModalVisible\"\n    :policy=\"selectedPolicy\"\n    :template-name=\"selectedTemplateName\"\n    @close=\"onCreationModalClose\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentPolicyBadge.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Policy } from \"@halo-dev/api-client\";\nimport { IconCheckboxCircle } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport { useFetchAttachmentPolicyTemplate } from \"../composables/use-attachment-policy\";\n\nconst props = withDefaults(\n  defineProps<{\n    policy?: Policy;\n    isSelected?: boolean;\n    features?: { checkIcon?: boolean };\n  }>(),\n  {\n    policy: undefined,\n    isSelected: false,\n    features: () => {\n      return {\n        checkIcon: false,\n      };\n    },\n  }\n);\n\nconst { data: policyTemplates } = useFetchAttachmentPolicyTemplate();\n\nconst policyTemplate = computed(() => {\n  return policyTemplates.value?.find(\n    (template) => template.metadata.name === props.policy?.spec.templateName\n  );\n});\n</script>\n\n<template>\n  <button\n    type=\"button\"\n    class=\"inline-flex h-full w-full items-center gap-2 rounded-md border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-800 hover:bg-gray-50 hover:shadow-sm\"\n    :class=\"{ '!bg-gray-100 shadow-sm': isSelected }\"\n  >\n    <div class=\"inline-flex w-full flex-1 flex-col space-y-0.5 text-left\">\n      <slot name=\"text\">\n        <div>\n          {{ policy?.spec.displayName }}\n        </div>\n        <div class=\"text-xs font-normal text-gray-600\">\n          {{ policyTemplate?.spec?.displayName || \"--\" }}\n        </div>\n      </slot>\n    </div>\n\n    <div class=\"flex-none\">\n      <IconCheckboxCircle\n        v-if=\"isSelected && features.checkIcon\"\n        class=\"text-primary\"\n      />\n      <slot name=\"actions\" />\n    </div>\n  </button>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentPolicyEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitSchemaCondition, FormKitSchemaNode } from \"@formkit/core\";\nimport type {\n  JsonPatchInner,\n  Policy,\n  PolicyV1alpha1ApiListPolicyRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  paginate,\n} from \"@halo-dev/api-client\";\nimport { Toast, VButton, VLoading, VModal, VSpace } from \"@halo-dev/components\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, onMounted, ref, toRaw, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { attachmentPolicyLabels } from \"@/constants/labels\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst CONFIG_MAP_GROUP = \"default\";\n\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    policy?: Policy;\n    templateName?: string;\n  }>(),\n  {\n    policy: undefined,\n    templateName: undefined,\n  }\n);\n\nconst { policy } = toRefs(props);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst isUpdateMode = computed(() => !!props.policy);\n\nonMounted(async () => {\n  setFocus(\"displayNameInput\");\n});\n\nconst templateName = computed(() => {\n  return props.policy?.spec.templateName || props.templateName;\n});\n\nconst { data: policyTemplate } = useQuery({\n  queryKey: [\"core:attachment:policy-template\", templateName],\n  cacheTime: 0,\n  queryFn: async () => {\n    if (!templateName.value) {\n      throw new Error(\"No template name found\");\n    }\n\n    const { data } =\n      await coreApiClient.storage.policyTemplate.getPolicyTemplate({\n        name: templateName.value,\n      });\n    return data;\n  },\n  retry: 0,\n  enabled: computed(() => !!templateName.value),\n});\n\nconst { data: setting, isLoading } = useQuery({\n  queryKey: [\"core:attachment:policy-template:setting\", policyTemplate],\n  cacheTime: 0,\n  queryFn: async () => {\n    if (!policyTemplate.value?.spec?.settingName) {\n      throw new Error(\"No setting found\");\n    }\n\n    const { data } = await coreApiClient.setting.getSetting({\n      name: policyTemplate.value.spec.settingName,\n    });\n\n    return data;\n  },\n  retry: 0,\n  enabled: computed(() => !!policyTemplate.value?.spec?.settingName),\n});\n\nconst { data: configMapGroupData } = useQuery({\n  queryKey: [\"core:attachment:policy-template:configMap\", policy],\n  cacheTime: 0,\n  retry: 0,\n  queryFn: async () => {\n    if (!policy.value) {\n      return {};\n    }\n\n    const { data } =\n      await consoleApiClient.storage.policy.getPolicyConfigByGroup({\n        name: policy.value.metadata.name,\n        group: CONFIG_MAP_GROUP,\n      });\n\n    return (data || {}) as Record<string, unknown>;\n  },\n});\n\nconst formSchema = computed(() => {\n  if (!setting.value) {\n    return;\n  }\n  const { forms } = setting.value.spec;\n  return forms.find((item) => item.group === CONFIG_MAP_GROUP)?.formSchema as (\n    | FormKitSchemaCondition\n    | FormKitSchemaNode\n  )[];\n});\n\nconst isSubmitting = ref(false);\n\nconst handleSave = async (data: {\n  displayName: string;\n  hidden: string;\n  priority: string;\n  config: Record<string, unknown>;\n}) => {\n  try {\n    isSubmitting.value = true;\n    if (isUpdateMode.value) {\n      if (!policy.value) {\n        throw new Error(\"No policy found\");\n      }\n\n      await consoleApiClient.storage.policy.updatePolicyConfigByGroup({\n        name: policy.value.metadata.name,\n        group: CONFIG_MAP_GROUP,\n        body: data.config,\n      });\n\n      const jsonPatchInner: JsonPatchInner[] = [\n        {\n          op: \"add\",\n          path: \"/spec/displayName\",\n          value: data.displayName,\n        },\n      ];\n\n      if (policy.value.metadata.labels) {\n        jsonPatchInner.push({\n          op: \"add\",\n          path: `/metadata/labels/${attachmentPolicyLabels.HIDDEN_WITH_JSON_PATCH}`,\n          value: data.hidden,\n        });\n        jsonPatchInner.push({\n          op: \"add\",\n          path: `/metadata/labels/${attachmentPolicyLabels.PRIORITY_WITH_JSON_PATCH}`,\n          value: data.priority || \"0\",\n        });\n      } else {\n        jsonPatchInner.push({\n          op: \"add\",\n          path: `/metadata/labels`,\n          value: {\n            [attachmentPolicyLabels.HIDDEN]: data.hidden,\n            [attachmentPolicyLabels.PRIORITY]: data.priority || \"0\",\n          },\n        });\n      }\n\n      await coreApiClient.storage.policy.patchPolicy({\n        name: policy.value.metadata.name,\n        jsonPatchInner: jsonPatchInner,\n      });\n    } else {\n      const policies = await paginate<\n        PolicyV1alpha1ApiListPolicyRequest,\n        Policy\n      >((params) => coreApiClient.storage.policy.listPolicy(params), {\n        size: 1000,\n      });\n\n      const hasDisplayNameDuplicate = policies.some(\n        (policy) => policy.spec.displayName === data.displayName\n      );\n\n      if (hasDisplayNameDuplicate) {\n        Toast.error(\n          t(\"core.attachment.policy_editing_modal.toast.policy_name_exists\")\n        );\n        return;\n      }\n\n      const { data: newConfigMap } =\n        await coreApiClient.configMap.createConfigMap({\n          configMap: {\n            data: {\n              [CONFIG_MAP_GROUP]: JSON.stringify(data.config || {}),\n            },\n            apiVersion: \"v1alpha1\",\n            kind: \"ConfigMap\",\n            metadata: {\n              generateName: \"configMap-\",\n              name: \"\",\n            },\n          },\n        });\n\n      await coreApiClient.storage.policy.createPolicy({\n        policy: {\n          spec: {\n            displayName: data.displayName,\n            templateName: templateName.value as string,\n            configMapName: newConfigMap.metadata.name,\n          },\n          apiVersion: \"storage.halo.run/v1alpha1\",\n          kind: \"Policy\",\n          metadata: {\n            name: \"\",\n            generateName: \"attachment-policy-\",\n            labels: {\n              [attachmentPolicyLabels.HIDDEN]: data.hidden,\n            },\n          },\n        },\n      });\n    }\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n    modal.value?.close();\n  } catch (e) {\n    console.error(\"Failed to save attachment policy\", e);\n  } finally {\n    isSubmitting.value = false;\n    queryClient.invalidateQueries({ queryKey: [\"attachment-policies\"] });\n  }\n};\n\nconst modalTitle = props.policy\n  ? t(\"core.attachment.policy_editing_modal.titles.update\", {\n      policy: props.policy?.spec.displayName,\n    })\n  : t(\"core.attachment.policy_editing_modal.titles.create\", {\n      policy_template: policyTemplate.value?.spec?.displayName,\n    });\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    mount-to-body\n    :title=\"modalTitle\"\n    :width=\"650\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <VLoading v-if=\"isLoading\" />\n      <template v-else>\n        <FormKit\n          id=\"attachment-policy-form\"\n          v-slot=\"{ value }\"\n          name=\"attachment-policy-form\"\n          :preserve=\"true\"\n          type=\"form\"\n          :config=\"{ validationVisibility: 'submit' }\"\n          @submit=\"handleSave\"\n        >\n          <FormKit\n            id=\"displayNameInput\"\n            :value=\"policy?.spec.displayName\"\n            :label=\"\n              $t(\n                'core.attachment.policy_editing_modal.fields.display_name.label'\n              )\n            \"\n            type=\"text\"\n            name=\"displayName\"\n            validation=\"required|length:0,50\"\n          ></FormKit>\n          <FormKit\n            name=\"hidden\"\n            :value=\"\n              policy?.metadata.labels?.[attachmentPolicyLabels.HIDDEN] ||\n              'false'\n            \"\n            type=\"checkbox\"\n            :label=\"\n              $t('core.attachment.policy_editing_modal.fields.hidden.label')\n            \"\n            :help=\"\n              $t('core.attachment.policy_editing_modal.fields.hidden.help')\n            \"\n            on-value=\"true\"\n            off-value=\"false\"\n          ></FormKit>\n          <FormKit\n            v-if=\"value?.hidden === 'false'\"\n            name=\"priority\"\n            :value=\"policy?.metadata.labels?.[attachmentPolicyLabels.PRIORITY]\"\n            type=\"number\"\n            :label=\"\n              $t('core.attachment.policy_editing_modal.fields.priority.label')\n            \"\n            :help=\"\n              $t('core.attachment.policy_editing_modal.fields.priority.help')\n            \"\n          ></FormKit>\n          <FormKit\n            v-if=\"formSchema && configMapGroupData\"\n            :value=\"toRaw(configMapGroupData) || {}\"\n            type=\"group\"\n            name=\"config\"\n            :label=\"\n              $t('core.attachment.policy_editing_modal.fields.config.label')\n            \"\n          >\n            <FormKitSchema\n              :schema=\"toRaw(formSchema)\"\n              :data=\"toRaw(configMapGroupData) || {}\"\n            />\n          </FormKit>\n        </FormKit>\n      </template>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('attachment-policy-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentSelectorModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton, VModal, VSpace, VTabbar } from \"@halo-dev/components\";\nimport type {\n  AttachmentLike,\n  AttachmentSelectProvider,\n} from \"@halo-dev/ui-shared\";\nimport {\n  computed,\n  markRaw,\n  onMounted,\n  ref,\n  shallowRef,\n  useTemplateRef,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport CoreSelectorProvider from \"./selector-providers/CoreSelectorProvider.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    accepts?: string[];\n    min?: number;\n    max?: number;\n  }>(),\n  {\n    accepts: () => [\"*/*\"],\n    min: undefined,\n    max: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"select\", attachments: AttachmentLike[]): void;\n}>();\n\nconst selected = ref<AttachmentLike[]>([] as AttachmentLike[]);\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n\nconst attachmentSelectProviders = shallowRef<AttachmentSelectProvider[]>([\n  {\n    id: \"core\",\n    label: t(\"core.attachment.select_modal.providers.default.label\"),\n    component: markRaw(CoreSelectorProvider),\n  },\n]);\n\n// resolve plugin extension points\nconst { pluginModules } = usePluginModuleStore();\n\nonMounted(async () => {\n  for (const pluginModule of pluginModules) {\n    try {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"attachment:selector:create\"];\n\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const providers = await callbackFunction();\n      attachmentSelectProviders.value = [\n        ...attachmentSelectProviders.value,\n        ...providers,\n      ].flat();\n    } catch (error) {\n      console.error(`Error processing plugin module:`, pluginModule, error);\n    }\n  }\n});\n\nconst activeId = ref(attachmentSelectProviders.value[0].id);\n\nconst onChangeProvider = (providerId: string) => {\n  const provider = attachmentSelectProviders.value.find(\n    (provider) => provider.id === providerId\n  );\n\n  if (!provider) {\n    return;\n  }\n\n  activeId.value = providerId;\n};\n\nconst handleConfirm = () => {\n  emit(\"select\", Array.from(selected.value));\n  modal.value?.close();\n};\n\nconst confirmDisabled = computed(() => {\n  if (!selected.value.length) {\n    return true;\n  }\n\n  if (props.min === undefined) {\n    return false;\n  }\n\n  return selected.value.length < props.min;\n});\n\nconst confirmCountMessage = computed(() => {\n  if (!props.min && !props.max) {\n    return selected.value.length;\n  }\n  return `${selected.value.length} / ${props.max || props.min}`;\n});\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"1240\"\n    :mount-to-body=\"true\"\n    :layer-closable=\"true\"\n    :title=\"$t('core.attachment.select_modal.title')\"\n    height=\"calc(100vh - 20px)\"\n    @close=\"emit('close')\"\n  >\n    <VTabbar\n      v-model:active-id=\"activeId\"\n      :items=\"\n        attachmentSelectProviders.map((provider) => ({\n          id: provider.id,\n          label: provider.label,\n        }))\n      \"\n      class=\"w-full\"\n      type=\"outline\"\n    ></VTabbar>\n\n    <div class=\"mt-2\">\n      <template\n        v-for=\"provider in attachmentSelectProviders\"\n        :key=\"provider.id\"\n      >\n        <Suspense>\n          <component\n            :is=\"provider.component\"\n            v-if=\"activeId === provider.id\"\n            v-model:selected=\"selected\"\n            :accepts=\"accepts\"\n            :min=\"min\"\n            :max=\"max\"\n            @change-provider=\"onChangeProvider\"\n          ></component>\n          <template #fallback>\n            {{ $t(\"core.common.status.loading\") }}\n          </template>\n        </Suspense>\n      </template>\n    </div>\n    <template #footer>\n      <VSpace>\n        <VButton\n          type=\"secondary\"\n          :disabled=\"confirmDisabled\"\n          @click=\"handleConfirm\"\n        >\n          {{ $t(\"core.common.buttons.confirm\") }}\n          <span v-if=\"selected.length || props.min || props.max\">\n            {{\n              $t(\"core.attachment.select_modal.operations.select.result\", {\n                count: confirmCountMessage,\n              })\n            }}\n          </span>\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentUploadArea.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport { VTabItem, VTabs } from \"@halo-dev/components\";\nimport type { SuccessResponse } from \"@uppy/core\";\nimport { computed, ref } from \"vue\";\nimport UploadFromUrl from \"./UploadFromUrl.vue\";\n\nconst {\n  policyName = undefined,\n  groupName = undefined,\n  height = undefined,\n} = defineProps<{\n  policyName?: string;\n  groupName?: string;\n  height?: string;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"done\"): void;\n  (event: \"uploaded\", response: Attachment): void;\n}>();\n\nconst activeTab = ref(\"upload\");\n\nconst onUploaded = (response: SuccessResponse) => {\n  if (response.body) {\n    emit(\"uploaded\", response.body as Attachment);\n  }\n};\n\nconst UNGROUPED_GROUP_NAME = \"ungrouped\";\n\nconst finalGroupName = computed(() => {\n  if (!groupName || groupName === UNGROUPED_GROUP_NAME) {\n    return \"\";\n  }\n  return groupName;\n});\n</script>\n<template>\n  <VTabs v-model:active-id=\"activeTab\" type=\"outline\">\n    <VTabItem\n      id=\"upload\"\n      :label=\"$t('core.attachment.upload_modal.upload_options.local_upload')\"\n    >\n      <UppyUpload\n        endpoint=\"/apis/api.console.halo.run/v1alpha1/attachments/upload\"\n        :disabled=\"!policyName\"\n        :meta=\"{\n          policyName: policyName,\n          groupName: finalGroupName,\n        }\"\n        width=\"100%\"\n        :height=\"height\"\n        :allowed-meta-fields=\"['policyName', 'groupName']\"\n        :note=\"\n          policyName\n            ? ''\n            : $t('core.attachment.upload_modal.filters.policy.not_select')\n        \"\n        :done-button-handler=\"() => emit('done')\"\n        @uploaded=\"onUploaded\"\n      />\n    </VTabItem>\n    <VTabItem\n      id=\"download\"\n      :label=\"$t('core.attachment.upload_modal.upload_options.download')\"\n    >\n      <UploadFromUrl\n        :policy-name=\"policyName\"\n        :group-name=\"finalGroupName\"\n        @uploaded=\"(attachment) => emit('uploaded', attachment)\"\n      />\n    </VTabItem>\n  </VTabs>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/AttachmentUploadModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PolicyTemplate } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  VAlert,\n  VDropdown,\n  VDropdownItem,\n  VModal,\n} from \"@halo-dev/components\";\nimport { computed, onMounted, ref } from \"vue\";\nimport { attachmentPolicyLabels } from \"@/constants/labels\";\nimport { useFetchAttachmentGroup } from \"../composables/use-attachment-group\";\nimport {\n  useFetchAttachmentPolicy,\n  useFetchAttachmentPolicyTemplate,\n} from \"../composables/use-attachment-policy\";\nimport AttachmentGroupBadge from \"./AttachmentGroupBadge.vue\";\nimport AttachmentGroupEditingModal from \"./AttachmentGroupEditingModal.vue\";\nimport AttachmentPolicyBadge from \"./AttachmentPolicyBadge.vue\";\nimport AttachmentPolicyEditingModal from \"./AttachmentPolicyEditingModal.vue\";\nimport AttachmentUploadArea from \"./AttachmentUploadArea.vue\";\n\nconst { initialPolicyName = undefined, initialGroupName = undefined } =\n  defineProps<{\n    initialPolicyName?: string;\n    initialGroupName?: string;\n  }>();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst selectedGroupName = ref(initialGroupName || \"\");\nconst selectedPolicyName = ref(initialPolicyName);\nconst policyEditingModal = ref(false);\nconst groupEditingModal = ref(false);\nconst policyTemplateNameToCreate = ref();\n\nconst { groups, handleFetchGroups } = useFetchAttachmentGroup();\nconst { data: policyTemplates } = useFetchAttachmentPolicyTemplate();\nconst { data: allPolicies, refetch: handleFetchPolicies } =\n  useFetchAttachmentPolicy();\n\nconst policies = computed(() => {\n  return allPolicies.value?.filter((policy) => {\n    return policy.metadata.labels?.[attachmentPolicyLabels.HIDDEN] !== \"true\";\n  });\n});\n\nonMounted(() => {\n  const initialPolicy = policies.value?.find(\n    (p) => p.metadata.name === initialPolicyName\n  );\n  selectedPolicyName.value =\n    initialPolicy?.metadata.name || policies.value?.[0]?.metadata.name;\n});\n\nconst handleOpenCreateNewPolicyModal = async (\n  policyTemplate: PolicyTemplate\n) => {\n  policyTemplateNameToCreate.value = policyTemplate.metadata.name;\n  policyEditingModal.value = true;\n};\n\nconst handleOpenCreateNewGroupModal = () => {\n  groupEditingModal.value = true;\n};\n\nconst onEditingModalClose = async () => {\n  await handleFetchPolicies();\n  policyTemplateNameToCreate.value = undefined;\n  selectedPolicyName.value = policies.value?.[0].metadata.name;\n  policyEditingModal.value = false;\n};\n\nconst onGroupEditingModalClose = async () => {\n  await handleFetchGroups();\n  groupEditingModal.value = false;\n};\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :width=\"920\"\n    height=\"calc(100vh - 20px)\"\n    :title=\"$t('core.attachment.upload_modal.title')\"\n    mount-to-body\n    @close=\"emit('close')\"\n  >\n    <div class=\"w-full p-4\">\n      <div class=\"mb-2\">\n        <span class=\"text-sm text-gray-800\">\n          {{ $t(\"core.attachment.upload_modal.filters.policy.label\") }}\n        </span>\n      </div>\n      <div class=\"mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4\">\n        <AttachmentPolicyBadge\n          v-for=\"policy in policies\"\n          :key=\"policy.metadata.name\"\n          :policy=\"policy\"\n          :is-selected=\"selectedPolicyName === policy.metadata.name\"\n          :features=\"{ checkIcon: true }\"\n          @click=\"selectedPolicyName = policy.metadata.name\"\n        />\n\n        <VDropdown>\n          <AttachmentPolicyBadge>\n            <template #text>\n              <span>{{ $t(\"core.common.buttons.new\") }}</span>\n            </template>\n            <template #actions>\n              <IconAddCircle />\n            </template>\n          </AttachmentPolicyBadge>\n          <template #popper>\n            <VDropdownItem\n              v-for=\"policyTemplate in policyTemplates\"\n              :key=\"policyTemplate.metadata.name\"\n              @click=\"handleOpenCreateNewPolicyModal(policyTemplate)\"\n            >\n              {{ policyTemplate.spec?.displayName }}\n            </VDropdownItem>\n          </template>\n        </VDropdown>\n      </div>\n      <div v-if=\"!policies?.length\" class=\"mb-3\">\n        <VAlert\n          :title=\"$t('core.attachment.upload_modal.filters.policy.empty.title')\"\n          :description=\"\n            $t('core.attachment.upload_modal.filters.policy.empty.description')\n          \"\n          :closable=\"false\"\n        />\n      </div>\n      <div class=\"mb-2\">\n        <span class=\"text-sm text-gray-800\">\n          {{ $t(\"core.attachment.upload_modal.filters.group.label\") }}\n        </span>\n      </div>\n      <div class=\"mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4\">\n        <AttachmentGroupBadge\n          v-for=\"group in [\n            {\n              metadata: { name: '' },\n              apiVersion: '',\n              kind: '',\n              spec: {\n                displayName: $t('core.attachment.common.text.ungrouped'),\n              },\n            },\n            ...(groups || []),\n          ]\"\n          :key=\"group.metadata.name\"\n          :group=\"group\"\n          :is-selected=\"group.metadata.name === selectedGroupName\"\n          :features=\"{ actions: false, checkIcon: true }\"\n          @click=\"selectedGroupName = group.metadata.name\"\n        />\n\n        <AttachmentGroupBadge\n          :features=\"{ actions: false }\"\n          @click=\"handleOpenCreateNewGroupModal\"\n        >\n          <template #text>\n            <span>{{ $t(\"core.common.buttons.new\") }}</span>\n          </template>\n          <template #actions>\n            <IconAddCircle />\n          </template>\n        </AttachmentGroupBadge>\n      </div>\n\n      <div class=\"mb-3\">\n        <AttachmentUploadArea\n          :policy-name=\"selectedPolicyName\"\n          :group-name=\"selectedGroupName\"\n          @done=\"modal?.close()\"\n        />\n      </div>\n    </div>\n  </VModal>\n\n  <AttachmentPolicyEditingModal\n    v-if=\"policyEditingModal\"\n    :template-name=\"policyTemplateNameToCreate\"\n    @close=\"onEditingModalClose\"\n  />\n\n  <AttachmentGroupEditingModal\n    v-if=\"groupEditingModal\"\n    @close=\"onGroupEditingModalClose\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/DisplayNameEditForm.vue",
    "content": "<script setup lang=\"ts\">\nimport { coreApiClient, type Attachment } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VSpace } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(defineProps<{ attachment: Attachment }>(), {});\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nonMounted(() => {\n  setFocus(\"displayName\");\n});\n\nconst isSubmitting = ref(false);\n\nasync function onSubmit({ displayName }: { displayName: string }) {\n  try {\n    isSubmitting.value = true;\n\n    await coreApiClient.storage.attachment.patchAttachment({\n      name: props.attachment.metadata.name,\n      jsonPatchInner: [\n        {\n          op: \"add\",\n          path: \"/spec/displayName\",\n          value: displayName,\n        },\n      ],\n    });\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n\n    queryClient.invalidateQueries({\n      queryKey: [\"core:attachment-by-name\", props.attachment.metadata.name],\n    });\n\n    emit(\"close\");\n  } catch (error) {\n    console.error(\"Failed to update displayName\", error);\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  } finally {\n    isSubmitting.value = false;\n  }\n}\n</script>\n<template>\n  <FormKit\n    id=\"attachment-display-name-form\"\n    type=\"form\"\n    name=\"attachment-display-name-form\"\n    @submit=\"onSubmit\"\n  >\n    <FormKit\n      id=\"displayName\"\n      :model-value=\"attachment.spec.displayName\"\n      type=\"text\"\n      name=\"displayName\"\n      validation=\"required:trim\"\n      :classes=\"{ outer: '!pb-0' }\"\n      :help=\"$t('core.attachment.detail_modal.display_name_form.help')\"\n    ></FormKit>\n  </FormKit>\n  <VSpace class=\"mt-4\">\n    <VButton\n      type=\"secondary\"\n      @click=\"$formkit.submit('attachment-display-name-form')\"\n    >\n      {{ $t(\"core.common.buttons.save\") }}\n    </VButton>\n    <VButton @click=\"emit('close')\">\n      {{ $t(\"core.common.buttons.cancel\") }}\n    </VButton>\n  </VSpace>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/UploadFromUrl.vue",
    "content": "<script lang=\"ts\" setup>\nimport { reset } from \"@formkit/core\";\nimport { consoleApiClient, type Attachment } from \"@halo-dev/api-client\";\nimport { Toast, VButton } from \"@halo-dev/components\";\nimport { onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    policyName?: string;\n    groupName?: string;\n  }>(),\n  {\n    policyName: undefined,\n    groupName: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"uploaded\", attachment: Attachment): void;\n}>();\n\nonMounted(() => {\n  setFocus(\"url\");\n});\n\nconst downloading = ref(false);\n\nasync function onSubmit({ url }: { url: string }) {\n  try {\n    downloading.value = true;\n\n    if (!props.policyName) {\n      throw new Error(\"Policy name is required\");\n    }\n\n    const { data } =\n      await consoleApiClient.storage.attachment.externalTransferAttachment({\n        uploadFromUrlRequest: {\n          url: url,\n          policyName: props.policyName,\n          groupName: props.groupName,\n        },\n      });\n\n    Toast.success(\n      t(\"core.attachment.upload_modal.download_form.toast.success\")\n    );\n\n    reset(\"url\");\n    emit(\"uploaded\", data);\n  } catch (error) {\n    return error;\n  } finally {\n    downloading.value = false;\n  }\n}\n</script>\n<template>\n  <FormKit\n    id=\"upload-from-url\"\n    type=\"form\"\n    name=\"upload-from-url\"\n    :config=\"{ validationVisibility: 'submit' }\"\n    @submit=\"onSubmit\"\n  >\n    <FormKit\n      id=\"url\"\n      type=\"url\"\n      name=\"url\"\n      :label=\"$t('core.attachment.upload_modal.download_form.fields.url.label')\"\n      :validation=\"[['required'], ['url']]\"\n    />\n  </FormKit>\n\n  <div class=\"mt-4\">\n    <VButton\n      type=\"secondary\"\n      :loading=\"downloading\"\n      @click=\"$formkit.submit('upload-from-url')\"\n    >\n      {{ $t(\"core.common.buttons.download\") }}\n    </VButton>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/selector-providers/CoreSelectorProvider.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport {\n  IconArrowLeft,\n  IconArrowRight,\n  IconCheckboxCircle,\n  IconCheckboxFill,\n  IconClose,\n  IconEye,\n  IconGrid,\n  IconList,\n  IconRefreshLine,\n  IconUpload,\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport type { AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { throttle } from \"es-toolkit/compat\";\nimport { computed, ref, watch, watchEffect } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport AttachmentGridListItem from \"@/components/attachment/AttachmentGridListItem.vue\";\nimport { attachmentPolicyLabels } from \"@/constants/labels\";\nimport { matchMediaTypes } from \"@/utils/media-type\";\nimport { useAttachmentControl } from \"../../composables/use-attachment\";\nimport { useFetchAttachmentPolicy } from \"../../composables/use-attachment-policy\";\nimport AttachmentDetailModal from \"../AttachmentDetailModal.vue\";\nimport AttachmentUploadArea from \"../AttachmentUploadArea.vue\";\nimport AttachmentSelectorListItem from \"./components/AttachmentSelectorListItem.vue\";\nimport GroupFilter from \"./components/GroupFilter.vue\";\nimport PolicyFilter from \"./components/PolicyFilter.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    selected: AttachmentLike[];\n    accepts?: string[];\n    min?: number;\n    max?: number;\n  }>(),\n  {\n    accepts: () => [\"*/*\"],\n    min: undefined,\n    max: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:selected\", attachments: AttachmentLike[]): void;\n  (event: \"change-provider\", providerId: string): void;\n}>();\n\nconst keyword = ref(\"\");\nconst selectedGroup = ref();\nconst selectedPolicy = ref();\nconst selectedSort = ref();\nconst page = ref(1);\nconst size = ref(60);\n\nconst {\n  attachments,\n  isLoading,\n  total,\n  selectedAttachment,\n  selectedAttachments,\n  selectedAttachmentNames,\n  handleFetchAttachments,\n  handleSelect,\n  handleSelectPrevious,\n  handleSelectNext,\n  isChecked,\n  isFetching,\n} = useAttachmentControl({\n  groupName: selectedGroup,\n  policyName: selectedPolicy,\n  sort: selectedSort,\n  accepts: computed(() => {\n    return props.accepts;\n  }),\n  page,\n  size,\n  keyword,\n});\n\nconst { data: allPolicies } = useFetchAttachmentPolicy();\n\nconst policies = computed(() => {\n  return allPolicies.value?.filter((policy) => {\n    return policy.metadata.labels?.[attachmentPolicyLabels.HIDDEN] !== \"true\";\n  });\n});\n\nconst throttledFetchAttachments = throttle(handleFetchAttachments, 1000, {\n  leading: false,\n  trailing: true,\n});\n\nwatch(\n  () => [\n    selectedPolicy.value,\n    selectedSort.value,\n    keyword.value,\n    selectedGroup.value,\n  ],\n  () => {\n    page.value = 1;\n  }\n);\n\nconst hasFilters = computed(() => {\n  return selectedPolicy.value || selectedSort.value || selectedGroup.value;\n});\n\nfunction handleClearFilters() {\n  selectedPolicy.value = undefined;\n  selectedSort.value = undefined;\n  selectedGroup.value = undefined;\n}\n\nconst uploadVisible = ref(false);\n\nwatchEffect(() => {\n  emit(\"update:selected\", Array.from(selectedAttachments.value));\n});\n\nconst handleOpenDetail = (attachment: Attachment) => {\n  selectedAttachment.value = attachment;\n};\n\nconst isDisabled = (attachment: Attachment) => {\n  const isMatchMediaType = matchMediaTypes(\n    attachment.spec.mediaType || \"*/*\",\n    props.accepts\n  );\n\n  if (\n    props.max !== undefined &&\n    props.max <= selectedAttachmentNames.value.size &&\n    !isChecked(attachment)\n  ) {\n    return true;\n  }\n\n  return !isMatchMediaType;\n};\n\nfunction onDetailModalClose() {\n  selectedAttachment.value = undefined;\n}\n\n// View type\nconst viewTypes = [\n  {\n    name: \"list\",\n    tooltip: t(\"core.attachment.filters.view_type.items.list\"),\n    icon: IconList,\n  },\n  {\n    name: \"grid\",\n    tooltip: t(\"core.attachment.filters.view_type.items.grid\"),\n    icon: IconGrid,\n  },\n];\n\nconst viewType = useLocalStorage(\"attachment-selector-view-type\", \"grid\");\n\nfunction onUploadDone() {\n  handleFetchAttachments();\n  uploadVisible.value = false;\n}\n\nfunction onUploaded(attachment: Attachment) {\n  handleSelect(attachment);\n  page.value = 1;\n  throttledFetchAttachments();\n}\n\nfunction handleToggleUploadView() {\n  if (uploadVisible.value) {\n    uploadVisible.value = false;\n    return;\n  }\n\n  if (!selectedPolicy.value) {\n    selectedPolicy.value = policies.value?.[0].metadata.name;\n  }\n\n  uploadVisible.value = true;\n}\n</script>\n<template>\n  <div class=\"mb-3 block w-full rounded bg-gray-50 px-3 py-2\">\n    <div class=\"relative flex flex-col items-start sm:flex-row sm:items-center\">\n      <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n        <SearchInput v-model=\"keyword\" />\n      </div>\n      <div class=\"mt-4 flex sm:mt-0\">\n        <VSpace spacing=\"lg\">\n          <FilterCleanButton v-if=\"hasFilters\" @click=\"handleClearFilters\" />\n\n          <FilterDropdown\n            v-model=\"selectedSort\"\n            :label=\"$t('core.common.filters.labels.sort')\"\n            :items=\"[\n              {\n                label: t('core.common.filters.item_labels.default'),\n              },\n              {\n                label: t('core.attachment.filters.sort.items.create_time_desc'),\n                value: 'metadata.creationTimestamp,desc',\n              },\n              {\n                label: t('core.attachment.filters.sort.items.create_time_asc'),\n                value: 'metadata.creationTimestamp,asc',\n              },\n              {\n                label: t(\n                  'core.attachment.filters.sort.items.display_name_desc'\n                ),\n                value: 'spec.displayName,desc',\n              },\n              {\n                label: t('core.attachment.filters.sort.items.display_name_asc'),\n                value: 'spec.displayName,asc',\n              },\n              {\n                label: t('core.attachment.filters.sort.items.size_desc'),\n                value: 'spec.size,desc',\n              },\n              {\n                label: t('core.attachment.filters.sort.items.size_asc'),\n                value: 'spec.size,asc',\n              },\n            ]\"\n          />\n\n          <div class=\"flex flex-row gap-2\">\n            <div\n              v-for=\"item in viewTypes\"\n              :key=\"item.name\"\n              v-tooltip=\"`${item.tooltip}`\"\n              :class=\"{\n                'bg-gray-200 font-bold text-black': viewType === item.name,\n              }\"\n              class=\"cursor-pointer rounded p-1 hover:bg-gray-200\"\n              @click=\"viewType = item.name\"\n            >\n              <component :is=\"item.icon\" class=\"h-4 w-4\" />\n            </div>\n          </div>\n\n          <div class=\"flex flex-row gap-2\">\n            <div\n              class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n              @click=\"handleFetchAttachments()\"\n            >\n              <IconRefreshLine\n                v-tooltip=\"$t('core.common.buttons.refresh')\"\n                :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n              />\n            </div>\n          </div>\n        </VSpace>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"space-y-4\">\n    <PolicyFilter v-model=\"selectedPolicy\" />\n    <GroupFilter v-model=\"selectedGroup\" />\n  </div>\n\n  <HasPermission :permissions=\"['system:attachments:manage']\">\n    <div class=\"my-5 space-y-3\">\n      <VButton @click=\"handleToggleUploadView\">\n        <template #icon>\n          <IconUpload v-if=\"!uploadVisible\" />\n          <IconClose v-else />\n        </template>\n        {{\n          uploadVisible\n            ? $t(\"core.common.buttons.cancel_upload\")\n            : $t(\"core.common.buttons.upload\")\n        }}\n      </VButton>\n\n      <Transition v-if=\"uploadVisible\" appear name=\"fade\">\n        <AttachmentUploadArea\n          :policy-name=\"selectedPolicy\"\n          :group-name=\"selectedGroup\"\n          height=\"450px\"\n          @done=\"onUploadDone\"\n          @uploaded=\"onUploaded\"\n        />\n      </Transition>\n    </div>\n  </HasPermission>\n\n  <VLoading v-if=\"isLoading\" />\n\n  <VEmpty\n    v-else-if=\"!attachments?.length\"\n    :message=\"$t('core.attachment.empty.message')\"\n    :title=\"$t('core.attachment.empty.title')\"\n  >\n    <template #actions>\n      <VSpace>\n        <VButton @click=\"handleFetchAttachments\">\n          {{ $t(\"core.common.buttons.refresh\") }}\n        </VButton>\n        <VButton type=\"secondary\" @click=\"handleToggleUploadView\">\n          <template #icon>\n            <IconUpload />\n          </template>\n          {{ $t(\"core.attachment.empty.actions.upload\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VEmpty>\n\n  <div v-else>\n    <Transition v-if=\"viewType === 'grid'\" appear name=\"fade\">\n      <div\n        class=\"mt-2 grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10\"\n        role=\"list\"\n      >\n        <AttachmentGridListItem\n          v-for=\"attachment in attachments\"\n          :key=\"attachment.metadata.name\"\n          :attachment=\"attachment\"\n          :is-selected=\"isChecked(attachment)\"\n          :is-disabled=\"isDisabled(attachment)\"\n          @select=\"handleSelect(attachment)\"\n          @click=\"handleSelect(attachment)\"\n        >\n          <template #actions>\n            <IconEye\n              class=\"mr-1 mt-1 hidden h-6 w-6 cursor-pointer text-white transition-all hover:text-primary group-hover:block\"\n              @click.stop=\"handleOpenDetail(attachment)\"\n            />\n          </template>\n        </AttachmentGridListItem>\n      </div>\n    </Transition>\n    <Transition v-if=\"viewType === 'list'\" appear name=\"fade\">\n      <div class=\"overflow-hidden rounded-base border\">\n        <VEntityContainer>\n          <AttachmentSelectorListItem\n            v-for=\"attachment in attachments\"\n            :key=\"attachment.metadata.name\"\n            :attachment=\"attachment\"\n            :is-selected=\"isChecked(attachment)\"\n            @select=\"handleSelect\"\n            @open-detail=\"handleOpenDetail\"\n          >\n            <template #checkbox>\n              <input\n                :checked=\"isChecked(attachment)\"\n                :disabled=\"isDisabled(attachment)\"\n                type=\"checkbox\"\n                @click=\"handleSelect(attachment)\"\n              />\n            </template>\n          </AttachmentSelectorListItem>\n        </VEntityContainer>\n      </div>\n    </Transition>\n  </div>\n\n  <div class=\"mt-4\">\n    <VPagination\n      v-model:page=\"page\"\n      v-model:size=\"size\"\n      :page-label=\"$t('core.components.pagination.page_label')\"\n      :size-label=\"$t('core.components.pagination.size_label')\"\n      :total-label=\"\n        $t('core.components.pagination.total_label', { total: total })\n      \"\n      :total=\"total\"\n      :size-options=\"[60, 120, 200]\"\n    />\n  </div>\n  <AttachmentDetailModal\n    v-if=\"selectedAttachment\"\n    :mount-to-body=\"true\"\n    :name=\"selectedAttachment?.metadata.name\"\n    @close=\"onDetailModalClose\"\n  >\n    <template #actions>\n      <span\n        v-if=\"isChecked(selectedAttachment)\"\n        @click=\"handleSelect(selectedAttachment)\"\n      >\n        <IconCheckboxFill />\n      </span>\n      <span v-else @click=\"handleSelect(selectedAttachment)\">\n        <IconCheckboxCircle />\n      </span>\n\n      <span @click=\"handleSelectPrevious\">\n        <IconArrowLeft />\n      </span>\n      <span @click=\"handleSelectNext\">\n        <IconArrowRight />\n      </span>\n    </template>\n  </AttachmentDetailModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/selector-providers/components/AttachmentSelectorListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport {\n  VEntity,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport prettyBytes from \"pretty-bytes\";\nimport { computed, toRefs } from \"vue\";\nimport { useFetchAttachmentPolicy } from \"../../../composables/use-attachment-policy\";\n\nconst props = withDefaults(\n  defineProps<{\n    attachment: Attachment;\n    isSelected?: boolean;\n  }>(),\n  { isSelected: false }\n);\n\nconst { attachment } = toRefs(props);\n\nconst { data: policies } = useFetchAttachmentPolicy();\n\nconst emit = defineEmits<{\n  (event: \"select\", attachment?: Attachment): void;\n  (event: \"open-detail\", attachment: Attachment): void;\n}>();\n\nconst policyDisplayName = computed(() => {\n  const policy = policies.value?.find(\n    (p) => p.metadata.name === props.attachment.spec.policyName\n  );\n  return policy?.spec.displayName;\n});\n</script>\n\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template\n      v-if=\"utils.permission.has(['system:attachments:manage'])\"\n      #checkbox\n    >\n      <slot name=\"checkbox\" />\n    </template>\n    <template #start>\n      <VEntityField>\n        <template #description>\n          <div class=\"h-10 w-10 rounded border bg-white p-1 hover:shadow-sm\">\n            <AttachmentFileTypeIcon\n              :display-ext=\"false\"\n              :file-name=\"attachment.spec.displayName\"\n              :width=\"8\"\n              :height=\"8\"\n            />\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"attachment.spec.displayName\"\n        @click=\"emit('open-detail', attachment)\"\n      >\n        <template #description>\n          <VSpace>\n            <span class=\"text-xs text-gray-500\">\n              {{ attachment.spec.mediaType }}\n            </span>\n            <span class=\"text-xs text-gray-500\">\n              {{ prettyBytes(attachment.spec.size || 0) }}\n            </span>\n          </VSpace>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField :description=\"policyDisplayName\" />\n      <VEntityField :description=\"attachment.spec.ownerName\" />\n      <VEntityField v-if=\"attachment.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{ utils.date.format(attachment.metadata.creationTimestamp) }}\n          </span>\n        </template>\n      </VEntityField>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/selector-providers/components/GroupFilter.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconAddCircle,\n  IconArrowDown,\n  VDropdown,\n  VDropdownItem,\n} from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { drop, take } from \"es-toolkit\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useFetchAttachmentGroup } from \"../../../composables/use-attachment-group\";\nimport AttachmentGroupBadge from \"../../AttachmentGroupBadge.vue\";\nimport AttachmentGroupEditingModal from \"../../AttachmentGroupEditingModal.vue\";\n\nconst modelValue = defineModel<string | undefined>();\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst { groups } = useFetchAttachmentGroup();\n\nconst creationModalVisible = ref(false);\n\nconst onCreationModalClose = () => {\n  queryClient.invalidateQueries({ queryKey: [\"attachment-groups\"] });\n  creationModalVisible.value = false;\n};\n\nconst allGroups = computed(() => {\n  return [\n    {\n      spec: {\n        displayName: t(\"core.attachment.common.text.ungrouped\"),\n      },\n      apiVersion: \"\",\n      kind: \"\",\n      metadata: {\n        name: \"ungrouped\",\n      },\n    },\n    ...(groups.value || []),\n  ];\n});\n\nconst latestGroups = computed(() => {\n  return take(allGroups.value, 7);\n});\n\nconst moreGroups = computed(() => {\n  return drop(allGroups.value, 7);\n});\n</script>\n<template>\n  <div class=\"flex flex-col gap-2\">\n    <div class=\"text-sm text-gray-800\">\n      {{ $t(\"core.attachment.upload_modal.filters.group.label\") }}\n    </div>\n    <div class=\"grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-5\">\n      <AttachmentGroupBadge\n        :is-selected=\"!modelValue\"\n        :features=\"{ actions: false, checkIcon: true }\"\n        @click=\"modelValue = undefined\"\n      >\n        <template #text>\n          {{ $t(\"core.attachment.group_list.internal_groups.all\") }}\n        </template>\n      </AttachmentGroupBadge>\n\n      <AttachmentGroupBadge\n        v-for=\"group in latestGroups\"\n        :key=\"group.metadata.name\"\n        :group=\"group\"\n        :is-selected=\"group.metadata.name === modelValue\"\n        :features=\"{ actions: false, checkIcon: true }\"\n        @click=\"modelValue = group.metadata.name\"\n      />\n\n      <VDropdown v-if=\"moreGroups.length > 0\">\n        <AttachmentGroupBadge :features=\"{ actions: false, checkIcon: false }\">\n          <template #text> {{ $t(\"core.common.buttons.more\") }} </template>\n          <template #actions>\n            <IconArrowDown />\n          </template>\n        </AttachmentGroupBadge>\n        <template #popper>\n          <div class=\"max-h-80 overflow-y-auto\">\n            <VDropdownItem\n              v-for=\"value in moreGroups\"\n              :key=\"value.metadata.name\"\n              :selected=\"value.metadata.name === modelValue\"\n              @click=\"modelValue = value.metadata.name\"\n            >\n              {{ value.spec.displayName }}\n            </VDropdownItem>\n          </div>\n        </template>\n      </VDropdown>\n\n      <HasPermission :permissions=\"['system:attachments:manage']\">\n        <AttachmentGroupBadge\n          :features=\"{ actions: false }\"\n          @click=\"creationModalVisible = true\"\n        >\n          <template #text>\n            <span>{{ $t(\"core.common.buttons.new\") }}</span>\n          </template>\n          <template #actions>\n            <IconAddCircle />\n          </template>\n        </AttachmentGroupBadge>\n      </HasPermission>\n    </div>\n  </div>\n\n  <AttachmentGroupEditingModal\n    v-if=\"creationModalVisible\"\n    @close=\"onCreationModalClose\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/components/selector-providers/components/PolicyFilter.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PolicyTemplate } from \"@halo-dev/api-client\";\nimport { IconAddCircle, VDropdown, VDropdownItem } from \"@halo-dev/components\";\nimport { computed, ref } from \"vue\";\nimport { attachmentPolicyLabels } from \"@/constants/labels\";\nimport {\n  useFetchAttachmentPolicy,\n  useFetchAttachmentPolicyTemplate,\n} from \"../../../composables/use-attachment-policy\";\nimport AttachmentPolicyBadge from \"../../AttachmentPolicyBadge.vue\";\nimport AttachmentPolicyEditingModal from \"../../AttachmentPolicyEditingModal.vue\";\n\nconst modelValue = defineModel<string | undefined>();\n\nconst { data: policyTemplates } = useFetchAttachmentPolicyTemplate();\nconst { data: allPolicies, refetch: handleFetchPolicies } =\n  useFetchAttachmentPolicy();\n\nconst policies = computed(() => {\n  return allPolicies.value?.filter((policy) => {\n    return policy.metadata.labels?.[attachmentPolicyLabels.HIDDEN] !== \"true\";\n  });\n});\n\nconst policyCreationModalVisible = ref(false);\nconst policyTemplateNameToCreate = ref<string | undefined>();\n\nasync function onEditingModalClose() {\n  await handleFetchPolicies();\n  policyTemplateNameToCreate.value = undefined;\n  modelValue.value = policies.value?.[0].metadata.name;\n  policyCreationModalVisible.value = false;\n}\n\nfunction handleOpenCreationModal(policyTemplate: PolicyTemplate) {\n  policyTemplateNameToCreate.value = policyTemplate.metadata.name;\n  policyCreationModalVisible.value = true;\n}\n</script>\n<template>\n  <div class=\"flex flex-col gap-2\">\n    <div class=\"text-sm text-gray-800\">\n      {{ $t(\"core.attachment.upload_modal.filters.policy.label\") }}\n    </div>\n    <div class=\"grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-5\">\n      <AttachmentPolicyBadge\n        :features=\"{ checkIcon: true }\"\n        :is-selected=\"!modelValue\"\n        @click=\"modelValue = undefined\"\n      >\n        <template #text>\n          <span>{{ $t(\"core.common.filters.item_labels.all\") }}</span>\n        </template>\n      </AttachmentPolicyBadge>\n      <AttachmentPolicyBadge\n        v-for=\"policy in policies\"\n        :key=\"policy.metadata.name\"\n        :policy=\"policy\"\n        :is-selected=\"modelValue === policy.metadata.name\"\n        :features=\"{ checkIcon: true }\"\n        @click=\"modelValue = policy.metadata.name\"\n      />\n\n      <HasPermission :permissions=\"['system:attachments:manage']\">\n        <VDropdown>\n          <AttachmentPolicyBadge>\n            <template #text>\n              <span>{{ $t(\"core.common.buttons.new\") }}</span>\n            </template>\n            <template #actions>\n              <IconAddCircle />\n            </template>\n          </AttachmentPolicyBadge>\n          <template #popper>\n            <VDropdownItem\n              v-for=\"policyTemplate in policyTemplates\"\n              :key=\"policyTemplate.metadata.name\"\n              @click=\"handleOpenCreationModal(policyTemplate)\"\n            >\n              {{ policyTemplate.spec?.displayName }}\n            </VDropdownItem>\n          </template>\n        </VDropdown>\n      </HasPermission>\n    </div>\n  </div>\n\n  <AttachmentPolicyEditingModal\n    v-if=\"policyCreationModalVisible\"\n    :template-name=\"policyTemplateNameToCreate\"\n    @close=\"onEditingModalClose\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/composables/use-attachment-group.ts",
    "content": "import type {\n  Group,\n  GroupV1alpha1ApiListGroupRequest,\n} from \"@halo-dev/api-client\";\nimport { coreApiClient, paginate } from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport type { Ref } from \"vue\";\n\ninterface useFetchAttachmentGroupReturn {\n  groups: Ref<Group[] | undefined>;\n  isLoading: Ref<boolean>;\n  handleFetchGroups: () => void;\n}\n\nexport function useFetchAttachmentGroup(): useFetchAttachmentGroupReturn {\n  const { data, isLoading, refetch } = useQuery<Group[]>({\n    queryKey: [\"attachment-groups\"],\n    queryFn: async () => {\n      return await paginate<GroupV1alpha1ApiListGroupRequest, Group>(\n        (params) => coreApiClient.storage.group.listGroup(params),\n        {\n          size: 1000,\n          labelSelector: [\"!halo.run/hidden\"],\n          sort: [\"metadata.creationTimestamp,asc\"],\n        }\n      );\n    },\n    refetchInterval(data) {\n      const hasDeletingGroup = data?.some(\n        (group) => !!group.metadata.deletionTimestamp\n      );\n\n      return hasDeletingGroup ? 1000 : false;\n    },\n  });\n\n  return {\n    groups: data,\n    isLoading,\n    handleFetchGroups: refetch,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/composables/use-attachment-policy.ts",
    "content": "import type {\n  Policy,\n  PolicyTemplate,\n  PolicyTemplateV1alpha1ApiListPolicyTemplateRequest,\n  PolicyV1alpha1ApiListPolicyRequest,\n} from \"@halo-dev/api-client\";\nimport { coreApiClient, paginate } from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { attachmentPolicyLabels } from \"@/constants/labels\";\n\nexport function useFetchAttachmentPolicy() {\n  return useQuery<Policy[]>({\n    queryKey: [\"attachment-policies\"],\n    queryFn: async () => {\n      const policies = await paginate<\n        PolicyV1alpha1ApiListPolicyRequest,\n        Policy\n      >((params) => coreApiClient.storage.policy.listPolicy(params), {\n        size: 1000,\n      });\n      return policies.sort((a, b) => {\n        const priorityA = parseInt(\n          a.metadata.labels?.[attachmentPolicyLabels.PRIORITY] || \"0\",\n          10\n        );\n        const priorityB = parseInt(\n          b.metadata.labels?.[attachmentPolicyLabels.PRIORITY] || \"0\",\n          10\n        );\n        return priorityB - priorityA;\n      });\n    },\n    refetchInterval(data) {\n      const hasDeletingPolicy = data?.some(\n        (policy) => !!policy.metadata.deletionTimestamp\n      );\n      return hasDeletingPolicy ? 1000 : false;\n    },\n  });\n}\n\nexport function useFetchAttachmentPolicyTemplate() {\n  return useQuery<PolicyTemplate[]>({\n    queryKey: [\"attachment-policy-templates\"],\n    queryFn: async () => {\n      return await paginate<\n        PolicyTemplateV1alpha1ApiListPolicyTemplateRequest,\n        PolicyTemplate\n      >(\n        (params) =>\n          coreApiClient.storage.policyTemplate.listPolicyTemplate(params),\n        {\n          size: 1000,\n        }\n      );\n    },\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/composables/use-attachment.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport {\n  computed,\n  nextTick,\n  ref,\n  watch,\n  type ComputedRef,\n  type Ref,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\ninterface useAttachmentControlReturn {\n  attachments: Ref<Attachment[] | undefined>;\n  isLoading: Ref<boolean>;\n  isFetching: Ref<boolean>;\n  selectedAttachment: Ref<Attachment | undefined>;\n  selectedAttachments: ComputedRef<Attachment[]>;\n  selectedAttachmentNames: Ref<Set<string>>;\n  checkedAll: Ref<boolean>;\n  total: Ref<number>;\n  handleFetchAttachments: () => void;\n  handleSelectPrevious: () => void;\n  handleSelectNext: () => void;\n  handleDeleteInBatch: () => void;\n  handleCheckAll: (checkAll: boolean) => void;\n  handleSelect: (attachment: Attachment | undefined) => void;\n  isChecked: (attachment: Attachment) => boolean;\n  handleReset: () => void;\n}\n\nexport function useAttachmentControl(filterOptions: {\n  policyName?: Ref<string | undefined>;\n  groupName?: Ref<string | undefined>;\n  user?: Ref<string | undefined>;\n  accepts?: Ref<string[]>;\n  keyword?: Ref<string | undefined>;\n  sort?: Ref<string | undefined>;\n  page: Ref<number>;\n  size: Ref<number>;\n}): useAttachmentControlReturn {\n  const { t } = useI18n();\n\n  const { user, policyName, groupName, keyword, sort, page, size, accepts } =\n    filterOptions;\n\n  const selectedAttachment = ref<Attachment>();\n  const selectedAttachmentNames = ref<Set<string>>(new Set<string>());\n  const checkedAll = ref(false);\n\n  const total = ref(0);\n  const hasPrevious = ref(false);\n  const hasNext = ref(false);\n\n  const { data, isLoading, isFetching, refetch } = useQuery<Attachment[]>({\n    queryKey: [\n      \"attachments\",\n      policyName,\n      keyword,\n      groupName,\n      user,\n      accepts,\n      page,\n      size,\n      sort,\n    ],\n    queryFn: async () => {\n      const isUnGrouped = groupName?.value === \"ungrouped\";\n\n      const fieldSelectorMap: Record<string, string | undefined> = {\n        \"spec.policyName\": policyName?.value,\n        \"spec.ownerName\": user?.value,\n        \"spec.groupName\": isUnGrouped ? undefined : groupName?.value,\n      };\n\n      const fieldSelector = Object.entries(fieldSelectorMap)\n        .map(([key, value]) => {\n          if (value) {\n            return `${key}=${value}`;\n          }\n        })\n        .filter(Boolean) as string[];\n\n      const { data } =\n        await consoleApiClient.storage.attachment.searchAttachments({\n          fieldSelector,\n          page: page.value,\n          size: size.value,\n          ungrouped: isUnGrouped,\n          accepts: accepts?.value,\n          keyword: keyword?.value,\n          sort: [sort?.value as string].filter(Boolean),\n        });\n\n      total.value = data.total;\n      hasPrevious.value = data.hasPrevious;\n      hasNext.value = data.hasNext;\n\n      return data.items;\n    },\n    refetchInterval(data) {\n      const hasDeletingAttachment = data?.some(\n        (attachment) => !!attachment.metadata.deletionTimestamp\n      );\n      return hasDeletingAttachment ? 1000 : false;\n    },\n  });\n\n  const handleSelectPrevious = async () => {\n    if (!data.value) return;\n\n    const index = data.value?.findIndex(\n      (attachment) =>\n        attachment.metadata.name === selectedAttachment.value?.metadata.name\n    );\n\n    if (index === undefined) return;\n\n    if (index > 0) {\n      selectedAttachment.value = data.value[index - 1];\n      return;\n    }\n\n    if (index === 0 && hasPrevious.value) {\n      page.value--;\n      await nextTick();\n      await refetch();\n      selectedAttachment.value = data.value[data.value.length - 1];\n    }\n  };\n\n  const handleSelectNext = async () => {\n    if (!data.value) return;\n\n    const index = data.value?.findIndex(\n      (attachment) =>\n        attachment.metadata.name === selectedAttachment.value?.metadata.name\n    );\n\n    if (index === undefined) return;\n\n    if (index < data.value?.length - 1) {\n      selectedAttachment.value = data.value[index + 1];\n      return;\n    }\n\n    if (index === data.value.length - 1 && hasNext.value) {\n      page.value++;\n      await nextTick();\n      await refetch();\n      selectedAttachment.value = data.value[0];\n    }\n  };\n\n  const handleDeleteInBatch = () => {\n    Dialog.warning({\n      title: t(\"core.attachment.operations.delete_in_batch.title\"),\n      description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n      confirmType: \"danger\",\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        try {\n          const promises = Array.from(selectedAttachmentNames.value).map(\n            (name) => {\n              return coreApiClient.storage.attachment.deleteAttachment({\n                name,\n              });\n            }\n          );\n          await Promise.all(promises);\n          selectedAttachmentNames.value.clear();\n\n          Toast.success(t(\"core.common.toast.delete_success\"));\n        } catch (e) {\n          console.error(\"Failed to delete attachments\", e);\n        } finally {\n          await refetch();\n        }\n      },\n    });\n  };\n\n  const handleCheckAll = (checkAll: boolean) => {\n    if (checkAll) {\n      data.value?.forEach((attachment) => {\n        selectedAttachmentNames.value.add(attachment.metadata.name);\n      });\n    } else {\n      selectedAttachmentNames.value.clear();\n    }\n  };\n\n  const handleSelect = async (attachment: Attachment | undefined) => {\n    if (!attachment) return;\n    if (selectedAttachmentNames.value.has(attachment.metadata.name)) {\n      selectedAttachmentNames.value.delete(attachment.metadata.name);\n      return;\n    }\n    selectedAttachmentNames.value.add(attachment.metadata.name);\n  };\n\n  watch(\n    () => selectedAttachmentNames.value.size,\n    (newValue) => {\n      checkedAll.value = newValue === data.value?.length;\n    }\n  );\n\n  const isChecked = (attachment: Attachment) => {\n    return selectedAttachmentNames.value.has(attachment.metadata.name);\n  };\n\n  const handleReset = () => {\n    page.value = 1;\n    selectedAttachment.value = undefined;\n    selectedAttachmentNames.value.clear();\n    checkedAll.value = false;\n  };\n\n  const selectedAttachments = computed(() => {\n    return (\n      data.value?.filter((attachment) =>\n        selectedAttachmentNames.value.has(attachment.metadata.name)\n      ) || []\n    );\n  });\n\n  return {\n    attachments: data,\n    isLoading,\n    isFetching,\n    selectedAttachment,\n    selectedAttachments,\n    selectedAttachmentNames,\n    checkedAll,\n    total,\n    handleFetchAttachments: refetch,\n    handleSelectPrevious,\n    handleSelectNext,\n    handleDeleteInBatch,\n    handleCheckAll,\n    handleSelect,\n    isChecked,\n    handleReset,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/attachments/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconFolder } from \"@halo-dev/components\";\nimport { definePlugin, utils } from \"@halo-dev/ui-shared\";\nimport { defineAsyncComponent, markRaw } from \"vue\";\n\ndeclare module \"vue\" {\n  interface GlobalComponents {\n    AttachmentSelectorModal:\n      | (typeof import(\"@console/modules/contents/attachments/components/AttachmentSelectorModal.vue\"))[\"default\"]\n      | (typeof import(\"@uc/modules/contents/attachments/components/AttachmentSelectorModal.vue\"))[\"default\"];\n  }\n}\n\nexport default definePlugin({\n  components: {\n    AttachmentSelectorModal: defineAsyncComponent({\n      loader: () => {\n        if (utils.permission.has([\"system:attachments:manage\"])) {\n          return import(\"@console/modules/contents/attachments/components/AttachmentSelectorModal.vue\");\n        }\n        return import(\"@uc/modules/contents/attachments/components/AttachmentSelectorModal.vue\");\n      },\n    }),\n  },\n  routes: [\n    {\n      path: \"/attachments\",\n      name: \"AttachmentsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.attachment.title\",\n        permissions: [\"system:attachments:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.attachments\",\n          group: \"content\",\n          icon: markRaw(IconFolder),\n          priority: 3,\n          mobile: true,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Attachments\",\n          component: () => import(\"./AttachmentList.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/CommentList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedComment } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconMessage,\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VCard,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { chunk } from \"es-toolkit\";\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport UserFilterDropdown from \"@/components/filter/UserFilterDropdown.vue\";\nimport CommentListItem from \"./components/CommentListItem.vue\";\nimport useCommentsFetch from \"./composables/use-comments-fetch\";\n\nconst { t } = useI18n();\n\nconst checkAll = ref(false);\nconst selectedCommentNames = ref<string[]>([]);\n\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\nconst selectedApprovedStatus = useRouteQuery<string | undefined>(\"approved\");\nconst selectedSort = useRouteQuery<string | undefined>(\"sort\");\nconst selectedUser = useRouteQuery<string | undefined>(\"user\");\n\nwatch(\n  () => [\n    selectedApprovedStatus.value,\n    selectedSort.value,\n    selectedUser.value,\n    keyword.value,\n  ],\n  () => {\n    page.value = 1;\n  }\n);\n\nconst hasFilters = computed(() => {\n  return (\n    selectedApprovedStatus.value !== undefined ||\n    selectedSort.value ||\n    selectedUser.value\n  );\n});\n\nfunction handleClearFilters() {\n  selectedApprovedStatus.value = undefined;\n  selectedSort.value = undefined;\n  selectedUser.value = undefined;\n}\n\nconst page = useRouteQuery<number>(\"page\", 1, {\n  transform: Number,\n});\nconst size = useRouteQuery<number>(\"size\", 20, {\n  transform: Number,\n});\n\nconst {\n  data: comments,\n  isLoading,\n  isFetching,\n  refetch,\n} = useCommentsFetch(\n  \"core:comments\",\n  page,\n  size,\n  selectedApprovedStatus,\n  selectedSort,\n  selectedUser,\n  keyword\n);\n\n// Selection\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n\n  if (checked) {\n    selectedCommentNames.value =\n      comments.value?.items.map((comment) => {\n        return comment.comment.metadata.name;\n      }) || [];\n  } else {\n    selectedCommentNames.value = [];\n  }\n};\n\nconst isSelection = (comment: ListedComment) => {\n  return selectedCommentNames.value.includes(comment.comment.metadata.name);\n};\n\nwatch(\n  () => selectedCommentNames.value,\n  (newValue) => {\n    checkAll.value = newValue.length === comments.value?.items.length;\n  }\n);\n\nconst handleDeleteInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.comment.operations.delete_comment_in_batch.title\"),\n    description: t(\n      \"core.comment.operations.delete_comment_in_batch.description\"\n    ),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        const commentChunk = chunk(selectedCommentNames.value, 5);\n\n        for (const item of commentChunk) {\n          await Promise.all(\n            item.map((name) => {\n              return coreApiClient.content.comment.deleteComment({\n                name,\n              });\n            })\n          );\n        }\n\n        selectedCommentNames.value = [];\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (e) {\n        console.error(\"Failed to delete comments\", e);\n      } finally {\n        refetch();\n      }\n    },\n  });\n};\n\nconst handleApproveInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.comment.operations.approve_comment_in_batch.title\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        const commentsToUpdate = comments.value?.items.filter((comment) => {\n          return (\n            selectedCommentNames.value.includes(\n              comment.comment.metadata.name\n            ) && !comment.comment.spec.approved\n          );\n        });\n\n        if (!commentsToUpdate?.length) {\n          return;\n        }\n\n        const commentChunk = chunk(commentsToUpdate, 5);\n\n        for (const item of commentChunk) {\n          await Promise.all(\n            item.map((comment) => {\n              return coreApiClient.content.comment.patchComment({\n                name: comment.comment.metadata.name,\n                jsonPatchInner: [\n                  {\n                    op: \"add\",\n                    path: \"/spec/approved\",\n                    value: true,\n                  },\n                  {\n                    op: \"add\",\n                    path: \"/spec/approvedTime\",\n                    value: new Date().toISOString(),\n                  },\n                ],\n              });\n            })\n          );\n        }\n\n        selectedCommentNames.value = [];\n\n        Toast.success(t(\"core.common.toast.operation_success\"));\n      } catch (e) {\n        console.error(\"Failed to approve comments in batch\", e);\n      } finally {\n        refetch();\n      }\n    },\n  });\n};\n</script>\n<template>\n  <VPageHeader :title=\"$t('core.comment.title')\">\n    <template #icon>\n      <IconMessage />\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <div\n              v-permission=\"['system:comments:manage']\"\n              class=\"hidden items-center sm:flex\"\n            >\n              <input\n                v-model=\"checkAll\"\n                type=\"checkbox\"\n                @change=\"handleCheckAllChange\"\n              />\n            </div>\n            <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n              <SearchInput\n                v-if=\"!selectedCommentNames.length\"\n                v-model=\"keyword\"\n              />\n              <VSpace v-else>\n                <VButton type=\"secondary\" @click=\"handleApproveInBatch\">\n                  {{\n                    $t(\n                      \"core.comment.operations.approve_comment_in_batch.button\"\n                    )\n                  }}\n                </VButton>\n                <VButton type=\"danger\" @click=\"handleDeleteInBatch\">\n                  {{ $t(\"core.common.buttons.delete\") }}\n                </VButton>\n              </VSpace>\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <FilterCleanButton\n                v-if=\"hasFilters\"\n                @click=\"handleClearFilters\"\n              />\n              <FilterDropdown\n                v-model=\"selectedApprovedStatus\"\n                :label=\"$t('core.common.filters.labels.status')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.all'),\n                  },\n                  {\n                    label: t('core.comment.filters.status.items.approved'),\n                    value: 'true',\n                  },\n                  {\n                    label: t(\n                      'core.comment.filters.status.items.pending_review'\n                    ),\n                    value: 'false',\n                  },\n                ]\"\n              />\n              <HasPermission :permissions=\"['system:users:view']\">\n                <UserFilterDropdown\n                  v-model=\"selectedUser\"\n                  :label=\"$t('core.comment.filters.owner.label')\"\n                />\n              </HasPermission>\n              <FilterDropdown\n                v-model=\"selectedSort\"\n                :label=\"$t('core.common.filters.labels.sort')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.default'),\n                  },\n                  {\n                    label: t(\n                      'core.comment.filters.sort.items.last_reply_time_desc'\n                    ),\n                    value: 'status.lastReplyTime,desc',\n                  },\n                  {\n                    label: t(\n                      'core.comment.filters.sort.items.last_reply_time_asc'\n                    ),\n                    value: 'status.lastReplyTime,asc',\n                  },\n                  {\n                    label: t(\n                      'core.comment.filters.sort.items.reply_count_desc'\n                    ),\n                    value: 'status.replyCount,desc',\n                  },\n                  {\n                    label: t('core.comment.filters.sort.items.reply_count_asc'),\n                    value: 'status.replyCount,asc',\n                  },\n                  {\n                    label: t(\n                      'core.comment.filters.sort.items.create_time_desc'\n                    ),\n                    value: 'metadata.creationTimestamp,desc',\n                  },\n                  {\n                    label: t('core.comment.filters.sort.items.create_time_asc'),\n                    value: 'metadata.creationTimestamp,asc',\n                  },\n                ]\"\n              />\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"refetch()\"\n                >\n                  <IconRefreshLine\n                    v-tooltip=\"$t('core.common.buttons.refresh')\"\n                    :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else-if=\"!comments?.items.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.comment.empty.message')\"\n          :title=\"$t('core.comment.empty.title')\"\n        >\n          <template #actions>\n            <VButton @click=\"refetch\">\n              {{ $t(\"core.common.buttons.refresh\") }}\n            </VButton>\n          </template>\n        </VEmpty>\n      </Transition>\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <CommentListItem\n            v-for=\"comment in comments.items\"\n            :key=\"comment.comment.metadata.name\"\n            :comment=\"comment\"\n            :is-selected=\"isSelection(comment)\"\n          >\n            <template #checkbox>\n              <input\n                v-model=\"selectedCommentNames\"\n                :value=\"comment?.comment?.metadata.name\"\n                name=\"comment-checkbox\"\n                type=\"checkbox\"\n              />\n            </template>\n          </CommentListItem>\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', {\n              total: comments?.total || 0,\n            })\n          \"\n          :total=\"comments?.total || 0\"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/CommentDetailModal.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  consoleApiClient,\n  coreApiClient,\n  type ListedComment,\n} from \"@halo-dev/api-client\";\nimport {\n  IconExternalLinkLine,\n  Toast,\n  VButton,\n  VDescription,\n  VDescriptionItem,\n  VModal,\n  VSpace,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useUserAgent } from \"@uc/modules/profile/tabs/composables/use-user-agent\";\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useContentProviderExtensionPoint } from \"../composables/use-content-provider-extension-point\";\nimport { useSubjectRef } from \"../composables/use-subject-ref\";\nimport CommentEditor from \"./CommentEditor.vue\";\nimport OwnerButton from \"./OwnerButton.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    comment: ListedComment;\n  }>(),\n  {}\n);\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (e: \"close\"): void;\n}>();\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n\nconst { os, browser } = useUserAgent(props.comment.comment.spec.userAgent);\n\nconst creationTime = computed(() => {\n  return (\n    props.comment?.comment.spec.creationTime ||\n    props.comment?.comment.metadata.creationTimestamp\n  );\n});\n\nconst editorContent = ref(\"\");\nconst editorCharacterCount = ref(0);\n\nfunction onCommentEditorUpdate(value: {\n  content: string;\n  characterCount: number;\n}) {\n  editorContent.value = value.content;\n  editorCharacterCount.value = value.characterCount;\n}\n\nasync function handleApprove() {\n  if (!editorCharacterCount.value) {\n    await coreApiClient.content.comment.patchComment({\n      name: props.comment.comment.metadata.name,\n      jsonPatchInner: [\n        {\n          op: \"add\",\n          path: \"/spec/approved\",\n          value: true,\n        },\n        {\n          op: \"add\",\n          path: \"/spec/approvedTime\",\n          value: new Date().toISOString(),\n        },\n      ],\n    });\n  } else {\n    await consoleApiClient.content.comment.createReply({\n      name: props.comment?.comment.metadata.name as string,\n      replyRequest: {\n        raw: editorContent.value,\n        content: editorContent.value,\n        allowNotification: true,\n        quoteReply: undefined,\n      },\n    });\n  }\n  modal.value?.close();\n  queryClient.invalidateQueries({ queryKey: [\"core:comments\"] });\n  Toast.success(t(\"core.common.toast.operation_success\"));\n}\n\nconst { subjectRefResult } = useSubjectRef(props.comment);\n\nconst websiteOfAnonymous = computed(() => {\n  return props.comment.comment.spec.owner.annotations?.[\"website\"];\n});\n\nconst { data: contentProvider } = useContentProviderExtensionPoint();\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :width=\"900\"\n    :title=\"$t('core.comment.comment_detail_modal.title')\"\n    mount-to-body\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <VDescription>\n        <VDescriptionItem :label=\"$t('core.comment.detail_modal.fields.owner')\">\n          <div class=\"flex items-center gap-3\">\n            <OwnerButton\n              v-if=\"comment.comment.spec.owner.kind === 'User'\"\n              :owner=\"comment.comment.spec.owner\"\n              @click=\"\n                $router.push({\n                  name: 'UserDetail',\n                  params: { name: comment.comment.spec.owner.name },\n                })\n              \"\n            />\n            <ul v-else class=\"space-y-1\">\n              <li>{{ comment.comment.spec.owner.displayName }}</li>\n              <li>{{ comment.comment.spec.owner.name }}</li>\n              <li v-if=\"websiteOfAnonymous\">\n                <a :href=\"websiteOfAnonymous\" target=\"_blank\">{{\n                  websiteOfAnonymous\n                }}</a>\n              </li>\n            </ul>\n          </div>\n        </VDescriptionItem>\n        <VDescriptionItem label=\"IP\">\n          {{ comment.comment.spec.ipAddress }}\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.detail_modal.fields.user_agent')\"\n        >\n          <span v-tooltip=\"comment.comment.spec.userAgent\">\n            {{ os }} {{ browser }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.detail_modal.fields.creation_time')\"\n        >\n          <span v-tooltip=\"utils.date.format(creationTime)\">\n            {{ utils.date.timeAgo(creationTime) }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.detail_modal.fields.commented_on')\"\n        >\n          <div class=\"flex items-center gap-2\">\n            <RouterLink\n              v-tooltip=\"`${subjectRefResult.label}`\"\n              :to=\"subjectRefResult.route || $route\"\n              class=\"inline-block text-sm hover:text-gray-600\"\n            >\n              {{ subjectRefResult.title }}\n            </RouterLink>\n            <a\n              v-if=\"subjectRefResult.externalUrl\"\n              :href=\"subjectRefResult.externalUrl\"\n              target=\"_blank\"\n              class=\"text-gray-600 hover:text-gray-900\"\n            >\n              <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n            </a>\n          </div>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.comment_detail_modal.fields.content')\"\n        >\n          <div v-if=\"comment.comment.spec.hidden\" class=\"mb-2\">\n            <VTag>\n              {{ $t(\"core.comment.list.fields.private\") }}\n            </VTag>\n          </div>\n          <component\n            :is=\"contentProvider?.component\"\n            :content=\"comment.comment.spec.content\"\n          />\n        </VDescriptionItem>\n        <HasPermission :permissions=\"['system:comments:manage']\">\n          <VDescriptionItem\n            v-if=\"!comment.comment.spec.approved\"\n            :label=\"$t('core.comment.detail_modal.fields.new_reply')\"\n          >\n            <CommentEditor @update=\"onCommentEditorUpdate\" />\n          </VDescriptionItem>\n        </HasPermission>\n      </VDescription>\n    </div>\n    <template #footer>\n      <VSpace>\n        <HasPermission :permissions=\"['system:comments:manage']\">\n          <VButton\n            v-if=\"!comment.comment.spec.approved\"\n            type=\"secondary\"\n            @click=\"handleApprove\"\n          >\n            {{\n              editorCharacterCount > 0\n                ? $t(\"core.comment.operations.reply_and_approve.button\")\n                : $t(\"core.comment.operations.approve.button\")\n            }}\n          </VButton>\n        </HasPermission>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n\n<style scoped>\n:deep(.description-item__content) {\n  @apply lg:col-span-5;\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/CommentEditor.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VLoading } from \"@halo-dev/components\";\nimport type { CommentEditorProvider } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { markRaw } from \"vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport DefaultCommentEditor from \"./DefaultCommentEditor.vue\";\n\nwithDefaults(\n  defineProps<{\n    autoFocus?: boolean;\n  }>(),\n  {\n    autoFocus: false,\n  }\n);\n\nconst defaultProvider: CommentEditorProvider = {\n  component: markRaw(DefaultCommentEditor),\n};\n\nconst { pluginModules } = usePluginModuleStore();\n\nconst emit = defineEmits<{\n  (event: \"update\", value: { content: string; characterCount: number }): void;\n}>();\n\nconst { data: provider, isLoading } = useQuery({\n  queryKey: [\"core:comment:provider\"],\n  queryFn: async () => {\n    const result: CommentEditorProvider[] = [];\n    for (const pluginModule of pluginModules) {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"comment:editor:replace\"];\n\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const item = await callbackFunction();\n\n      result.push(item);\n    }\n\n    if (result.length) {\n      return result[0];\n    }\n\n    return defaultProvider;\n  },\n});\n\nfunction onUpdate(value: { content: string; characterCount: number }) {\n  emit(\"update\", value);\n}\n</script>\n<template>\n  <VLoading v-if=\"isLoading\" />\n  <component\n    :is=\"provider?.component\"\n    v-else\n    :auto-focus=\"autoFocus\"\n    @update=\"onUpdate\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/CommentListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport type {\n  ListedComment,\n  ListedReply,\n  ReplyV1alpha1ConsoleApiListRepliesRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  paginate,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconExternalLinkLine,\n  Toast,\n  VButton,\n  VDropdownDivider,\n  VDropdownItem,\n  VEmpty,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n  VSpace,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils, type OperationItem } from \"@halo-dev/ui-shared\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, markRaw, provide, ref, toRefs, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport EntityDropdownItems from \"@/components/entity/EntityDropdownItems.vue\";\nimport { useCommentLastReadTimeMutate } from \"../composables/use-comment-last-readtime-mutate\";\nimport { useContentProviderExtensionPoint } from \"../composables/use-content-provider-extension-point\";\nimport { useSubjectRef } from \"../composables/use-subject-ref\";\nimport CommentDetailModal from \"./CommentDetailModal.vue\";\nimport OwnerButton from \"./OwnerButton.vue\";\nimport ReplyCreationModal from \"./ReplyCreationModal.vue\";\nimport ReplyListItem from \"./ReplyListItem.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    comment: ListedComment;\n    isSelected?: boolean;\n  }>(),\n  {\n    isSelected: false,\n  }\n);\n\nconst { comment } = toRefs(props);\n\nconst hoveredReply = ref<ListedReply>();\nconst showReplies = ref(false);\nconst replyModal = ref(false);\nconst detailModalVisible = ref(false);\n\nprovide<Ref<ListedReply | undefined>>(\"hoveredReply\", hoveredReply);\n\nconst creationTime = computed(() => {\n  return (\n    props.comment?.comment.spec.creationTime ||\n    props.comment?.comment.metadata.creationTimestamp\n  );\n});\n\nconst handleDelete = async () => {\n  Dialog.warning({\n    title: t(\"core.comment.operations.delete_comment.title\"),\n    description: t(\"core.comment.operations.delete_comment.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.content.comment.deleteComment({\n          name: props.comment?.comment?.metadata.name as string,\n        });\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (error) {\n        console.error(\"Failed to delete comment\", error);\n      } finally {\n        queryClient.invalidateQueries({ queryKey: [\"core:comments\"] });\n      }\n    },\n  });\n};\n\nconst handleApproveReplyInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.comment.operations.approve_applies_in_batch.title\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        const repliesToUpdate = replies.value?.filter((reply) => {\n          return !reply.reply.spec.approved;\n        });\n\n        if (!repliesToUpdate?.length) {\n          return;\n        }\n\n        await Promise.all(\n          repliesToUpdate?.map((reply) => {\n            return coreApiClient.content.reply.patchReply({\n              name: reply.reply.metadata.name,\n              jsonPatchInner: [\n                {\n                  op: \"add\",\n                  path: \"/spec/approved\",\n                  value: true,\n                },\n                {\n                  op: \"add\",\n                  path: \"/spec/approvedTime\",\n                  value: new Date().toISOString(),\n                },\n              ],\n            });\n          })\n        );\n\n        Toast.success(t(\"core.common.toast.operation_success\"));\n      } catch (e) {\n        console.error(\"Failed to approve comment replies in batch\", e);\n      } finally {\n        await refetch();\n      }\n    },\n  });\n};\n\nasync function handleCancelApprove() {\n  await coreApiClient.content.comment.patchComment({\n    name: props.comment.comment.metadata.name,\n    jsonPatchInner: [\n      {\n        op: \"add\",\n        path: \"/spec/approved\",\n        value: false,\n      },\n      {\n        op: \"add\",\n        path: \"/spec/approvedTime\",\n        value: \"\",\n      },\n    ],\n  });\n\n  Toast.success(t(\"core.common.toast.operation_success\"));\n\n  queryClient.invalidateQueries({ queryKey: [\"core:comments\"] });\n}\n\nconst {\n  data: replies,\n  isLoading,\n  refetch,\n} = useQuery<ListedReply[]>({\n  queryKey: [\n    \"core:comment-replies\",\n    props.comment.comment.metadata.name,\n    showReplies,\n  ],\n  queryFn: async () => {\n    return await paginate<\n      ReplyV1alpha1ConsoleApiListRepliesRequest,\n      ListedReply\n    >((params) => consoleApiClient.content.reply.listReplies(params), {\n      commentName: props.comment.comment.metadata.name,\n      size: 1000,\n    });\n  },\n  refetchInterval(data) {\n    const hasDeletingReplies = data?.some(\n      (reply) => !!reply.reply.metadata.deletionTimestamp\n    );\n    return hasDeletingReplies ? 1000 : false;\n  },\n  enabled: computed(() => showReplies.value),\n});\n\nconst { mutate: updateCommentLastReadTimeMutate } =\n  useCommentLastReadTimeMutate(props.comment);\n\nconst handleToggleShowReplies = async () => {\n  showReplies.value = !showReplies.value;\n\n  if (props.comment.comment.status?.unreadReplyCount && showReplies.value) {\n    updateCommentLastReadTimeMutate();\n  }\n};\n\nconst onReplyCreationModalClose = () => {\n  if (showReplies.value) {\n    refetch();\n  }\n\n  queryClient.invalidateQueries({ queryKey: [\"core:comments\"] });\n  updateCommentLastReadTimeMutate();\n  replyModal.value = false;\n  detailModalVisible.value = false;\n};\n\nconst { subjectRefResult } = useSubjectRef(props.comment);\n\nconst { data: operationItems } = useOperationItemExtensionPoint<ListedComment>(\n  \"comment:list-item:operation:create\",\n  comment,\n  computed((): OperationItem<ListedComment>[] => [\n    {\n      priority: 0,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.comment.operations.review.button\"),\n      action: () => {\n        detailModalVisible.value = true;\n      },\n      hidden: props.comment?.comment.spec.approved,\n    },\n    {\n      priority: 10,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.text.detail\"),\n      hidden: !props.comment?.comment.spec.approved,\n      action: () => {\n        detailModalVisible.value = true;\n      },\n    },\n    {\n      priority: 20,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.comment.operations.approve_applies_in_batch.button\"),\n      action: handleApproveReplyInBatch,\n    },\n    {\n      priority: 30,\n      component: markRaw(VDropdownDivider),\n    },\n    {\n      priority: 40,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.comment.operations.cancel_approve.button\"),\n      hidden: !props.comment?.comment.spec.approved,\n      action: handleCancelApprove,\n    },\n    {\n      priority: 50,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.delete\"),\n      action: handleDelete,\n    },\n  ])\n);\n\nconst { data: contentProvider } = useContentProviderExtensionPoint();\n</script>\n\n<template>\n  <ReplyCreationModal\n    v-if=\"replyModal\"\n    :comment=\"comment\"\n    @close=\"onReplyCreationModalClose\"\n  />\n  <CommentDetailModal\n    v-if=\"detailModalVisible\"\n    :comment=\"comment\"\n    @close=\"onReplyCreationModalClose\"\n  />\n  <VEntity :is-selected=\"isSelected\">\n    <template\n      v-if=\"utils.permission.has(['system:comments:manage']) && $slots.checkbox\"\n      #checkbox\n    >\n      <slot name=\"checkbox\" />\n    </template>\n    <template #start>\n      <VEntityField width=\"100%\" max-width=\"100%\">\n        <template #description>\n          <div class=\"flex flex-col gap-2\">\n            <div class=\"mb-1 flex items-center gap-2\">\n              <OwnerButton\n                :owner=\"comment?.owner\"\n                @click=\"detailModalVisible = true\"\n              />\n              <VTag v-if=\"comment.comment.spec.hidden\">\n                {{ $t(\"core.comment.list.fields.private\") }}\n              </VTag>\n              <span class=\"whitespace-nowrap text-sm text-gray-900\">\n                {{ $t(\"core.comment.text.commented_on\") }}\n              </span>\n              <RouterLink\n                v-tooltip=\"`${subjectRefResult.label}`\"\n                :to=\"subjectRefResult.route || $route\"\n                class=\"inline-block max-w-md truncate text-sm font-medium text-gray-900 hover:text-gray-600\"\n              >\n                {{ subjectRefResult.title }}\n              </RouterLink>\n              <a\n                v-if=\"subjectRefResult.externalUrl\"\n                :href=\"subjectRefResult.externalUrl\"\n                target=\"_blank\"\n                class=\"invisible text-gray-600 hover:text-gray-900 group-hover:visible\"\n              >\n                <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n              </a>\n            </div>\n            <component\n              :is=\"contentProvider?.component\"\n              :content=\"comment?.comment?.spec.content\"\n            />\n            <div class=\"flex items-center gap-3 text-xs\">\n              <span\n                class=\"cursor-pointer select-none text-gray-700 hover:text-gray-900\"\n                @click=\"handleToggleShowReplies\"\n              >\n                {{\n                  $t(\"core.comment.list.fields.reply_count\", {\n                    count: comment?.comment?.status?.replyCount || 0,\n                  })\n                }}\n              </span>\n              <VStatusDot\n                v-show=\"(comment?.comment?.status?.unreadReplyCount || 0) > 0\"\n                v-tooltip=\"$t('core.comment.list.fields.has_new_replies')\"\n                state=\"success\"\n                animate\n              />\n              <HasPermission :permissions=\"['system:comments:manage']\">\n                <span\n                  class=\"cursor-pointer select-none text-gray-700 hover:text-gray-900\"\n                  @click=\"replyModal = true\"\n                >\n                  {{ $t(\"core.comment.operations.reply.button\") }}\n                </span>\n              </HasPermission>\n            </div>\n          </div>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"!comment?.comment.spec.approved\">\n        <template #description>\n          <VStatusDot\n            state=\"warning\"\n            animate\n            :text=\"$t('core.comment.list.fields.pending_review')\"\n          />\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"comment?.comment?.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField\n        v-tooltip=\"utils.date.format(creationTime)\"\n        :description=\"utils.date.timeAgo(creationTime)\"\n      />\n    </template>\n    <template\n      v-if=\"utils.permission.has(['system:comments:manage'])\"\n      #dropdownItems\n    >\n      <EntityDropdownItems\n        :dropdown-items=\"operationItems || []\"\n        :item=\"comment\"\n      />\n    </template>\n\n    <template v-if=\"showReplies\" #footer>\n      <div class=\"pl-8\">\n        <VLoading v-if=\"isLoading\" />\n        <Transition v-else-if=\"!replies?.length\" appear name=\"fade\">\n          <VEmpty\n            :message=\"$t('core.comment.reply_empty.message')\"\n            :title=\"$t('core.comment.reply_empty.title')\"\n          >\n            <template #actions>\n              <VSpace>\n                <VButton @click=\"refetch()\">\n                  {{ $t(\"core.common.buttons.refresh\") }}\n                </VButton>\n                <HasPermission :permissions=\"['system:comments:manage']\">\n                  <VButton type=\"secondary\" @click=\"replyModal = true\">\n                    <template #icon>\n                      <IconAddCircle />\n                    </template>\n                    {{ $t(\"core.comment.reply_empty.new\") }}\n                  </VButton>\n                </HasPermission>\n              </VSpace>\n            </template>\n          </VEmpty>\n        </Transition>\n        <Transition v-else appear name=\"fade\">\n          <VEntityContainer>\n            <ReplyListItem\n              v-for=\"reply in replies\"\n              :key=\"reply.reply.metadata.name\"\n              :reply=\"reply\"\n              :comment=\"comment\"\n              :replies=\"replies\"\n            ></ReplyListItem>\n          </VEntityContainer>\n        </Transition>\n      </div>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/DefaultCommentContent.vue",
    "content": "<script lang=\"ts\" setup>\nimport sanitizeHtml from \"sanitize-html\";\ndefineProps<{\n  content: string;\n}>();\n</script>\n<template>\n  <div\n    class=\"comment-content markdown-body whitespace-pre-wrap rounded-lg !bg-transparent !text-sm !text-gray-900\"\n    v-html=\"\n      sanitizeHtml(content, {\n        allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),\n        allowedAttributes: {\n          ...sanitizeHtml.defaults.allowedAttributes,\n          code: ['class'],\n        },\n      })\n    \"\n  ></div>\n</template>\n\n<style scoped lang=\"scss\">\n.comment-content :deep(ul) {\n  list-style: disc !important;\n}\n\n.comment-content :deep(ol) {\n  list-style: decimal !important;\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/DefaultCommentEditor.vue",
    "content": "<script setup lang=\"ts\">\nimport i18n from \"@emoji-mart/data/i18n/zh.json\";\nimport { IconMotionLine, VDropdown } from \"@halo-dev/components\";\nimport { onMounted, ref, watch } from \"vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    autoFocus?: boolean;\n  }>(),\n  {\n    autoFocus: true,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update\", value: { content: string; characterCount: number }): void;\n}>();\n\nconst emojiPickerRef = ref<HTMLElement | null>(null);\n\nconst handleCreateEmojiPicker = async () => {\n  if (emojiPickerRef.value?.childElementCount) {\n    return;\n  }\n\n  const { Picker } = await import(\"emoji-mart\");\n  const data = await import(\"@emoji-mart/data\");\n\n  const emojiPicker = new Picker({\n    data: Object.assign({}, data),\n    theme: \"light\",\n    autoFocus: true,\n    i18n: i18n,\n    onEmojiSelect: onEmojiSelect,\n  });\n\n  emojiPickerRef.value?.appendChild(emojiPicker as unknown as Node);\n};\n\nconst raw = ref(\"\");\n\nconst onEmojiSelect = (emoji: { native: string }) => {\n  raw.value += emoji.native;\n  setFocus(\"content-input\");\n};\n\nonMounted(() => {\n  if (props.autoFocus) {\n    setFocus(\"content-input\");\n  }\n});\n\nwatch(\n  () => raw.value,\n  (value) => {\n    emit(\"update\", {\n      content: value,\n      characterCount: value.length,\n    });\n  }\n);\n</script>\n<template>\n  <FormKit\n    id=\"content-input\"\n    v-model=\"raw\"\n    type=\"textarea\"\n    name=\"raw\"\n    :validation-label=\"$t('core.comment.reply_modal.fields.content.label')\"\n    :rows=\"6\"\n    value=\"\"\n  ></FormKit>\n  <div class=\"flex w-full justify-end sm:max-w-lg\">\n    <VDropdown\n      popper-class=\"[&_.v-popper\\_\\_inner]:!p-0\"\n      @show=\"handleCreateEmojiPicker\"\n    >\n      <IconMotionLine\n        class=\"h-5 w-5 cursor-pointer text-gray-500 transition-all hover:text-gray-900\"\n      />\n      <template #popper>\n        <div ref=\"emojiPickerRef\"></div>\n      </template>\n    </VDropdown>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/OwnerButton.vue",
    "content": "<script setup lang=\"ts\">\nimport type { OwnerInfo } from \"@halo-dev/api-client\";\nimport { VAvatar } from \"@halo-dev/components\";\n\ndefineProps<{\n  owner: OwnerInfo;\n}>();\n</script>\n<template>\n  <div\n    class=\"-m-1 inline-flex cursor-pointer items-center gap-1.5 rounded-lg p-1 transition-colors hover:bg-gray-100\"\n  >\n    <VAvatar circle :src=\"owner?.avatar\" :alt=\"owner?.displayName\" size=\"xs\" />\n    <span class=\"text-sm font-medium text-gray-900\">\n      {{ owner?.displayName }}\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/ReplyCreationModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedComment, ListedReply } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport CommentEditor from \"./CommentEditor.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    comment?: ListedComment;\n    reply?: ListedReply;\n  }>(),\n  {\n    visible: false,\n    comment: undefined,\n    reply: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst isSubmitting = ref(false);\nconst characterCount = ref(0);\nconst content = ref(\"\");\n\nconst handleSubmit = async () => {\n  try {\n    isSubmitting.value = true;\n\n    await consoleApiClient.content.comment.createReply({\n      name: props.comment?.comment.metadata.name as string,\n      replyRequest: {\n        raw: content.value,\n        content: content.value,\n        allowNotification: true,\n        quoteReply: props.reply?.reply.metadata.name,\n      },\n    });\n\n    modal.value?.close();\n\n    Toast.success(\n      t(\"core.comment.reply_modal.operations.submit.toast_success\")\n    );\n  } catch (error) {\n    console.error(\"Failed to create comment reply\", error);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n\nfunction onUpdate(value: { content: string; characterCount: number }) {\n  content.value = value.content;\n  characterCount.value = value.characterCount;\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"$t('core.comment.reply_modal.title')\"\n    :width=\"600\"\n    :mount-to-body=\"true\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <CommentEditor :auto-focus=\"true\" @update=\"onUpdate\" />\n    </div>\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          :disabled=\"characterCount === 0\"\n          @submit=\"handleSubmit\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/ReplyDetailModal.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  consoleApiClient,\n  coreApiClient,\n  type ListedComment,\n  type ListedReply,\n} from \"@halo-dev/api-client\";\nimport {\n  IconExternalLinkLine,\n  IconReplyLine,\n  Toast,\n  VButton,\n  VDescription,\n  VDescriptionItem,\n  VModal,\n  VSpace,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useUserAgent } from \"@uc/modules/profile/tabs/composables/use-user-agent\";\nimport sanitizeHtml from \"sanitize-html\";\nimport { computed, ref, useTemplateRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useContentProviderExtensionPoint } from \"../composables/use-content-provider-extension-point\";\nimport { useSubjectRef } from \"../composables/use-subject-ref\";\nimport CommentEditor from \"./CommentEditor.vue\";\nimport OwnerButton from \"./OwnerButton.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    comment: ListedComment;\n    reply: ListedReply;\n    quoteReply?: ListedReply;\n  }>(),\n  {\n    quoteReply: undefined,\n  }\n);\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (e: \"close\"): void;\n}>();\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n\nconst { os, browser } = useUserAgent(props.reply.reply.spec.userAgent);\n\nconst creationTime = computed(() => {\n  return (\n    props.reply?.reply.spec.creationTime ||\n    props.reply?.reply.metadata.creationTimestamp\n  );\n});\n\nconst editorContent = ref(\"\");\nconst editorCharacterCount = ref(0);\n\nfunction onCommentEditorUpdate(value: {\n  content: string;\n  characterCount: number;\n}) {\n  editorContent.value = value.content;\n  editorCharacterCount.value = value.characterCount;\n}\n\nasync function handleApprove() {\n  if (!editorCharacterCount.value) {\n    await coreApiClient.content.reply.patchReply({\n      name: props.reply.reply.metadata.name,\n      jsonPatchInner: [\n        {\n          op: \"add\",\n          path: \"/spec/approved\",\n          value: true,\n        },\n        {\n          op: \"add\",\n          path: \"/spec/approvedTime\",\n          value: new Date().toISOString(),\n        },\n      ],\n    });\n  } else {\n    await consoleApiClient.content.comment.createReply({\n      name: props.comment?.comment.metadata.name as string,\n      replyRequest: {\n        raw: editorContent.value,\n        content: editorContent.value,\n        allowNotification: true,\n        quoteReply: props.reply.reply.metadata.name,\n      },\n    });\n  }\n  modal.value?.close();\n  queryClient.invalidateQueries({\n    queryKey: [\"core:comment-replies\", props.comment.comment.metadata.name],\n  });\n  Toast.success(t(\"core.common.toast.operation_success\"));\n}\n\nconst { subjectRefResult } = useSubjectRef(props.comment);\n\nconst websiteOfAnonymous = computed(() => {\n  return props.reply.reply.spec.owner.annotations?.[\"website\"];\n});\n\nconst { data: contentProvider } = useContentProviderExtensionPoint();\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :width=\"900\"\n    :title=\"$t('core.comment.reply_detail_modal.title')\"\n    mount-to-body\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <VDescription>\n        <VDescriptionItem :label=\"$t('core.comment.detail_modal.fields.owner')\">\n          <div class=\"flex items-center gap-3\">\n            <OwnerButton\n              v-if=\"reply.owner.kind === 'User'\"\n              :owner=\"reply.owner\"\n              @click=\"\n                $router.push({\n                  name: 'UserDetail',\n                  params: { name: reply.owner.name },\n                })\n              \"\n            />\n            <ul v-else class=\"space-y-1\">\n              <li>{{ reply.owner.displayName }}</li>\n              <li>{{ reply.owner.name }}</li>\n              <li v-if=\"websiteOfAnonymous\">\n                <a :href=\"websiteOfAnonymous\" target=\"_blank\">{{\n                  websiteOfAnonymous\n                }}</a>\n              </li>\n            </ul>\n          </div>\n        </VDescriptionItem>\n        <VDescriptionItem label=\"IP\">\n          {{ reply.reply.spec.ipAddress }}\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.detail_modal.fields.user_agent')\"\n        >\n          <span v-tooltip=\"reply.reply.spec.userAgent\">\n            {{ os }} {{ browser }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.detail_modal.fields.creation_time')\"\n        >\n          <span v-tooltip=\"utils.date.format(creationTime)\">\n            {{ utils.date.timeAgo(creationTime) }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.detail_modal.fields.commented_on')\"\n        >\n          <div class=\"flex items-center gap-2\">\n            <RouterLink\n              v-tooltip=\"`${subjectRefResult.label}`\"\n              :to=\"subjectRefResult.route || $route\"\n              class=\"inline-block text-sm hover:text-gray-600\"\n            >\n              {{ subjectRefResult.title }}\n            </RouterLink>\n            <a\n              v-if=\"subjectRefResult.externalUrl\"\n              :href=\"subjectRefResult.externalUrl\"\n              target=\"_blank\"\n              class=\"text-gray-600 hover:text-gray-900\"\n            >\n              <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n            </a>\n          </div>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.reply_detail_modal.fields.original_comment')\"\n        >\n          <div class=\"mb-2 flex items-center gap-2\">\n            <OwnerButton :owner=\"comment.owner\" />\n            <VTag v-if=\"comment.comment.spec.hidden\">\n              {{ $t(\"core.comment.list.fields.private\") }}\n            </VTag>\n          </div>\n          <component\n            :is=\"contentProvider?.component\"\n            :content=\"comment.comment.spec.content\"\n          />\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.comment.reply_detail_modal.fields.content')\"\n        >\n          <div v-if=\"reply.reply.spec.hidden\" class=\"mb-2\">\n            <VTag>\n              {{ $t(\"core.comment.list.fields.private\") }}\n            </VTag>\n          </div>\n          <div>\n            <span\n              v-if=\"quoteReply\"\n              v-tooltip=\"{\n                content: sanitizeHtml(\n                  `${quoteReply.owner.displayName}: ${quoteReply.reply.spec.content}`\n                ),\n                html: true,\n              }\"\n              class=\"mr-1 inline-flex cursor-pointer flex-row items-center gap-1 rounded bg-slate-100 px-1 py-0.5 text-xs font-medium text-slate-700 hover:bg-slate-200 hover:text-slate-800 hover:underline\"\n            >\n              <IconReplyLine />\n              <span>{{ quoteReply.owner.displayName }}</span> </span\n            ><br v-if=\"quoteReply\" /><component\n              :is=\"contentProvider?.component\"\n              :content=\"reply?.reply.spec.content\"\n            />\n          </div>\n        </VDescriptionItem>\n        <VDescriptionItem\n          v-if=\"!reply.reply.spec.approved\"\n          :label=\"$t('core.comment.detail_modal.fields.new_reply')\"\n        >\n          <CommentEditor @update=\"onCommentEditorUpdate\" />\n        </VDescriptionItem>\n      </VDescription>\n    </div>\n    <template #footer>\n      <VSpace>\n        <VButton\n          v-if=\"!reply.reply.spec.approved\"\n          type=\"secondary\"\n          @click=\"handleApprove\"\n        >\n          {{\n            editorCharacterCount > 0\n              ? $t(\"core.comment.operations.reply_and_approve.button\")\n              : $t(\"core.comment.operations.approve.button\")\n          }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n\n<style scoped>\n:deep(.description-item__content) {\n  @apply lg:col-span-5;\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/ReplyListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport type { ListedComment, ListedReply } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconReplyLine,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils, type OperationItem } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, inject, markRaw, ref, toRefs, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport EntityDropdownItems from \"@/components/entity/EntityDropdownItems.vue\";\nimport { useCommentLastReadTimeMutate } from \"../composables/use-comment-last-readtime-mutate\";\nimport { useContentProviderExtensionPoint } from \"../composables/use-content-provider-extension-point\";\nimport OwnerButton from \"./OwnerButton.vue\";\nimport ReplyCreationModal from \"./ReplyCreationModal.vue\";\nimport ReplyDetailModal from \"./ReplyDetailModal.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    comment: ListedComment;\n    reply: ListedReply;\n    replies?: ListedReply[];\n  }>(),\n  {\n    replies: undefined,\n  }\n);\n\nconst { reply } = toRefs(props);\n\nconst creationTime = computed(() => {\n  return (\n    props.reply?.reply.spec.creationTime ||\n    props.reply?.reply.metadata.creationTimestamp\n  );\n});\n\nconst quoteReply = computed(() => {\n  const { quoteReply: replyName } = props.reply.reply.spec;\n\n  if (!replyName) {\n    return undefined;\n  }\n\n  return props.replies?.find(\n    (reply) => reply.reply.metadata.name === replyName\n  );\n});\n\nconst handleDelete = async () => {\n  Dialog.warning({\n    title: t(\"core.comment.operations.delete_reply.title\"),\n    description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.content.reply.deleteReply({\n          name: props.reply?.reply.metadata.name as string,\n        });\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (error) {\n        console.error(\"Failed to delete comment reply\", error);\n      } finally {\n        queryClient.invalidateQueries({\n          queryKey: [\n            \"core:comment-replies\",\n            props.comment.comment.metadata.name,\n          ],\n        });\n      }\n    },\n  });\n};\n\nasync function handleCancelApprove() {\n  await coreApiClient.content.reply.patchReply({\n    name: props.reply?.reply.metadata.name as string,\n    jsonPatchInner: [\n      {\n        op: \"add\",\n        path: \"/spec/approved\",\n        value: false,\n      },\n      {\n        op: \"add\",\n        path: \"/spec/approvedTime\",\n        value: \"\",\n      },\n    ],\n  });\n  Toast.success(t(\"core.common.toast.operation_success\"));\n  queryClient.invalidateQueries({\n    queryKey: [\"core:comment-replies\", props.comment.comment.metadata.name],\n  });\n}\n\n// Show hovered reply\nconst hoveredReply = inject<Ref<ListedReply | undefined>>(\"hoveredReply\");\n\nconst handleShowQuoteReply = (show: boolean) => {\n  if (hoveredReply) {\n    hoveredReply.value = show ? quoteReply.value : undefined;\n  }\n};\n\nconst isHoveredReply = computed(() => {\n  return (\n    hoveredReply?.value?.reply.metadata.name === props.reply.reply.metadata.name\n  );\n});\n\n// Create reply\nconst replyModal = ref(false);\nconst detailModalVisible = ref(false);\n\nconst { mutate: updateCommentLastReadTimeMutate } =\n  useCommentLastReadTimeMutate(props.comment);\n\nfunction onReplyCreationModalClose() {\n  queryClient.invalidateQueries({\n    queryKey: [\"core:comment-replies\", props.comment.comment.metadata.name],\n  });\n  queryClient.invalidateQueries({ queryKey: [\"core:comments\"] });\n  updateCommentLastReadTimeMutate();\n  replyModal.value = false;\n  detailModalVisible.value = false;\n}\n\nconst { data: operationItems } = useOperationItemExtensionPoint<ListedReply>(\n  \"reply:list-item:operation:create\",\n  reply,\n  computed((): OperationItem<ListedReply>[] => [\n    {\n      priority: 0,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.comment.operations.review.button\"),\n      permissions: [\"system:comments:manage\"],\n      action: () => {\n        detailModalVisible.value = true;\n      },\n      hidden: props.reply?.reply.spec.approved,\n    },\n    {\n      priority: 10,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.text.detail\"),\n      hidden: !props.reply?.reply.spec.approved,\n      action: () => {\n        detailModalVisible.value = true;\n      },\n    },\n    {\n      priority: 20,\n      component: markRaw(VDropdownDivider),\n    },\n    {\n      priority: 30,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.comment.operations.cancel_approve.button\"),\n      hidden: !props.reply?.reply.spec.approved,\n      action: handleCancelApprove,\n    },\n    {\n      priority: 40,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.delete\"),\n      permissions: [\"system:comments:manage\"],\n      action: handleDelete,\n    },\n  ])\n);\n\nconst { data: contentProvider } = useContentProviderExtensionPoint();\n</script>\n\n<template>\n  <ReplyCreationModal\n    v-if=\"replyModal\"\n    :comment=\"comment\"\n    :reply=\"reply\"\n    @close=\"onReplyCreationModalClose\"\n  />\n  <ReplyDetailModal\n    v-if=\"detailModalVisible\"\n    :comment=\"comment\"\n    :reply=\"reply\"\n    :quote-reply=\"quoteReply\"\n    @close=\"onReplyCreationModalClose\"\n  />\n  <VEntity\n    v-bind=\"$attrs\"\n    class=\"border-l border-dashed !border-gray-200\"\n    :class=\"{ 'animate-breath': isHoveredReply }\"\n  >\n    <template #start>\n      <VEntityField width=\"100%\" max-width=\"100%\">\n        <template #description>\n          <div class=\"flex flex-col gap-2\">\n            <div class=\"mb-1 flex items-center gap-2\">\n              <OwnerButton\n                :owner=\"reply?.owner\"\n                @click=\"detailModalVisible = true\"\n              />\n              <VTag v-if=\"comment.comment.spec.hidden\">\n                {{ $t(\"core.comment.list.fields.private\") }}\n              </VTag>\n              <span class=\"whitespace-nowrap text-sm text-gray-900\">\n                {{ $t(\"core.comment.text.replied_below\") }}\n              </span>\n            </div>\n            <div>\n              <a\n                v-if=\"quoteReply\"\n                class=\"mr-1 inline-flex flex-row items-center gap-1 rounded bg-slate-100 px-1 py-0.5 text-xs font-medium text-slate-700 hover:bg-slate-200 hover:text-slate-800 hover:underline\"\n                href=\"javascript:void(0)\"\n                @mouseenter=\"handleShowQuoteReply(true)\"\n                @mouseleave=\"handleShowQuoteReply(false)\"\n              >\n                <IconReplyLine />\n                <span>{{ quoteReply.owner.displayName }}</span> </a\n              ><br v-if=\"quoteReply\" /><component\n                :is=\"contentProvider?.component\"\n                :content=\"reply?.reply.spec.content\"\n              />\n            </div>\n            <HasPermission :permissions=\"['system:comments:manage']\">\n              <div class=\"flex items-center gap-3 text-xs\">\n                <span\n                  class=\"cursor-pointer select-none text-gray-700 hover:text-gray-900\"\n                  @click=\"replyModal = true\"\n                >\n                  {{ $t(\"core.comment.operations.reply.button\") }}\n                </span>\n              </div>\n            </HasPermission>\n          </div>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"!reply?.reply.spec.approved\">\n        <template #description>\n          <VStatusDot\n            state=\"warning\"\n            animate\n            :text=\"$t('core.comment.list.fields.pending_review')\"\n          />\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"reply?.reply.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField\n        v-tooltip=\"utils.date.format(creationTime)\"\n        :description=\"utils.date.timeAgo(creationTime)\"\n      />\n    </template>\n    <template\n      v-if=\"utils.permission.has(['system:comments:manage'])\"\n      #dropdownItems\n    >\n      <EntityDropdownItems\n        :dropdown-items=\"operationItems || []\"\n        :item=\"reply\"\n      />\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/SubjectQueryCommentList.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconRefreshLine,\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { computed, ref, toRefs, watch } from \"vue\";\nimport UserFilterDropdown from \"@/components/filter/UserFilterDropdown.vue\";\nimport useCommentsFetch from \"../composables/use-comments-fetch\";\nimport CommentListItem from \"./CommentListItem.vue\";\n\nconst props = defineProps<{\n  subjectRefKey: string;\n}>();\n\nconst { subjectRefKey } = toRefs(props);\n\nconst selectedApprovedStatus = ref();\nconst selectedSort = ref();\nconst selectedUser = ref();\nconst page = ref(1);\nconst size = ref(20);\nconst keyword = ref(\"\");\n\nconst {\n  data: comments,\n  isLoading,\n  isFetching,\n  refetch,\n} = useCommentsFetch(\n  \"core:comments:with-subject\",\n  page,\n  size,\n  selectedApprovedStatus,\n  selectedSort,\n  selectedUser,\n  keyword,\n  subjectRefKey\n);\n\nwatch(\n  () => [\n    selectedApprovedStatus.value,\n    selectedSort.value,\n    selectedUser.value,\n    keyword.value,\n  ],\n  () => {\n    page.value = 1;\n  }\n);\n\nconst hasFilters = computed(() => {\n  return (\n    selectedApprovedStatus.value !== undefined ||\n    selectedSort.value ||\n    selectedUser.value\n  );\n});\n\nfunction handleClearFilters() {\n  selectedApprovedStatus.value = undefined;\n  selectedSort.value = undefined;\n  selectedUser.value = undefined;\n}\n</script>\n<template>\n  <div>\n    <div class=\"mb-4 flex flex-wrap items-center justify-between gap-4\">\n      <SearchInput v-model=\"keyword\" />\n      <VSpace spacing=\"lg\" class=\"flex-wrap\">\n        <FilterCleanButton v-if=\"hasFilters\" @click=\"handleClearFilters\" />\n        <FilterDropdown\n          v-model=\"selectedApprovedStatus\"\n          :label=\"$t('core.common.filters.labels.status')\"\n          :items=\"[\n            {\n              label: $t('core.common.filters.item_labels.all'),\n            },\n            {\n              label: $t('core.comment.filters.status.items.approved'),\n              value: 'true',\n            },\n            {\n              label: $t('core.comment.filters.status.items.pending_review'),\n              value: 'false',\n            },\n          ]\"\n        />\n        <HasPermission :permissions=\"['system:users:view']\">\n          <UserFilterDropdown\n            v-model=\"selectedUser\"\n            :label=\"$t('core.comment.filters.owner.label')\"\n          />\n        </HasPermission>\n        <FilterDropdown\n          v-model=\"selectedSort\"\n          :label=\"$t('core.common.filters.labels.sort')\"\n          :items=\"[\n            {\n              label: $t('core.common.filters.item_labels.default'),\n            },\n            {\n              label: $t('core.comment.filters.sort.items.last_reply_time_desc'),\n              value: 'status.lastReplyTime,desc',\n            },\n            {\n              label: $t('core.comment.filters.sort.items.last_reply_time_asc'),\n              value: 'status.lastReplyTime,asc',\n            },\n            {\n              label: $t('core.comment.filters.sort.items.reply_count_desc'),\n              value: 'status.replyCount,desc',\n            },\n            {\n              label: $t('core.comment.filters.sort.items.reply_count_asc'),\n              value: 'status.replyCount,asc',\n            },\n            {\n              label: $t('core.comment.filters.sort.items.create_time_desc'),\n              value: 'metadata.creationTimestamp,desc',\n            },\n            {\n              label: $t('core.comment.filters.sort.items.create_time_asc'),\n              value: 'metadata.creationTimestamp,asc',\n            },\n          ]\"\n        />\n        <div class=\"flex flex-row gap-2\">\n          <div\n            class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n            @click=\"refetch()\"\n          >\n            <IconRefreshLine\n              v-tooltip=\"$t('core.common.buttons.refresh')\"\n              :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n              class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n            />\n          </div>\n        </div>\n      </VSpace>\n    </div>\n    <VLoading v-if=\"isLoading\" />\n    <Transition v-else-if=\"!comments?.items.length\" appear name=\"fade\">\n      <VEmpty\n        :message=\"$t('core.comment.empty.message')\"\n        :title=\"$t('core.comment.empty.title')\"\n      >\n        <template #actions>\n          <VButton @click=\"refetch\">\n            {{ $t(\"core.common.buttons.refresh\") }}\n          </VButton>\n        </template>\n      </VEmpty>\n    </Transition>\n    <Transition v-else appear name=\"fade\">\n      <div class=\"overflow-hidden rounded-base border\">\n        <VEntityContainer>\n          <CommentListItem\n            v-for=\"comment in comments.items\"\n            :key=\"comment.comment.metadata.name\"\n            :comment=\"comment\"\n          >\n          </CommentListItem>\n        </VEntityContainer>\n      </div>\n    </Transition>\n    <div class=\"mt-4\">\n      <VPagination\n        v-model:page=\"page\"\n        v-model:size=\"size\"\n        :page-label=\"$t('core.components.pagination.page_label')\"\n        :size-label=\"$t('core.components.pagination.size_label')\"\n        :total-label=\"\n          $t('core.components.pagination.total_label', {\n            total: comments?.total || 0,\n          })\n        \"\n        :total=\"comments?.total || 0\"\n        :size-options=\"[20, 30, 50, 100]\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/components/SubjectQueryCommentListModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton, VModal } from \"@halo-dev/components\";\nimport { useTemplateRef } from \"vue\";\n\nconst props = defineProps<{\n  subjectRefKey: string;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :centered=\"false\"\n    :width=\"1400\"\n    :title=\"$t('core.comment.title')\"\n    :layer-closable=\"true\"\n    mount-to-body\n    @close=\"emit('close')\"\n  >\n    <SubjectQueryCommentList :subject-ref-key=\"props.subjectRefKey\" />\n    <template #footer>\n      <VButton @click=\"modal?.close()\">\n        {{ $t(\"core.common.buttons.close\") }}\n      </VButton>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/composables/use-comment-last-readtime-mutate.ts",
    "content": "import { coreApiClient, type ListedComment } from \"@halo-dev/api-client\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\n\nexport const useCommentLastReadTimeMutate = (comment: ListedComment) => {\n  const queryClient = useQueryClient();\n  return useMutation({\n    mutationKey: [\"update-comment-last-read-time\"],\n    mutationFn: async () => {\n      const { data } = await coreApiClient.content.comment.patchComment(\n        {\n          name: comment.comment.metadata.name,\n          jsonPatchInner: [\n            {\n              op: \"add\",\n              path: \"/spec/lastReadTime\",\n              value: new Date().toISOString(),\n            },\n          ],\n        },\n        {\n          mute: true,\n        }\n      );\n      if (data.status?.unreadReplyCount) {\n        throw new Error(\"Unread reply count is not 0, retry\");\n      }\n      return data;\n    },\n    retry: 5,\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"core:comments\"] });\n    },\n  });\n};\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/composables/use-comments-fetch.ts",
    "content": "import { consoleApiClient, type ListedCommentList } from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport type { Ref } from \"vue\";\n\nexport default function useCommentsFetch(\n  queryKey: string,\n  page: Ref<number>,\n  size: Ref<number>,\n  approved: Ref<string | undefined>,\n  sort: Ref<string | undefined>,\n  user: Ref<string | undefined>,\n  keyword: Ref<string | undefined>,\n  subjectRefKey?: Ref<string | undefined>\n) {\n  return useQuery<ListedCommentList>({\n    queryKey: [\n      queryKey,\n      page,\n      size,\n      approved,\n      sort,\n      user,\n      keyword,\n      subjectRefKey,\n    ],\n    queryFn: async () => {\n      const fieldSelectorMap: Record<string, string | boolean | undefined> = {\n        \"spec.approved\": approved.value,\n        \"spec.subjectRef\": subjectRefKey?.value,\n      };\n\n      const fieldSelector = Object.entries(fieldSelectorMap)\n        .map(([key, value]) => {\n          if (value !== undefined) {\n            return `${key}=${value}`;\n          }\n        })\n        .filter(Boolean) as string[];\n\n      const defaultSort = [\n        \"metadata.creationTimestamp,desc\",\n        \"status.lastReplyTime,desc\",\n      ];\n\n      const { data } = await consoleApiClient.content.comment.listComments({\n        fieldSelector,\n        page: page.value,\n        size: size.value,\n        sort: sort.value ? [sort.value] : defaultSort,\n        keyword: keyword.value,\n        ownerName: user.value,\n        ownerKind: user.value ? \"User\" : undefined,\n      });\n\n      return data;\n    },\n    refetchInterval(data) {\n      const hasDeletingData = data?.items.some(\n        (comment) => !!comment.comment.metadata.deletionTimestamp\n      );\n      return hasDeletingData ? 1000 : false;\n    },\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/composables/use-content-provider-extension-point.ts",
    "content": "import type { CommentContentProvider } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { markRaw } from \"vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport DefaultCommentContent from \"../components/DefaultCommentContent.vue\";\n\nexport function useContentProviderExtensionPoint() {\n  const defaultProvider: CommentContentProvider = {\n    component: markRaw(DefaultCommentContent),\n  };\n\n  const { pluginModules } = usePluginModuleStore();\n\n  return useQuery({\n    queryKey: [\"core:comment:list-item:content:provider\"],\n    queryFn: async () => {\n      const result: CommentContentProvider[] = [];\n      for (const pluginModule of pluginModules) {\n        const callbackFunction =\n          pluginModule?.extensionPoints?.[\"comment:list-item:content:replace\"];\n\n        if (typeof callbackFunction !== \"function\") {\n          continue;\n        }\n\n        const item = await callbackFunction();\n\n        result.push(item);\n      }\n\n      if (result.length) {\n        return result[0];\n      }\n\n      return defaultProvider;\n    },\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/composables/use-subject-ref.ts",
    "content": "import type {\n  Extension,\n  ListedComment,\n  Post,\n  SinglePage,\n} from \"@halo-dev/api-client\";\nimport type {\n  CommentSubjectRefProvider,\n  CommentSubjectRefResult,\n} from \"@halo-dev/ui-shared\";\nimport { computed, onMounted, shallowRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nexport function useSubjectRef(comment: ListedComment) {\n  const { t } = useI18n();\n\n  const SubjectRefProviders = shallowRef<CommentSubjectRefProvider[]>([\n    {\n      kind: \"Post\",\n      group: \"content.halo.run\",\n      resolve: (subject: Extension): CommentSubjectRefResult => {\n        const post = subject as Post;\n        return {\n          label: t(\"core.comment.subject_refs.post\"),\n          title: post.spec.title,\n          externalUrl: post.status?.permalink,\n          route: {\n            name: \"PostEditor\",\n            query: {\n              name: post.metadata.name,\n            },\n          },\n        };\n      },\n    },\n    {\n      kind: \"SinglePage\",\n      group: \"content.halo.run\",\n      resolve: (subject: Extension): CommentSubjectRefResult => {\n        const singlePage = subject as SinglePage;\n        return {\n          label: t(\"core.comment.subject_refs.page\"),\n          title: singlePage.spec.title,\n          externalUrl: singlePage.status?.permalink,\n          route: {\n            name: \"SinglePageEditor\",\n            query: {\n              name: singlePage.metadata.name,\n            },\n          },\n        };\n      },\n    },\n  ]);\n\n  const { pluginModules } = usePluginModuleStore();\n\n  onMounted(() => {\n    for (const pluginModule of pluginModules) {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"comment:subject-ref:create\"];\n\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const providers = callbackFunction();\n\n      SubjectRefProviders.value = [...SubjectRefProviders.value, ...providers];\n    }\n  });\n\n  const subjectRefResult = computed(() => {\n    const { subject } = comment;\n    if (!subject) {\n      return {\n        label: t(\"core.comment.subject_refs.unknown\"),\n        title: t(\"core.comment.subject_refs.unknown\"),\n      };\n    }\n    const subjectRef = SubjectRefProviders.value.find(\n      (provider) =>\n        provider.kind === subject.kind &&\n        subject.apiVersion.startsWith(provider.group)\n    );\n    if (!subjectRef) {\n      return {\n        label: t(\"core.comment.subject_refs.unknown\"),\n        title: t(\"core.comment.subject_refs.unknown\"),\n      };\n    }\n    return subjectRef.resolve(subject);\n  });\n\n  return {\n    subjectRefResult,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/comments/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconMessage, VLoading } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { defineAsyncComponent, markRaw } from \"vue\";\nimport SubjectQueryCommentListModal from \"./components/SubjectQueryCommentListModal.vue\";\n\ndeclare module \"vue\" {\n  interface GlobalComponents {\n    SubjectQueryCommentList: (typeof import(\"./components/SubjectQueryCommentList.vue\"))[\"default\"];\n    SubjectQueryCommentListModal: (typeof import(\"./components/SubjectQueryCommentListModal.vue\"))[\"default\"];\n  }\n}\n\nexport default definePlugin({\n  components: {\n    SubjectQueryCommentList: defineAsyncComponent({\n      loader: () => import(\"./components/SubjectQueryCommentList.vue\"),\n      loadingComponent: VLoading,\n    }),\n    SubjectQueryCommentListModal,\n  },\n  routes: [\n    {\n      path: \"/comments\",\n      name: \"CommentsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.comment.title\",\n        searchable: true,\n        permissions: [\"system:comments:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.comments\",\n          group: \"content\",\n          icon: markRaw(IconMessage),\n          priority: 2,\n          mobile: true,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Comments\",\n          component: () => import(\"./CommentList.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/DeletedSinglePageList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedSinglePage, SinglePage } from \"@halo-dev/api-client\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  GetThumbnailByUriSizeEnum,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconDeleteBin,\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VCard,\n  VDropdownItem,\n  VEmpty,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport PostContributorList from \"@/components/user/PostContributorList.vue\";\n\nconst { t } = useI18n();\n\nconst selectedPageNames = ref<string[]>([]);\nconst checkedAll = ref(false);\nconst keyword = ref(\"\");\n\nconst page = ref(1);\nconst size = ref(20);\nconst total = ref(0);\n\nconst {\n  data: singlePages,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery<ListedSinglePage[]>({\n  queryKey: [\"deleted-singlePages\", page, size, keyword],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.content.singlePage.listSinglePages({\n      labelSelector: [`content.halo.run/deleted=true`],\n      page: page.value,\n      size: size.value,\n      keyword: keyword.value,\n    });\n\n    total.value = data.total;\n\n    return data.items;\n  },\n  refetchInterval(data) {\n    const deletedSinglePages = data?.filter(\n      (singlePage) =>\n        !!singlePage.page.metadata.deletionTimestamp ||\n        !singlePage.page.spec.deleted\n    );\n    return deletedSinglePages?.length ? 1000 : false;\n  },\n});\n\nconst checkSelection = (singlePage: SinglePage) => {\n  return selectedPageNames.value.includes(singlePage.metadata.name);\n};\n\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n\n  if (checked) {\n    selectedPageNames.value =\n      singlePages.value?.map((singlePage) => {\n        return singlePage.page.metadata.name;\n      }) || [];\n  } else {\n    selectedPageNames.value = [];\n  }\n};\n\nconst handleDeletePermanently = async (singlePage: SinglePage) => {\n  Dialog.warning({\n    title: t(\"core.deleted_page.operations.delete.title\"),\n    description: t(\"core.deleted_page.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await coreApiClient.content.singlePage.deleteSinglePage({\n        name: singlePage.metadata.name,\n      });\n      await refetch();\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst handleDeletePermanentlyInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.deleted_page.operations.delete_in_batch.title\"),\n    description: t(\"core.deleted_page.operations.delete_in_batch.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await Promise.all(\n        selectedPageNames.value.map((name) => {\n          return coreApiClient.content.singlePage.deleteSinglePage({\n            name,\n          });\n        })\n      );\n      await refetch();\n      selectedPageNames.value = [];\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst handleRecovery = async (singlePage: SinglePage) => {\n  Dialog.warning({\n    title: t(\"core.deleted_page.operations.recovery.title\"),\n    description: t(\"core.deleted_page.operations.recovery.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await coreApiClient.content.singlePage.patchSinglePage({\n        name: singlePage.metadata.name,\n        jsonPatchInner: [\n          {\n            op: \"add\",\n            path: \"/spec/deleted\",\n            value: false,\n          },\n        ],\n      });\n\n      await refetch();\n\n      Toast.success(t(\"core.common.toast.recovery_success\"));\n    },\n  });\n};\n\nconst handleRecoveryInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.deleted_page.operations.recovery_in_batch.title\"),\n    description: t(\n      \"core.deleted_page.operations.recovery_in_batch.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await Promise.all(\n        selectedPageNames.value.map((name) => {\n          const singlePage = singlePages.value?.find(\n            (item) => item.page.metadata.name === name\n          )?.page;\n\n          if (!singlePage) {\n            return Promise.resolve();\n          }\n\n          return coreApiClient.content.singlePage.patchSinglePage({\n            name: singlePage.metadata.name,\n            jsonPatchInner: [\n              {\n                op: \"add\",\n                path: \"/spec/deleted\",\n                value: false,\n              },\n            ],\n          });\n        })\n      );\n      await refetch();\n      selectedPageNames.value = [];\n\n      Toast.success(t(\"core.common.toast.recovery_success\"));\n    },\n  });\n};\n\nwatch(selectedPageNames, (newValue) => {\n  checkedAll.value = newValue.length === singlePages.value?.length;\n});\n\nwatch(\n  () => keyword.value,\n  () => {\n    page.value = 1;\n  }\n);\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.deleted_page.title')\">\n    <template #icon>\n      <IconDeleteBin class=\"text-green-600\" />\n    </template>\n    <template #actions>\n      <VButton :route=\"{ name: 'SinglePages' }\" size=\"sm\">\n        {{ $t(\"core.common.buttons.back\") }}\n      </VButton>\n      <VButton\n        v-permission=\"['system:singlepages:manage']\"\n        :route=\"{ name: 'SinglePageEditor' }\"\n        type=\"secondary\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <div\n              v-permission=\"['system:singlepages:manage']\"\n              class=\"hidden items-center sm:flex\"\n            >\n              <input\n                v-model=\"checkedAll\"\n                type=\"checkbox\"\n                @change=\"handleCheckAllChange\"\n              />\n            </div>\n            <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n              <SearchInput v-if=\"!selectedPageNames.length\" v-model=\"keyword\" />\n              <VSpace v-else>\n                <VButton type=\"danger\" @click=\"handleDeletePermanentlyInBatch\">\n                  {{ $t(\"core.common.buttons.delete_permanently\") }}\n                </VButton>\n                <VButton type=\"default\" @click=\"handleRecoveryInBatch\">\n                  {{ $t(\"core.common.buttons.restore\") }}\n                </VButton>\n              </VSpace>\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"refetch()\"\n                >\n                  <IconRefreshLine\n                    :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else-if=\"!singlePages?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.deleted_page.empty.message')\"\n          :title=\"$t('core.deleted_page.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"refetch\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton\n                v-permission=\"['system:singlepages:view']\"\n                :route=\"{ name: 'SinglePages' }\"\n                type=\"secondary\"\n              >\n                {{ $t(\"core.common.buttons.back\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <VEntity\n            v-for=\"singlePage in singlePages\"\n            :key=\"singlePage.page.metadata.name\"\n            :is-selected=\"checkSelection(singlePage.page)\"\n          >\n            <template\n              v-if=\"utils.permission.has(['system:singlepages:manage'])\"\n              #checkbox\n            >\n              <input\n                v-model=\"selectedPageNames\"\n                :value=\"singlePage.page.metadata.name\"\n                type=\"checkbox\"\n              />\n            </template>\n            <template #start>\n              <VEntityField v-if=\"singlePage.page.spec.cover\">\n                <template #description>\n                  <div\n                    class=\"aspect-h-2 aspect-w-3 w-20 overflow-hidden rounded-md\"\n                  >\n                    <img\n                      class=\"h-full w-full object-cover\"\n                      :src=\"\n                        utils.attachment.getThumbnailUrl(\n                          singlePage.page.spec.cover,\n                          GetThumbnailByUriSizeEnum.S\n                        )\n                      \"\n                    />\n                  </div>\n                </template>\n              </VEntityField>\n              <VEntityField\n                :title=\"singlePage.page.spec.title\"\n                max-width=\"30rem\"\n              >\n                <template #description>\n                  <VSpace>\n                    <span class=\"text-xs text-gray-500\">\n                      {{\n                        $t(\"core.page.list.fields.visits\", {\n                          visits: singlePage.stats.visit || 0,\n                        })\n                      }}\n                    </span>\n                    <span class=\"text-xs text-gray-500\">\n                      {{\n                        $t(\"core.page.list.fields.comments\", {\n                          comments: singlePage.stats.totalComment || 0,\n                        })\n                      }}\n                    </span>\n                  </VSpace>\n                </template>\n              </VEntityField>\n            </template>\n            <template #end>\n              <VEntityField>\n                <template #description>\n                  <PostContributorList\n                    :owner=\"singlePage.owner\"\n                    :contributors=\"singlePage.contributors\"\n                  />\n                </template>\n              </VEntityField>\n              <VEntityField v-if=\"!singlePage?.page?.spec.deleted\">\n                <template #description>\n                  <VStatusDot\n                    v-tooltip=\"$t('core.common.tooltips.recovering')\"\n                    state=\"success\"\n                    animate\n                  />\n                </template>\n              </VEntityField>\n              <VEntityField v-if=\"singlePage?.page?.metadata.deletionTimestamp\">\n                <template #description>\n                  <VStatusDot\n                    v-tooltip=\"$t('core.common.status.deleting')\"\n                    state=\"warning\"\n                    animate\n                  />\n                </template>\n              </VEntityField>\n              <VEntityField\n                v-if=\"singlePage.page.spec.publishTime\"\n                v-tooltip=\"utils.date.format(singlePage.page.spec.publishTime)\"\n                :description=\"\n                  utils.date.timeAgo(singlePage.page.spec.publishTime)\n                \"\n              >\n              </VEntityField>\n            </template>\n            <template\n              v-if=\"utils.permission.has(['system:singlepages:manage'])\"\n              #dropdownItems\n            >\n              <VDropdownItem\n                type=\"danger\"\n                @click=\"handleDeletePermanently(singlePage.page)\"\n              >\n                {{ $t(\"core.common.buttons.delete_permanently\") }}\n              </VDropdownItem>\n              <VDropdownItem @click=\"handleRecovery(singlePage.page)\">\n                {{ $t(\"core.common.buttons.restore\") }}\n              </VDropdownItem>\n            </template>\n          </VEntity>\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', { total: total })\n          \"\n          :total=\"total\"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/SinglePageEditor.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useContentSnapshot } from \"@console/composables/use-content-snapshot\";\nimport { useSaveKeybinding } from \"@console/composables/use-save-keybinding\";\nimport type { SinglePage, SinglePageRequest } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconEye,\n  IconHistoryLine,\n  IconPages,\n  IconSave,\n  IconSendPlaneFill,\n  IconSettings,\n  Toast,\n  VButton,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport { utils, type EditorProvider } from \"@halo-dev/ui-shared\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { isEqual } from \"es-toolkit\";\nimport {\n  computed,\n  nextTick,\n  onMounted,\n  provide,\n  ref,\n  shallowRef,\n  toRef,\n  watch,\n  type ComputedRef,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRouter } from \"vue-router\";\nimport EditorProviderSelector from \"@/components/dropdown-selector/EditorProviderSelector.vue\";\nimport UrlPreviewModal from \"@/components/preview/UrlPreviewModal.vue\";\nimport { useAutoSaveContent } from \"@/composables/use-auto-save-content\";\nimport { useContentCache } from \"@/composables/use-content-cache\";\nimport { useEditorExtensionPoints } from \"@/composables/use-editor-extension-points\";\nimport { useSessionKeepAlive } from \"@/composables/use-session-keep-alive\";\nimport { contentAnnotations } from \"@/constants/annotations\";\nimport SinglePageSettingModal from \"./components/SinglePageSettingModal.vue\";\nimport { usePageUpdateMutate } from \"./composables/use-page-update-mutate\";\n\nconst router = useRouter();\nconst { t } = useI18n();\nconst { mutateAsync: singlePageUpdateMutate } = usePageUpdateMutate();\n\n// Editor providers\nconst { editorProviders, fetchEditorProviders } = useEditorExtensionPoints();\nconst currentEditorProvider = shallowRef<EditorProvider>();\nconst storedEditorProviderName = useLocalStorage(\"editor-provider-name\", \"\");\n\nconst handleChangeEditorProvider = async (provider: EditorProvider) => {\n  currentEditorProvider.value = provider;\n  storedEditorProviderName.value = provider.name;\n  formState.value.page.metadata.annotations = {\n    ...formState.value.page.metadata.annotations,\n    [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n  };\n  formState.value.content.rawType = provider.rawType;\n\n  if (isUpdateMode.value) {\n    const { data } = await singlePageUpdateMutate(formState.value.page);\n    formState.value.page = data;\n  }\n};\n\n// SinglePage form\nconst formState = ref<SinglePageRequest>({\n  page: {\n    spec: {\n      title: \"\",\n      slug: \"\",\n      template: \"\",\n      cover: \"\",\n      deleted: false,\n      publish: false,\n      publishTime: undefined,\n      pinned: false,\n      allowComment: true,\n      visible: \"PUBLIC\",\n      priority: 0,\n      excerpt: {\n        autoGenerate: true,\n        raw: \"\",\n      },\n      htmlMetas: [],\n    },\n    apiVersion: \"content.halo.run/v1alpha1\",\n    kind: \"SinglePage\",\n    metadata: {\n      name: utils.id.uuid(),\n      annotations: {},\n    },\n  },\n  content: {\n    raw: \"\",\n    content: \"\",\n    rawType: \"HTML\",\n  },\n});\nconst saving = ref(false);\nconst publishing = ref(false);\nconst settingModal = ref(false);\n\nconst needsUpdatePage = ref(false);\nwatch(\n  [\n    () => formState.value.page.spec.title,\n    () => formState.value.page.spec.cover,\n  ],\n  (value, oldValue) => {\n    needsUpdatePage.value = !isEqual(value, oldValue);\n  }\n);\n\nconst isUpdateMode = computed(() => {\n  return !!formState.value.page.metadata.creationTimestamp;\n});\n\n// provide some data to editor\nprovide<ComputedRef<string | undefined>>(\n  \"owner\",\n  computed(() => formState.value.page.spec.owner)\n);\nprovide<ComputedRef<string | undefined>>(\n  \"publishTime\",\n  computed(() => formState.value.page.spec.publishTime)\n);\nprovide<ComputedRef<string | undefined>>(\n  \"permalink\",\n  computed(() => formState.value.page.status?.permalink)\n);\n\nconst routeQueryName = useRouteQuery<string>(\"name\");\n\nconst handleSave = async (options?: { mute?: boolean }) => {\n  try {\n    if (!options?.mute) {\n      saving.value = true;\n    }\n\n    //Set default title and slug\n    if (!formState.value.page.spec.title) {\n      formState.value.page.spec.title = t(\"core.page_editor.untitled\");\n    }\n\n    if (!formState.value.page.spec.slug) {\n      formState.value.page.spec.slug = new Date().getTime().toString();\n    }\n\n    if (isUpdateMode.value) {\n      if (needsUpdatePage.value) {\n        formState.value.page = (\n          await singlePageUpdateMutate(formState.value.page)\n        ).data;\n      }\n\n      const { data } =\n        await consoleApiClient.content.singlePage.updateSinglePageContent({\n          name: formState.value.page.metadata.name,\n          content: formState.value.content,\n        });\n\n      formState.value.page = data;\n      needsUpdatePage.value = false;\n    } else {\n      // Clear new page content cache\n      handleClearCache();\n\n      const { data } =\n        await consoleApiClient.content.singlePage.draftSinglePage({\n          singlePageRequest: formState.value,\n        });\n      formState.value.page = data;\n      routeQueryName.value = data.metadata.name;\n    }\n\n    if (!options?.mute) {\n      Toast.success(t(\"core.common.toast.save_success\"));\n    }\n\n    handleClearCache(formState.value.page.metadata.name as string);\n    await handleFetchContent();\n    await handleFetchSnapshot();\n  } catch (error) {\n    console.error(\"Failed to save single page\", error);\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  } finally {\n    saving.value = false;\n  }\n};\n\nconst returnToView = useRouteQuery<string>(\"returnToView\");\n\nconst handlePublish = async () => {\n  try {\n    publishing.value = true;\n\n    if (isUpdateMode.value) {\n      const { name: singlePageName } = formState.value.page.metadata;\n      const { permalink } = formState.value.page.status || {};\n\n      if (needsUpdatePage.value) {\n        formState.value.page = (\n          await singlePageUpdateMutate(formState.value.page)\n        ).data;\n      }\n\n      await consoleApiClient.content.singlePage.updateSinglePageContent({\n        name: singlePageName,\n        content: formState.value.content,\n      });\n\n      await consoleApiClient.content.singlePage.publishSinglePage({\n        name: singlePageName,\n      });\n\n      if (returnToView.value && permalink) {\n        handleClearCache(routeQueryName.value);\n        window.location.href = permalink;\n        return;\n      }\n\n      if (router.options.history.state.back === null) {\n        router.push({ name: \"SinglePages\" });\n      } else {\n        router.back();\n      }\n    } else {\n      formState.value.page.spec.publish = true;\n      await consoleApiClient.content.singlePage.draftSinglePage({\n        singlePageRequest: formState.value,\n      });\n\n      // Clear new page content cache\n      handleClearCache();\n\n      router.push({ name: \"SinglePages\" });\n    }\n\n    Toast.success(t(\"core.common.toast.publish_success\"));\n    handleClearCache(routeQueryName.value);\n  } catch (error) {\n    console.error(\"Failed to publish single page\", error);\n    Toast.error(t(\"core.common.toast.publish_failed_and_retry\"));\n  } finally {\n    publishing.value = false;\n  }\n};\n\nconst handlePublishClick = () => {\n  if (isUpdateMode.value) {\n    handlePublish();\n  } else {\n    // Set editor title to page\n    settingModal.value = true;\n  }\n};\n\nconst handleFetchContent = async () => {\n  if (!formState.value.page.spec.headSnapshot) {\n    return;\n  }\n  const { data } =\n    await consoleApiClient.content.singlePage.fetchSinglePageHeadContent({\n      name: formState.value.page.metadata.name,\n    });\n\n  formState.value.content = Object.assign(formState.value.content, data);\n\n  // get editor provider\n  if (!currentEditorProvider.value) {\n    const preferredEditor = editorProviders.value.find(\n      (provider) =>\n        provider.name ===\n        formState.value.page.metadata.annotations?.[\n          contentAnnotations.PREFERRED_EDITOR\n        ]\n    );\n    const provider =\n      preferredEditor ||\n      editorProviders.value.find(\n        (provider) =>\n          provider.rawType.toLowerCase() === data.rawType?.toLowerCase()\n      );\n    if (provider) {\n      currentEditorProvider.value = provider;\n      formState.value.page.metadata.annotations = {\n        ...formState.value.page.metadata.annotations,\n        [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n      };\n\n      const { data } = await singlePageUpdateMutate(formState.value.page);\n\n      formState.value.page = data;\n    } else {\n      Dialog.warning({\n        title: t(\"core.common.dialog.titles.warning\"),\n        description: t(\"core.common.dialog.descriptions.editor_not_found\", {\n          raw_type: data.rawType,\n        }),\n        confirmText: t(\"core.common.buttons.confirm\"),\n        showCancel: false,\n        onConfirm: () => {\n          router.back();\n        },\n      });\n    }\n    await nextTick();\n  }\n};\n\n// SinglePage settings\nconst handleOpenSettingModal = async () => {\n  await handleSave({ mute: true });\n  settingModal.value = true;\n};\n\nconst onSettingSaved = (page: SinglePage) => {\n  // Set route query parameter\n  if (!isUpdateMode.value) {\n    routeQueryName.value = page.metadata.name;\n  }\n\n  formState.value.page = page;\n\n  if (!isUpdateMode.value) {\n    handleSave();\n  }\n};\n\nconst onSettingPublished = (singlePage: SinglePage) => {\n  formState.value.page = singlePage;\n  handlePublish();\n};\n\nonMounted(async () => {\n  await fetchEditorProviders();\n\n  if (routeQueryName.value) {\n    const { data: singlePage } =\n      await coreApiClient.content.singlePage.getSinglePage({\n        name: routeQueryName.value,\n      });\n    formState.value.page = singlePage;\n\n    // fetch single page content\n    await handleFetchContent();\n  } else {\n    // Set default editor\n    const provider =\n      editorProviders.value.find(\n        (provider) => provider.name === storedEditorProviderName.value\n      ) || editorProviders.value[0];\n    if (provider) {\n      currentEditorProvider.value = provider;\n      formState.value.content.rawType = provider.rawType;\n    }\n    formState.value.page.metadata.annotations = {\n      [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n    };\n  }\n\n  handleResetCache();\n});\n\nconst headSnapshot = computed(() => {\n  return formState.value.page.spec.headSnapshot;\n});\n\nconst { version, handleFetchSnapshot } = useContentSnapshot(headSnapshot);\n\n// SinglePage content cache\nconst {\n  currentCache,\n  handleSetContentCache,\n  handleResetCache,\n  handleClearCache,\n} = useContentCache(\n  \"singlePage-content-cache\",\n  routeQueryName,\n  toRef(formState.value.content, \"raw\"),\n  version\n);\n\nuseAutoSaveContent(currentCache, toRef(formState.value.content, \"raw\"), () => {\n  // Do not save when the setting modal is open\n  if (settingModal.value) {\n    return;\n  }\n  handleSave({ mute: true });\n});\n\n// SinglePage preview\nconst previewModal = ref(false);\nconst previewPending = ref(false);\n\nconst handlePreview = async () => {\n  previewPending.value = true;\n  await handleSave({ mute: true });\n  previewModal.value = true;\n  previewPending.value = false;\n};\n\nuseSaveKeybinding(handleSave);\n\n// Keep session alive\nuseSessionKeepAlive();\n\n// Upload image\nasync function handleUploadImage(file: File, options?: AxiosRequestConfig) {\n  if (!utils.permission.has([\"system:attachments:manage\"])) {\n    return;\n  }\n\n  const { data } =\n    await consoleApiClient.storage.attachment.uploadAttachmentForConsole(\n      {\n        file,\n      },\n      options\n    );\n  return data;\n}\n</script>\n\n<template>\n  <SinglePageSettingModal\n    v-if=\"settingModal\"\n    :single-page=\"formState.page\"\n    :publish-support=\"!isUpdateMode\"\n    :only-emit=\"!isUpdateMode\"\n    @close=\"settingModal = false\"\n    @saved=\"onSettingSaved\"\n    @published=\"onSettingPublished\"\n  />\n\n  <UrlPreviewModal\n    v-if=\"previewModal\"\n    :title=\"formState.page.spec.title\"\n    :url=\"`/preview/singlepages/${formState.page.metadata.name}`\"\n    @close=\"previewModal = false\"\n  />\n\n  <VPageHeader :title=\"$t('core.page.title')\">\n    <template #icon>\n      <IconPages />\n    </template>\n    <template #actions>\n      <EditorProviderSelector\n        v-if=\"editorProviders.length > 1\"\n        :provider=\"currentEditorProvider\"\n        :allow-forced-select=\"!isUpdateMode\"\n        @select=\"handleChangeEditorProvider\"\n      />\n      <VButton\n        v-if=\"isUpdateMode\"\n        size=\"sm\"\n        type=\"default\"\n        @click=\"\n          $router.push({\n            name: 'SinglePageSnapshots',\n            query: { name: routeQueryName },\n          })\n        \"\n      >\n        <template #icon>\n          <IconHistoryLine />\n        </template>\n        {{ $t(\"core.page_editor.actions.snapshots\") }}\n      </VButton>\n      <VButton\n        size=\"sm\"\n        type=\"default\"\n        :loading=\"previewPending\"\n        @click=\"handlePreview\"\n      >\n        <template #icon>\n          <IconEye />\n        </template>\n        {{ $t(\"core.common.buttons.preview\") }}\n      </VButton>\n      <VButton :loading=\"saving\" size=\"sm\" type=\"default\" @click=\"handleSave\">\n        <template #icon>\n          <IconSave />\n        </template>\n        {{ $t(\"core.common.buttons.save\") }}\n      </VButton>\n      <VButton\n        v-if=\"isUpdateMode\"\n        size=\"sm\"\n        type=\"default\"\n        @click=\"handleOpenSettingModal\"\n      >\n        <template #icon>\n          <IconSettings />\n        </template>\n        {{ $t(\"core.common.buttons.setting\") }}\n      </VButton>\n      <VButton\n        type=\"secondary\"\n        :loading=\"publishing\"\n        @click=\"handlePublishClick\"\n      >\n        <template #icon>\n          <IconSendPlaneFill />\n        </template>\n        {{ $t(\"core.common.buttons.publish\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n  <div class=\"editor border-t\" style=\"height: calc(100vh - 3.5rem)\">\n    <component\n      :is=\"currentEditorProvider.component\"\n      v-if=\"currentEditorProvider\"\n      v-model:raw=\"formState.content.raw\"\n      v-model:content=\"formState.content.content\"\n      v-model:title=\"formState.page.spec.title\"\n      v-model:cover=\"formState.page.spec.cover\"\n      :upload-image=\"handleUploadImage\"\n      class=\"h-full\"\n      @update=\"handleSetContentCache\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/SinglePageList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedSinglePage, SinglePage } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconArrowLeft,\n  IconArrowRight,\n  IconPages,\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VCard,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { Ref } from \"vue\";\nimport { computed, provide, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport UserFilterDropdown from \"@/components/filter/UserFilterDropdown.vue\";\nimport { singlePageLabels } from \"@/constants/labels\";\nimport SinglePageListItem from \"./components/SinglePageListItem.vue\";\nimport SinglePageSettingModal from \"./components/SinglePageSettingModal.vue\";\n\nconst { t } = useI18n();\n\nconst settingModal = ref(false);\nconst selectedSinglePage = ref<SinglePage>();\nconst selectedPageNames = ref<string[]>([]);\nconst checkedAll = ref(false);\n\nprovide<Ref<string[]>>(\"selectedPageNames\", selectedPageNames);\n\n// Filters\nconst selectedContributor = useRouteQuery<string | undefined>(\"contributor\");\nconst selectedVisible = useRouteQuery<\n  \"PUBLIC\" | \"INTERNAL\" | \"PRIVATE\" | undefined\n>(\"visible\");\nconst selectedPublishStatus = useRouteQuery<string | undefined>(\"status\");\nconst selectedSort = useRouteQuery<string | undefined>(\"sort\");\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\n\nwatch(\n  () => [\n    selectedContributor.value,\n    selectedVisible.value,\n    selectedPublishStatus.value,\n    selectedSort.value,\n    keyword.value,\n  ],\n  () => {\n    page.value = 1;\n  }\n);\n\nconst hasFilters = computed(() => {\n  return (\n    selectedContributor.value ||\n    selectedVisible.value ||\n    selectedPublishStatus.value !== undefined ||\n    selectedSort.value\n  );\n});\n\nfunction handleClearFilters() {\n  selectedContributor.value = undefined;\n  selectedVisible.value = undefined;\n  selectedPublishStatus.value = undefined;\n  selectedSort.value = undefined;\n}\n\nconst page = useRouteQuery<number>(\"page\", 1, {\n  transform: Number,\n});\nconst size = useRouteQuery<number>(\"size\", 20, {\n  transform: Number,\n});\nconst total = ref(0);\nconst hasNext = ref(false);\nconst hasPrevious = ref(false);\n\nconst {\n  data: singlePages,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery<ListedSinglePage[]>({\n  queryKey: [\n    \"singlePages\",\n    selectedContributor,\n    selectedPublishStatus,\n    page,\n    size,\n    selectedVisible,\n    selectedSort,\n    keyword,\n  ],\n  queryFn: async () => {\n    let contributors: string[] | undefined;\n    const labelSelector: string[] = [\"content.halo.run/deleted=false\"];\n\n    if (selectedContributor.value) {\n      contributors = [selectedContributor.value];\n    }\n\n    if (selectedPublishStatus.value !== undefined) {\n      labelSelector.push(\n        `${singlePageLabels.PUBLISHED}=${selectedPublishStatus.value}`\n      );\n    }\n\n    const { data } = await consoleApiClient.content.singlePage.listSinglePages({\n      labelSelector,\n      page: page.value,\n      size: size.value,\n      visible: selectedVisible.value,\n      sort: [selectedSort.value].filter(Boolean) as string[],\n      keyword: keyword.value,\n      contributor: contributors,\n    });\n\n    total.value = data.total;\n    hasNext.value = data.hasNext;\n    hasPrevious.value = data.hasPrevious;\n\n    return data.items;\n  },\n  refetchInterval(data) {\n    const hasAbnormalSinglePage = data?.some((singlePage) => {\n      const { spec, metadata } = singlePage.page;\n      return (\n        spec.deleted ||\n        metadata.labels?.[singlePageLabels.PUBLISHED] !== spec.publish + \"\"\n      );\n    });\n    return hasAbnormalSinglePage ? 1000 : false;\n  },\n});\n\nconst handleOpenSettingModal = async (singlePage: SinglePage) => {\n  const { data } = await coreApiClient.content.singlePage.getSinglePage({\n    name: singlePage.metadata.name,\n  });\n  selectedSinglePage.value = data;\n  settingModal.value = true;\n};\n\nconst onSettingModalClose = () => {\n  selectedSinglePage.value = undefined;\n  settingModal.value = false;\n  refetch();\n};\n\nconst handleSelectPrevious = async () => {\n  if (!singlePages.value) return;\n\n  const index = singlePages.value.findIndex(\n    (singlePage) =>\n      singlePage.page.metadata.name === selectedSinglePage.value?.metadata.name\n  );\n  if (index > 0) {\n    const { data } = await coreApiClient.content.singlePage.getSinglePage({\n      name: singlePages.value[index - 1].page.metadata.name,\n    });\n    selectedSinglePage.value = data;\n    return;\n  }\n  if (index === 0 && hasPrevious.value) {\n    page.value--;\n    await refetch();\n    selectedSinglePage.value =\n      singlePages.value[singlePages.value.length - 1].page;\n  }\n};\n\nconst handleSelectNext = async () => {\n  if (!singlePages.value) return;\n\n  const index = singlePages.value.findIndex(\n    (singlePage) =>\n      singlePage.page.metadata.name === selectedSinglePage.value?.metadata.name\n  );\n  if (index < singlePages.value.length - 1) {\n    const { data } = await coreApiClient.content.singlePage.getSinglePage({\n      name: singlePages.value[index + 1].page.metadata.name,\n    });\n    selectedSinglePage.value = data;\n    return;\n  }\n  if (index === singlePages.value.length - 1 && hasNext.value) {\n    page.value++;\n    await refetch();\n    selectedSinglePage.value = singlePages.value[0].page;\n  }\n};\n\nconst checkSelection = (singlePage: SinglePage) => {\n  return (\n    singlePage.metadata.name === selectedSinglePage.value?.metadata.name ||\n    selectedPageNames.value.includes(singlePage.metadata.name)\n  );\n};\n\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n\n  if (checked) {\n    selectedPageNames.value =\n      singlePages.value?.map((singlePage) => {\n        return singlePage.page.metadata.name;\n      }) || [];\n  } else {\n    selectedPageNames.value = [];\n  }\n};\n\nconst handleDeleteInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.page.operations.delete_in_batch.title\"),\n    description: t(\"core.page.operations.delete_in_batch.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await Promise.all(\n        selectedPageNames.value.map((name) => {\n          const page = singlePages.value?.find(\n            (item) => item.page.metadata.name === name\n          )?.page;\n\n          if (!page) {\n            return Promise.resolve();\n          }\n\n          return coreApiClient.content.singlePage.patchSinglePage({\n            name: page.metadata.name,\n            jsonPatchInner: [\n              {\n                op: \"add\",\n                path: \"/spec/deleted\",\n                value: true,\n              },\n            ],\n          });\n        })\n      );\n      await refetch();\n      selectedPageNames.value = [];\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nwatch(selectedPageNames, (newValue) => {\n  checkedAll.value = newValue.length === singlePages.value?.length;\n});\n</script>\n\n<template>\n  <SinglePageSettingModal\n    v-if=\"settingModal\"\n    :single-page=\"selectedSinglePage\"\n    @close=\"onSettingModalClose\"\n  >\n    <template #actions>\n      <span @click=\"handleSelectPrevious\">\n        <IconArrowLeft v-tooltip=\"$t('core.common.buttons.previous')\" />\n      </span>\n      <span @click=\"handleSelectNext\">\n        <IconArrowRight v-tooltip=\"$t('core.common.buttons.next')\" />\n      </span>\n    </template>\n  </SinglePageSettingModal>\n\n  <VPageHeader :title=\"$t('core.page.title')\">\n    <template #icon>\n      <IconPages />\n    </template>\n    <template #actions>\n      <VButton\n        v-permission=\"['system:singlepages:view']\"\n        :route=\"{ name: 'DeletedSinglePages' }\"\n        size=\"sm\"\n      >\n        {{ $t(\"core.page.actions.recycle_bin\") }}\n      </VButton>\n      <VButton\n        v-permission=\"['system:singlepages:manage']\"\n        :route=\"{ name: 'SinglePageEditor' }\"\n        type=\"secondary\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <div\n              v-permission=\"['system:singlepages:manage']\"\n              class=\"hidden items-center sm:flex\"\n            >\n              <input\n                v-model=\"checkedAll\"\n                type=\"checkbox\"\n                @change=\"handleCheckAllChange\"\n              />\n            </div>\n            <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n              <SearchInput v-if=\"!selectedPageNames.length\" v-model=\"keyword\" />\n              <VSpace v-else>\n                <VButton type=\"danger\" @click=\"handleDeleteInBatch\">\n                  {{ $t(\"core.common.buttons.delete\") }}\n                </VButton>\n              </VSpace>\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <FilterCleanButton\n                v-if=\"hasFilters\"\n                @click=\"handleClearFilters\"\n              />\n              <FilterDropdown\n                v-model=\"selectedPublishStatus\"\n                :label=\"$t('core.common.filters.labels.status')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.all'),\n                    value: undefined,\n                  },\n                  {\n                    label: t('core.page.filters.status.items.published'),\n                    value: 'true',\n                  },\n                  {\n                    label: t('core.page.filters.status.items.draft'),\n                    value: 'false',\n                  },\n                ]\"\n              />\n              <FilterDropdown\n                v-model=\"selectedVisible\"\n                :label=\"$t('core.page.filters.visible.label')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.all'),\n                    value: undefined,\n                  },\n                  {\n                    label: t('core.page.filters.visible.items.public'),\n                    value: 'PUBLIC',\n                  },\n                  {\n                    label: t('core.page.filters.visible.items.private'),\n                    value: 'PRIVATE',\n                  },\n                ]\"\n              />\n              <HasPermission :permissions=\"['system:users:view']\">\n                <UserFilterDropdown\n                  v-model=\"selectedContributor\"\n                  :label=\"$t('core.page.filters.author.label')\"\n                />\n              </HasPermission>\n              <FilterDropdown\n                v-model=\"selectedSort\"\n                :label=\"$t('core.common.filters.labels.sort')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.default'),\n                  },\n                  {\n                    label: t('core.page.filters.sort.items.publish_time_desc'),\n                    value: 'publishTime,desc',\n                  },\n                  {\n                    label: t('core.page.filters.sort.items.publish_time_asc'),\n                    value: 'publishTime,asc',\n                  },\n                  {\n                    label: t('core.page.filters.sort.items.create_time_desc'),\n                    value: 'creationTimestamp,desc',\n                  },\n                  {\n                    label: t('core.page.filters.sort.items.create_time_asc'),\n                    value: 'creationTimestamp,asc',\n                  },\n                ]\"\n              />\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"refetch()\"\n                >\n                  <IconRefreshLine\n                    v-tooltip=\"$t('core.common.buttons.refresh')\"\n                    :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else-if=\"!singlePages?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.page.empty.message')\"\n          :title=\"$t('core.page.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"refetch\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton\n                v-permission=\"['system:singlepages:manage']\"\n                :route=\"{ name: 'SinglePageEditor' }\"\n                type=\"secondary\"\n              >\n                <template #icon>\n                  <IconAddCircle />\n                </template>\n                {{ $t(\"core.common.buttons.new\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <SinglePageListItem\n            v-for=\"singlePage in singlePages\"\n            :key=\"singlePage.page.metadata.name\"\n            :single-page=\"singlePage\"\n            :is-selected=\"checkSelection(singlePage.page)\"\n            @open-setting-modal=\"handleOpenSettingModal\"\n          />\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', { total: total })\n          \"\n          :total=\"total\"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/SinglePageSnapshots.vue",
    "content": "<script lang=\"ts\" setup>\nimport BaseSnapshots from \"@console/components/snapshots/BaseSnapshots.vue\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport { VLoading } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed } from \"vue\";\nimport { useRoute } from \"vue-router\";\n\nconst route = useRoute();\n\nconst singlePageName = computed(() => route.query.name as string);\n\nconst { data: singlePage, isLoading } = useQuery({\n  queryKey: [\"core:singlePage-by-name\", singlePageName],\n  queryFn: async () => {\n    const { data } = await coreApiClient.content.singlePage.getSinglePage({\n      name: singlePageName.value,\n    });\n    return data;\n  },\n  enabled: computed(() => !!singlePageName.value),\n});\n\nasync function listApi() {\n  const { data } =\n    await consoleApiClient.content.singlePage.listSinglePageSnapshots({\n      name: singlePageName.value,\n    });\n  return data;\n}\n\nasync function deleteApi(snapshotName: string) {\n  await consoleApiClient.content.singlePage.deleteSinglePageContent({\n    name: singlePageName.value,\n    snapshotName: snapshotName,\n  });\n}\n\nasync function revertApi(snapshotName: string) {\n  await consoleApiClient.content.singlePage.revertToSpecifiedSnapshotForSinglePage(\n    {\n      name: singlePageName.value,\n      revertSnapshotForSingleParam: {\n        snapshotName: snapshotName,\n      },\n    }\n  );\n}\n\nasync function getApi(snapshotName: string) {\n  const { data } =\n    await consoleApiClient.content.singlePage.fetchSinglePageContent({\n      name: singlePageName.value,\n      snapshotName: snapshotName,\n    });\n  return data;\n}\n</script>\n<template>\n  <VLoading v-if=\"isLoading\" />\n  <BaseSnapshots\n    v-else\n    :name=\"singlePageName\"\n    cache-key=\"singlePage\"\n    :title=\"singlePage?.spec.title || ''\"\n    :release-snapshot=\"singlePage?.spec.releaseSnapshot\"\n    :base-snapshot=\"singlePage?.spec.baseSnapshot\"\n    :head-snapshot=\"singlePage?.spec.headSnapshot\"\n    :list-api=\"listApi\"\n    :delete-api=\"deleteApi\"\n    :revert-api=\"revertApi\"\n    :get-api=\"getApi\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/SinglePageListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useEntityFieldItemExtensionPoint } from \"@console/composables/use-entity-extension-points\";\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport type { ListedSinglePage, SinglePage } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n} from \"@halo-dev/components\";\nimport {\n  utils,\n  type EntityFieldItem,\n  type OperationItem,\n} from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport type { Ref } from \"vue\";\nimport { computed, inject, markRaw, ref, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRouter } from \"vue-router\";\nimport EntityFieldItems from \"@/components/entity-fields/EntityFieldItems.vue\";\nimport StatusDotField from \"@/components/entity-fields/StatusDotField.vue\";\nimport EntityDropdownItems from \"@/components/entity/EntityDropdownItems.vue\";\nimport ContributorsField from \"./entity-fields/ContributorsField.vue\";\nimport CoverField from \"./entity-fields/CoverField.vue\";\nimport PublishStatusField from \"./entity-fields/PublishStatusField.vue\";\nimport PublishTimeField from \"./entity-fields/PublishTimeField.vue\";\nimport TitleField from \"./entity-fields/TitleField.vue\";\nimport VisibleField from \"./entity-fields/VisibleField.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\nconst router = useRouter();\n\nconst props = withDefaults(\n  defineProps<{\n    singlePage: ListedSinglePage;\n    isSelected?: boolean;\n  }>(),\n  {\n    isSelected: false,\n  }\n);\n\nconst { singlePage } = toRefs(props);\n\nconst emit = defineEmits<{\n  (event: \"open-setting-modal\", singlePage: SinglePage): void;\n}>();\n\nconst selectedPageNames = inject<Ref<string[]>>(\"selectedPageNames\", ref([]));\n\nconst handleDelete = async () => {\n  Dialog.warning({\n    title: t(\"core.page.operations.delete.title\"),\n    description: t(\"core.page.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await coreApiClient.content.singlePage.patchSinglePage({\n        name: props.singlePage.page.metadata.name,\n        jsonPatchInner: [\n          {\n            op: \"add\",\n            path: \"/spec/deleted\",\n            value: true,\n          },\n        ],\n      });\n\n      await queryClient.invalidateQueries({ queryKey: [\"singlePages\"] });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst { data: fields } = useEntityFieldItemExtensionPoint<ListedSinglePage>(\n  \"single-page:list-item:field:create\",\n  singlePage,\n  computed((): EntityFieldItem[] => [\n    {\n      priority: 10,\n      position: \"start\",\n      component: markRaw(CoverField),\n      hidden: !props.singlePage.page.spec.cover,\n      props: {\n        singlePage: props.singlePage,\n      },\n    },\n    {\n      priority: 20,\n      position: \"start\",\n      component: markRaw(TitleField),\n      props: {\n        singlePage: props.singlePage,\n      },\n    },\n    {\n      priority: 10,\n      position: \"end\",\n      component: markRaw(ContributorsField),\n      props: {\n        singlePage: props.singlePage,\n      },\n    },\n    {\n      priority: 20,\n      position: \"end\",\n      component: markRaw(PublishStatusField),\n      props: {\n        singlePage: props.singlePage,\n      },\n    },\n    {\n      priority: 30,\n      position: \"end\",\n      component: markRaw(VisibleField),\n      permissions: [\"system:singlepages:manage\"],\n      props: {\n        singlePage: props.singlePage,\n      },\n    },\n    {\n      priority: 40,\n      position: \"end\",\n      component: markRaw(StatusDotField),\n      props: {\n        tooltip: t(\"core.common.status.deleting\"),\n        state: \"warning\",\n        animate: true,\n      },\n      hidden: !props.singlePage?.page?.spec.deleted,\n    },\n    {\n      priority: 50,\n      position: \"end\",\n      component: markRaw(PublishTimeField),\n      hidden: !props.singlePage.page.spec.publishTime,\n      props: {\n        singlePage: props.singlePage,\n      },\n    },\n  ])\n);\n\nconst { data: operationItems } =\n  useOperationItemExtensionPoint<ListedSinglePage>(\n    \"single-page:list-item:operation:create\",\n    singlePage,\n    computed((): OperationItem<ListedSinglePage>[] => [\n      {\n        priority: 0,\n        component: markRaw(VDropdownItem),\n        label: t(\"core.common.buttons.edit\"),\n        action: async () => {\n          router.push({\n            name: \"SinglePageEditor\",\n            query: { name: props.singlePage.page.metadata.name },\n          });\n        },\n      },\n      {\n        priority: 10,\n        component: markRaw(VDropdownItem),\n        label: t(\"core.common.buttons.setting\"),\n        action: () => {\n          emit(\"open-setting-modal\", props.singlePage.page);\n        },\n      },\n      {\n        priority: 20,\n        component: markRaw(VDropdownDivider),\n      },\n      {\n        priority: 30,\n        component: markRaw(VDropdownItem),\n        props: {\n          type: \"danger\",\n        },\n        label: t(\"core.common.buttons.delete\"),\n        action: handleDelete,\n      },\n    ])\n  );\n</script>\n\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template\n      v-if=\"utils.permission.has(['system:singlepages:manage'])\"\n      #checkbox\n    >\n      <input\n        v-model=\"selectedPageNames\"\n        :value=\"singlePage.page.metadata.name\"\n        type=\"checkbox\"\n      />\n    </template>\n    <template #start>\n      <EntityFieldItems :fields=\"fields?.start || []\" />\n    </template>\n    <template #end>\n      <EntityFieldItems :fields=\"fields?.end || []\" />\n    </template>\n    <template\n      v-if=\"utils.permission.has(['system:singlepages:manage'])\"\n      #dropdownItems\n    >\n      <EntityDropdownItems\n        :dropdown-items=\"operationItems || []\"\n        :item=\"singlePage\"\n      />\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/SinglePageSettingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport useSlugify from \"@console/composables/use-slugify\";\nimport { useThemeCustomTemplates } from \"@console/modules/interface/themes/composables/use-theme\";\nimport { submitForm, type FormKitNode } from \"@formkit/core\";\nimport type { SinglePage } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { FormType, utils } from \"@halo-dev/ui-shared\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { computed, nextTick, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport { singlePageLabels } from \"@/constants/labels\";\nimport { usePageUpdateMutate } from \"../composables/use-page-update-mutate\";\n\nconst props = withDefaults(\n  defineProps<{\n    singlePage?: SinglePage;\n    publishSupport?: boolean;\n    onlyEmit?: boolean;\n  }>(),\n  {\n    singlePage: undefined,\n    publishSupport: true,\n    onlyEmit: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"saved\", singlePage: SinglePage): void;\n  (event: \"published\", singlePage: SinglePage): void;\n}>();\n\nconst { t } = useI18n();\n\nconst formState = ref<SinglePage>({\n  spec: {\n    title: \"\",\n    slug: \"\",\n    template: \"\",\n    cover: \"\",\n    deleted: false,\n    publish: false,\n    publishTime: undefined,\n    pinned: false,\n    allowComment: true,\n    visible: \"PUBLIC\",\n    priority: 0,\n    excerpt: {\n      autoGenerate: true,\n      raw: \"\",\n    },\n    htmlMetas: [],\n  },\n  apiVersion: \"content.halo.run/v1alpha1\",\n  kind: \"SinglePage\",\n  metadata: {\n    name: utils.id.uuid(),\n  },\n});\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst isSubmitting = ref(false);\nconst publishing = ref(false);\nconst publishCanceling = ref(false);\nconst submitType = ref<\"publish\" | \"save\">();\nconst publishTime = ref<string | undefined>(undefined);\n\nconst isUpdateMode = !!props.singlePage;\n\nconst annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();\n\nconst handleSubmit = () => {\n  if (submitType.value === \"publish\") {\n    handlePublish();\n  }\n  if (submitType.value === \"save\") {\n    handleSave();\n  }\n};\n\nconst handleSaveClick = () => {\n  submitType.value = \"save\";\n\n  nextTick(() => {\n    submitForm(\"singlePage-setting-form\");\n  });\n};\n\nconst handlePublishClick = () => {\n  submitType.value = \"publish\";\n\n  nextTick(() => {\n    submitForm(\"singlePage-setting-form\");\n  });\n};\n\n// Fix me:\n// Force update singlePage settings,\n// because currently there may be errors caused by changes in version due to asynchronous processing.\nconst { mutateAsync: singlePageUpdateMutate } = usePageUpdateMutate();\n\nconst handleSave = async () => {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  formState.value.metadata.annotations = {\n    ...annotations,\n    ...customAnnotations,\n  };\n\n  if (props.onlyEmit) {\n    emit(\"saved\", formState.value);\n    modal.value?.close();\n    return;\n  }\n\n  try {\n    isSubmitting.value = true;\n\n    const { data } = isUpdateMode\n      ? await singlePageUpdateMutate(formState.value)\n      : await coreApiClient.content.singlePage.createSinglePage({\n          singlePage: formState.value,\n        });\n\n    formState.value = data;\n    emit(\"saved\", data);\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (error) {\n    console.error(\"Failed to save single page\", error);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n\nconst handlePublish = async () => {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  formState.value.metadata.annotations = {\n    ...annotations,\n    ...customAnnotations,\n  };\n\n  if (props.onlyEmit) {\n    emit(\"published\", formState.value);\n    modal.value?.close();\n    return;\n  }\n\n  try {\n    publishing.value = true;\n\n    const singlePageToUpdate = cloneDeep(formState.value);\n\n    singlePageToUpdate.spec.releaseSnapshot =\n      singlePageToUpdate.spec.headSnapshot;\n    singlePageToUpdate.spec.publish = true;\n\n    const { data } = await coreApiClient.content.singlePage.updateSinglePage({\n      name: formState.value.metadata.name,\n      singlePage: singlePageToUpdate,\n    });\n\n    formState.value = data;\n\n    emit(\"published\", data);\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.publish_success\"));\n  } catch (error) {\n    console.error(\"Failed to publish single page\", error);\n  } finally {\n    publishing.value = false;\n  }\n};\n\nconst handleUnpublish = async () => {\n  try {\n    publishCanceling.value = true;\n\n    const { data: singlePage } =\n      await coreApiClient.content.singlePage.getSinglePage({\n        name: formState.value.metadata.name,\n      });\n\n    const singlePageToUpdate = cloneDeep(singlePage);\n    singlePageToUpdate.spec.publish = false;\n\n    const { data } = await coreApiClient.content.singlePage.updateSinglePage({\n      name: formState.value.metadata.name,\n      singlePage: singlePageToUpdate,\n    });\n\n    formState.value = data;\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.cancel_publish_success\"));\n  } catch (error) {\n    console.error(\"Failed to unpublish single page\", error);\n  } finally {\n    publishCanceling.value = false;\n  }\n};\n\nwatch(\n  () => props.singlePage,\n  (value) => {\n    if (value) {\n      formState.value = cloneDeep(value);\n      publishTime.value = utils.date.toDatetimeLocal(\n        formState.value.spec.publishTime\n      );\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nwatch(\n  () => publishTime.value,\n  (value) => {\n    formState.value.spec.publishTime = value\n      ? utils.date.toISOString(value)\n      : undefined;\n  }\n);\n\n// custom templates\nconst { templates } = useThemeCustomTemplates(\"page\");\n\n// slug\nconst { handleGenerateSlug } = useSlugify(\n  computed(() => formState.value.spec.title),\n  computed({\n    get() {\n      return formState.value.spec.slug;\n    },\n    set(value) {\n      formState.value.spec.slug = value;\n    },\n  }),\n  computed(() => !isUpdateMode),\n  FormType.SINGLE_PAGE\n);\n\n// fixme: check if slug is unique\n// Finally, we need to check if the slug is unique in the database\nasync function slugUniqueValidation(node: FormKitNode) {\n  const value = node.value;\n  if (!value) {\n    return true;\n  }\n\n  const fieldSelector = [`spec.slug=${value}`];\n\n  if (isUpdateMode) {\n    fieldSelector.push(`metadata.name!=${formState.value.metadata.name}`);\n  }\n\n  const { data: pagesWithSameSlug } =\n    await coreApiClient.content.singlePage.listSinglePage({\n      fieldSelector,\n      page: 1,\n      size: 1,\n    });\n\n  return !pagesWithSameSlug.total;\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"700\"\n    :title=\"$t('core.page.settings.title')\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <template #actions>\n      <slot name=\"actions\"></slot>\n    </template>\n\n    <FormKit\n      id=\"singlePage-setting-form\"\n      type=\"form\"\n      name=\"singlePage-setting-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleSubmit\"\n    >\n      <div>\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.page.settings.groups.general\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              v-model=\"formState.spec.title\"\n              :label=\"$t('core.page.settings.fields.title.label')\"\n              type=\"text\"\n              name=\"title\"\n              validation=\"required|length:0,1024\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.slug\"\n              :label=\"$t('core.page.settings.fields.slug.label')\"\n              name=\"slug\"\n              type=\"text\"\n              validation=\"required|length:0,1024|slugUniqueValidation\"\n              :validation-rules=\"{ slugUniqueValidation }\"\n              :validation-messages=\"{\n                slugUniqueValidation: $t(\n                  'core.common.form.validation.slug_unique'\n                ),\n              }\"\n              :help=\"$t('core.page.settings.fields.slug.help')\"\n            >\n              <template #suffix>\n                <div\n                  v-tooltip=\"\n                    $t('core.page.settings.fields.slug.refresh_message')\n                  \"\n                  class=\"group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100\"\n                  @click=\"handleGenerateSlug(true)\"\n                >\n                  <IconRefreshLine\n                    class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n                  />\n                </div>\n              </template>\n            </FormKit>\n            <FormKit\n              v-model=\"formState.spec.excerpt.autoGenerate\"\n              name=\"autoGenerate\"\n              :label=\"\n                $t('core.page.settings.fields.auto_generate_excerpt.label')\n              \"\n              type=\"checkbox\"\n            >\n            </FormKit>\n            <FormKit\n              v-if=\"!formState.spec.excerpt.autoGenerate\"\n              v-model=\"formState.spec.excerpt.raw\"\n              name=\"raw\"\n              :label=\"$t('core.page.settings.fields.raw_excerpt.label')\"\n              type=\"textarea\"\n              validation=\"length:0,1024\"\n              auto-height\n              :max-auto-height=\"200\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.cover\"\n              :label=\"$t('core.page.settings.fields.cover.label')\"\n              type=\"attachment\"\n              name=\"cover\"\n              width=\"50%\"\n              aspect-ratio=\"16/9\"\n              :accepts=\"['image/*']\"\n              validation=\"length:0,1024\"\n            ></FormKit>\n          </div>\n        </div>\n\n        <div class=\"py-5\">\n          <div class=\"border-t border-gray-200\"></div>\n        </div>\n\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.page.settings.groups.advanced\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              v-model=\"formState.spec.allowComment\"\n              name=\"allowComment\"\n              :label=\"$t('core.page.settings.fields.allow_comment.label')\"\n              type=\"checkbox\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.pinned\"\n              :label=\"$t('core.page.settings.fields.pinned.label')\"\n              name=\"pinned\"\n              type=\"checkbox\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.visible\"\n              :options=\"[\n                { label: $t('core.common.select.public'), value: 'PUBLIC' },\n                {\n                  label: $t('core.common.select.private'),\n                  value: 'PRIVATE',\n                },\n              ]\"\n              :label=\"$t('core.page.settings.fields.visible.label')\"\n              name=\"visible\"\n              type=\"select\"\n            ></FormKit>\n            <FormKit\n              v-model=\"publishTime\"\n              :label=\"$t('core.page.settings.fields.publish_time.label')\"\n              type=\"datetime-local\"\n              name=\"publishTime\"\n              min=\"0000-01-01T00:00\"\n              max=\"9999-12-31T23:59\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.template\"\n              :options=\"templates\"\n              :label=\"$t('core.page.settings.fields.template.label')\"\n              type=\"select\"\n              name=\"template\"\n            ></FormKit>\n          </div>\n        </div>\n      </div>\n    </FormKit>\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.page.settings.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"formState.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :form-data=\"formState\"\n          :value=\"formState.metadata.annotations\"\n          kind=\"SinglePage\"\n          group=\"content.halo.run\"\n        />\n      </div>\n    </div>\n\n    <template #footer>\n      <div class=\"flex items-center justify-between\">\n        <VSpace>\n          <VButton\n            v-if=\"\n              publishSupport &&\n              formState.metadata.labels?.[singlePageLabels.PUBLISHED] !== 'true'\n            \"\n            :loading=\"publishing\"\n            type=\"secondary\"\n            @click=\"handlePublishClick()\"\n          >\n            {{ $t(\"core.common.buttons.publish\") }}\n          </VButton>\n          <VButton\n            :loading=\"isSubmitting\"\n            type=\"secondary\"\n            @click=\"handleSaveClick\"\n          >\n            {{ $t(\"core.common.buttons.save\") }}\n          </VButton>\n          <VButton type=\"default\" @click=\"modal?.close()\">\n            {{ $t(\"core.common.buttons.close\") }}\n          </VButton>\n        </VSpace>\n\n        <VButton\n          v-if=\"\n            formState.metadata.labels?.[singlePageLabels.PUBLISHED] === 'true'\n          \"\n          :loading=\"publishCanceling\"\n          type=\"danger\"\n          ghost\n          @click=\"handleUnpublish()\"\n        >\n          {{ $t(\"core.common.buttons.cancel_publish\") }}\n        </VButton>\n      </div>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/entity-fields/ContributorsField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedSinglePage } from \"@halo-dev/api-client\";\nimport { VEntityField } from \"@halo-dev/components\";\nimport PostContributorList from \"@/components/user/PostContributorList.vue\";\n\nwithDefaults(\n  defineProps<{\n    singlePage: ListedSinglePage;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField>\n    <template #description>\n      <PostContributorList\n        :owner=\"singlePage.owner\"\n        :contributors=\"singlePage.contributors\"\n      />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/entity-fields/CoverField.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  GetThumbnailByUriSizeEnum,\n  type ListedSinglePage,\n} from \"@halo-dev/api-client\";\nimport { VEntityField } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\n\nwithDefaults(\n  defineProps<{\n    singlePage: ListedSinglePage;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField v-if=\"singlePage.page.spec.cover\">\n    <template #description>\n      <div class=\"aspect-h-2 aspect-w-3 w-20 overflow-hidden rounded-md\">\n        <img\n          class=\"h-full w-full object-cover\"\n          :src=\"\n            utils.attachment.getThumbnailUrl(\n              singlePage.page.spec.cover,\n              GetThumbnailByUriSizeEnum.S\n            )\n          \"\n        />\n      </div>\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/entity-fields/PublishStatusField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedSinglePage } from \"@halo-dev/api-client\";\nimport { VEntityField, VStatusDot } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { singlePageLabels } from \"@/constants/labels\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    singlePage: ListedSinglePage;\n  }>(),\n  {}\n);\n\nconst publishStatus = computed(() => {\n  const { labels } = props.singlePage.page.metadata;\n  return labels?.[singlePageLabels.PUBLISHED] === \"true\"\n    ? t(\"core.page.filters.status.items.published\")\n    : t(\"core.page.filters.status.items.draft\");\n});\n\nconst isPublishing = computed(() => {\n  const { spec, status, metadata } = props.singlePage.page;\n  return (\n    (spec.publish &&\n      metadata.labels?.[singlePageLabels.PUBLISHED] !== \"true\") ||\n    (spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)\n  );\n});\n</script>\n\n<template>\n  <VEntityField :description=\"publishStatus\">\n    <template v-if=\"isPublishing\" #description>\n      <VStatusDot :text=\"$t('core.common.tooltips.publishing')\" animate />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/entity-fields/PublishTimeField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedSinglePage } from \"@halo-dev/api-client\";\nimport { VEntityField } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\n\nwithDefaults(\n  defineProps<{\n    singlePage: ListedSinglePage;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField\n    v-if=\"singlePage.page.spec.publishTime\"\n    v-tooltip=\"utils.date.format(singlePage.page.spec.publishTime)\"\n    :description=\"utils.date.timeAgo(singlePage.page.spec.publishTime)\"\n  >\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/entity-fields/TitleField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedSinglePage } from \"@halo-dev/api-client\";\nimport {\n  IconExternalLinkLine,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { singlePageLabels } from \"@/constants/labels\";\n\nconst props = withDefaults(\n  defineProps<{\n    singlePage: ListedSinglePage;\n  }>(),\n  {}\n);\n\nconst { t } = useI18n();\n\nconst externalUrl = computed(() => {\n  const { metadata, status } = props.singlePage.page;\n  if (metadata.labels?.[singlePageLabels.PUBLISHED] === \"true\") {\n    return status?.permalink;\n  }\n  return `/preview/singlepages/${metadata.name}`;\n});\n\nconst commentSubjectRefKey = `content.halo.run/SinglePage/${props.singlePage.page.metadata.name}`;\nconst commentListVisible = ref(false);\n\nconst commentText = computed(() => {\n  const { totalComment, approvedComment } = props.singlePage.stats || {};\n\n  let text = t(\"core.page.list.fields.comments\", {\n    comments: totalComment || 0,\n  });\n\n  const pendingComments = (totalComment || 0) - (approvedComment || 0);\n\n  if (pendingComments > 0) {\n    text += t(\"core.page.list.fields.comments-with-pending\", {\n      count: pendingComments,\n    });\n  }\n  return text;\n});\n</script>\n\n<template>\n  <VEntityField\n    :title=\"singlePage.page.spec.title\"\n    :route=\"{\n      name: 'SinglePageEditor',\n      query: { name: singlePage.page.metadata.name },\n    }\"\n    max-width=\"30rem\"\n  >\n    <template #extra>\n      <VSpace>\n        <RouterLink\n          v-if=\"singlePage.page.status?.inProgress\"\n          v-tooltip=\"$t('core.common.tooltips.unpublished_content_tip')\"\n          :to=\"{\n            name: 'SinglePageEditor',\n            query: { name: singlePage.page.metadata.name },\n          }\"\n          class=\"flex items-center\"\n        >\n          <VStatusDot state=\"success\" animate />\n        </RouterLink>\n        <a\n          target=\"_blank\"\n          :href=\"externalUrl\"\n          class=\"text-gray-600 opacity-0 transition-all hover:text-gray-900 group-hover:opacity-100\"\n        >\n          <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n        </a>\n      </VSpace>\n    </template>\n    <template #description>\n      <div class=\"flex w-full flex-col gap-1\">\n        <VSpace class=\"w-full\">\n          <span class=\"text-xs text-gray-500\">\n            {{\n              $t(\"core.page.list.fields.visits\", {\n                visits: singlePage.stats.visit || 0,\n              })\n            }}\n          </span>\n          <span\n            class=\"cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline\"\n            @click=\"commentListVisible = true\"\n          >\n            {{ commentText }}\n          </span>\n        </VSpace>\n      </div>\n\n      <SubjectQueryCommentListModal\n        v-if=\"commentListVisible\"\n        :subject-ref-key=\"commentSubjectRefKey\"\n        @close=\"commentListVisible = false\"\n      />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/components/entity-fields/VisibleField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedSinglePage, SinglePage } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { IconEye, IconEyeOff, Toast, VEntityField } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nwithDefaults(\n  defineProps<{\n    singlePage: ListedSinglePage;\n  }>(),\n  {}\n);\n\nconst { mutate: changeVisibleMutation } = useMutation({\n  mutationFn: async (singlePage: SinglePage) => {\n    return await coreApiClient.content.singlePage.patchSinglePage({\n      name: singlePage.metadata.name,\n      jsonPatchInner: [\n        {\n          op: \"add\",\n          path: \"/spec/visible\",\n          value: singlePage.spec.visible === \"PRIVATE\" ? \"PUBLIC\" : \"PRIVATE\",\n        },\n      ],\n    });\n  },\n  retry: 3,\n  onSuccess: () => {\n    Toast.success(t(\"core.common.toast.operation_success\"));\n    queryClient.invalidateQueries({ queryKey: [\"singlePages\"] });\n  },\n  onError: () => {\n    Toast.error(t(\"core.common.toast.operation_failed\"));\n  },\n});\n</script>\n\n<template>\n  <VEntityField>\n    <template #description>\n      <IconEye\n        v-if=\"singlePage.page.spec.visible === 'PUBLIC'\"\n        v-tooltip=\"$t('core.page.filters.visible.items.public')\"\n        class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n        @click=\"changeVisibleMutation(singlePage.page)\"\n      />\n      <IconEyeOff\n        v-if=\"singlePage.page.spec.visible === 'PRIVATE'\"\n        v-tooltip=\"$t('core.page.filters.visible.items.private')\"\n        class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n        @click=\"changeVisibleMutation(singlePage.page)\"\n      />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/composables/use-page-update-mutate.ts",
    "content": "import type { SinglePage } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { Toast } from \"@halo-dev/components\";\nimport { useMutation } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nexport function usePageUpdateMutate() {\n  const { t } = useI18n();\n  return useMutation({\n    mutationKey: [\"singlePage-update\"],\n    mutationFn: async (page: SinglePage) => {\n      const { data: latestSinglePage } =\n        await coreApiClient.content.singlePage.getSinglePage({\n          name: page.metadata.name,\n        });\n\n      return coreApiClient.content.singlePage.updateSinglePage(\n        {\n          name: page.metadata.name,\n          singlePage: {\n            ...latestSinglePage,\n            spec: page.spec,\n            metadata: {\n              ...latestSinglePage.metadata,\n              annotations: page.metadata.annotations,\n            },\n          },\n        },\n        {\n          mute: true,\n        }\n      );\n    },\n    retry: 3,\n    onError: (error) => {\n      console.error(\"Failed to update singlePage\", error);\n      Toast.error(t(\"core.common.toast.server_internal_error\"));\n    },\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/pages/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconPages } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  routes: [\n    {\n      path: \"/single-pages\",\n      name: \"SinglePagesRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.page.title\",\n        searchable: true,\n        permissions: [\"system:singlepages:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.single_pages\",\n          group: \"content\",\n          icon: markRaw(IconPages),\n          priority: 1,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"SinglePages\",\n          component: () => import(\"./SinglePageList.vue\"),\n        },\n        {\n          path: \"deleted\",\n          name: \"DeletedSinglePages\",\n          component: () => import(\"./DeletedSinglePageList.vue\"),\n          meta: {\n            title: \"core.deleted_page.title\",\n            searchable: true,\n            permissions: [\"system:singlepages:view\"],\n          },\n        },\n        {\n          path: \"editor\",\n          name: \"SinglePageEditor\",\n          component: () => import(\"./SinglePageEditor.vue\"),\n          meta: {\n            title: \"core.page_editor.title\",\n            searchable: true,\n            hideFooter: true,\n            permissions: [\"system:singlepages:manage\"],\n          },\n        },\n      ],\n    },\n    {\n      path: \"/single-pages/snapshots\",\n      name: \"SinglePageSnapshots\",\n      component: () => import(\"./SinglePageSnapshots.vue\"),\n      meta: {\n        title: \"core.page_snapshots.title\",\n        searchable: false,\n        hideFooter: true,\n        permissions: [\"system:singlepages:manage\"],\n      },\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/DeletedPostList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost, Post } from \"@halo-dev/api-client\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  GetThumbnailByUriSizeEnum,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconDeleteBin,\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VCard,\n  VDropdownItem,\n  VEmpty,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { chunk } from \"es-toolkit\";\nimport { ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport PostContributorList from \"@/components/user/PostContributorList.vue\";\nimport PostTag from \"./tags/components/PostTag.vue\";\n\nconst { t } = useI18n();\n\nconst checkedAll = ref(false);\nconst selectedPostNames = ref<string[]>([]);\nconst keyword = ref(\"\");\n\nconst page = ref(1);\nconst size = ref(20);\nconst total = ref(0);\n\nconst {\n  data: posts,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery<ListedPost[]>({\n  queryKey: [\"deleted-posts\", page, size, keyword],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.content.post.listPosts({\n      labelSelector: [`content.halo.run/deleted=true`],\n      page: page.value,\n      size: size.value,\n      keyword: keyword.value,\n    });\n\n    total.value = data.total;\n\n    return data.items;\n  },\n  refetchInterval: (data) => {\n    const deletingPosts = data?.some(\n      (post) =>\n        !!post.post.metadata.deletionTimestamp || !post.post.spec.deleted\n    );\n    return deletingPosts ? 1000 : false;\n  },\n});\n\nconst checkSelection = (post: Post) => {\n  return selectedPostNames.value.includes(post.metadata.name);\n};\n\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n\n  if (checked) {\n    selectedPostNames.value =\n      posts.value?.map((post) => {\n        return post.post.metadata.name;\n      }) || [];\n  } else {\n    selectedPostNames.value = [];\n  }\n};\n\nconst handleDeletePermanently = async (post: Post) => {\n  Dialog.warning({\n    title: t(\"core.deleted_post.operations.delete.title\"),\n    description: t(\"core.deleted_post.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await coreApiClient.content.post.deletePost({\n        name: post.metadata.name,\n      });\n      await refetch();\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst handleDeletePermanentlyInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.deleted_post.operations.delete_in_batch.title\"),\n    description: t(\"core.deleted_post.operations.delete_in_batch.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const chunks = chunk(selectedPostNames.value, 5);\n\n      for (const chunk of chunks) {\n        await Promise.all(\n          chunk.map((name) => {\n            return coreApiClient.content.post.deletePost({\n              name,\n            });\n          })\n        );\n      }\n\n      await refetch();\n      selectedPostNames.value = [];\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst handleRecovery = async (post: Post) => {\n  Dialog.warning({\n    title: t(\"core.deleted_post.operations.recovery.title\"),\n    description: t(\"core.deleted_post.operations.recovery.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await coreApiClient.content.post.patchPost({\n        name: post.metadata.name,\n        jsonPatchInner: [\n          {\n            op: \"add\",\n            path: \"/spec/deleted\",\n            value: false,\n          },\n        ],\n      });\n\n      await refetch();\n\n      Toast.success(t(\"core.common.toast.recovery_success\"));\n    },\n  });\n};\n\nconst handleRecoveryInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.deleted_post.operations.recovery_in_batch.title\"),\n    description: t(\n      \"core.deleted_post.operations.recovery_in_batch.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const chunks = chunk(selectedPostNames.value, 5);\n\n      for (const chunk of chunks) {\n        await Promise.all(\n          chunk.map((name) => {\n            const isPostExist = posts.value?.some(\n              (item) => item.post.metadata.name === name\n            );\n\n            if (!isPostExist) {\n              return Promise.resolve();\n            }\n\n            return coreApiClient.content.post.patchPost({\n              name: name,\n              jsonPatchInner: [\n                {\n                  op: \"add\",\n                  path: \"/spec/deleted\",\n                  value: false,\n                },\n              ],\n            });\n          })\n        );\n      }\n\n      await refetch();\n      selectedPostNames.value = [];\n\n      Toast.success(t(\"core.common.toast.recovery_success\"));\n    },\n  });\n};\n\nwatch(selectedPostNames, (newValue) => {\n  checkedAll.value = newValue.length === posts.value?.length;\n});\n\nwatch(\n  () => keyword.value,\n  () => {\n    page.value = 1;\n  }\n);\n</script>\n<template>\n  <VPageHeader :title=\"$t('core.deleted_post.title')\">\n    <template #icon>\n      <IconDeleteBin class=\"text-green-600\" />\n    </template>\n    <template #actions>\n      <VButton :route=\"{ name: 'Posts' }\" size=\"sm\">\n        {{ $t(\"core.common.buttons.back\") }}\n      </VButton>\n      <VButton\n        v-permission=\"['system:posts:manage']\"\n        :route=\"{ name: 'PostEditor' }\"\n        type=\"secondary\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <div\n              v-permission=\"['system:posts:manage']\"\n              class=\"hidden items-center sm:flex\"\n            >\n              <input\n                v-model=\"checkedAll\"\n                type=\"checkbox\"\n                @change=\"handleCheckAllChange\"\n              />\n            </div>\n            <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n              <SearchInput v-if=\"!selectedPostNames.length\" v-model=\"keyword\" />\n              <VSpace v-else>\n                <VButton type=\"danger\" @click=\"handleDeletePermanentlyInBatch\">\n                  {{ $t(\"core.common.buttons.delete_permanently\") }}\n                </VButton>\n                <VButton type=\"default\" @click=\"handleRecoveryInBatch\">\n                  {{ $t(\"core.common.buttons.restore\") }}\n                </VButton>\n              </VSpace>\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"refetch()\"\n                >\n                  <IconRefreshLine\n                    :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n\n      <VLoading v-if=\"isLoading\" />\n\n      <Transition v-else-if=\"!posts?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.deleted_post.empty.message')\"\n          :title=\"$t('core.deleted_post.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"refetch\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton :route=\"{ name: 'Posts' }\" type=\"secondary\">\n                {{ $t(\"core.common.buttons.back\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <VEntity\n            v-for=\"post in posts\"\n            :key=\"post.post.metadata.name\"\n            :is-selected=\"checkSelection(post.post)\"\n          >\n            <template\n              v-if=\"utils.permission.has(['system:posts:manage'])\"\n              #checkbox\n            >\n              <input\n                v-model=\"selectedPostNames\"\n                :value=\"post.post.metadata.name\"\n                name=\"post-checkbox\"\n                type=\"checkbox\"\n              />\n            </template>\n            <template #start>\n              <VEntityField v-if=\"post.post.spec.cover\">\n                <template #description>\n                  <div\n                    class=\"aspect-h-2 aspect-w-3 w-20 overflow-hidden rounded-md\"\n                  >\n                    <img\n                      class=\"h-full w-full object-cover\"\n                      :src=\"\n                        utils.attachment.getThumbnailUrl(\n                          post.post.spec.cover,\n                          GetThumbnailByUriSizeEnum.S\n                        )\n                      \"\n                    />\n                  </div>\n                </template>\n              </VEntityField>\n              <VEntityField :title=\"post.post.spec.title\" max-width=\"30rem\">\n                <template #description>\n                  <div class=\"flex flex-col gap-1.5\">\n                    <VSpace class=\"flex-wrap !gap-y-1\">\n                      <p\n                        v-if=\"post.categories.length\"\n                        class=\"inline-flex flex-wrap gap-1 text-xs text-gray-500\"\n                      >\n                        {{ $t(\"core.post.list.fields.categories\") }}\n                        <span\n                          v-for=\"(category, categoryIndex) in post.categories\"\n                          :key=\"categoryIndex\"\n                          class=\"cursor-pointer hover:text-gray-900\"\n                        >\n                          {{ category.spec.displayName }}\n                        </span>\n                      </p>\n                      <span class=\"text-xs text-gray-500\">\n                        {{\n                          $t(\"core.post.list.fields.visits\", {\n                            visits: post.stats.visit,\n                          })\n                        }}\n                      </span>\n                      <span class=\"text-xs text-gray-500\">\n                        {{\n                          $t(\"core.post.list.fields.comments\", {\n                            comments: post.stats.totalComment || 0,\n                          })\n                        }}\n                      </span>\n                    </VSpace>\n                    <VSpace v-if=\"post.tags.length\" class=\"flex-wrap\">\n                      <PostTag\n                        v-for=\"(tag, tagIndex) in post.tags\"\n                        :key=\"tagIndex\"\n                        :tag=\"tag\"\n                        route\n                      ></PostTag>\n                    </VSpace>\n                  </div>\n                </template>\n              </VEntityField>\n            </template>\n            <template #end>\n              <VEntityField>\n                <template #description>\n                  <PostContributorList\n                    :owner=\"post.owner\"\n                    :contributors=\"post.contributors\"\n                  />\n                </template>\n              </VEntityField>\n              <VEntityField v-if=\"!post?.post?.spec.deleted\">\n                <template #description>\n                  <VStatusDot\n                    v-tooltip=\"$t('core.common.tooltips.recovering')\"\n                    state=\"success\"\n                    animate\n                  />\n                </template>\n              </VEntityField>\n              <VEntityField v-if=\"post?.post?.metadata.deletionTimestamp\">\n                <template #description>\n                  <VStatusDot\n                    v-tooltip=\"$t('core.common.status.deleting')\"\n                    state=\"warning\"\n                    animate\n                  />\n                </template>\n              </VEntityField>\n              <VEntityField\n                v-if=\"post.post.spec.publishTime\"\n                v-tooltip=\"utils.date.format(post.post.spec.publishTime)\"\n                :description=\"utils.date.timeAgo(post.post.spec.publishTime)\"\n              >\n              </VEntityField>\n            </template>\n            <template\n              v-if=\"utils.permission.has(['system:posts:manage'])\"\n              #dropdownItems\n            >\n              <VDropdownItem\n                type=\"danger\"\n                @click=\"handleDeletePermanently(post.post)\"\n              >\n                {{ $t(\"core.common.buttons.delete_permanently\") }}\n              </VDropdownItem>\n              <VDropdownItem @click=\"handleRecovery(post.post)\">\n                {{ $t(\"core.common.buttons.restore\") }}\n              </VDropdownItem>\n            </template>\n          </VEntity>\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', { total: total })\n          \"\n          :total=\"total\"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/PostEditor.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useContentSnapshot } from \"@console/composables/use-content-snapshot\";\nimport { useSaveKeybinding } from \"@console/composables/use-save-keybinding\";\nimport useSlugify from \"@console/composables/use-slugify\";\nimport type { Post, PostRequest } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconBookRead,\n  IconEye,\n  IconHistoryLine,\n  IconSave,\n  IconSendPlaneFill,\n  IconSettings,\n  Toast,\n  VButton,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport type { EditorProvider } from \"@halo-dev/ui-shared\";\nimport { FormType, utils } from \"@halo-dev/ui-shared\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { isEqual } from \"es-toolkit\";\nimport ShortUniqueId from \"short-unique-id\";\nimport {\n  computed,\n  nextTick,\n  onMounted,\n  provide,\n  ref,\n  shallowRef,\n  toRef,\n  watch,\n  type ComputedRef,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRouter } from \"vue-router\";\nimport EditorProviderSelector from \"@/components/dropdown-selector/EditorProviderSelector.vue\";\nimport UrlPreviewModal from \"@/components/preview/UrlPreviewModal.vue\";\nimport { useAutoSaveContent } from \"@/composables/use-auto-save-content\";\nimport { useContentCache } from \"@/composables/use-content-cache\";\nimport { useEditorExtensionPoints } from \"@/composables/use-editor-extension-points\";\nimport { useSessionKeepAlive } from \"@/composables/use-session-keep-alive\";\nimport { contentAnnotations } from \"@/constants/annotations\";\nimport PostSettingModal from \"./components/PostSettingModal.vue\";\nimport { usePostUpdateMutate } from \"./composables/use-post-update-mutate\";\n\nconst uid = new ShortUniqueId();\n\nconst router = useRouter();\nconst { t } = useI18n();\nconst { mutateAsync: postUpdateMutate } = usePostUpdateMutate();\n\n// Editor providers\nconst { editorProviders, fetchEditorProviders } = useEditorExtensionPoints();\nconst currentEditorProvider = shallowRef<EditorProvider>();\nconst storedEditorProviderName = useLocalStorage(\"editor-provider-name\", \"\");\n\nconst handleChangeEditorProvider = async (provider: EditorProvider) => {\n  currentEditorProvider.value = provider;\n  storedEditorProviderName.value = provider.name;\n\n  formState.value.post.metadata.annotations = {\n    ...formState.value.post.metadata.annotations,\n    [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n  };\n\n  formState.value.content.rawType = provider.rawType;\n\n  if (isUpdateMode.value) {\n    const { data } = await postUpdateMutate(formState.value.post);\n    formState.value.post = data;\n  }\n};\n\n// Post form\nconst formState = ref<PostRequest>({\n  post: {\n    spec: {\n      title: \"\",\n      slug: \"\",\n      template: \"\",\n      cover: \"\",\n      deleted: false,\n      publish: false,\n      publishTime: undefined,\n      pinned: false,\n      allowComment: true,\n      visible: \"PUBLIC\",\n      priority: 0,\n      excerpt: {\n        autoGenerate: true,\n        raw: \"\",\n      },\n      categories: [],\n      tags: [],\n      htmlMetas: [],\n    },\n    apiVersion: \"content.halo.run/v1alpha1\",\n    kind: \"Post\",\n    metadata: {\n      name: utils.id.uuid(),\n      annotations: {},\n    },\n  },\n  content: {\n    raw: \"\",\n    content: \"\",\n    rawType: \"HTML\",\n  },\n});\nconst settingModal = ref(false);\nconst saving = ref(false);\nconst publishing = ref(false);\n\nconst needsUpdatePost = ref(false);\nwatch(\n  [\n    () => formState.value.post.spec.title,\n    () => formState.value.post.spec.cover,\n  ],\n  (value, oldValue) => {\n    needsUpdatePost.value = !isEqual(value, oldValue);\n  }\n);\n\nconst isUpdateMode = computed(() => {\n  return !!formState.value.post.metadata.creationTimestamp;\n});\n\n// provide some data to editor\nprovide<ComputedRef<string | undefined>>(\n  \"owner\",\n  computed(() => formState.value.post.spec.owner)\n);\nprovide<ComputedRef<string | undefined>>(\n  \"publishTime\",\n  computed(() => formState.value.post.spec.publishTime)\n);\nprovide<ComputedRef<string | undefined>>(\n  \"permalink\",\n  computed(() => formState.value.post.status?.permalink)\n);\n\n// Slug generation\nconst { handleGenerateSlug } = useSlugify(\n  computed(() => formState.value.post.spec.title),\n  computed({\n    get() {\n      return formState.value.post.spec.slug;\n    },\n    set(value) {\n      formState.value.post.spec.slug = value;\n    },\n  }),\n  computed(() => !isUpdateMode.value),\n  FormType.POST\n);\n\nconst handleSave = async (options?: { mute?: boolean }) => {\n  try {\n    if (!options?.mute) {\n      saving.value = true;\n    }\n\n    // Set default title and slug\n    if (!formState.value.post.spec.title) {\n      formState.value.post.spec.title = t(\"core.post_editor.untitled\");\n    }\n\n    if (isUpdateMode.value) {\n      if (needsUpdatePost.value) {\n        formState.value.post = (\n          await postUpdateMutate(formState.value.post)\n        ).data;\n      }\n\n      const { data } = await consoleApiClient.content.post.updatePostContent({\n        name: formState.value.post.metadata.name,\n        content: formState.value.content,\n      });\n\n      formState.value.post = data;\n\n      needsUpdatePost.value = false;\n    } else {\n      // Clear new post content cache\n      handleClearCache();\n\n      if (!formState.value.post.spec.slug) {\n        handleGenerateSlug(true);\n      }\n\n      // fixme: check if slug is unique\n      // Finally, we need to check if the slug is unique in the database\n      const { data: postsWithSameSlug } =\n        await coreApiClient.content.post.listPost({\n          fieldSelector: [`spec.slug=${formState.value.post.spec.slug}`],\n          page: 1,\n          size: 1,\n        });\n\n      if (postsWithSameSlug.total) {\n        formState.value.post.spec.slug = `${formState.value.post.spec.slug}-${uid.randomUUID(8)}`;\n      }\n\n      const { data } = await consoleApiClient.content.post.draftPost({\n        postRequest: formState.value,\n      });\n      formState.value.post = data;\n      name.value = data.metadata.name;\n    }\n\n    if (!options?.mute) {\n      Toast.success(t(\"core.common.toast.save_success\"));\n    }\n    handleClearCache(formState.value.post.metadata.name as string);\n    await handleFetchContent();\n    await handleFetchSnapshot();\n  } catch (e) {\n    console.error(\"Failed to save post\", e);\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  } finally {\n    saving.value = false;\n  }\n};\n\nconst returnToView = useRouteQuery<string>(\"returnToView\");\n\nconst handlePublish = async () => {\n  try {\n    publishing.value = true;\n\n    if (isUpdateMode.value) {\n      const { name: postName } = formState.value.post.metadata;\n      const { permalink } = formState.value.post.status || {};\n\n      if (needsUpdatePost.value) {\n        formState.value.post = (\n          await postUpdateMutate(formState.value.post)\n        ).data;\n      }\n\n      await consoleApiClient.content.post.updatePostContent({\n        name: postName,\n        content: formState.value.content,\n      });\n\n      await consoleApiClient.content.post.publishPost({\n        name: postName,\n      });\n\n      if (returnToView.value === \"true\" && permalink) {\n        handleClearCache(name.value);\n        window.location.href = permalink;\n        return;\n      }\n\n      if (router.options.history.state.back === null) {\n        router.push({ name: \"Posts\" });\n      } else {\n        router.back();\n      }\n    } else {\n      const { data } = await consoleApiClient.content.post.draftPost({\n        postRequest: formState.value,\n      });\n\n      await consoleApiClient.content.post.publishPost({\n        name: data.metadata.name,\n      });\n\n      // Clear new post content cache\n      handleClearCache();\n\n      router.push({ name: \"Posts\" });\n    }\n\n    Toast.success(t(\"core.common.toast.publish_success\"), {\n      duration: 2000,\n    });\n    handleClearCache(name.value as string);\n  } catch (error) {\n    console.error(\"Failed to publish post\", error);\n    Toast.error(t(\"core.common.toast.publish_failed_and_retry\"));\n  } finally {\n    publishing.value = false;\n  }\n};\n\nconst handlePublishClick = () => {\n  if (isUpdateMode.value) {\n    handlePublish();\n  } else {\n    // Set editor title to post\n    settingModal.value = true;\n  }\n};\n\nconst handleFetchContent = async () => {\n  if (!formState.value.post.spec.headSnapshot) {\n    return;\n  }\n\n  const { data } = await consoleApiClient.content.post.fetchPostHeadContent({\n    name: formState.value.post.metadata.name,\n  });\n\n  formState.value.content = Object.assign(formState.value.content, data);\n\n  // get editor provider\n  if (!currentEditorProvider.value) {\n    const preferredEditor = editorProviders.value.find(\n      (provider) =>\n        provider.name ===\n        formState.value.post.metadata.annotations?.[\n          contentAnnotations.PREFERRED_EDITOR\n        ]\n    );\n\n    const provider =\n      preferredEditor ||\n      editorProviders.value.find(\n        (provider) =>\n          provider.rawType.toLowerCase() === data.rawType?.toLowerCase()\n      );\n\n    if (provider) {\n      currentEditorProvider.value = provider;\n\n      formState.value.post.metadata.annotations = {\n        ...formState.value.post.metadata.annotations,\n        [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n      };\n\n      const { data } = await postUpdateMutate(formState.value.post);\n\n      formState.value.post = data;\n    } else {\n      Dialog.warning({\n        title: t(\"core.common.dialog.titles.warning\"),\n        description: t(\"core.common.dialog.descriptions.editor_not_found\", {\n          raw_type: data.rawType,\n        }),\n        confirmText: t(\"core.common.buttons.confirm\"),\n        showCancel: false,\n        onConfirm: () => {\n          router.back();\n        },\n      });\n    }\n\n    await nextTick();\n  }\n};\n\nconst handleOpenSettingModal = async () => {\n  await handleSave({ mute: true });\n  settingModal.value = true;\n};\n\n// Post settings\nconst onSettingSaved = (post: Post) => {\n  // Set route query parameter\n  if (!isUpdateMode.value) {\n    name.value = post.metadata.name;\n  }\n\n  formState.value.post = post;\n\n  if (!isUpdateMode.value) {\n    handleSave();\n  }\n};\n\nconst onSettingPublished = (post: Post) => {\n  formState.value.post = post;\n  handlePublish();\n};\n\n// Get post data when the route contains the name parameter\nconst name = useRouteQuery<string>(\"name\");\nonMounted(async () => {\n  await fetchEditorProviders();\n\n  if (name.value) {\n    // fetch post\n    const { data: post } = await coreApiClient.content.post.getPost({\n      name: name.value as string,\n    });\n    formState.value.post = post;\n\n    // fetch post content\n    await handleFetchContent();\n  } else {\n    // Set default editor\n    const provider =\n      editorProviders.value.find(\n        (provider) => provider.name === storedEditorProviderName.value\n      ) || editorProviders.value[0];\n\n    if (provider) {\n      currentEditorProvider.value = provider;\n      formState.value.content.rawType = provider.rawType;\n    }\n\n    formState.value.post.metadata.annotations = {\n      [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n    };\n  }\n  handleResetCache();\n});\n\nconst headSnapshot = computed(() => {\n  return formState.value.post.spec.headSnapshot;\n});\n\nconst { version, handleFetchSnapshot } = useContentSnapshot(headSnapshot);\n\n// Post content cache\nconst {\n  currentCache,\n  handleSetContentCache,\n  handleResetCache,\n  handleClearCache,\n} = useContentCache(\n  \"post-content-cache\",\n  name,\n  toRef(formState.value.content, \"raw\"),\n  version\n);\n\nuseAutoSaveContent(currentCache, toRef(formState.value.content, \"raw\"), () => {\n  // Do not save when the setting modal is open\n  if (settingModal.value) {\n    return;\n  }\n  handleSave({ mute: true });\n});\n\n// Post preview\nconst previewModal = ref(false);\nconst previewPending = ref(false);\n\nconst handlePreview = async () => {\n  previewPending.value = true;\n  await handleSave({ mute: true });\n  previewModal.value = true;\n  previewPending.value = false;\n};\n\nuseSaveKeybinding(handleSave);\n\n// Keep session alive\nuseSessionKeepAlive();\n\n// Upload image\nasync function handleUploadImage(file: File, options?: AxiosRequestConfig) {\n  if (!utils.permission.has([\"system:attachments:manage\"])) {\n    return;\n  }\n\n  const { data } =\n    await consoleApiClient.storage.attachment.uploadAttachmentForConsole(\n      {\n        file,\n      },\n      options\n    );\n  return data;\n}\n</script>\n\n<template>\n  <PostSettingModal\n    v-if=\"settingModal\"\n    :post=\"formState.post\"\n    :publish-support=\"!isUpdateMode\"\n    :only-emit=\"!isUpdateMode\"\n    @close=\"settingModal = false\"\n    @saved=\"onSettingSaved\"\n    @published=\"onSettingPublished\"\n  />\n\n  <UrlPreviewModal\n    v-if=\"previewModal\"\n    :title=\"formState.post.spec.title\"\n    :url=\"`/preview/posts/${formState.post.metadata.name}`\"\n    @close=\"previewModal = false\"\n  />\n\n  <VPageHeader :title=\"$t('core.post.title')\">\n    <template #icon>\n      <IconBookRead />\n    </template>\n    <template #actions>\n      <EditorProviderSelector\n        v-if=\"editorProviders.length > 1\"\n        :provider=\"currentEditorProvider\"\n        :allow-forced-select=\"!isUpdateMode\"\n        @select=\"handleChangeEditorProvider\"\n      />\n      <VButton\n        v-if=\"isUpdateMode\"\n        size=\"sm\"\n        type=\"default\"\n        @click=\"$router.push({ name: 'PostSnapshots', query: { name: name } })\"\n      >\n        <template #icon>\n          <IconHistoryLine />\n        </template>\n        {{ $t(\"core.post_editor.actions.snapshots\") }}\n      </VButton>\n      <VButton\n        size=\"sm\"\n        type=\"default\"\n        :loading=\"previewPending\"\n        @click=\"handlePreview\"\n      >\n        <template #icon>\n          <IconEye />\n        </template>\n        {{ $t(\"core.common.buttons.preview\") }}\n      </VButton>\n      <VButton :loading=\"saving\" size=\"sm\" type=\"default\" @click=\"handleSave\">\n        <template #icon>\n          <IconSave />\n        </template>\n        {{ $t(\"core.common.buttons.save\") }}\n      </VButton>\n      <VButton\n        v-if=\"isUpdateMode\"\n        size=\"sm\"\n        type=\"default\"\n        @click=\"handleOpenSettingModal\"\n      >\n        <template #icon>\n          <IconSettings />\n        </template>\n        {{ $t(\"core.common.buttons.setting\") }}\n      </VButton>\n      <VButton\n        type=\"secondary\"\n        :loading=\"publishing\"\n        @click=\"handlePublishClick\"\n      >\n        <template #icon>\n          <IconSendPlaneFill />\n        </template>\n        {{ $t(\"core.common.buttons.publish\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n  <div class=\"editor border-t\" style=\"height: calc(100vh - 3.5rem)\">\n    <component\n      :is=\"currentEditorProvider.component\"\n      v-if=\"currentEditorProvider\"\n      v-model:raw=\"formState.content.raw\"\n      v-model:content=\"formState.content.content\"\n      v-model:title=\"formState.post.spec.title\"\n      v-model:cover=\"formState.post.spec.cover\"\n      :upload-image=\"handleUploadImage\"\n      class=\"size-full\"\n      @update=\"handleSetContentCache\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/PostList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost, Post } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconArrowLeft,\n  IconArrowRight,\n  IconBookRead,\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VCard,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { chunk } from \"es-toolkit\";\nimport type { Ref } from \"vue\";\nimport { computed, provide, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport CategoryFilterDropdown from \"@/components/filter/CategoryFilterDropdown.vue\";\nimport TagFilterDropdown from \"@/components/filter/TagFilterDropdown.vue\";\nimport UserFilterDropdown from \"@/components/filter/UserFilterDropdown.vue\";\nimport { postLabels } from \"@/constants/labels\";\nimport PostBatchSettingModal from \"./components/PostBatchSettingModal.vue\";\nimport PostListItem from \"./components/PostListItem.vue\";\nimport PostSettingModal from \"./components/PostSettingModal.vue\";\n\nconst { t } = useI18n();\n\nconst settingModal = ref(false);\nconst selectedPost = ref<Post>();\nconst checkedAll = ref(false);\nconst selectedPostNames = ref<string[]>([]);\n\nprovide<Ref<string[]>>(\"selectedPostNames\", selectedPostNames);\n\n// Filters\nconst page = useRouteQuery<number>(\"page\", 1, {\n  transform: Number,\n});\nconst size = useRouteQuery<number>(\"size\", 20, {\n  transform: Number,\n});\nconst selectedVisible = useRouteQuery<\n  \"PUBLIC\" | \"INTERNAL\" | \"PRIVATE\" | undefined\n>(\"visible\");\nconst selectedPublishStatus = useRouteQuery<string | undefined>(\"status\");\nconst selectedSort = useRouteQuery<string | undefined>(\"sort\");\nconst selectedCategory = useRouteQuery<string | undefined>(\"category\");\nconst selectedTag = useRouteQuery<string | undefined>(\"tag\");\nconst selectedContributor = useRouteQuery<string | undefined>(\"contributor\");\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\nconst total = ref(0);\nconst hasPrevious = ref(false);\nconst hasNext = ref(false);\n\nwatch(\n  () => [\n    selectedVisible.value,\n    selectedPublishStatus.value,\n    selectedSort.value,\n    selectedCategory.value,\n    selectedTag.value,\n    selectedContributor.value,\n    keyword.value,\n  ],\n  () => {\n    page.value = 1;\n  }\n);\n\nfunction handleClearFilters() {\n  selectedVisible.value = undefined;\n  selectedPublishStatus.value = undefined;\n  selectedSort.value = undefined;\n  selectedCategory.value = undefined;\n  selectedTag.value = undefined;\n  selectedContributor.value = undefined;\n}\n\nconst hasFilters = computed(() => {\n  return (\n    selectedVisible.value ||\n    selectedPublishStatus.value !== undefined ||\n    selectedSort.value ||\n    selectedCategory.value ||\n    selectedTag.value ||\n    selectedContributor.value\n  );\n});\n\nconst {\n  data: posts,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery<ListedPost[]>({\n  queryKey: [\n    \"posts\",\n    page,\n    size,\n    selectedCategory,\n    selectedTag,\n    selectedContributor,\n    selectedPublishStatus,\n    selectedVisible,\n    selectedSort,\n    keyword,\n  ],\n  queryFn: async () => {\n    const labelSelector: string[] = [\"content.halo.run/deleted=false\"];\n    const fieldSelector: string[] = [];\n\n    if (selectedCategory.value) {\n      fieldSelector.push(`spec.categories=${selectedCategory.value}`);\n    }\n\n    if (selectedTag.value) {\n      fieldSelector.push(`spec.tags=${selectedTag.value}`);\n    }\n\n    if (selectedContributor.value) {\n      fieldSelector.push(`status.contributors=${selectedContributor.value}`);\n    }\n\n    if (selectedVisible.value) {\n      fieldSelector.push(`spec.visible=${selectedVisible.value}`);\n    }\n\n    if (selectedPublishStatus.value !== undefined) {\n      labelSelector.push(selectedPublishStatus.value);\n    }\n\n    const { data } = await consoleApiClient.content.post.listPosts({\n      labelSelector,\n      fieldSelector,\n      page: page.value,\n      size: size.value,\n      sort: [selectedSort.value].filter(Boolean) as string[],\n      keyword: keyword.value,\n    });\n\n    total.value = data.total;\n    hasNext.value = data.hasNext;\n    hasPrevious.value = data.hasPrevious;\n\n    return data.items;\n  },\n  refetchInterval: (data) => {\n    const hasDeletingPost = data?.some((post) => post.post.spec.deleted);\n\n    if (hasDeletingPost) {\n      return 1000;\n    }\n\n    const hasPublishingPost = data?.some((post) => {\n      const { spec, metadata } = post.post;\n      return (\n        metadata.labels?.[postLabels.PUBLISHED] !== spec.publish + \"\" &&\n        metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== \"true\"\n      );\n    });\n\n    if (hasPublishingPost) {\n      return 1000;\n    }\n\n    const hasCancelingPublishPost = data?.some((post) => {\n      const { spec, metadata } = post.post;\n      return (\n        !spec.publish &&\n        (metadata.labels?.[postLabels.PUBLISHED] === \"true\" ||\n          metadata.labels?.[postLabels.SCHEDULING_PUBLISH] === \"true\")\n      );\n    });\n\n    return hasCancelingPublishPost ? 1000 : false;\n  },\n});\n\nconst handleOpenSettingModal = async (post: Post) => {\n  const { data } = await coreApiClient.content.post.getPost({\n    name: post.metadata.name,\n  });\n  selectedPost.value = data;\n  settingModal.value = true;\n};\n\nconst onSettingModalClose = () => {\n  selectedPost.value = undefined;\n  settingModal.value = false;\n  refetch();\n};\n\nconst handleSelectPrevious = async () => {\n  if (!posts.value) return;\n\n  const index = posts.value.findIndex(\n    (post) => post.post.metadata.name === selectedPost.value?.metadata.name\n  );\n\n  if (index > 0) {\n    const { data: previousPost } = await coreApiClient.content.post.getPost({\n      name: posts.value[index - 1].post.metadata.name,\n    });\n    selectedPost.value = previousPost;\n    return;\n  }\n  if (index === 0 && hasPrevious.value) {\n    page.value--;\n    await refetch();\n    selectedPost.value = posts.value[posts.value.length - 1].post;\n  }\n};\n\nconst handleSelectNext = async () => {\n  if (!posts.value) return;\n\n  const index = posts.value.findIndex(\n    (post) => post.post.metadata.name === selectedPost.value?.metadata.name\n  );\n  if (index < posts.value.length - 1) {\n    const { data: nextPost } = await coreApiClient.content.post.getPost({\n      name: posts.value[index + 1].post.metadata.name,\n    });\n    selectedPost.value = nextPost;\n    return;\n  }\n  if (index === posts.value.length - 1 && hasNext) {\n    page.value++;\n    await refetch();\n    selectedPost.value = posts.value[0].post;\n  }\n};\n\nconst checkSelection = (post: Post) => {\n  return (\n    post.metadata.name === selectedPost.value?.metadata.name ||\n    selectedPostNames.value.includes(post.metadata.name)\n  );\n};\n\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n\n  if (checked) {\n    selectedPostNames.value =\n      posts.value?.map((post) => {\n        return post.post.metadata.name;\n      }) || [];\n  } else {\n    selectedPostNames.value = [];\n  }\n};\n\nconst handleDeleteInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.post.operations.delete_in_batch.title\"),\n    description: t(\"core.post.operations.delete_in_batch.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const chunks = chunk(selectedPostNames.value, 5);\n\n      for (const chunk of chunks) {\n        await Promise.all(\n          chunk.map((name) => {\n            return consoleApiClient.content.post.recyclePost({\n              name,\n            });\n          })\n        );\n      }\n\n      await refetch();\n      selectedPostNames.value = [];\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst handlePublishInBatch = async () => {\n  Dialog.info({\n    title: t(\"core.post.operations.publish_in_batch.title\"),\n    description: t(\"core.post.operations.publish_in_batch.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const chunks = chunk(selectedPostNames.value, 5);\n\n      for (const chunk of chunks) {\n        await Promise.all(\n          chunk.map((name) => {\n            return consoleApiClient.content.post.publishPost({ name });\n          })\n        );\n      }\n\n      await refetch();\n      selectedPostNames.value = [];\n\n      Toast.success(t(\"core.common.toast.publish_success\"));\n    },\n  });\n};\n\nconst handleCancelPublishInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.post.operations.cancel_publish_in_batch.title\"),\n    description: t(\"core.post.operations.cancel_publish_in_batch.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const chunks = chunk(selectedPostNames.value, 5);\n\n      for (const chunk of chunks) {\n        await Promise.all(\n          chunk.map((name) => {\n            return consoleApiClient.content.post.unpublishPost({ name });\n          })\n        );\n      }\n\n      await refetch();\n      selectedPostNames.value = [];\n\n      Toast.success(t(\"core.common.toast.cancel_publish_success\"));\n    },\n  });\n};\n\n// Batch settings\nconst batchSettingModalVisible = ref(false);\nconst batchSettingPosts = ref<ListedPost[]>([]);\n\nfunction handleOpenBatchSettingModal() {\n  batchSettingPosts.value = selectedPostNames.value.map((name) => {\n    return posts.value?.find((post) => post.post.metadata.name === name);\n  }) as ListedPost[];\n\n  batchSettingModalVisible.value = true;\n}\n\nfunction onBatchSettingModalClose() {\n  batchSettingModalVisible.value = false;\n  batchSettingPosts.value = [];\n}\n\nwatch(\n  () => selectedPostNames.value,\n  (newValue) => {\n    checkedAll.value = newValue.length === posts.value?.length;\n  }\n);\n</script>\n<template>\n  <PostSettingModal\n    v-if=\"settingModal\"\n    :post=\"selectedPost\"\n    @close=\"onSettingModalClose\"\n  >\n    <template #actions>\n      <span @click=\"handleSelectPrevious\">\n        <IconArrowLeft v-tooltip=\"$t('core.common.buttons.previous')\" />\n      </span>\n      <span @click=\"handleSelectNext\">\n        <IconArrowRight v-tooltip=\"$t('core.common.buttons.next')\" />\n      </span>\n    </template>\n  </PostSettingModal>\n  <PostBatchSettingModal\n    v-if=\"batchSettingModalVisible\"\n    :posts=\"batchSettingPosts\"\n    @close=\"onBatchSettingModalClose\"\n  />\n  <VPageHeader :title=\"$t('core.post.title')\">\n    <template #icon>\n      <IconBookRead />\n    </template>\n    <template #actions>\n      <VButton :route=\"{ name: 'Categories' }\" size=\"sm\">\n        {{ $t(\"core.post.actions.categories\") }}\n      </VButton>\n      <VButton :route=\"{ name: 'Tags' }\" size=\"sm\">\n        {{ $t(\"core.post.actions.tags\") }}\n      </VButton>\n      <VButton :route=\"{ name: 'DeletedPosts' }\" size=\"sm\">\n        {{ $t(\"core.post.actions.recycle_bin\") }}\n      </VButton>\n\n      <VButton\n        v-permission=\"['system:posts:manage']\"\n        :route=\"{ name: 'PostEditor' }\"\n        type=\"secondary\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <div\n              v-permission=\"['system:posts:manage']\"\n              class=\"hidden items-center sm:flex\"\n            >\n              <input\n                v-model=\"checkedAll\"\n                type=\"checkbox\"\n                @change=\"handleCheckAllChange\"\n              />\n            </div>\n            <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n              <SearchInput v-if=\"!selectedPostNames.length\" v-model=\"keyword\" />\n              <VSpace v-else>\n                <VButton @click=\"handlePublishInBatch\">\n                  {{ $t(\"core.common.buttons.publish\") }}\n                </VButton>\n                <VButton @click=\"handleCancelPublishInBatch\">\n                  {{ $t(\"core.common.buttons.cancel_publish\") }}\n                </VButton>\n                <VButton @click=\"handleOpenBatchSettingModal\">\n                  {{ $t(\"core.post.operations.batch_setting.button\") }}\n                </VButton>\n                <VButton type=\"danger\" @click=\"handleDeleteInBatch\">\n                  {{ $t(\"core.common.buttons.delete\") }}\n                </VButton>\n              </VSpace>\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <FilterCleanButton\n                v-if=\"hasFilters\"\n                @click=\"handleClearFilters\"\n              />\n              <FilterDropdown\n                v-model=\"selectedPublishStatus\"\n                :label=\"$t('core.common.filters.labels.status')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.all'),\n                    value: undefined,\n                  },\n                  {\n                    label: t('core.post.filters.status.items.published'),\n                    value: `${postLabels.PUBLISHED}=true`,\n                  },\n                  {\n                    label: t('core.post.filters.status.items.draft'),\n                    value: `${postLabels.PUBLISHED}=false`,\n                  },\n                  {\n                    label: t('core.post.filters.status.items.scheduling'),\n                    value: `${postLabels.SCHEDULING_PUBLISH}=true`,\n                  },\n                ]\"\n              />\n              <FilterDropdown\n                v-model=\"selectedVisible\"\n                :label=\"$t('core.post.filters.visible.label')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.all'),\n                    value: undefined,\n                  },\n                  {\n                    label: t('core.post.filters.visible.items.public'),\n                    value: 'PUBLIC',\n                  },\n                  {\n                    label: t('core.post.filters.visible.items.private'),\n                    value: 'PRIVATE',\n                  },\n                ]\"\n              />\n              <CategoryFilterDropdown\n                v-model=\"selectedCategory\"\n                :label=\"$t('core.post.filters.category.label')\"\n              />\n              <TagFilterDropdown\n                v-model=\"selectedTag\"\n                :label=\"$t('core.post.filters.tag.label')\"\n              />\n              <HasPermission :permissions=\"['system:users:view']\">\n                <UserFilterDropdown\n                  v-model=\"selectedContributor\"\n                  :label=\"$t('core.post.filters.author.label')\"\n                />\n              </HasPermission>\n              <FilterDropdown\n                v-model=\"selectedSort\"\n                :label=\"$t('core.common.filters.labels.sort')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.default'),\n                  },\n                  {\n                    label: t('core.post.filters.sort.items.publish_time_desc'),\n                    value: 'spec.publishTime,desc',\n                  },\n                  {\n                    label: t('core.post.filters.sort.items.publish_time_asc'),\n                    value: 'spec.publishTime,asc',\n                  },\n                  {\n                    label: t('core.post.filters.sort.items.create_time_desc'),\n                    value: 'metadata.creationTimestamp,desc',\n                  },\n                  {\n                    label: t('core.post.filters.sort.items.create_time_asc'),\n                    value: 'metadata.creationTimestamp,asc',\n                  },\n                  {\n                    label: t(\n                      'core.post.filters.sort.items.last_modify_time_desc'\n                    ),\n                    value: 'status.lastModifyTime,desc',\n                  },\n                  {\n                    label: t(\n                      'core.post.filters.sort.items.last_modify_time_asc'\n                    ),\n                    value: 'status.lastModifyTime,asc',\n                  },\n                  {\n                    label: t('core.post.filters.sort.items.visit_desc'),\n                    value: 'stats.visit,desc',\n                  },\n                  {\n                    label: t('core.post.filters.sort.items.comment_desc'),\n                    value: 'stats.totalComment,desc',\n                  },\n                ]\"\n              />\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"refetch()\"\n                >\n                  <IconRefreshLine\n                    v-tooltip=\"$t('core.common.buttons.refresh')\"\n                    :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else-if=\"!posts?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.post.empty.message')\"\n          :title=\"$t('core.post.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"refetch\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton\n                v-permission=\"['system:posts:manage']\"\n                :route=\"{ name: 'PostEditor' }\"\n                type=\"secondary\"\n              >\n                <template #icon>\n                  <IconAddCircle />\n                </template>\n                {{ $t(\"core.common.buttons.new\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <PostListItem\n            v-for=\"post in posts\"\n            :key=\"post.post.metadata.name\"\n            :post=\"post\"\n            :is-selected=\"checkSelection(post.post)\"\n            @open-setting-modal=\"handleOpenSettingModal\"\n          />\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', { total: total })\n          \"\n          :total=\"total\"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/PostSnapshots.vue",
    "content": "<script lang=\"ts\" setup>\nimport BaseSnapshots from \"@console/components/snapshots/BaseSnapshots.vue\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport { VLoading } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed } from \"vue\";\nimport { useRoute } from \"vue-router\";\n\nconst route = useRoute();\n\nconst postName = computed(() => route.query.name as string);\n\nconst { data: post, isLoading } = useQuery({\n  queryKey: [\"core:post-by-name\", postName],\n  queryFn: async () => {\n    const { data } = await coreApiClient.content.post.getPost({\n      name: postName.value,\n    });\n    return data;\n  },\n  enabled: computed(() => !!postName.value),\n});\n\nasync function listApi() {\n  const { data } = await consoleApiClient.content.post.listPostSnapshots({\n    name: postName.value,\n  });\n  return data;\n}\n\nasync function deleteApi(snapshotName: string) {\n  await consoleApiClient.content.post.deletePostContent({\n    name: postName.value,\n    snapshotName: snapshotName,\n  });\n}\n\nasync function revertApi(snapshotName: string) {\n  await consoleApiClient.content.post.revertToSpecifiedSnapshotForPost({\n    name: postName.value,\n    revertSnapshotForPostParam: {\n      snapshotName: snapshotName,\n    },\n  });\n}\n\nasync function getApi(snapshotName: string) {\n  const { data } = await consoleApiClient.content.post.fetchPostContent({\n    name: postName.value,\n    snapshotName: snapshotName,\n  });\n  return data;\n}\n</script>\n<template>\n  <VLoading v-if=\"isLoading\" />\n  <BaseSnapshots\n    v-else\n    :name=\"postName\"\n    cache-key=\"post\"\n    :title=\"post?.spec.title || ''\"\n    :release-snapshot=\"post?.spec.releaseSnapshot\"\n    :base-snapshot=\"post?.spec.baseSnapshot\"\n    :head-snapshot=\"post?.spec.headSnapshot\"\n    :list-api=\"listApi\"\n    :delete-api=\"deleteApi\"\n    :revert-api=\"revertApi\"\n    :get-api=\"getApi\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/categories/CategoryList.vue",
    "content": "<script lang=\"ts\" setup>\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  IconBookRead,\n  VButton,\n  VCard,\n  VEmpty,\n  VLoading,\n  VPageHeader,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { Draggable } from \"@he-tree/vue\";\nimport \"@he-tree/vue/style/default.css\";\nimport { ref } from \"vue\";\nimport CategoryEditingModal from \"./components/CategoryEditingModal.vue\";\nimport CategoryListItem from \"./components/CategoryListItem.vue\";\nimport { usePostCategory } from \"./composables/use-post-category\";\nimport { convertTreeToCategories, resetCategoriesTreePriority } from \"./utils\";\n\nconst creationModal = ref(false);\n\nconst { categories, categoriesTree, isLoading, handleFetchCategories } =\n  usePostCategory();\n\nconst batchUpdating = ref(false);\n\nasync function handleUpdateInBatch() {\n  const categoriesTreeToUpdate = resetCategoriesTreePriority(\n    categoriesTree.value\n  );\n  const categoriesToUpdate = convertTreeToCategories(categoriesTreeToUpdate);\n  try {\n    batchUpdating.value = true;\n    const promises = categoriesToUpdate.map((category) =>\n      coreApiClient.content.category.patchCategory({\n        name: category.metadata.name,\n        jsonPatchInner: [\n          {\n            op: \"add\",\n            path: \"/spec/children\",\n            value: category.spec.children || [],\n          },\n          {\n            op: \"add\",\n            path: \"/spec/priority\",\n            value: category.spec.priority || 0,\n          },\n        ],\n      })\n    );\n    await Promise.all(promises);\n  } catch (e) {\n    console.error(\"Failed to update categories\", e);\n  } finally {\n    await handleFetchCategories();\n    batchUpdating.value = false;\n  }\n}\n</script>\n<template>\n  <CategoryEditingModal v-if=\"creationModal\" @close=\"creationModal = false\" />\n  <VPageHeader :title=\"$t('core.post_category.title')\">\n    <template #icon>\n      <IconBookRead />\n    </template>\n\n    <template #actions>\n      <VButton\n        v-permission=\"['system:posts:manage']\"\n        type=\"secondary\"\n        @click=\"creationModal = true\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col items-start sm:flex-row sm:items-center\"\n          >\n            <div class=\"flex w-full flex-1 sm:w-auto\">\n              <span class=\"text-base font-medium\">\n                {{\n                  $t(\"core.post_category.header.title\", {\n                    count: categories?.length || 0,\n                  })\n                }}\n              </span>\n            </div>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else-if=\"!categories?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.post_category.empty.message')\"\n          :title=\"$t('core.post_category.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"handleFetchCategories\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton\n                v-permission=\"['system:posts:manage']\"\n                type=\"secondary\"\n                @click=\"creationModal = true\"\n              >\n                <template #icon>\n                  <IconAddCircle />\n                </template>\n                {{ $t(\"core.common.buttons.new\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n      <Transition v-else appear name=\"fade\">\n        <Draggable\n          v-model=\"categoriesTree\"\n          :class=\"{\n            'cursor-progress opacity-60': batchUpdating,\n          }\"\n          :disable-drag=\"batchUpdating\"\n          trigger-class=\"drag-element\"\n          :indent=\"40\"\n          @after-drop=\"handleUpdateInBatch\"\n        >\n          <template #default=\"{ node, stat }\">\n            <CategoryListItem\n              :category-tree-node=\"node\"\n              :is-child-level=\"stat.level > 1\"\n            />\n          </template>\n        </Draggable>\n      </Transition>\n    </VCard>\n  </div>\n</template>\n<style scoped>\n:deep(.vtlist-inner) {\n  @apply divide-y divide-gray-100;\n}\n:deep(.he-tree-drag-placeholder) {\n  height: 60px;\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport useSlugify from \"@console/composables/use-slugify\";\nimport { useThemeCustomTemplates } from \"@console/modules/interface/themes/composables/use-theme\";\nimport { reset, submitForm, type FormKitNode } from \"@formkit/core\";\nimport type { Category } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { FormType } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { computed, nextTick, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    category?: Category;\n    parentCategory?: Category;\n    isChildLevelCategory?: boolean;\n  }>(),\n  {\n    category: undefined,\n    parentCategory: undefined,\n    isChildLevelCategory: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst formState = ref<Category>({\n  spec: {\n    displayName: \"\",\n    slug: \"\",\n    description: \"\",\n    cover: \"\",\n    template: \"\",\n    postTemplate: \"\",\n    priority: 0,\n    children: [],\n    preventParentPostCascadeQuery: false,\n  },\n  status: {},\n  apiVersion: \"content.halo.run/v1alpha1\",\n  kind: \"Category\",\n  metadata: {\n    name: \"\",\n    generateName: \"category-\",\n  },\n});\nconst selectedParentCategory = ref();\nconst saving = ref(false);\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst keepAddingSubmit = ref(false);\n\nconst isUpdateMode = !!props.category;\n\nconst modalTitle = props.category\n  ? t(\"core.post_category.editing_modal.titles.update\")\n  : t(\"core.post_category.editing_modal.titles.create\");\n\nconst annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();\n\nconst handleSaveCategory = async () => {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  formState.value.metadata.annotations = {\n    ...annotations,\n    ...customAnnotations,\n  };\n\n  try {\n    saving.value = true;\n    if (isUpdateMode) {\n      await coreApiClient.content.category.updateCategory({\n        name: formState.value.metadata.name,\n        category: formState.value,\n      });\n    } else {\n      // Gets parent category, calculates priority and updates it.\n      let parentCategory: Category | undefined = undefined;\n\n      if (selectedParentCategory.value) {\n        const { data } = await coreApiClient.content.category.getCategory({\n          name: selectedParentCategory.value,\n        });\n        parentCategory = data;\n      }\n\n      formState.value.spec.priority = parentCategory?.spec.children\n        ? parentCategory.spec.children.length + 1\n        : 0;\n\n      const { data: createdCategory } =\n        await coreApiClient.content.category.createCategory({\n          category: formState.value,\n        });\n\n      if (parentCategory) {\n        await coreApiClient.content.category.patchCategory({\n          name: selectedParentCategory.value,\n          jsonPatchInner: [\n            {\n              op: \"add\",\n              path: \"/spec/children\",\n              value: Array.from(\n                new Set([\n                  ...(parentCategory.spec.children || []),\n                  createdCategory.metadata.name,\n                ])\n              ),\n            },\n          ],\n        });\n      }\n    }\n\n    if (keepAddingSubmit.value) {\n      reset(\"category-form\");\n    } else {\n      modal.value?.close();\n    }\n\n    queryClient.invalidateQueries({ queryKey: [\"post-categories\"] });\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (e) {\n    console.error(\"Failed to create category\", e);\n  } finally {\n    saving.value = false;\n  }\n};\n\nconst handleSubmit = (keepAdding = false) => {\n  keepAddingSubmit.value = keepAdding;\n  submitForm(\"category-form\");\n};\n\nonMounted(() => {\n  if (props.category) {\n    formState.value = cloneDeep(props.category);\n  }\n  selectedParentCategory.value = props.parentCategory?.metadata.name;\n  setFocus(\"displayNameInput\");\n});\n\n// custom templates\nconst { templates } = useThemeCustomTemplates(\"category\");\nconst { templates: postTemplates } = useThemeCustomTemplates(\"post\");\n\n// slug\nconst { handleGenerateSlug } = useSlugify(\n  computed(() => formState.value.spec.displayName),\n  computed({\n    get() {\n      return formState.value.spec.slug;\n    },\n    set(value) {\n      formState.value.spec.slug = value;\n    },\n  }),\n  computed(() => !isUpdateMode),\n  FormType.CATEGORY\n);\n\n// fixme: check if slug is unique\n// Finally, we need to check if the slug is unique in the database\nasync function slugUniqueValidation(node: FormKitNode) {\n  const value = node.value;\n  if (!value) {\n    return true;\n  }\n\n  const fieldSelector = [`spec.slug=${value}`];\n\n  if (props.category) {\n    fieldSelector.push(`metadata.name!=${props.category.metadata.name}`);\n  }\n\n  const { data: categoriesWithSameSlug } =\n    await coreApiClient.content.category.listCategory({\n      fieldSelector,\n      page: 1,\n      size: 1,\n    });\n\n  return !categoriesWithSameSlug.total;\n}\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    mount-to-body\n    :title=\"modalTitle\"\n    :width=\"700\"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      id=\"category-form\"\n      type=\"form\"\n      name=\"category-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleSaveCategory\"\n    >\n      <div>\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.post_category.editing_modal.groups.general\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              v-if=\"!isUpdateMode\"\n              v-model=\"selectedParentCategory\"\n              type=\"categorySelect\"\n              :label=\"\n                $t('core.post_category.editing_modal.fields.parent.label')\n              \"\n            ></FormKit>\n            <FormKit\n              id=\"displayNameInput\"\n              v-model=\"formState.spec.displayName\"\n              name=\"displayName\"\n              :label=\"\n                $t('core.post_category.editing_modal.fields.display_name.label')\n              \"\n              type=\"text\"\n              validation=\"required|length:0,512\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.slug\"\n              :help=\"$t('core.post_category.editing_modal.fields.slug.help')\"\n              name=\"slug\"\n              :label=\"$t('core.post_category.editing_modal.fields.slug.label')\"\n              type=\"text\"\n              validation=\"required|length:0,512|slugUniqueValidation\"\n              :validation-rules=\"{ slugUniqueValidation }\"\n              :validation-messages=\"{\n                slugUniqueValidation: $t(\n                  'core.common.form.validation.slug_unique'\n                ),\n              }\"\n            >\n              <template #suffix>\n                <div\n                  v-tooltip=\"\n                    $t(\n                      'core.post_category.editing_modal.fields.slug.refresh_message'\n                    )\n                  \"\n                  class=\"group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100\"\n                  @click=\"handleGenerateSlug(true)\"\n                >\n                  <IconRefreshLine\n                    class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n                  />\n                </div>\n              </template>\n            </FormKit>\n            <FormKit\n              v-model=\"formState.spec.template\"\n              :options=\"templates\"\n              :label=\"\n                $t('core.post_category.editing_modal.fields.template.label')\n              \"\n              :help=\"\n                $t('core.post_category.editing_modal.fields.template.help')\n              \"\n              type=\"select\"\n              name=\"template\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.postTemplate\"\n              :options=\"postTemplates\"\n              :label=\"\n                $t(\n                  'core.post_category.editing_modal.fields.post_template.label'\n                )\n              \"\n              :help=\"\n                $t('core.post_category.editing_modal.fields.post_template.help')\n              \"\n              type=\"select\"\n              name=\"postTemplate\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.cover\"\n              :help=\"$t('core.post_category.editing_modal.fields.cover.help')\"\n              name=\"cover\"\n              :label=\"$t('core.post_category.editing_modal.fields.cover.label')\"\n              type=\"attachment\"\n              :accepts=\"['image/*']\"\n              validation=\"length:0,1024\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.hideFromList\"\n              :disabled=\"isChildLevelCategory\"\n              :label=\"\n                $t(\n                  'core.post_category.editing_modal.fields.hide_from_list.label'\n                )\n              \"\n              :help=\"\n                $t(\n                  'core.post_category.editing_modal.fields.hide_from_list.help'\n                )\n              \"\n              type=\"checkbox\"\n              name=\"hideFromList\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.preventParentPostCascadeQuery\"\n              :label=\"\n                $t(\n                  'core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.label'\n                )\n              \"\n              :help=\"\n                $t(\n                  'core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.help'\n                )\n              \"\n              type=\"checkbox\"\n              name=\"preventParentPostCascadeQuery\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.description\"\n              name=\"description\"\n              :help=\"\n                $t('core.post_category.editing_modal.fields.description.help')\n              \"\n              :label=\"\n                $t('core.post_category.editing_modal.fields.description.label')\n              \"\n              type=\"textarea\"\n              auto-height\n              :max-auto-height=\"200\"\n              validation=\"length:0,200\"\n            ></FormKit>\n          </div>\n        </div>\n      </div>\n    </FormKit>\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.post_category.editing_modal.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"formState.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :form-data=\"formState\"\n          :value=\"formState.metadata.annotations\"\n          kind=\"Category\"\n          group=\"content.halo.run\"\n        />\n      </div>\n    </div>\n\n    <template #footer>\n      <div class=\"flex justify-between\">\n        <VSpace>\n          <SubmitButton\n            :loading=\"saving && !keepAddingSubmit\"\n            :disabled=\"saving && keepAddingSubmit\"\n            type=\"secondary\"\n            :text=\"$t('core.common.buttons.submit')\"\n            @submit=\"handleSubmit\"\n          >\n          </SubmitButton>\n          <VButton\n            v-if=\"!isUpdateMode\"\n            :loading=\"saving && keepAddingSubmit\"\n            :disabled=\"saving && !keepAddingSubmit\"\n            @click=\"handleSubmit(true)\"\n          >\n            {{ $t(\"core.common.buttons.save_and_continue\") }}\n          </VButton>\n        </VSpace>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </div>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/categories/components/CategoryListItem.vue",
    "content": "<script setup lang=\"ts\">\nimport { coreApiClient, type Category } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconEyeOff,\n  IconList,\n  IconMore,\n  Toast,\n  VDropdown,\n  VDropdownItem,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport \"@he-tree/vue/style/default.css\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport GridiconsLinkBreak from \"~icons/gridicons/link-break\";\nimport { convertCategoryTreeToCategory, type CategoryTreeNode } from \"../utils\";\nimport CategoryEditingModal from \"./CategoryEditingModal.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    isChildLevel?: boolean;\n    categoryTreeNode: CategoryTreeNode;\n  }>(),\n  {\n    isChildLevel: false,\n  }\n);\n\nconst handleDelete = async () => {\n  Dialog.warning({\n    title: t(\"core.post_category.operations.delete.title\"),\n    description: t(\"core.post_category.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.content.category.deleteCategory({\n          name: props.categoryTreeNode.metadata.name,\n        });\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n\n        queryClient.invalidateQueries({ queryKey: [\"post-categories\"] });\n      } catch (e) {\n        console.error(\"Failed to delete category\", e);\n      }\n    },\n  });\n};\n\n// Editing category\nconst editingModal = ref(false);\nconst selectedCategory = ref<Category>();\nconst selectedParentCategory = ref<Category>();\n\nfunction onEditingModalClose() {\n  selectedCategory.value = undefined;\n  selectedParentCategory.value = undefined;\n  editingModal.value = false;\n}\n\nconst handleOpenEditingModal = () => {\n  selectedCategory.value = convertCategoryTreeToCategory(\n    props.categoryTreeNode\n  );\n  editingModal.value = true;\n};\n\nconst handleOpenCreateByParentModal = () => {\n  selectedParentCategory.value = convertCategoryTreeToCategory(\n    props.categoryTreeNode\n  );\n  editingModal.value = true;\n};\n</script>\n<template>\n  <div\n    class=\"group relative flex w-full items-center justify-between px-4 py-3 hover:bg-gray-50\"\n  >\n    <div class=\"min-w-0 flex-1 shrink\">\n      <div\n        v-permission=\"['system:posts:manage']\"\n        class=\"drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex\"\n      >\n        <IconList class=\"h-3.5 w-3.5\" />\n      </div>\n      <div class=\"flex flex-col gap-1\">\n        <div class=\"inline-flex items-center gap-2\">\n          <span class=\"truncate text-sm font-medium text-gray-900\">\n            {{ categoryTreeNode.spec.displayName }}\n          </span>\n        </div>\n        <a\n          v-if=\"categoryTreeNode.status?.permalink\"\n          :href=\"categoryTreeNode.status?.permalink\"\n          :title=\"categoryTreeNode.status?.permalink\"\n          target=\"_blank\"\n          class=\"truncate text-xs text-gray-500 group-hover:text-gray-900\"\n        >\n          {{ categoryTreeNode.status.permalink }}\n        </a>\n      </div>\n    </div>\n    <div class=\"flex flex-none items-center gap-6\">\n      <VStatusDot\n        v-if=\"categoryTreeNode.metadata.deletionTimestamp\"\n        v-tooltip=\"$t('core.common.status.deleting')\"\n        state=\"warning\"\n        animate\n      />\n      <IconEyeOff\n        v-if=\"categoryTreeNode.spec.hideFromList\"\n        v-tooltip=\"$t('core.post_category.list.fields.hide_from_list')\"\n        class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n      />\n      <GridiconsLinkBreak\n        v-if=\"categoryTreeNode.spec.preventParentPostCascadeQuery\"\n        v-tooltip=\"\n          $t('core.post_category.list.fields.prevent_parent_post_cascade_query')\n        \"\n        class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n      />\n      <span class=\"truncate text-xs text-gray-500\">\n        {{\n          $t(\"core.common.fields.post_count\", {\n            count: categoryTreeNode.status?.postCount || 0,\n          })\n        }}\n      </span>\n      <span class=\"truncate text-xs tabular-nums text-gray-500\">\n        {{ utils.date.format(categoryTreeNode.metadata.creationTimestamp) }}\n      </span>\n      <VDropdown v-if=\"utils.permission.has(['system:posts:manage'])\">\n        <div\n          class=\"cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-200/60\"\n          @click.stop\n        >\n          <IconMore />\n        </div>\n        <template #popper>\n          <VDropdownItem @click=\"handleOpenEditingModal\">\n            {{ $t(\"core.common.buttons.edit\") }}\n          </VDropdownItem>\n          <VDropdownItem @click=\"handleOpenCreateByParentModal\">\n            {{ $t(\"core.post_category.operations.add_sub_category.button\") }}\n          </VDropdownItem>\n          <VDropdownItem type=\"danger\" @click=\"handleDelete\">\n            {{ $t(\"core.common.buttons.delete\") }}\n          </VDropdownItem>\n        </template>\n      </VDropdown>\n    </div>\n\n    <CategoryEditingModal\n      v-if=\"editingModal\"\n      :is-child-level-category=\"isChildLevel\"\n      :category=\"selectedCategory\"\n      :parent-category=\"selectedParentCategory\"\n      @close=\"onEditingModalClose\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/categories/components/__tests__/CategoryEditingModal.spec.ts",
    "content": "import messages from \"@intlify/unplugin-vue-i18n/messages\";\nimport { VueQueryPlugin } from \"@tanstack/vue-query\";\nimport { mount } from \"@vue/test-utils\";\nimport { createPinia, setActivePinia } from \"pinia\";\nimport { beforeEach, describe, expect, it } from \"vitest\";\nimport { createI18n } from \"vue-i18n\";\nimport CategoryEditingModal from \"../CategoryEditingModal.vue\";\n\ndescribe(\"CategoryEditingModal\", function () {\n  beforeEach(() => {\n    setActivePinia(createPinia());\n  });\n\n  it(\"should render\", function () {\n    expect(\n      mount(CategoryEditingModal, {\n        global: {\n          plugins: [\n            createI18n({\n              legacy: false,\n              locale: \"en\",\n              messages,\n            }),\n            VueQueryPlugin,\n          ],\n        },\n      })\n    ).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/categories/composables/use-post-category.ts",
    "content": "import {\n  coreApiClient,\n  paginate,\n  type Category,\n  type CategoryV1alpha1ApiListCategoryRequest,\n} from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { buildCategoriesTree, type CategoryTreeNode } from \"../utils\";\n\nexport function usePostCategory() {\n  const categoriesTree = ref<CategoryTreeNode[]>([] as CategoryTreeNode[]);\n\n  const {\n    data: categories,\n    isLoading,\n    refetch,\n  } = useQuery({\n    queryKey: [\"post-categories\"],\n    queryFn: async () => {\n      return await paginate<CategoryV1alpha1ApiListCategoryRequest, Category>(\n        (params) => coreApiClient.content.category.listCategory(params),\n        {\n          size: 1000,\n          sort: [\"metadata.creationTimestamp,desc\"],\n        }\n      );\n    },\n    refetchInterval(data) {\n      const hasAbnormalCategory = data?.some(\n        (category) =>\n          !!category.metadata.deletionTimestamp || !category.status?.permalink\n      );\n      return hasAbnormalCategory ? 1000 : false;\n    },\n    onSuccess(data) {\n      categoriesTree.value = buildCategoriesTree(data);\n    },\n  });\n\n  return {\n    categories,\n    categoriesTree,\n    isLoading,\n    handleFetchCategories: refetch,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/categories/utils/__tests__/index.spec.ts",
    "content": "import type { Category } from \"@halo-dev/api-client\";\nimport { describe, expect, it } from \"vitest\";\nimport {\n  buildCategoriesTree,\n  convertCategoryTreeToCategory,\n  convertTreeToCategories,\n  getCategoryPath,\n  resetCategoriesTreePriority,\n  sortCategoriesTree,\n  type CategoryTreeNode,\n} from \"../index\";\n\nfunction createMockCategory(\n  name: string,\n  priority = 0,\n  children: string[] = []\n): Category {\n  return {\n    metadata: {\n      name,\n      annotations: {},\n      labels: {},\n      version: 1,\n      creationTimestamp: new Date().toISOString(),\n    },\n    apiVersion: \"content.halo.run/v1alpha1\",\n    kind: \"Category\",\n    spec: {\n      displayName: `Category ${name}`,\n      slug: name,\n      description: `Description for ${name}`,\n      cover: \"\",\n      template: \"\",\n      priority: priority,\n      children: children,\n    },\n  };\n}\n\nfunction createMockCategoryTreeNode(\n  name: string,\n  priority = 0,\n  children: CategoryTreeNode[] = []\n): CategoryTreeNode {\n  return {\n    metadata: {\n      name,\n      annotations: {},\n      labels: {},\n      version: 1,\n      creationTimestamp: new Date().toISOString(),\n    },\n    apiVersion: \"content.halo.run/v1alpha1\",\n    kind: \"Category\",\n    spec: {\n      displayName: `Category ${name}`,\n      slug: name,\n      description: `Description for ${name}`,\n      cover: \"\",\n      template: \"\",\n      priority: priority,\n      children: children.map((child) => child.metadata.name),\n    },\n    children: children,\n  };\n}\n\ndescribe(\"buildCategoriesTree\", () => {\n  it(\"should convert flat category array to tree structure\", () => {\n    // Prepare test data\n    const categories: Category[] = [\n      createMockCategory(\"parent1\", 0, [\"child1\", \"child2\"]),\n      createMockCategory(\"child1\", 0),\n      createMockCategory(\"child2\", 1),\n      createMockCategory(\"parent2\", 1, [\"child3\"]),\n      createMockCategory(\"child3\", 0),\n    ];\n\n    // Execute test\n    const result = buildCategoriesTree(categories);\n\n    // Verify results\n    expect(result.length).toBe(2); // Should have two root nodes\n    expect(result[0].metadata.name).toBe(\"parent1\"); // First root node should be parent1\n    expect(result[1].metadata.name).toBe(\"parent2\"); // Second root node should be parent2\n    expect(result[0].children.length).toBe(2); // parent1 should have two children\n    expect(result[1].children.length).toBe(1); // parent2 should have one child\n    expect(result[0].children[0].metadata.name).toBe(\"child1\");\n    expect(result[0].children[1].metadata.name).toBe(\"child2\");\n    expect(result[1].children[0].metadata.name).toBe(\"child3\");\n  });\n\n  it(\"should handle empty array input\", () => {\n    const result = buildCategoriesTree([]);\n    expect(result).toEqual([]);\n  });\n\n  it(\"should handle categories without parent-child relationships\", () => {\n    const categories: Category[] = [\n      createMockCategory(\"category1\", 0),\n      createMockCategory(\"category2\", 1),\n      createMockCategory(\"category3\", 2),\n    ];\n\n    const result = buildCategoriesTree(categories);\n\n    expect(result.length).toBe(3);\n    expect(result.every((node) => node.children.length === 0)).toBe(true);\n  });\n\n  it(\"should handle multi-level nested category structure\", () => {\n    const categories: Category[] = [\n      createMockCategory(\"root\", 0, [\"level1\"]),\n      createMockCategory(\"level1\", 0, [\"level2\"]),\n      createMockCategory(\"level2\", 0, [\"level3\"]),\n      createMockCategory(\"level3\", 0),\n    ];\n\n    const result = buildCategoriesTree(categories);\n\n    expect(result.length).toBe(1);\n    expect(result[0].metadata.name).toBe(\"root\");\n    expect(result[0].children[0].metadata.name).toBe(\"level1\");\n    expect(result[0].children[0].children[0].metadata.name).toBe(\"level2\");\n    expect(result[0].children[0].children[0].children[0].metadata.name).toBe(\n      \"level3\"\n    );\n  });\n});\n\ndescribe(\"sortCategoriesTree\", () => {\n  it(\"should sort category tree by priority\", () => {\n    const categoriesTree: CategoryTreeNode[] = [\n      createMockCategoryTreeNode(\"node3\", 2),\n      createMockCategoryTreeNode(\"node1\", 0),\n      createMockCategoryTreeNode(\"node2\", 1),\n    ];\n\n    const result = sortCategoriesTree(categoriesTree);\n\n    expect(result[0].metadata.name).toBe(\"node1\");\n    expect(result[1].metadata.name).toBe(\"node2\");\n    expect(result[2].metadata.name).toBe(\"node3\");\n  });\n\n  it(\"should recursively sort child nodes\", () => {\n    const categoriesTree: CategoryTreeNode[] = [\n      createMockCategoryTreeNode(\"parent1\", 0, [\n        createMockCategoryTreeNode(\"child3\", 2),\n        createMockCategoryTreeNode(\"child1\", 0),\n        createMockCategoryTreeNode(\"child2\", 1),\n      ]),\n    ];\n\n    const result = sortCategoriesTree(categoriesTree);\n\n    expect(result[0].children[0].metadata.name).toBe(\"child1\");\n    expect(result[0].children[1].metadata.name).toBe(\"child2\");\n    expect(result[0].children[2].metadata.name).toBe(\"child3\");\n  });\n\n  it(\"should handle empty array input\", () => {\n    const result = sortCategoriesTree([]);\n    expect(result).toEqual([]);\n  });\n});\n\ndescribe(\"resetCategoriesTreePriority\", () => {\n  it(\"should reset priority values of all nodes in the tree\", () => {\n    const categoriesTree: CategoryTreeNode[] = [\n      createMockCategoryTreeNode(\"node1\", 5),\n      createMockCategoryTreeNode(\"node2\", 10),\n      createMockCategoryTreeNode(\"node3\", 15),\n    ];\n\n    const result = resetCategoriesTreePriority(categoriesTree);\n\n    expect(result[0].spec.priority).toBe(0);\n    expect(result[1].spec.priority).toBe(1);\n    expect(result[2].spec.priority).toBe(2);\n  });\n\n  it(\"should recursively reset child node priorities\", () => {\n    const categoriesTree: CategoryTreeNode[] = [\n      createMockCategoryTreeNode(\"parent\", 5, [\n        createMockCategoryTreeNode(\"child1\", 10),\n        createMockCategoryTreeNode(\"child2\", 15),\n      ]),\n    ];\n\n    const result = resetCategoriesTreePriority(categoriesTree);\n\n    expect(result[0].spec.priority).toBe(0);\n    expect(result[0].children[0].spec.priority).toBe(0);\n    expect(result[0].children[1].spec.priority).toBe(1);\n  });\n\n  it(\"should handle empty array input\", () => {\n    const result = resetCategoriesTreePriority([]);\n    expect(result).toEqual([]);\n  });\n});\n\ndescribe(\"convertTreeToCategories\", () => {\n  it(\"should convert tree structure back to flat category array\", () => {\n    const child1 = createMockCategoryTreeNode(\"child1\", 0);\n    const child2 = createMockCategoryTreeNode(\"child2\", 1);\n    const parent = createMockCategoryTreeNode(\"parent\", 0, [child1, child2]);\n    const categoriesTree: CategoryTreeNode[] = [parent];\n\n    const result = convertTreeToCategories(categoriesTree);\n\n    expect(result.length).toBe(3);\n\n    // Verify parent node\n    const parentCategory = result.find((c) => c.metadata.name === \"parent\");\n    expect(parentCategory).toBeDefined();\n    expect(parentCategory?.spec.children).toContain(\"child1\");\n    expect(parentCategory?.spec.children).toContain(\"child2\");\n\n    // Verify child nodes\n    const child1Category = result.find((c) => c.metadata.name === \"child1\");\n    const child2Category = result.find((c) => c.metadata.name === \"child2\");\n    expect(child1Category).toBeDefined();\n    expect(child2Category).toBeDefined();\n    expect(child1Category?.spec.children).toEqual([]);\n    expect(child2Category?.spec.children).toEqual([]);\n  });\n\n  it(\"should handle multi-level nested structure\", () => {\n    const level3 = createMockCategoryTreeNode(\"level3\", 0);\n    const level2 = createMockCategoryTreeNode(\"level2\", 0, [level3]);\n    const level1 = createMockCategoryTreeNode(\"level1\", 0, [level2]);\n    const root = createMockCategoryTreeNode(\"root\", 0, [level1]);\n    const categoriesTree: CategoryTreeNode[] = [root];\n\n    const result = convertTreeToCategories(categoriesTree);\n\n    expect(result.length).toBe(4);\n\n    const rootCategory = result.find((c) => c.metadata.name === \"root\");\n    const level1Category = result.find((c) => c.metadata.name === \"level1\");\n    const level2Category = result.find((c) => c.metadata.name === \"level2\");\n    const level3Category = result.find((c) => c.metadata.name === \"level3\");\n\n    expect(rootCategory?.spec.children).toContain(\"level1\");\n    expect(level1Category?.spec.children).toContain(\"level2\");\n    expect(level2Category?.spec.children).toContain(\"level3\");\n    expect(level3Category?.spec.children).toEqual([]);\n  });\n\n  it(\"should handle empty array input\", () => {\n    const result = convertTreeToCategories([]);\n    expect(result).toEqual([]);\n  });\n});\n\ndescribe(\"convertCategoryTreeToCategory\", () => {\n  it(\"should convert a single tree node to a category object\", () => {\n    const child1 = createMockCategoryTreeNode(\"child1\", 0);\n    const child2 = createMockCategoryTreeNode(\"child2\", 1);\n    const parent = createMockCategoryTreeNode(\"parent\", 0, [child1, child2]);\n\n    const result = convertCategoryTreeToCategory(parent);\n\n    expect(result.metadata.name).toBe(\"parent\");\n    expect(result.spec.children).toContain(\"child1\");\n    expect(result.spec.children).toContain(\"child2\");\n    expect(result.spec.children?.length).toBe(2);\n    // eslint-disable-next-line\n    expect((result as any).children).toBeUndefined();\n  });\n\n  it(\"should handle nodes without children\", () => {\n    const node = createMockCategoryTreeNode(\"node\", 0);\n\n    const result = convertCategoryTreeToCategory(node);\n\n    expect(result.metadata.name).toBe(\"node\");\n    expect(result.spec.children).toEqual([]);\n  });\n});\n\ndescribe(\"getCategoryPath\", () => {\n  it(\"should return path from root to specified category\", () => {\n    const level3 = createMockCategoryTreeNode(\"level3\", 0);\n    const level2 = createMockCategoryTreeNode(\"level2\", 0, [level3]);\n    const level1 = createMockCategoryTreeNode(\"level1\", 0, [level2]);\n    const root = createMockCategoryTreeNode(\"root\", 0, [level1]);\n    const categoriesTree: CategoryTreeNode[] = [root];\n\n    const result = getCategoryPath(categoriesTree, \"level3\");\n\n    expect(result).toBeDefined();\n    expect(result?.length).toBe(4);\n    expect(result?.[0].metadata.name).toBe(\"root\");\n    expect(result?.[1].metadata.name).toBe(\"level1\");\n    expect(result?.[2].metadata.name).toBe(\"level2\");\n    expect(result?.[3].metadata.name).toBe(\"level3\");\n  });\n\n  it(\"should handle case when category is not found\", () => {\n    const categoriesTree: CategoryTreeNode[] = [\n      createMockCategoryTreeNode(\"node1\", 0),\n      createMockCategoryTreeNode(\"node2\", 1),\n    ];\n\n    const result = getCategoryPath(categoriesTree, \"nonexistent\");\n\n    expect(result).toBeUndefined();\n  });\n\n  it(\"should handle multiple branches\", () => {\n    const child1 = createMockCategoryTreeNode(\"child1\", 0);\n    const child2 = createMockCategoryTreeNode(\"child2\", 1);\n    const target = createMockCategoryTreeNode(\"target\", 0);\n    const branch1 = createMockCategoryTreeNode(\"branch1\", 0, [child1, child2]);\n    const branch2 = createMockCategoryTreeNode(\"branch2\", 1, [target]);\n    const root = createMockCategoryTreeNode(\"root\", 0, [branch1, branch2]);\n    const categoriesTree: CategoryTreeNode[] = [root];\n\n    const result = getCategoryPath(categoriesTree, \"target\");\n\n    expect(result).toBeDefined();\n    expect(result?.length).toBe(3);\n    expect(result?.[0].metadata.name).toBe(\"root\");\n    expect(result?.[1].metadata.name).toBe(\"branch2\");\n    expect(result?.[2].metadata.name).toBe(\"target\");\n  });\n\n  it(\"should handle empty array input\", () => {\n    const result = getCategoryPath([], \"any\");\n    expect(result).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/categories/utils/index.ts",
    "content": "import type { Category } from \"@halo-dev/api-client\";\nimport { cloneDeep } from \"es-toolkit\";\n\nexport interface CategoryTreeNode extends Category {\n  children: CategoryTreeNode[];\n}\n\nexport function buildCategoriesTree(\n  categories: Category[]\n): CategoryTreeNode[] {\n  const categoriesToUpdate = cloneDeep(categories);\n\n  const categoriesMap: Record<string, CategoryTreeNode> = {};\n  const parentMap: Record<string, string> = {};\n\n  categoriesToUpdate.forEach((category) => {\n    categoriesMap[category.metadata.name] = {\n      ...category,\n      children: [],\n    } as CategoryTreeNode;\n\n    if (category.spec.children) {\n      category.spec.children.forEach((child) => {\n        parentMap[child] = category.metadata.name;\n      });\n    }\n  });\n\n  categoriesToUpdate.forEach((category) => {\n    const parentName = parentMap[category.metadata.name];\n    if (parentName && categoriesMap[parentName]) {\n      categoriesMap[parentName].children.push(\n        categoriesMap[category.metadata.name]\n      );\n    }\n  });\n\n  const categoriesTree = Object.values(categoriesMap).filter(\n    (node) => parentMap[node.metadata.name] === undefined\n  );\n\n  return sortCategoriesTree(categoriesTree);\n}\n\nexport function sortCategoriesTree(\n  categoriesTree: CategoryTreeNode[]\n): CategoryTreeNode[] {\n  return categoriesTree\n    .sort((a, b) => {\n      if (a.spec.priority < b.spec.priority) {\n        return -1;\n      }\n      if (a.spec.priority > b.spec.priority) {\n        return 1;\n      }\n      return 0;\n    })\n    .map((category) => {\n      if (category.children && category.children.length) {\n        return {\n          ...category,\n          children: sortCategoriesTree(category.children),\n        };\n      }\n      return category;\n    });\n}\n\nexport function resetCategoriesTreePriority(\n  categoriesTree: CategoryTreeNode[]\n): CategoryTreeNode[] {\n  for (let i = 0; i < categoriesTree.length; i++) {\n    categoriesTree[i].spec.priority = i;\n    if (categoriesTree[i].children && categoriesTree[i].children.length) {\n      resetCategoriesTreePriority(categoriesTree[i].children);\n    }\n  }\n  return categoriesTree;\n}\n\nexport function convertTreeToCategories(categoriesTree: CategoryTreeNode[]) {\n  const categories: Category[] = [];\n  const categoriesMap = new Map<string, Category>();\n\n  const convertCategory = (node: CategoryTreeNode | undefined) => {\n    if (!node) {\n      return;\n    }\n\n    const children = node.children || [];\n\n    categoriesMap.set(node.metadata.name, {\n      ...node,\n      spec: {\n        ...node.spec,\n        children: children.map((child) => child.metadata.name),\n      },\n    });\n\n    children.forEach((child) => {\n      convertCategory(child);\n    });\n  };\n\n  categoriesTree.forEach((node) => {\n    convertCategory(node);\n  });\n\n  categoriesMap.forEach((node) => {\n    categories.push(node);\n  });\n\n  return categories;\n}\n\nexport function convertCategoryTreeToCategory(\n  categoryTree: CategoryTreeNode\n): Category {\n  const childNames = categoryTree.children.map((child) => child.metadata.name);\n\n  const { children: _, ...categoryWithoutChildren } = categoryTree;\n\n  return {\n    ...categoryWithoutChildren,\n    spec: {\n      ...categoryTree.spec,\n      children: childNames,\n    },\n  };\n}\n\nexport const getCategoryPath = (\n  categories: CategoryTreeNode[],\n  name: string,\n  path: CategoryTreeNode[] = []\n): CategoryTreeNode[] | undefined => {\n  for (const category of categories) {\n    if (category.metadata && category.metadata.name === name) {\n      return path.concat([category]);\n    }\n\n    if (category.children && category.children.length) {\n      const found = getCategoryPath(\n        category.children,\n        name,\n        path.concat([category])\n      );\n      if (found) {\n        return found;\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/PostBatchSettingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  coreApiClient,\n  type JsonPatchInner,\n  type ListedPost,\n} from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { chunk } from \"es-toolkit\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\ntype ArrayPatchOp = \"add\" | \"replace\" | \"removeAll\";\n\ninterface FormData {\n  category: {\n    enabled: boolean;\n    names?: string[];\n    op: ArrayPatchOp;\n  };\n  tag: {\n    enabled: boolean;\n    names?: string[];\n    op: ArrayPatchOp;\n  };\n  owner: {\n    enabled: boolean;\n    value: string;\n  };\n  visible: {\n    enabled: boolean;\n    value: \"PUBLIC\" | \"PRIVATE\";\n  };\n  allowComment: {\n    enabled: boolean;\n    value: boolean;\n  };\n}\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(defineProps<{ posts: ListedPost[] }>(), {});\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"batch-update-posts\"],\n  mutationFn: async ({ data }: { data: FormData }) => {\n    const postChunks = chunk(props.posts, 5);\n\n    for (const postChunk of postChunks) {\n      await Promise.all(\n        postChunk.map((post) => {\n          const jsonPatchInner: JsonPatchInner[] = [];\n          if (data.category.enabled) {\n            jsonPatchInner.push({\n              op: \"add\",\n              path: \"/spec/categories\",\n              value: computeArrayPatchValue(\n                data.category.op,\n                post.post.spec.categories || [],\n                data.category.names || []\n              ),\n            });\n          }\n\n          if (data.tag.enabled) {\n            jsonPatchInner.push({\n              op: \"add\",\n              path: \"/spec/tags\",\n              value: computeArrayPatchValue(\n                data.tag.op,\n                post.post.spec.tags || [],\n                data.tag.names || []\n              ),\n            });\n          }\n\n          if (data.owner.enabled) {\n            jsonPatchInner.push({\n              op: \"add\",\n              path: \"/spec/owner\",\n              value: data.owner.value,\n            });\n          }\n\n          if (data.visible.enabled) {\n            jsonPatchInner.push({\n              op: \"add\",\n              path: \"/spec/visible\",\n              value: data.visible.value,\n            });\n          }\n\n          if (data.allowComment.enabled) {\n            jsonPatchInner.push({\n              op: \"add\",\n              path: \"/spec/allowComment\",\n              value: data.allowComment.value,\n            });\n          }\n\n          return coreApiClient.content.post.patchPost({\n            name: post.post.metadata.name,\n            jsonPatchInner,\n          });\n        })\n      );\n    }\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  },\n  onSuccess() {\n    queryClient.invalidateQueries({ queryKey: [\"posts\"] });\n    modal.value?.close();\n  },\n  onError() {\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  },\n});\n\nfunction computeArrayPatchValue(\n  op: ArrayPatchOp,\n  oldValue: string[],\n  newValue: string[]\n) {\n  if (op === \"add\") {\n    return Array.from(new Set([...oldValue, ...newValue]));\n  } else if (op === \"replace\") {\n    return newValue;\n  } else if (op === \"removeAll\") {\n    return [];\n  }\n}\n\nfunction onSubmit(data: FormData) {\n  mutate({ data });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    height=\"calc(100vh - 20px)\"\n    :title=\"$t('core.post.batch_setting_modal.title')\"\n    :width=\"700\"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      id=\"post-batch-settings-form\"\n      type=\"form\"\n      name=\"post-batch-settings-form\"\n      @submit=\"onSubmit\"\n    >\n      <FormKit\n        v-slot=\"{ value }\"\n        name=\"category\"\n        type=\"group\"\n        :label=\"$t('core.post.batch_setting_modal.fields.category_group')\"\n      >\n        <FormKit\n          :value=\"false\"\n          :label=\"$t('core.post.batch_setting_modal.fields.common.enabled')\"\n          type=\"checkbox\"\n          name=\"enabled\"\n        ></FormKit>\n        <FormKit\n          v-if=\"value?.enabled\"\n          type=\"select\"\n          :options=\"[\n            {\n              value: 'add',\n              label: $t(\n                'core.post.batch_setting_modal.fields.common.op.options.add'\n              ),\n            },\n            {\n              value: 'replace',\n              label: $t(\n                'core.post.batch_setting_modal.fields.common.op.options.replace'\n              ),\n            },\n            {\n              value: 'removeAll',\n              label: $t(\n                'core.post.batch_setting_modal.fields.common.op.options.remove_all'\n              ),\n            },\n          ]\"\n          :label=\"$t('core.post.batch_setting_modal.fields.common.op.label')\"\n          name=\"op\"\n          value=\"add\"\n        ></FormKit>\n        <FormKit\n          v-if=\"value?.enabled && value?.op !== 'removeAll'\"\n          :label=\"$t('core.post.batch_setting_modal.fields.category_names')\"\n          type=\"categorySelect\"\n          :multiple=\"true\"\n          name=\"names\"\n          validation=\"required\"\n        ></FormKit>\n      </FormKit>\n      <FormKit\n        v-slot=\"{ value }\"\n        type=\"group\"\n        name=\"tag\"\n        :label=\"$t('core.post.batch_setting_modal.fields.tag_group')\"\n      >\n        <FormKit\n          :value=\"false\"\n          :label=\"$t('core.post.batch_setting_modal.fields.common.enabled')\"\n          type=\"checkbox\"\n          name=\"enabled\"\n        ></FormKit>\n        <FormKit\n          v-if=\"value?.enabled\"\n          type=\"select\"\n          :options=\"[\n            {\n              value: 'add',\n              label: $t(\n                'core.post.batch_setting_modal.fields.common.op.options.add'\n              ),\n            },\n            {\n              value: 'replace',\n              label: $t(\n                'core.post.batch_setting_modal.fields.common.op.options.replace'\n              ),\n            },\n            {\n              value: 'removeAll',\n              label: $t(\n                'core.post.batch_setting_modal.fields.common.op.options.remove_all'\n              ),\n            },\n          ]\"\n          :label=\"$t('core.post.batch_setting_modal.fields.common.op.label')\"\n          name=\"op\"\n          value=\"add\"\n        ></FormKit>\n        <FormKit\n          v-if=\"value?.enabled && value?.op !== 'removeAll'\"\n          :label=\"$t('core.post.batch_setting_modal.fields.tag_names')\"\n          type=\"tagSelect\"\n          :multiple=\"true\"\n          name=\"names\"\n          validation=\"required\"\n        ></FormKit>\n      </FormKit>\n      <FormKit\n        v-slot=\"{ value }\"\n        type=\"group\"\n        name=\"owner\"\n        :label=\"$t('core.post.batch_setting_modal.fields.owner_group')\"\n      >\n        <FormKit\n          :value=\"false\"\n          :label=\"$t('core.post.batch_setting_modal.fields.common.enabled')\"\n          type=\"checkbox\"\n          name=\"enabled\"\n        ></FormKit>\n        <FormKit\n          v-if=\"value?.enabled\"\n          :label=\"$t('core.post.batch_setting_modal.fields.owner_value')\"\n          name=\"value\"\n          type=\"userSelect\"\n        ></FormKit>\n      </FormKit>\n      <FormKit\n        v-slot=\"{ value }\"\n        type=\"group\"\n        name=\"visible\"\n        :label=\"$t('core.post.batch_setting_modal.fields.visible_group')\"\n      >\n        <FormKit\n          :value=\"false\"\n          :label=\"$t('core.post.batch_setting_modal.fields.common.enabled')\"\n          type=\"checkbox\"\n          name=\"enabled\"\n        ></FormKit>\n        <FormKit\n          v-if=\"value?.enabled\"\n          :options=\"[\n            { label: $t('core.common.select.public'), value: 'PUBLIC' },\n            {\n              label: $t('core.common.select.private'),\n              value: 'PRIVATE',\n            },\n          ]\"\n          :label=\"$t('core.post.batch_setting_modal.fields.visible_value')\"\n          name=\"value\"\n          type=\"select\"\n          value=\"PUBLIC\"\n        ></FormKit>\n      </FormKit>\n      <FormKit\n        v-slot=\"{ value }\"\n        type=\"group\"\n        name=\"allowComment\"\n        :label=\"$t('core.post.batch_setting_modal.fields.allow_comment_group')\"\n      >\n        <FormKit\n          :value=\"false\"\n          :label=\"$t('core.post.batch_setting_modal.fields.common.enabled')\"\n          type=\"checkbox\"\n          name=\"enabled\"\n        ></FormKit>\n        <FormKit\n          v-if=\"value?.enabled\"\n          :options=\"[\n            { label: $t('core.common.radio.yes'), value: true },\n            { label: $t('core.common.radio.no'), value: false },\n          ]\"\n          :label=\"\n            $t('core.post.batch_setting_modal.fields.allow_comment_value')\n          \"\n          name=\"value\"\n          type=\"radio\"\n          :value=\"true\"\n        ></FormKit>\n      </FormKit>\n    </FormKit>\n    <template #footer>\n      <VSpace>\n        <VButton\n          type=\"secondary\"\n          :loading=\"isLoading\"\n          @click=\"$formkit.submit('post-batch-settings-form')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/PostListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useEntityFieldItemExtensionPoint } from \"@console/composables/use-entity-extension-points\";\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport type { ListedPost, Post } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n} from \"@halo-dev/components\";\nimport {\n  utils,\n  type EntityFieldItem,\n  type OperationItem,\n} from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport type { Ref } from \"vue\";\nimport { computed, inject, markRaw, ref, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRouter } from \"vue-router\";\nimport EntityFieldItems from \"@/components/entity-fields/EntityFieldItems.vue\";\nimport StatusDotField from \"@/components/entity-fields/StatusDotField.vue\";\nimport EntityDropdownItems from \"@/components/entity/EntityDropdownItems.vue\";\nimport { postLabels } from \"@/constants/labels\";\nimport ContributorsField from \"./entity-fields/ContributorsField.vue\";\nimport CoverField from \"./entity-fields/CoverField.vue\";\nimport PublishStatusField from \"./entity-fields/PublishStatusField.vue\";\nimport PublishTimeField from \"./entity-fields/PublishTimeField.vue\";\nimport TitleField from \"./entity-fields/TitleField.vue\";\nimport VisibleField from \"./entity-fields/VisibleField.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\nconst router = useRouter();\n\nconst props = withDefaults(\n  defineProps<{\n    post: ListedPost;\n    isSelected?: boolean;\n  }>(),\n  {\n    isSelected: false,\n  }\n);\n\nconst { post } = toRefs(props);\n\nconst emit = defineEmits<{\n  (event: \"open-setting-modal\", post: Post): void;\n}>();\n\nconst selectedPostNames = inject<Ref<string[]>>(\"selectedPostNames\", ref([]));\n\nconst handleDelete = async () => {\n  Dialog.warning({\n    title: t(\"core.post.operations.delete.title\"),\n    description: t(\"core.post.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await consoleApiClient.content.post.recyclePost({\n        name: props.post.post.metadata.name,\n      });\n      await queryClient.invalidateQueries({ queryKey: [\"posts\"] });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst { data: operationItems } = useOperationItemExtensionPoint<ListedPost>(\n  \"post:list-item:operation:create\",\n  post,\n  computed((): OperationItem<ListedPost>[] => [\n    {\n      priority: 0,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.publish\"),\n      action: async () => {\n        await consoleApiClient.content.post.publishPost({\n          name: props.post.post.metadata.name,\n        });\n\n        Toast.success(t(\"core.common.toast.publish_success\"));\n\n        queryClient.invalidateQueries({\n          queryKey: [\"posts\"],\n        });\n      },\n      hidden:\n        props.post.post.metadata.labels?.[postLabels.PUBLISHED] == \"true\" ||\n        props.post.post.metadata.labels?.[postLabels.SCHEDULING_PUBLISH] ==\n          \"true\",\n    },\n    {\n      priority: 10,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.edit\"),\n      permissions: [],\n      action: () => {\n        router.push({\n          name: \"PostEditor\",\n          query: { name: props.post.post.metadata.name },\n        });\n      },\n    },\n    {\n      priority: 20,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.setting\"),\n      permissions: [],\n      action: () => {\n        emit(\"open-setting-modal\", props.post.post);\n      },\n    },\n    {\n      priority: 30,\n      component: markRaw(VDropdownDivider),\n    },\n    {\n      priority: 40,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.cancel_publish\"),\n      action: async () => {\n        await consoleApiClient.content.post.unpublishPost({\n          name: props.post.post.metadata.name,\n        });\n\n        Toast.success(t(\"core.common.toast.cancel_publish_success\"));\n\n        queryClient.invalidateQueries({\n          queryKey: [\"posts\"],\n        });\n      },\n      hidden:\n        props.post.post.metadata.labels?.[postLabels.PUBLISHED] !== \"true\" &&\n        props.post.post.metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !==\n          \"true\",\n    },\n    {\n      priority: 50,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.delete\"),\n      permissions: [],\n      action: handleDelete,\n    },\n  ])\n);\n\nconst { data: fields } = useEntityFieldItemExtensionPoint<ListedPost>(\n  \"post:list-item:field:create\",\n  post,\n  computed((): EntityFieldItem[] => [\n    {\n      priority: 10,\n      position: \"start\",\n      component: markRaw(CoverField),\n      hidden: !props.post.post.spec.cover,\n      props: {\n        post: props.post,\n      },\n    },\n    {\n      priority: 20,\n      position: \"start\",\n      component: markRaw(TitleField),\n      props: {\n        post: props.post,\n      },\n    },\n    {\n      priority: 10,\n      position: \"end\",\n      component: markRaw(ContributorsField),\n      props: {\n        post: props.post,\n      },\n    },\n    {\n      priority: 20,\n      position: \"end\",\n      component: markRaw(PublishStatusField),\n      props: {\n        post: props.post,\n      },\n    },\n    {\n      priority: 30,\n      position: \"end\",\n      component: markRaw(VisibleField),\n      permissions: [\"system:posts:manage\"],\n      props: {\n        post: props.post,\n      },\n    },\n    {\n      priority: 40,\n      position: \"end\",\n      component: markRaw(StatusDotField),\n      props: {\n        tooltip: t(\"core.common.status.deleting\"),\n        state: \"warning\",\n        animate: true,\n      },\n      hidden: !props.post.post.spec.deleted,\n    },\n    {\n      priority: 50,\n      position: \"end\",\n      component: markRaw(PublishTimeField),\n      hidden: !props.post.post.spec.publishTime,\n      props: {\n        post: props.post,\n      },\n    },\n  ])\n);\n</script>\n\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template v-if=\"utils.permission.has(['system:posts:manage'])\" #checkbox>\n      <input\n        v-model=\"selectedPostNames\"\n        :value=\"post.post.metadata.name\"\n        name=\"post-checkbox\"\n        type=\"checkbox\"\n      />\n    </template>\n    <template #start>\n      <EntityFieldItems :fields=\"fields?.start || []\" />\n    </template>\n    <template #end>\n      <EntityFieldItems :fields=\"fields?.end || []\" />\n    </template>\n    <template\n      v-if=\"utils.permission.has(['system:posts:manage'])\"\n      #dropdownItems\n    >\n      <EntityDropdownItems\n        :dropdown-items=\"operationItems || []\"\n        :item=\"post\"\n      />\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/PostSettingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport useSlugify from \"@console/composables/use-slugify\";\nimport { useThemeCustomTemplates } from \"@console/modules/interface/themes/composables/use-theme\";\nimport { submitForm, type FormKitNode } from \"@formkit/core\";\nimport type { Post } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { FormType, utils } from \"@halo-dev/ui-shared\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { computed, nextTick, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport { postLabels } from \"@/constants/labels\";\nimport { usePostUpdateMutate } from \"../composables/use-post-update-mutate\";\n\nconst props = withDefaults(\n  defineProps<{\n    post?: Post;\n    publishSupport?: boolean;\n    onlyEmit?: boolean;\n  }>(),\n  {\n    post: undefined,\n    publishSupport: true,\n    onlyEmit: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"saved\", post: Post): void;\n  (event: \"published\", post: Post): void;\n}>();\n\nconst { t } = useI18n();\n\nconst modal = ref<InstanceType<typeof VModal>>();\nconst formState = ref<Post>({\n  spec: {\n    title: \"\",\n    slug: \"\",\n    template: \"\",\n    cover: \"\",\n    deleted: false,\n    publish: false,\n    publishTime: undefined,\n    pinned: false,\n    allowComment: true,\n    visible: \"PUBLIC\",\n    priority: 0,\n    excerpt: {\n      autoGenerate: true,\n      raw: \"\",\n    },\n    categories: [],\n    tags: [],\n    htmlMetas: [],\n  },\n  apiVersion: \"content.halo.run/v1alpha1\",\n  kind: \"Post\",\n  metadata: {\n    name: utils.id.uuid(),\n  },\n});\nconst isSubmitting = ref(false);\nconst publishing = ref(false);\nconst publishCanceling = ref(false);\nconst submitType = ref<\"publish\" | \"save\">();\nconst publishTime = ref<string | undefined>(undefined);\n\nconst isUpdateMode = computed(() => {\n  return !!formState.value.metadata.creationTimestamp;\n});\n\nconst handleSubmit = () => {\n  if (submitType.value === \"publish\") {\n    handlePublish();\n  }\n  if (submitType.value === \"save\") {\n    handleSave();\n  }\n};\n\nconst handleSaveClick = () => {\n  submitType.value = \"save\";\n\n  nextTick(() => {\n    submitForm(\"post-setting-form\");\n  });\n};\n\nconst handlePublishClick = () => {\n  submitType.value = \"publish\";\n\n  nextTick(() => {\n    submitForm(\"post-setting-form\");\n  });\n};\n\n// Fix me:\n// Force update post settings,\n// because currently there may be errors caused by changes in version due to asynchronous processing.\nconst { mutateAsync: postUpdateMutate } = usePostUpdateMutate();\n\nconst handleSave = async () => {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  formState.value.metadata.annotations = {\n    ...annotations,\n    ...customAnnotations,\n  };\n\n  if (props.onlyEmit) {\n    emit(\"saved\", formState.value);\n    modal.value?.close();\n    return;\n  }\n\n  try {\n    isSubmitting.value = true;\n\n    const { data } = isUpdateMode.value\n      ? await postUpdateMutate(formState.value)\n      : await coreApiClient.content.post.createPost({\n          post: formState.value,\n        });\n\n    formState.value = data;\n    emit(\"saved\", data);\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (e) {\n    console.error(\"Failed to save post\", e);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n\nconst handlePublish = async () => {\n  if (props.onlyEmit) {\n    emit(\"published\", formState.value);\n    modal.value?.close();\n    return;\n  }\n\n  try {\n    publishing.value = true;\n\n    await postUpdateMutate(formState.value);\n\n    const { data } = await consoleApiClient.content.post.publishPost({\n      name: formState.value.metadata.name,\n    });\n\n    formState.value = data;\n\n    emit(\"published\", data);\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.publish_success\"));\n  } catch (e) {\n    console.error(\"Failed to publish post\", e);\n  } finally {\n    publishing.value = false;\n  }\n};\n\nconst handleUnpublish = async () => {\n  try {\n    publishCanceling.value = true;\n\n    await consoleApiClient.content.post.unpublishPost({\n      name: formState.value.metadata.name,\n    });\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.cancel_publish_success\"));\n  } catch (e) {\n    console.error(\"Failed to publish post\", e);\n  } finally {\n    publishCanceling.value = false;\n  }\n};\n\n// publish time\nwatch(\n  () => props.post,\n  (value) => {\n    if (value) {\n      formState.value = cloneDeep(value);\n      publishTime.value = utils.date.toDatetimeLocal(\n        formState.value.spec.publishTime\n      );\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nwatch(\n  () => publishTime.value,\n  (value) => {\n    formState.value.spec.publishTime = value\n      ? utils.date.toISOString(value)\n      : undefined;\n  }\n);\n\nconst isScheduledPublish = computed(() => {\n  return (\n    formState.value.spec.publishTime &&\n    new Date(formState.value.spec.publishTime) > new Date()\n  );\n});\n\nconst publishTimeHelp = computed(() => {\n  return isScheduledPublish.value\n    ? t(\"core.post.settings.fields.publish_time.help.schedule_publish\", {\n        datetime: utils.date.format(publishTime.value),\n      })\n    : \"\";\n});\n\n// custom templates\nconst { templates } = useThemeCustomTemplates(\"post\");\n\nconst annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();\n\n// slug\nconst { handleGenerateSlug } = useSlugify(\n  computed(() => formState.value.spec.title),\n  computed({\n    get() {\n      return formState.value.spec.slug;\n    },\n    set(value) {\n      formState.value.spec.slug = value;\n    },\n  }),\n  computed(() => !isUpdateMode.value),\n  FormType.POST\n);\n\n// fixme: check if slug is unique\n// Finally, we need to check if the slug is unique in the database\nasync function slugUniqueValidation(node: FormKitNode) {\n  const value = node.value;\n  if (!value) {\n    return true;\n  }\n\n  const fieldSelector = [`spec.slug=${value}`];\n\n  if (isUpdateMode.value) {\n    fieldSelector.push(`metadata.name!=${formState.value.metadata.name}`);\n  }\n\n  const { data: postsWithSameSlug } = await coreApiClient.content.post.listPost(\n    {\n      fieldSelector,\n      page: 1,\n      size: 1,\n    }\n  );\n\n  return !postsWithSameSlug.total;\n}\n\n// Buttons condition\nconst showPublishButton = computed(() => {\n  if (!props.publishSupport) {\n    return false;\n  }\n\n  const {\n    [postLabels.PUBLISHED]: published,\n    [postLabels.SCHEDULING_PUBLISH]: schedulingPublish,\n  } = formState.value.metadata.labels || {};\n\n  return published !== \"true\" && schedulingPublish !== \"true\";\n});\n\nconst showCancelPublishButton = computed(() => {\n  const {\n    [postLabels.PUBLISHED]: published,\n    [postLabels.SCHEDULING_PUBLISH]: schedulingPublish,\n  } = formState.value.metadata.labels || {};\n\n  return published === \"true\" || schedulingPublish === \"true\";\n});\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"700\"\n    :title=\"$t('core.post.settings.title')\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <template #actions>\n      <slot name=\"actions\"></slot>\n    </template>\n\n    <FormKit\n      id=\"post-setting-form\"\n      type=\"form\"\n      name=\"post-setting-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleSubmit\"\n    >\n      <div>\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.post.settings.groups.general\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              v-model=\"formState.spec.title\"\n              :label=\"$t('core.post.settings.fields.title.label')\"\n              type=\"text\"\n              name=\"title\"\n              validation=\"required|length:0,1024\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.slug\"\n              :label=\"$t('core.post.settings.fields.slug.label')\"\n              name=\"slug\"\n              type=\"text\"\n              validation=\"required|length:0,1024|slugUniqueValidation\"\n              :validation-rules=\"{ slugUniqueValidation }\"\n              :validation-messages=\"{\n                slugUniqueValidation: $t(\n                  'core.common.form.validation.slug_unique'\n                ),\n              }\"\n              :help=\"$t('core.post.settings.fields.slug.help')\"\n            >\n              <template #suffix>\n                <div\n                  v-tooltip=\"\n                    $t('core.post.settings.fields.slug.refresh_message')\n                  \"\n                  class=\"group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100\"\n                  @click=\"handleGenerateSlug(true)\"\n                >\n                  <IconRefreshLine\n                    class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n                  />\n                </div>\n              </template>\n            </FormKit>\n            <FormKit\n              v-model=\"formState.spec.categories\"\n              :label=\"$t('core.post.settings.fields.categories.label')\"\n              name=\"categories\"\n              type=\"categorySelect\"\n              :multiple=\"true\"\n            />\n            <FormKit\n              v-model=\"formState.spec.tags\"\n              :label=\"$t('core.post.settings.fields.tags.label')\"\n              name=\"tags\"\n              type=\"tagSelect\"\n              :multiple=\"true\"\n            />\n            <FormKit\n              v-model=\"formState.spec.excerpt.autoGenerate\"\n              name=\"autoGenerate\"\n              :label=\"\n                $t('core.post.settings.fields.auto_generate_excerpt.label')\n              \"\n              type=\"checkbox\"\n            >\n            </FormKit>\n            <FormKit\n              v-if=\"!formState.spec.excerpt.autoGenerate\"\n              v-model=\"formState.spec.excerpt.raw\"\n              :label=\"$t('core.post.settings.fields.raw_excerpt.label')\"\n              name=\"raw\"\n              type=\"textarea\"\n              auto-height\n              :max-auto-height=\"200\"\n              validation=\"length:0,1024\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.cover\"\n              name=\"cover\"\n              width=\"50%\"\n              aspect-ratio=\"16/9\"\n              :label=\"$t('core.post.settings.fields.cover.label')\"\n              type=\"attachment\"\n              :accepts=\"['image/*']\"\n              validation=\"length:0,1024\"\n            ></FormKit>\n          </div>\n        </div>\n\n        <div class=\"py-5\">\n          <div class=\"border-t border-gray-200\"></div>\n        </div>\n\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.post.settings.groups.advanced\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              v-model=\"formState.spec.owner\"\n              :label=\"$t('core.post.settings.fields.owner.label')\"\n              type=\"userSelect\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.allowComment\"\n              :label=\"$t('core.post.settings.fields.allow_comment.label')\"\n              type=\"checkbox\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.pinned\"\n              :label=\"$t('core.post.settings.fields.pinned.label')\"\n              name=\"pinned\"\n              type=\"checkbox\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.visible\"\n              :options=\"[\n                { label: $t('core.common.select.public'), value: 'PUBLIC' },\n                {\n                  label: $t('core.common.select.private'),\n                  value: 'PRIVATE',\n                },\n              ]\"\n              :label=\"$t('core.post.settings.fields.visible.label')\"\n              name=\"visible\"\n              type=\"select\"\n            ></FormKit>\n            <FormKit\n              v-model=\"publishTime\"\n              :label=\"$t('core.post.settings.fields.publish_time.label')\"\n              type=\"datetime-local\"\n              min=\"0000-01-01T00:00\"\n              max=\"9999-12-31T23:59\"\n              :help=\"publishTimeHelp\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.template\"\n              :options=\"templates\"\n              :label=\"$t('core.post.settings.fields.template.label')\"\n              name=\"template\"\n              type=\"select\"\n            ></FormKit>\n          </div>\n        </div>\n      </div>\n    </FormKit>\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.post.settings.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"formState.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :value=\"formState.metadata.annotations\"\n          kind=\"Post\"\n          :form-data=\"formState\"\n          group=\"content.halo.run\"\n        />\n      </div>\n    </div>\n\n    <template #footer>\n      <div class=\"flex items-center justify-between\">\n        <VSpace>\n          <VButton\n            v-if=\"showPublishButton\"\n            :loading=\"publishing\"\n            type=\"secondary\"\n            @click=\"handlePublishClick()\"\n          >\n            {{\n              isScheduledPublish\n                ? $t(\"core.common.buttons.schedule_publish\")\n                : $t(\"core.common.buttons.publish\")\n            }}\n          </VButton>\n          <VButton\n            :loading=\"isSubmitting\"\n            type=\"secondary\"\n            @click=\"handleSaveClick()\"\n          >\n            {{ $t(\"core.common.buttons.save\") }}\n          </VButton>\n          <VButton type=\"default\" @click=\"modal?.close()\">\n            {{ $t(\"core.common.buttons.close\") }}\n          </VButton>\n        </VSpace>\n\n        <VButton\n          v-if=\"showCancelPublishButton\"\n          :loading=\"publishCanceling\"\n          type=\"danger\"\n          ghost\n          @click=\"handleUnpublish()\"\n        >\n          {{ $t(\"core.common.buttons.cancel_publish\") }}\n        </VButton>\n      </div>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/__tests__/PostSettingModal.spec.ts",
    "content": "import messages from \"@intlify/unplugin-vue-i18n/messages\";\nimport { VueQueryPlugin } from \"@tanstack/vue-query\";\nimport { mount } from \"@vue/test-utils\";\nimport { createPinia, setActivePinia } from \"pinia\";\nimport { beforeEach, describe, expect, it } from \"vitest\";\nimport { createI18n } from \"vue-i18n\";\nimport PostSettingModal from \"../PostSettingModal.vue\";\n\ndescribe(\"PostSettingModal\", () => {\n  beforeEach(() => {\n    setActivePinia(createPinia());\n  });\n\n  it(\"should render\", () => {\n    const wrapper = mount(\n      {\n        components: {\n          PostSettingModal,\n        },\n        template: `<PostSettingModal></PostSettingModal>`,\n      },\n      {\n        global: {\n          plugins: [\n            VueQueryPlugin,\n            createI18n({\n              legacy: false,\n              locale: \"en\",\n              messages,\n            }),\n          ],\n        },\n      }\n    );\n    expect(wrapper).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/entity-fields/ContributorsField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport { VEntityField } from \"@halo-dev/components\";\nimport PostContributorList from \"@/components/user/PostContributorList.vue\";\n\nwithDefaults(\n  defineProps<{\n    post: ListedPost;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField>\n    <template #description>\n      <PostContributorList\n        :owner=\"post.owner\"\n        :contributors=\"post.contributors\"\n      />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/entity-fields/CoverField.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  GetThumbnailByUriSizeEnum,\n  type ListedPost,\n} from \"@halo-dev/api-client\";\nimport { VEntityField } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\n\nwithDefaults(\n  defineProps<{\n    post: ListedPost;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField v-if=\"post.post.spec.cover\">\n    <template #description>\n      <div class=\"aspect-h-2 aspect-w-3 w-20 overflow-hidden rounded-md\">\n        <img\n          class=\"h-full w-full object-cover\"\n          :src=\"\n            utils.attachment.getThumbnailUrl(\n              post.post.spec.cover,\n              GetThumbnailByUriSizeEnum.S\n            )\n          \"\n        />\n      </div>\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/entity-fields/PublishStatusField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport { VEntityField, VStatusDot } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { postLabels } from \"@/constants/labels\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    post: ListedPost;\n  }>(),\n  {}\n);\n\nconst publishStatus = computed(() => {\n  const { labels } = props.post.post.metadata;\n  return labels?.[postLabels.PUBLISHED] === \"true\"\n    ? t(\"core.post.filters.status.items.published\")\n    : t(\"core.post.filters.status.items.draft\");\n});\n\nconst isPublishing = computed(() => {\n  const { spec, metadata } = props.post.post;\n  return (\n    spec.publish &&\n    metadata.labels?.[postLabels.PUBLISHED] !== \"true\" &&\n    metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== \"true\"\n  );\n});\n</script>\n\n<template>\n  <VEntityField :description=\"publishStatus\">\n    <template v-if=\"isPublishing\" #description>\n      <VStatusDot :text=\"$t('core.common.tooltips.publishing')\" animate />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/entity-fields/PublishTimeField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport { IconTimerLine, VEntityField } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { postLabels } from \"@/constants/labels\";\n\nwithDefaults(\n  defineProps<{\n    post: ListedPost;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField>\n    <template #description>\n      <div class=\"inline-flex items-center space-x-2\">\n        <span\n          v-tooltip=\"utils.date.format(post.post.spec.publishTime)\"\n          class=\"entity-field-description\"\n        >\n          {{ utils.date.timeAgo(post.post.spec.publishTime) }}\n        </span>\n        <IconTimerLine\n          v-if=\"\n            post.post.metadata.labels?.[postLabels.SCHEDULING_PUBLISH] ===\n            'true'\n          \"\n          v-tooltip=\"$t('core.post.list.fields.schedule_publish.tooltip')\"\n          class=\"text-sm\"\n        />\n      </div>\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/entity-fields/TitleField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport {\n  IconExternalLinkLine,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { postLabels } from \"@/constants/labels\";\nimport PostTag from \"../../tags/components/PostTag.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    post: ListedPost;\n  }>(),\n  {}\n);\n\nconst { t } = useI18n();\n\nconst externalUrl = computed(() => {\n  const { status, metadata } = props.post.post;\n  if (metadata.labels?.[postLabels.PUBLISHED] === \"true\") {\n    return status?.permalink;\n  }\n  return `/preview/posts/${metadata.name}`;\n});\n\nconst commentSubjectRefKey = `content.halo.run/Post/${props.post.post.metadata.name}`;\nconst commentListVisible = ref(false);\n\nconst commentText = computed(() => {\n  const { totalComment, approvedComment } = props.post.stats || {};\n\n  let text = t(\"core.post.list.fields.comments\", {\n    comments: totalComment || 0,\n  });\n\n  const pendingComments = (totalComment || 0) - (approvedComment || 0);\n\n  if (pendingComments > 0) {\n    text += t(\"core.post.list.fields.comments-with-pending\", {\n      count: pendingComments,\n    });\n  }\n  return text;\n});\n</script>\n\n<template>\n  <VEntityField\n    :title=\"post.post.spec.title\"\n    :route=\"{\n      name: 'PostEditor',\n      query: { name: post.post.metadata.name },\n    }\"\n    max-width=\"30rem\"\n  >\n    <template #extra>\n      <VSpace>\n        <RouterLink\n          v-if=\"post.post.status?.inProgress\"\n          v-tooltip=\"$t('core.common.tooltips.unpublished_content_tip')\"\n          :to=\"{\n            name: 'PostEditor',\n            query: { name: post.post.metadata.name },\n          }\"\n          class=\"flex items-center\"\n        >\n          <VStatusDot state=\"success\" animate />\n        </RouterLink>\n        <a\n          target=\"_blank\"\n          :href=\"externalUrl\"\n          class=\"text-gray-600 opacity-0 transition-all hover:text-gray-900 group-hover:opacity-100\"\n        >\n          <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n        </a>\n      </VSpace>\n    </template>\n    <template #description>\n      <div class=\"flex flex-col gap-1.5\">\n        <VSpace class=\"flex-wrap !gap-y-1\">\n          <p\n            v-if=\"post.categories.length\"\n            class=\"inline-flex flex-wrap gap-1 text-xs text-gray-500\"\n          >\n            {{ $t(\"core.post.list.fields.categories\") }}\n            <a\n              v-for=\"(category, categoryIndex) in post.categories\"\n              :key=\"categoryIndex\"\n              :href=\"category.status?.permalink\"\n              :title=\"category.status?.permalink\"\n              target=\"_blank\"\n              class=\"cursor-pointer hover:text-gray-900\"\n            >\n              {{ category.spec.displayName }}\n            </a>\n          </p>\n          <span class=\"text-xs text-gray-500\">\n            {{\n              $t(\"core.post.list.fields.visits\", {\n                visits: post.stats.visit,\n              })\n            }}\n          </span>\n          <span\n            class=\"cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline\"\n            @click=\"commentListVisible = true\"\n          >\n            {{ commentText }}\n          </span>\n          <span v-if=\"post.post.spec.pinned\" class=\"text-xs text-gray-500\">\n            {{ $t(\"core.post.list.fields.pinned\") }}\n          </span>\n        </VSpace>\n        <VSpace v-if=\"post.tags.length\" class=\"flex-wrap\">\n          <PostTag\n            v-for=\"(tag, tagIndex) in post.tags\"\n            :key=\"tagIndex\"\n            :tag=\"tag\"\n            route\n          ></PostTag>\n        </VSpace>\n      </div>\n\n      <SubjectQueryCommentListModal\n        v-if=\"commentListVisible\"\n        :subject-ref-key=\"commentSubjectRefKey\"\n        @close=\"commentListVisible = false\"\n      />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/components/entity-fields/VisibleField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost, Post } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { IconEye, IconEyeOff, Toast, VEntityField } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nwithDefaults(\n  defineProps<{\n    post: ListedPost;\n  }>(),\n  {}\n);\n\nconst { mutate: changeVisibleMutation } = useMutation({\n  mutationFn: async (post: Post) => {\n    return await coreApiClient.content.post.patchPost({\n      name: post.metadata.name,\n      jsonPatchInner: [\n        {\n          op: \"add\",\n          path: \"/spec/visible\",\n          value: post.spec.visible === \"PRIVATE\" ? \"PUBLIC\" : \"PRIVATE\",\n        },\n      ],\n    });\n  },\n  retry: 3,\n  onSuccess: () => {\n    Toast.success(t(\"core.common.toast.operation_success\"));\n    queryClient.invalidateQueries({ queryKey: [\"posts\"] });\n  },\n  onError: () => {\n    Toast.error(t(\"core.common.toast.operation_failed\"));\n  },\n});\n</script>\n\n<template>\n  <VEntityField>\n    <template #description>\n      <IconEye\n        v-if=\"post.post.spec.visible === 'PUBLIC'\"\n        v-tooltip=\"$t('core.post.filters.visible.items.public')\"\n        class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n        @click=\"changeVisibleMutation(post.post)\"\n      />\n      <IconEyeOff\n        v-if=\"post.post.spec.visible === 'PRIVATE'\"\n        v-tooltip=\"$t('core.post.filters.visible.items.private')\"\n        class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n        @click=\"changeVisibleMutation(post.post)\"\n      />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/composables/use-post-update-mutate.ts",
    "content": "import type { Post } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { Toast } from \"@halo-dev/components\";\nimport { useMutation } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nexport function usePostUpdateMutate() {\n  const { t } = useI18n();\n\n  return useMutation({\n    mutationKey: [\"post-update\"],\n    mutationFn: async (post: Post) => {\n      const { data: latestPost } = await coreApiClient.content.post.getPost({\n        name: post.metadata.name,\n      });\n\n      return await coreApiClient.content.post.updatePost(\n        {\n          name: post.metadata.name,\n          post: {\n            ...latestPost,\n            spec: post.spec,\n            metadata: {\n              ...latestPost.metadata,\n              annotations: post.metadata.annotations,\n            },\n          },\n        },\n        {\n          mute: true,\n        }\n      );\n    },\n    retry: 3,\n    onError: (error) => {\n      console.error(\"Failed to update post\", error);\n      Toast.error(t(\"core.common.toast.server_internal_error\"));\n    },\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport BlankLayout from \"@console/layouts/BlankLayout.vue\";\nimport { IconBookRead } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  routes: [\n    {\n      path: \"/posts\",\n      name: \"PostsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.post.title\",\n        searchable: true,\n        permissions: [\"system:posts:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.posts\",\n          group: \"content\",\n          icon: markRaw(IconBookRead),\n          priority: 0,\n          mobile: true,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Posts\",\n          component: () => import(\"./PostList.vue\"),\n        },\n        {\n          path: \"deleted\",\n          name: \"DeletedPosts\",\n          component: () => import(\"./DeletedPostList.vue\"),\n          meta: {\n            title: \"core.deleted_post.title\",\n            searchable: true,\n            permissions: [\"system:posts:view\"],\n          },\n        },\n        {\n          path: \"editor\",\n          name: \"PostEditor\",\n          component: () => import(\"./PostEditor.vue\"),\n          meta: {\n            title: \"core.post_editor.title\",\n            searchable: true,\n            hideFooter: true,\n            permissions: [\"system:posts:manage\"],\n          },\n        },\n        {\n          path: \"categories\",\n          component: BlankLayout,\n          children: [\n            {\n              path: \"\",\n              name: \"Categories\",\n              component: () => import(\"./categories/CategoryList.vue\"),\n              meta: {\n                title: \"core.post_category.title\",\n                searchable: true,\n                permissions: [\"system:posts:view\"],\n              },\n            },\n          ],\n        },\n        {\n          path: \"tags\",\n          component: BlankLayout,\n          children: [\n            {\n              path: \"\",\n              name: \"Tags\",\n              component: () => import(\"./tags/TagList.vue\"),\n              meta: {\n                title: \"core.post_tag.title\",\n                searchable: true,\n                permissions: [\"system:posts:view\"],\n              },\n            },\n          ],\n        },\n      ],\n    },\n    {\n      path: \"/posts/snapshots\",\n      name: \"PostSnapshots\",\n      component: () => import(\"./PostSnapshots.vue\"),\n      meta: {\n        title: \"core.post_snapshots.title\",\n        searchable: false,\n        hideFooter: true,\n        permissions: [\"system:posts:manage\"],\n      },\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/tags/TagList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Tag } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  IconBookRead,\n  IconRefreshLine,\n  VButton,\n  VCard,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { computed, onMounted, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport TagEditingModal from \"./components/TagEditingModal.vue\";\nimport TagListItem from \"./components/TagListItem.vue\";\nimport { usePostTag } from \"./composables/use-post-tag\";\n\nconst { t } = useI18n();\n\nconst editingModal = ref(false);\nconst selectedTag = ref<Tag>();\n\nconst selectedTagNames = ref<string[]>([]);\nconst checkedAll = ref(false);\n\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\nconst page = useRouteQuery<number>(\"page\", 1, {\n  transform: Number,\n});\nconst size = useRouteQuery<number>(\"size\", 20, {\n  transform: Number,\n});\nconst selectedSort = useRouteQuery<string | undefined>(\"sort\");\n\nconst hasFilters = computed(() => {\n  return !!selectedSort.value;\n});\n\nconst handleClearFilters = () => {\n  selectedSort.value = undefined;\n};\n\nconst {\n  tags,\n  total,\n  hasNext,\n  hasPrevious,\n  isLoading,\n  isFetching,\n  handleFetchTags,\n  handleDelete,\n  handleDeleteInBatch,\n} = usePostTag({\n  page,\n  size,\n  keyword,\n  sort: selectedSort,\n});\n\nconst handleOpenEditingModal = (tag?: Tag) => {\n  selectedTag.value = tag;\n  editingModal.value = true;\n};\n\nconst handleDeleteTagInBatch = () => {\n  handleDeleteInBatch(selectedTagNames.value).then(() => {\n    selectedTagNames.value = [];\n  });\n};\n\nconst handleCheckAllChange = () => {\n  if (checkedAll.value) {\n    selectedTagNames.value = tags.value?.map((tag) => tag.metadata.name) || [];\n  } else {\n    selectedTagNames.value = [];\n  }\n};\n\nconst handleSelectPrevious = async () => {\n  if (!tags.value) return;\n\n  const currentIndex = tags.value.findIndex(\n    (tag) => tag.metadata.name === selectedTag.value?.metadata.name\n  );\n\n  if (currentIndex > 0) {\n    selectedTag.value = tags.value[currentIndex - 1];\n    return;\n  }\n\n  if (currentIndex === 0 && hasPrevious.value) {\n    page.value--;\n    await handleFetchTags();\n    setTimeout(() => {\n      if (!tags.value) return;\n      selectedTag.value = tags.value[tags.value.length - 1];\n    });\n  }\n};\n\nconst handleSelectNext = async () => {\n  if (!tags.value) return;\n\n  if (!selectedTag.value) {\n    selectedTag.value = tags.value[0];\n    return;\n  }\n  const currentIndex = tags.value.findIndex(\n    (tag) => tag.metadata.name === selectedTag.value?.metadata.name\n  );\n  if (currentIndex !== tags.value.length - 1) {\n    selectedTag.value = tags.value[currentIndex + 1];\n  }\n\n  if (currentIndex === tags.value.length - 1 && hasNext.value) {\n    page.value++;\n    await handleFetchTags();\n    setTimeout(() => {\n      if (!tags.value) return;\n      selectedTag.value = tags.value[0];\n    });\n  }\n};\n\nconst onEditingModalClose = () => {\n  selectedTag.value = undefined;\n  queryName.value = null;\n  editingModal.value = false;\n  handleFetchTags();\n};\n\nconst queryName = useRouteQuery(\"name\");\n\nonMounted(async () => {\n  if (queryName.value) {\n    const { data } = await coreApiClient.content.tag.getTag({\n      name: queryName.value as string,\n    });\n    selectedTag.value = data;\n    editingModal.value = true;\n  }\n});\n\nwatch(selectedTagNames, (newVal) => {\n  checkedAll.value = newVal.length === tags.value?.length;\n});\n</script>\n<template>\n  <TagEditingModal\n    v-if=\"editingModal\"\n    :tag=\"selectedTag\"\n    @close=\"onEditingModalClose\"\n    @next=\"handleSelectNext\"\n    @previous=\"handleSelectPrevious\"\n  />\n  <VPageHeader :title=\"$t('core.post_tag.title')\">\n    <template #icon>\n      <IconBookRead />\n    </template>\n    <template #actions>\n      <VButton\n        v-permission=\"['system:posts:manage']\"\n        type=\"secondary\"\n        @click=\"editingModal = true\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex h-9 flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <HasPermission :permissions=\"['system:posts:manage']\">\n              <div class=\"hidden items-center sm:flex\">\n                <input\n                  v-model=\"checkedAll\"\n                  type=\"checkbox\"\n                  @change=\"handleCheckAllChange\"\n                />\n              </div>\n            </HasPermission>\n            <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n              <VSpace v-if=\"selectedTagNames.length > 0\">\n                <VButton type=\"danger\" @click=\"handleDeleteTagInBatch\">\n                  {{ $t(\"core.common.buttons.delete\") }}\n                </VButton>\n              </VSpace>\n              <SearchInput v-else v-model=\"keyword\" />\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <FilterCleanButton\n                v-if=\"hasFilters\"\n                @click=\"handleClearFilters\"\n              />\n              <FilterDropdown\n                v-model=\"selectedSort\"\n                :label=\"$t('core.common.filters.labels.sort')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.default'),\n                  },\n                  {\n                    label: t(\n                      'core.post.tag.filters.sort.items.create_time_desc'\n                    ),\n                    value: 'metadata.creationTimestamp,desc',\n                  },\n                  {\n                    label: t(\n                      'core.post.tag.filters.sort.items.create_time_asc'\n                    ),\n                    value: 'metadata.creationTimestamp,asc',\n                  },\n                  {\n                    label: t(\n                      'core.post.tag.filters.sort.items.display_name_desc'\n                    ),\n                    value: 'spec.displayName,desc',\n                  },\n                  {\n                    label: t(\n                      'core.post.tag.filters.sort.items.display_name_asc'\n                    ),\n                    value: 'spec.displayName,asc',\n                  },\n                  {\n                    label: t('core.post.tag.filters.sort.items.post_desc'),\n                    value: 'status.postCount,desc',\n                  },\n                ]\"\n              />\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"handleFetchTags()\"\n                >\n                  <IconRefreshLine\n                    v-tooltip=\"$t('core.common.buttons.refresh')\"\n                    :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else-if=\"!tags?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.post_tag.empty.message')\"\n          :title=\"$t('core.post_tag.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"() => handleFetchTags\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton type=\"secondary\" @click=\"editingModal = true\">\n                <template #icon>\n                  <IconAddCircle />\n                </template>\n                {{ $t(\"core.common.buttons.new\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n\n      <Transition appear name=\"fade\">\n        <VEntityContainer>\n          <TagListItem\n            v-for=\"tag in tags\"\n            :key=\"tag.metadata.name\"\n            :tag=\"tag\"\n            :is-selected=\"selectedTag?.metadata.name === tag.metadata.name\"\n            @editing=\"handleOpenEditingModal\"\n            @delete=\"handleDelete\"\n          >\n            <template #checkbox>\n              <input\n                v-model=\"selectedTagNames\"\n                :value=\"tag.metadata.name\"\n                type=\"checkbox\"\n              />\n            </template>\n          </TagListItem>\n        </VEntityContainer>\n      </Transition>\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', { total: total })\n          \"\n          :total=\"total\"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/tags/components/PostTag.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Tag } from \"@halo-dev/api-client\";\nimport { VTag } from \"@halo-dev/components\";\nimport Color from \"colorjs.io\";\nimport { computed } from \"vue\";\nimport { useRouter } from \"vue-router\";\n\nconst props = withDefaults(\n  defineProps<{\n    tag: Tag;\n    rounded?: boolean;\n    route?: boolean;\n  }>(),\n  {\n    rounded: false,\n    route: false,\n  }\n);\n\nconst labelColor = computed(() => {\n  const { color } = props.tag.spec;\n  if (!color) {\n    return \"inherit\";\n  }\n  const onWhite = Math.abs(Color.contrast(color, \"white\", \"APCA\"));\n  const onBlack = Math.abs(Color.contrast(color, \"black\", \"APCA\"));\n  return onWhite > onBlack ? \"white\" : \"#333\";\n});\n\nconst router = useRouter();\n\nconst handleRouteToDetail = () => {\n  if (!props.route) {\n    return;\n  }\n  router.push({\n    name: \"Tags\",\n    query: { name: props.tag.metadata.name },\n  });\n};\n</script>\n<template>\n  <VTag\n    :styles=\"{\n      background: tag.spec.color,\n      color: labelColor,\n    }\"\n    :rounded=\"rounded\"\n    @click=\"handleRouteToDetail\"\n  >\n    {{ tag.spec.displayName }}\n\n    <template #rightIcon>\n      <slot name=\"rightIcon\" />\n    </template>\n  </VTag>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/tags/components/TagEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport useSlugify from \"@console/composables/use-slugify\";\nimport { reset, submitForm, type FormKitNode } from \"@formkit/core\";\nimport type { Tag } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconArrowLeft,\n  IconArrowRight,\n  IconRefreshLine,\n  Toast,\n  VButton,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { FormType } from \"@halo-dev/ui-shared\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { computed, nextTick, onMounted, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    tag?: Tag;\n  }>(),\n  {\n    tag: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"previous\"): void;\n  (event: \"next\"): void;\n}>();\n\nconst { t } = useI18n();\n\nconst formState = ref<Tag>({\n  spec: {\n    displayName: \"\",\n    slug: \"\",\n    description: \"\",\n    cover: \"\",\n  },\n  apiVersion: \"content.halo.run/v1alpha1\",\n  kind: \"Tag\",\n  metadata: {\n    name: \"\",\n    generateName: \"tag-\",\n  },\n});\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst saving = ref(false);\n\nconst keepAddingSubmit = ref(false);\n\nconst isUpdateMode = computed(() => !!props.tag);\n\nconst modalTitle = computed(() => {\n  return isUpdateMode.value\n    ? t(\"core.post_tag.editing_modal.titles.update\")\n    : t(\"core.post_tag.editing_modal.titles.create\");\n});\n\nconst annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();\n\nconst handleSaveTag = async () => {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  formState.value.metadata.annotations = {\n    ...annotations,\n    ...customAnnotations,\n  };\n\n  try {\n    saving.value = true;\n    if (isUpdateMode.value) {\n      await coreApiClient.content.tag.updateTag({\n        name: formState.value.metadata.name,\n        tag: formState.value,\n      });\n    } else {\n      await coreApiClient.content.tag.createTag({\n        tag: formState.value,\n      });\n    }\n\n    if (keepAddingSubmit.value) {\n      reset(\"tag-form\");\n    } else {\n      modal.value?.close();\n    }\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (e) {\n    console.error(\"Failed to create tag\", e);\n  } finally {\n    saving.value = false;\n  }\n};\n\nconst handleSubmit = (keepAdding = false) => {\n  keepAddingSubmit.value = keepAdding;\n  submitForm(\"tag-form\");\n};\n\nonMounted(() => {\n  setFocus(\"displayNameInput\");\n});\n\nwatch(\n  () => props.tag,\n  (tag) => {\n    if (tag) {\n      formState.value = cloneDeep(tag);\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\n// slug\nconst { handleGenerateSlug } = useSlugify(\n  computed(() => formState.value.spec.displayName),\n  computed({\n    get() {\n      return formState.value.spec.slug;\n    },\n    set(value) {\n      formState.value.spec.slug = value;\n    },\n  }),\n  computed(() => !isUpdateMode.value),\n  FormType.TAG\n);\n\n// fixme: check if slug is unique\n// Finally, we need to check if the slug is unique in the database\nasync function slugUniqueValidation(node: FormKitNode) {\n  const value = node.value;\n  if (!value) {\n    return true;\n  }\n\n  const fieldSelector = [`spec.slug=${value}`];\n\n  if (props.tag) {\n    fieldSelector.push(`metadata.name!=${props.tag.metadata.name}`);\n  }\n\n  const { data: tagsWithSameSlug } = await coreApiClient.content.tag.listTag({\n    fieldSelector,\n    page: 1,\n    size: 1,\n  });\n\n  return !tagsWithSameSlug.total;\n}\n</script>\n<template>\n  <VModal ref=\"modal\" :title=\"modalTitle\" :width=\"700\" @close=\"emit('close')\">\n    <template #actions>\n      <span @click=\"emit('previous')\">\n        <IconArrowLeft />\n      </span>\n      <span @click=\"emit('next')\">\n        <IconArrowRight />\n      </span>\n    </template>\n\n    <FormKit\n      id=\"tag-form\"\n      type=\"form\"\n      name=\"tag-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleSaveTag\"\n    >\n      <div>\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.post_tag.editing_modal.groups.general\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              id=\"displayNameInput\"\n              v-model=\"formState.spec.displayName\"\n              name=\"displayName\"\n              :label=\"\n                $t('core.post_tag.editing_modal.fields.display_name.label')\n              \"\n              type=\"text\"\n              validation=\"required|length:0,50\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.slug\"\n              :help=\"$t('core.post_tag.editing_modal.fields.slug.help')\"\n              :label=\"$t('core.post_tag.editing_modal.fields.slug.label')\"\n              name=\"slug\"\n              type=\"text\"\n              validation=\"required|length:0,50|slugUniqueValidation\"\n              :validation-rules=\"{ slugUniqueValidation }\"\n              :validation-messages=\"{\n                slugUniqueValidation: $t(\n                  'core.common.form.validation.slug_unique'\n                ),\n              }\"\n            >\n              <template #suffix>\n                <div\n                  v-tooltip=\"\n                    $t(\n                      'core.post_tag.editing_modal.fields.slug.refresh_message'\n                    )\n                  \"\n                  class=\"group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100\"\n                  @click=\"handleGenerateSlug(true)\"\n                >\n                  <IconRefreshLine\n                    class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n                  />\n                </div>\n              </template>\n            </FormKit>\n            <FormKit\n              v-model=\"formState.spec.color\"\n              name=\"color\"\n              :help=\"$t('core.post_tag.editing_modal.fields.color.help')\"\n              :label=\"$t('core.post_tag.editing_modal.fields.color.label')\"\n              type=\"color\"\n              validation=\"length:0,50\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.cover\"\n              name=\"cover\"\n              :help=\"$t('core.post_tag.editing_modal.fields.cover.help')\"\n              :label=\"$t('core.post_tag.editing_modal.fields.cover.label')\"\n              type=\"attachment\"\n              :accepts=\"['image/*']\"\n              validation=\"length:0,1024\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.description\"\n              name=\"description\"\n              :help=\"$t('core.post_tag.editing_modal.fields.description.help')\"\n              :label=\"\n                $t('core.post_tag.editing_modal.fields.description.label')\n              \"\n              type=\"textarea\"\n              auto-height\n              :max-auto-height=\"200\"\n              validation=\"length:0,200\"\n            ></FormKit>\n          </div>\n        </div>\n      </div>\n    </FormKit>\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.post_tag.editing_modal.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"formState.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :value=\"formState.metadata.annotations\"\n          :form-data=\"formState\"\n          kind=\"Tag\"\n          group=\"content.halo.run\"\n        />\n      </div>\n    </div>\n\n    <template #footer>\n      <div class=\"flex justify-between\">\n        <VSpace>\n          <SubmitButton\n            :loading=\"saving && !keepAddingSubmit\"\n            :disabled=\"saving && keepAddingSubmit\"\n            type=\"secondary\"\n            :text=\"$t('core.common.buttons.submit')\"\n            @submit=\"handleSubmit\"\n          >\n          </SubmitButton>\n          <VButton\n            v-if=\"!isUpdateMode\"\n            :loading=\"saving && keepAddingSubmit\"\n            :disabled=\"saving && !keepAddingSubmit\"\n            @click=\"handleSubmit(true)\"\n          >\n            {{ $t(\"core.common.buttons.save_and_continue\") }}\n          </VButton>\n        </VSpace>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </div>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/tags/components/TagListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Tag } from \"@halo-dev/api-client\";\nimport {\n  IconExternalLinkLine,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport PostTag from \"./PostTag.vue\";\n\nwithDefaults(\n  defineProps<{\n    tag: Tag;\n    isSelected?: boolean;\n  }>(),\n  { isSelected: false }\n);\n\nconst emit = defineEmits<{\n  (event: \"editing\", tag: Tag): void;\n  (event: \"delete\", tag: Tag): void;\n}>();\n</script>\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template #checkbox>\n      <HasPermission :permissions=\"['system:posts:manage']\">\n        <slot name=\"checkbox\" />\n      </HasPermission>\n    </template>\n\n    <template #start>\n      <VEntityField>\n        <template #title>\n          <PostTag :tag=\"tag\" />\n        </template>\n        <template #description>\n          <VSpace>\n            <div\n              v-if=\"tag.status?.permalink\"\n              :title=\"tag.status?.permalink\"\n              target=\"_blank\"\n              class=\"truncate text-xs text-gray-500 group-hover:text-gray-900\"\n            >\n              {{ tag.status.permalink }}\n            </div>\n            <a\n              target=\"_blank\"\n              :href=\"tag.status?.permalink\"\n              class=\"text-gray-600 opacity-0 transition-all hover:text-gray-900 group-hover:opacity-100\"\n            >\n              <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n            </a>\n          </VSpace>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"tag.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField\n        :description=\"\n          $t('core.common.fields.post_count', {\n            count: tag.status?.postCount || 0,\n          })\n        \"\n      />\n      <VEntityField>\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{ utils.date.format(tag.metadata.creationTimestamp) }}\n          </span>\n        </template>\n      </VEntityField>\n    </template>\n    <template #dropdownItems>\n      <HasPermission :permissions=\"['system:posts:manage']\">\n        <VDropdownItem @click=\"emit('editing', tag)\">\n          {{ $t(\"core.common.buttons.edit\") }}\n        </VDropdownItem>\n        <VDropdownItem type=\"danger\" @click=\"emit('delete', tag)\">\n          {{ $t(\"core.common.buttons.delete\") }}\n        </VDropdownItem>\n      </HasPermission>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/contents/posts/tags/composables/use-post-tag.ts",
    "content": "import type {\n  Tag,\n  TagV1alpha1ConsoleApiListPostTagsRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  paginate,\n} from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { useQuery, type QueryObserverResult } from \"@tanstack/vue-query\";\nimport { ref, watch, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\ninterface usePostTagReturn {\n  tags: Ref<Tag[] | undefined>;\n  total: Ref<number>;\n  hasPrevious: Ref<boolean>;\n  hasNext: Ref<boolean>;\n  isLoading: Ref<boolean>;\n  isFetching: Ref<boolean>;\n  handleFetchTags: () => Promise<QueryObserverResult<Tag[], unknown>>;\n  handleDelete: (tag: Tag) => void;\n  handleDeleteInBatch: (tagNames: string[]) => Promise<void>;\n}\n\nexport function usePostTag(filterOptions?: {\n  sort?: Ref<string | undefined>;\n  page?: Ref<number>;\n  size?: Ref<number>;\n  keyword?: Ref<string>;\n}): usePostTagReturn {\n  const { t } = useI18n();\n\n  const { sort, page, size, keyword } = filterOptions || {};\n\n  const total = ref(0);\n  const hasPrevious = ref(false);\n  const hasNext = ref(false);\n\n  const {\n    data: tags,\n    isLoading,\n    isFetching,\n    refetch,\n  } = useQuery({\n    queryKey: [\"post-tags\", sort, page, size, keyword],\n    queryFn: async () => {\n      const { data } = await consoleApiClient.content.tag.listPostTags({\n        page: page?.value || 0,\n        size: size?.value || 0,\n        sort: [sort?.value as string].filter(Boolean) || [\n          \"metadata.creationTimestamp,desc\",\n        ],\n        keyword: keyword?.value,\n      });\n\n      total.value = data.total;\n      hasPrevious.value = data.hasPrevious;\n      hasNext.value = data.hasNext;\n\n      return data.items;\n    },\n    refetchInterval(data) {\n      const hasAbnormalTag = data?.some(\n        (tag) => !!tag.metadata.deletionTimestamp || !tag.status?.permalink\n      );\n      return hasAbnormalTag ? 1000 : false;\n    },\n  });\n\n  const handleDelete = async (tag: Tag) => {\n    Dialog.warning({\n      title: t(\"core.post_tag.operations.delete.title\"),\n      description: t(\"core.post_tag.operations.delete.description\"),\n      confirmType: \"danger\",\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        try {\n          await coreApiClient.content.tag.deleteTag({\n            name: tag.metadata.name,\n          });\n\n          Toast.success(t(\"core.common.toast.delete_success\"));\n        } catch (e) {\n          console.error(\"Failed to delete tag\", e);\n        } finally {\n          await refetch();\n        }\n      },\n    });\n  };\n\n  const handleDeleteInBatch = (tagNames: string[]) => {\n    return new Promise<void>((resolve) => {\n      Dialog.warning({\n        title: t(\"core.post_tag.operations.delete_in_batch.title\"),\n        description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n        confirmType: \"danger\",\n        confirmText: t(\"core.common.buttons.confirm\"),\n        cancelText: t(\"core.common.buttons.cancel\"),\n        onConfirm: async () => {\n          try {\n            await Promise.all(\n              tagNames.map((tagName) => {\n                coreApiClient.content.tag.deleteTag({\n                  name: tagName,\n                });\n              })\n            );\n\n            Toast.success(t(\"core.common.toast.delete_success\"));\n            resolve();\n          } catch (e) {\n            console.error(\"Failed to delete tags in batch\", e);\n          } finally {\n            await refetch();\n          }\n        },\n      });\n    });\n  };\n\n  watch(\n    () => [sort?.value, keyword?.value],\n    () => {\n      if (page?.value) {\n        page.value = 1;\n      }\n    }\n  );\n\n  return {\n    tags,\n    total,\n    hasPrevious,\n    hasNext,\n    isLoading,\n    isFetching,\n    handleFetchTags: refetch,\n    handleDelete,\n    handleDeleteInBatch,\n  };\n}\n\nexport function useAllPostTagsQuery() {\n  return useQuery({\n    queryKey: [\"core:post-tags:all\"],\n    queryFn: async () => {\n      return await paginate<TagV1alpha1ConsoleApiListPostTagsRequest, Tag>(\n        (params) => consoleApiClient.content.tag.listPostTags(params),\n        {\n          sort: [\"metadata.creationTimestamp,desc\"],\n          size: 1000,\n        }\n      );\n    },\n    refetchInterval(data) {\n      const hasAbnormalTag = data?.some(\n        (tag) => !!tag.metadata.deletionTimestamp || !tag.status?.permalink\n      );\n      return hasAbnormalTag ? 1000 : false;\n    },\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/Dashboard.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconDashboard,\n  IconSettings,\n  VButton,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport type { DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { computed, type ComputedRef, provide, ref, toRaw } from \"vue\";\nimport WidgetViewItem from \"./components/WidgetViewItem.vue\";\nimport { useDashboardExtensionPoint } from \"./composables/use-dashboard-extension-point\";\nimport { useDashboardWidgetsViewFetch } from \"./composables/use-dashboard-widgets-fetch\";\nimport \"./styles/dashboard.css\";\nimport { internalWidgetDefinitions } from \"./widgets\";\n\nconst currentBreakpoint = ref();\n\nfunction breakpointChangedEvent(breakpoint: string) {\n  currentBreakpoint.value = breakpoint;\n}\n\nconst { data } = useDashboardWidgetsViewFetch(currentBreakpoint);\n\nconst { widgetDefinitions } = useDashboardExtensionPoint();\n\nconst availableWidgetDefinitions = computed(() => {\n  return [...internalWidgetDefinitions, ...widgetDefinitions.value];\n});\n\nprovide<ComputedRef<DashboardWidgetDefinition[]>>(\n  \"availableWidgetDefinitions\",\n  availableWidgetDefinitions\n);\n</script>\n<template>\n  <VPageHeader :title=\"$t('core.dashboard.title')\">\n    <template #icon>\n      <IconDashboard />\n    </template>\n    <template #actions>\n      <VButton\n        type=\"secondary\"\n        @click=\"$router.push({ name: 'DashboardDesigner' })\"\n      >\n        <template #icon>\n          <IconSettings />\n        </template>\n        {{ $t(\"core.dashboard.actions.setting\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"dashboard m-4\">\n    <grid-layout\n      :layout=\"toRaw(data?.layout || [])\"\n      :responsive-layouts=\"data?.layouts\"\n      :col-num=\"12\"\n      :is-draggable=\"false\"\n      :is-resizable=\"false\"\n      :margin=\"[10, 10]\"\n      :responsive=\"true\"\n      :row-height=\"30\"\n      :use-css-transforms=\"true\"\n      :vertical-compact=\"true\"\n      :breakpoints=\"{ lg: 1200, md: 996, sm: 768, xs: 480 }\"\n      :cols=\"{ lg: 12, md: 12, sm: 6, xs: 4 }\"\n      @breakpoint-changed=\"breakpointChangedEvent\"\n    >\n      <WidgetViewItem v-for=\"item in data?.layout\" :key=\"item.i\" :item=\"item\" />\n    </grid-layout>\n  </div>\n</template>\n\n<style scoped>\n:deep(.vue-grid-item) {\n  -ms-touch-action: unset !important;\n  touch-action: unset !important;\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/DashboardDesigner.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconComputer,\n  IconDashboard,\n  IconPhone,\n  IconSave,\n  IconTablet,\n  Toast,\n  VButton,\n  VDropdown,\n  VDropdownItem,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport {\n  utils,\n  type DashboardWidget,\n  type DashboardWidgetDefinition,\n} from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useEventListener } from \"@vueuse/core\";\nimport { cloneDeep, isEqual } from \"es-toolkit\";\nimport {\n  computed,\n  defineComponent,\n  h,\n  markRaw,\n  provide,\n  ref,\n  useTemplateRef,\n  watch,\n  type ComputedRef,\n} from \"vue\";\nimport type { GridLayout } from \"vue-grid-layout\";\nimport { useI18n } from \"vue-i18n\";\nimport { onBeforeRouteLeave, useRouter } from \"vue-router\";\nimport RiBox3Line from \"~icons/ri/box-3-line\";\nimport RiFileCopyLine from \"~icons/ri/file-copy-line\";\nimport WidgetEditableItem from \"./components/WidgetEditableItem.vue\";\nimport WidgetHubModal from \"./components/WidgetHubModal.vue\";\nimport { useDashboardExtensionPoint } from \"./composables/use-dashboard-extension-point\";\nimport { useDashboardWidgetsFetch } from \"./composables/use-dashboard-widgets-fetch\";\nimport \"./styles/dashboard.css\";\nimport { internalWidgetDefinitions } from \"./widgets\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\nconst router = useRouter();\n\nconst currentBreakpoint = ref(\"lg\");\nconst originalBreakpoint = ref();\n\nconst gridLayoutRef =\n  useTemplateRef<InstanceType<typeof GridLayout>>(\"gridLayoutRef\");\n\nconst { widgetDefinitions } = useDashboardExtensionPoint();\n\nconst availableWidgetDefinitions = computed(() => {\n  return [...internalWidgetDefinitions, ...widgetDefinitions.value];\n});\n\nprovide<ComputedRef<DashboardWidgetDefinition[]>>(\n  \"availableWidgetDefinitions\",\n  availableWidgetDefinitions\n);\n\nconst { layouts, layout, originalLayout, isLoading } =\n  useDashboardWidgetsFetch(currentBreakpoint);\n\nconst hasLayoutChanged = computed(() => {\n  if (isLoading.value) {\n    return false;\n  }\n  return !isEqual(originalLayout.value, layout.value);\n});\n\nwatch(\n  () => layout.value,\n  () => {\n    layouts.value[currentBreakpoint.value] = layout.value;\n    if (currentBreakpoint.value === \"xs\") {\n      layouts.value.xxs = layout.value;\n    }\n    if (gridLayoutRef.value) {\n      gridLayoutRef.value.initResponsiveFeatures();\n    }\n  },\n  {\n    immediate: true,\n    deep: true,\n  }\n);\n\nconst selectBreakpoint = ref();\n\nasync function handleBreakpointChange(breakpoint: string | number) {\n  if (isLoading.value) {\n    return;\n  }\n\n  if (hasLayoutChanged.value) {\n    Toast.error(\n      t(\"core.dashboard_designer.operations.change_breakpoint.tips_not_saved\")\n    );\n    return;\n  }\n\n  selectBreakpoint.value = breakpoint;\n}\n\nfunction onBreakpointChange(breakpoint: string) {\n  if (!originalBreakpoint.value) {\n    originalBreakpoint.value = breakpoint;\n  }\n  if (!selectBreakpoint.value) {\n    selectBreakpoint.value = breakpoint;\n  }\n  currentBreakpoint.value = breakpoint;\n}\n\nconst deviceOptionDefinitions = [\n  {\n    id: \"lg\",\n    pixels: 1200,\n    text: t(\"core.dashboard_designer.breakpoints.lg\"),\n    icon: markRaw(IconComputer),\n  },\n  {\n    id: \"md\",\n    pixels: 996,\n    text: t(\"core.dashboard_designer.breakpoints.md\"),\n    icon: markRaw(\n      defineComponent({\n        render() {\n          return h(IconTablet, {\n            class: \"-rotate-90\",\n          });\n        },\n      })\n    ),\n  },\n  {\n    id: \"sm\",\n    pixels: 768,\n    text: t(\"core.dashboard_designer.breakpoints.sm\"),\n    icon: markRaw(IconTablet),\n  },\n  {\n    id: \"xs\",\n    pixels: 480,\n    text: t(\"core.dashboard_designer.breakpoints.xs\"),\n    icon: markRaw(IconPhone),\n  },\n];\n\nconst deviceOptions = computed(() => {\n  const breakpointOrder = [\"lg\", \"md\", \"sm\", \"xs\"];\n  const currentIndex = breakpointOrder.indexOf(originalBreakpoint.value);\n\n  return deviceOptionDefinitions.filter((option) => {\n    const optionIndex = breakpointOrder.indexOf(option.id);\n    return optionIndex >= currentIndex;\n  });\n});\n\nconst designContainerStyles = computed(() => {\n  if (currentBreakpoint.value === \"lg\" && !selectBreakpoint.value) {\n    return {};\n  }\n  if (originalBreakpoint.value === \"xs\") {\n    return {};\n  }\n  if (selectBreakpoint.value === \"lg\") {\n    return {};\n  }\n  const device = deviceOptions.value.find(\n    (option) => option.id === selectBreakpoint.value\n  );\n  return {\n    width: `${device?.pixels}px`,\n    margin: \"1rem auto\",\n    border: \"1px dashed #e0e0e0\",\n    borderRadius: \"0.75rem\",\n    padding: \"2px\",\n  };\n});\n\nfunction handleAddWidget(widgetDefinition: DashboardWidgetDefinition) {\n  const zeroXWidgets = layout.value.filter((widget) => widget.x === 0);\n  const maxY = zeroXWidgets.reduce((max, widget) => {\n    return Math.max(max, widget.y + widget.h);\n  }, 0);\n\n  const newWidget: DashboardWidget = {\n    i: utils.id.uuid(),\n    x: 0,\n    y: maxY + 1,\n    w: widgetDefinition.defaultSize.w,\n    h: widgetDefinition.defaultSize.h,\n    minW: widgetDefinition.defaultSize.minW,\n    minH: widgetDefinition.defaultSize.minH,\n    maxW: widgetDefinition.defaultSize.maxW,\n    maxH: widgetDefinition.defaultSize.maxH,\n    id: widgetDefinition.id,\n    config: widgetDefinition.defaultConfig,\n    permissions: widgetDefinition.permissions,\n  };\n\n  const newLayout = [...layout.value, newWidget];\n\n  layout.value = newLayout;\n\n  widgetsHubModalVisible.value = false;\n\n  window.scrollTo({\n    top: document.body.scrollHeight,\n    behavior: \"smooth\",\n  });\n}\n\nfunction handleRemove(item: DashboardWidget) {\n  const widgetsToUpdate = cloneDeep(layout.value);\n  widgetsToUpdate.splice(\n    widgetsToUpdate.findIndex((widget) => widget.i === item.i),\n    1\n  );\n  layout.value = widgetsToUpdate;\n}\n\nfunction handleUpdate(item: DashboardWidget) {\n  const widgetsToUpdate = cloneDeep(layout.value);\n  const index = widgetsToUpdate.findIndex((widget) => widget.i === item.i);\n  widgetsToUpdate[index] = item;\n  layout.value = widgetsToUpdate;\n}\n\nconst widgetsHubModalVisible = ref(false);\n\nconst isSubmitting = ref(false);\n\nasync function handleSave() {\n  try {\n    isSubmitting.value = true;\n\n    await ucApiClient.user.preference.updateMyPreference({\n      group: \"dashboard-widgets\",\n      body: layouts.value,\n    });\n\n    await queryClient.invalidateQueries({\n      queryKey: [\"core:dashboard:widgets\"],\n    });\n\n    await queryClient.invalidateQueries({\n      queryKey: [\"core:dashboard:widgets:view\"],\n    });\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (error) {\n    console.error(\"Failed to save dashboard widgets config\", error);\n  } finally {\n    isSubmitting.value = false;\n  }\n}\n\nfunction handleBack() {\n  router.replace({ name: \"Dashboard\" });\n}\n\nonBeforeRouteLeave((_, __, next) => {\n  if (hasLayoutChanged.value) {\n    handleShowLeaveWarning(next);\n    return;\n  }\n  next();\n});\n\nfunction handleShowLeaveWarning(next: () => void) {\n  Dialog.warning({\n    title: t(\"core.dashboard_designer.operations.back.title\"),\n    description: t(\"core.dashboard_designer.operations.back.description\"),\n    confirmText: t(\"core.dashboard_designer.operations.back.confirm_text\"),\n    confirmType: \"danger\",\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: () => {\n      next();\n    },\n  });\n}\n\nuseEventListener(window, \"beforeunload\", (e) => {\n  if (hasLayoutChanged.value) {\n    e.preventDefault();\n    e.returnValue = t(\"core.dashboard_designer.operations.back.description\");\n    return t(\"core.dashboard_designer.operations.back.description\");\n  }\n});\n\nfunction handleCopyFromLayout(breakpoint: string) {\n  const layoutToCopy = layouts.value[breakpoint] as DashboardWidget[];\n  if (!layoutToCopy) {\n    return;\n  }\n\n  const zeroXWidgets = layout.value.filter((widget) => widget.x === 0);\n  const maxY = zeroXWidgets.reduce((max, widget) => {\n    return Math.max(max, widget.y + widget.h);\n  }, 0);\n\n  layout.value = [\n    ...layout.value,\n    ...layoutToCopy.map((widget, index) => ({\n      ...widget,\n      i: utils.id.uuid(),\n      y: maxY + index + 1,\n    })),\n  ];\n}\n</script>\n<template>\n  <div class=\"page-header py-1.5\">\n    <h2 class=\"page-header__title\">\n      <IconDashboard class=\"mr-2 self-center\" />\n      <span class=\"page-header__title-text\">\n        {{ $t(\"core.dashboard_designer.title\") }}\n      </span>\n    </h2>\n    <div\n      class=\"hidden sm:block\"\n      :class=\"{ '!cursor-progress opacity-50': isLoading }\"\n    >\n      <VTabbar\n        :active-id=\"selectBreakpoint\"\n        :items=\"deviceOptions as any\"\n        type=\"outline\"\n        @change=\"handleBreakpointChange\"\n      ></VTabbar>\n    </div>\n    <div class=\"page-header__actions\">\n      <VButton ghost @click=\"handleBack\">\n        {{ $t(\"core.common.buttons.back\") }}\n      </VButton>\n      <VDropdown>\n        <VButton>\n          <template #icon>\n            <IconAddCircle />\n          </template>\n          {{ $t(\"core.dashboard_designer.actions.add_widget\") }}\n        </VButton>\n        <template #popper>\n          <VDropdownItem @click=\"widgetsHubModalVisible = true\">\n            <template #prefix-icon>\n              <RiBox3Line />\n            </template>\n            {{\n              $t(\"core.dashboard_designer.operations.open_widgets_hub.button\")\n            }}\n          </VDropdownItem>\n          <VDropdown :triggers=\"['click']\" placement=\"left\">\n            <VDropdownItem>\n              <template #prefix-icon>\n                <RiFileCopyLine />\n              </template>\n              {{\n                $t(\"core.dashboard_designer.operations.copy_from_layout.button\")\n              }}\n            </VDropdownItem>\n            <template #popper>\n              <VDropdownItem\n                v-for=\"item in [\n                  ...deviceOptionDefinitions.filter(\n                    (item) => item.id !== currentBreakpoint\n                  ),\n                ]\"\n                :key=\"item.id\"\n                @click=\"handleCopyFromLayout(item.id)\"\n              >\n                <template #prefix-icon>\n                  <component :is=\"item.icon\" />\n                </template>\n                {{ item.text }}\n              </VDropdownItem>\n            </template>\n          </VDropdown>\n        </template>\n      </VDropdown>\n      <VButton\n        :disabled=\"!hasLayoutChanged\"\n        type=\"secondary\"\n        :loading=\"isSubmitting\"\n        @click=\"handleSave\"\n      >\n        <template #icon>\n          <IconSave />\n        </template>\n        {{ $t(\"core.common.buttons.save\") }}\n      </VButton>\n    </div>\n  </div>\n\n  <div class=\"dashboard m-4 transition-all\" :style=\"designContainerStyles\">\n    <grid-layout\n      ref=\"gridLayoutRef\"\n      v-model:layout=\"layout\"\n      :responsive-layouts=\"layouts\"\n      :col-num=\"12\"\n      :is-draggable=\"true\"\n      :is-resizable=\"true\"\n      :margin=\"[10, 10]\"\n      :responsive=\"true\"\n      :row-height=\"30\"\n      :use-css-transforms=\"true\"\n      :vertical-compact=\"true\"\n      :breakpoints=\"{ lg: 1200, md: 996, sm: 768, xs: 480 }\"\n      :cols=\"{ lg: 12, md: 12, sm: 6, xs: 4 }\"\n      @breakpoint-changed=\"onBreakpointChange\"\n    >\n      <WidgetEditableItem\n        v-for=\"item in layout\"\n        :key=\"item.i\"\n        :item=\"item\"\n        @remove=\"handleRemove(item)\"\n        @update=\"handleUpdate\"\n      />\n    </grid-layout>\n  </div>\n  <WidgetHubModal\n    v-if=\"widgetsHubModalVisible\"\n    @close=\"widgetsHubModalVisible = false\"\n    @add-widget=\"handleAddWidget\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/components/ActionButton.vue",
    "content": "<script setup lang=\"ts\"></script>\n<template>\n  <div\n    class=\"flex h-full w-8 cursor-pointer items-center justify-center text-base text-white transition-all hover:opacity-80\"\n  >\n    <slot />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/components/WidgetCard.vue",
    "content": "<script setup lang=\"ts\">\ndefineProps<{\n  title?: string;\n  bodyClass?: string[];\n}>();\n</script>\n<template>\n  <div\n    class=\"flex h-full w-full flex-col overflow-hidden rounded-lg bg-white shadow-sm ring-1 ring-[#eaecf0]\"\n  >\n    <div\n      v-if=\"title || $slots.title || $slots.actions\"\n      class=\"flex h-10 flex-none items-center justify-between border-b border-[#eaecf0] px-4\"\n    >\n      <slot name=\"title\">\n        <div class=\"flex-1 shrink text-base font-medium\">\n          {{ title }}\n        </div>\n      </slot>\n      <div class=\"flex-none text-base font-medium\">\n        <slot name=\"actions\" />\n      </div>\n    </div>\n    <div :class=\"bodyClass\" class=\"min-h-0 w-full flex-1 shrink\">\n      <slot />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/components/WidgetConfigFormModal.vue",
    "content": "<script setup lang=\"ts\">\nimport type { FormKitSchemaDefinition } from \"@formkit/core\";\nimport { VButton, VLoading, VModal, VSpace } from \"@halo-dev/components\";\nimport type { DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { onMounted, ref, toRaw, useTemplateRef } from \"vue\";\n\nconst props = defineProps<{\n  widgetDefinition: DashboardWidgetDefinition;\n  config?: Record<string, unknown>;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"close\"): void;\n  (e: \"save\", config: Record<string, unknown>): void;\n}>();\n\nconst formSchema = ref<FormKitSchemaDefinition>();\nconst isLoading = ref(false);\n\nonMounted(async () => {\n  const { configFormKitSchema } = props.widgetDefinition || {};\n  const isFunction = typeof configFormKitSchema === \"function\";\n\n  try {\n    isLoading.value = true;\n    const schema = isFunction\n      ? await configFormKitSchema()\n      : configFormKitSchema;\n    formSchema.value = (schema || []) as FormKitSchemaDefinition;\n  } catch (e) {\n    console.error(e);\n  } finally {\n    isLoading.value = false;\n  }\n});\n\nconst initialConfig =\n  props.config || props.widgetDefinition.defaultConfig || {};\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n\nfunction onSubmit(config: Record<string, unknown>) {\n  emit(\"save\", config);\n}\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    mount-to-body\n    :title=\"$t('core.dashboard_designer.config_modal.title')\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <VLoading v-if=\"isLoading\" />\n      <FormKit\n        v-if=\"formSchema\"\n        :id=\"widgetDefinition.id\"\n        :value=\"initialConfig\"\n        :name=\"widgetDefinition.id\"\n        :preserve=\"true\"\n        type=\"form\"\n        @submit=\"onSubmit\"\n      >\n        <FormKitSchema\n          :schema=\"toRaw(formSchema)\"\n          :data=\"toRaw(initialConfig)\"\n        />\n      </FormKit>\n    </div>\n    <template #footer>\n      <VSpace>\n        <VButton type=\"secondary\" @click=\"$formkit.submit(widgetDefinition.id)\">\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/components/WidgetEditableItem.vue",
    "content": "<script setup lang=\"ts\">\nimport { IconCloseCircle, IconSettings } from \"@halo-dev/components\";\nimport {\n  utils,\n  type DashboardWidget,\n  type DashboardWidgetDefinition,\n} from \"@halo-dev/ui-shared\";\nimport { computed, inject, ref, type ComputedRef } from \"vue\";\nimport ActionButton from \"./ActionButton.vue\";\nimport WidgetConfigFormModal from \"./WidgetConfigFormModal.vue\";\n\nconst props = defineProps<{\n  item: DashboardWidget;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"remove\"): void;\n  (e: \"update\", item: DashboardWidget): void;\n}>();\n\nconst availableWidgetDefinitions = inject<\n  ComputedRef<DashboardWidgetDefinition[]>\n>(\"availableWidgetDefinitions\");\n\nconst widgetDefinition = computed(() => {\n  return availableWidgetDefinitions?.value?.find(\n    (definition) => definition.id === props.item.id\n  );\n});\n\nconst configModalVisible = ref(false);\n\nfunction handleSaveConfig(config: Record<string, unknown>) {\n  emit(\"update\", {\n    ...props.item,\n    config,\n  });\n  configModalVisible.value = false;\n}\n</script>\n<template>\n  <grid-item\n    v-if=\"utils.permission.has(item.permissions || [])\"\n    :key=\"item.i\"\n    class=\"group/grid-item\"\n    :h=\"item.h\"\n    :i=\"item.i\"\n    :w=\"item.w\"\n    :x=\"item.x\"\n    :y=\"item.y\"\n    :min-w=\"item.minW\"\n    :min-h=\"item.minH\"\n    :max-w=\"item.maxW\"\n    :max-h=\"item.maxH\"\n  >\n    <component\n      :is=\"widgetDefinition?.component\"\n      edit-mode\n      :config=\"item.config\"\n      @update:config=\"handleSaveConfig\"\n    />\n    <div\n      class=\"absolute right-0 top-0 z-[100] hidden h-8 items-center overflow-hidden rounded-tr-lg bg-gray-100 group-hover/grid-item:inline-flex\"\n    >\n      <ActionButton\n        v-if=\"widgetDefinition?.configFormKitSchema\"\n        class=\"bg-black\"\n        @click=\"configModalVisible = true\"\n      >\n        <IconSettings />\n      </ActionButton>\n      <ActionButton class=\"bg-red-500\" @click=\"emit('remove')\">\n        <IconCloseCircle />\n      </ActionButton>\n    </div>\n  </grid-item>\n  <WidgetConfigFormModal\n    v-if=\"\n      widgetDefinition &&\n      widgetDefinition.configFormKitSchema &&\n      configModalVisible\n    \"\n    :widget-definition=\"widgetDefinition\"\n    :config=\"item.config\"\n    @close=\"configModalVisible = false\"\n    @save=\"handleSaveConfig\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/components/WidgetHubModal.vue",
    "content": "<script setup lang=\"ts\">\nimport { VButton, VModal, VTabbar } from \"@halo-dev/components\";\nimport { utils, type DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { computed, inject, ref, useTemplateRef, type ComputedRef } from \"vue\";\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"add-widget\", widget: DashboardWidgetDefinition): void;\n}>();\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n\nconst activeId = ref(\"\");\n\nconst availableWidgetDefinitions = inject<\n  ComputedRef<DashboardWidgetDefinition[]>\n>(\"availableWidgetDefinitions\");\n\nconst groupedWidgetDefinitions = computed(() => {\n  const filteredWidgets = availableWidgetDefinitions?.value?.filter(\n    (widget) => activeId.value === \"\" || widget.group === activeId.value\n  );\n\n  const groups = filteredWidgets?.reduce(\n    (acc, widget) => {\n      const key = `${widget.defaultSize.w}-${widget.defaultSize.h}`;\n      if (!acc[key]) {\n        acc[key] = [];\n      }\n      acc[key].push(widget);\n      return acc;\n    },\n    {} as Record<string, DashboardWidgetDefinition[]>\n  );\n\n  return Object.entries(groups || {})\n    .map(([key, widgets]) => {\n      const [w, h] = key.split(\"-\").map(Number);\n      return { w, h, widgets };\n    })\n    .sort((a, b) => {\n      if (a.w !== b.w) {\n        return a.w - b.w;\n      }\n      return a.h - b.h;\n    })\n    .map((group) => group.widgets);\n});\n\nconst groupWidgetDefinitions = computed(() => {\n  return availableWidgetDefinitions?.value?.reduce(\n    (acc, item) => {\n      acc[item.group] = acc[item.group] || [];\n      acc[item.group].push(item);\n      return acc;\n    },\n    {} as Record<string, DashboardWidgetDefinition[]>\n  );\n});\n\nconst groupWidgetDefinitionsKeys = computed(() => {\n  return Object.keys(groupWidgetDefinitions.value || {});\n});\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    height=\"calc(100vh - 20px)\"\n    :width=\"1380\"\n    :layer-closable=\"true\"\n    :title=\"$t('core.dashboard_designer.widgets_modal.title')\"\n    mount-to-body\n    @close=\"emit('close')\"\n  >\n    <VTabbar\n      v-model:active-id=\"activeId\"\n      :items=\"[\n        { id: '', label: $t('core.common.text.all') },\n        ...groupWidgetDefinitionsKeys.map((group) => {\n          return { id: group, label: $t(group, group) };\n        }),\n      ]\"\n      type=\"outline\"\n    ></VTabbar>\n    <div class=\"-m-2 mt-4 flex flex-col gap-5\">\n      <div\n        v-for=\"(group, index) in groupedWidgetDefinitions\"\n        :key=\"index\"\n        class=\"flex flex-wrap\"\n      >\n        <template v-for=\"item in group\" :key=\"item.id\">\n          <div\n            v-if=\"\n              (activeId === '' || item.group === activeId) &&\n              utils.permission.has(item.permissions || [])\n            \"\n            :style=\"{\n              width: `${item.defaultSize.w * 100}px`,\n              height: `${item.defaultSize.h * 36}px`,\n            }\"\n            class=\"cursor-pointer p-2\"\n            @click=\"emit('add-widget', item)\"\n          >\n            <div class=\"pointer-events-none h-full w-full\">\n              <component\n                :is=\"item.component\"\n                preview-mode\n                :config=\"item.defaultConfig\"\n              />\n            </div>\n          </div>\n        </template>\n      </div>\n    </div>\n    <template #footer>\n      <VButton @click=\"modal?.close()\">\n        {{ $t(\"core.common.buttons.close\") }}\n      </VButton>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/components/WidgetViewItem.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  utils,\n  type DashboardWidget,\n  type DashboardWidgetDefinition,\n} from \"@halo-dev/ui-shared\";\nimport { inject, type ComputedRef } from \"vue\";\n\ndefineProps<{\n  item: DashboardWidget;\n}>();\n\nconst availableWidgetDefinitions = inject<\n  ComputedRef<DashboardWidgetDefinition[]>\n>(\"availableWidgetDefinitions\");\n\nconst getWidgetDefinition = (id: string) => {\n  return availableWidgetDefinitions?.value?.find(\n    (definition) => definition.id === id\n  );\n};\n</script>\n\n<template>\n  <grid-item\n    v-if=\"utils.permission.has(item.permissions || [])\"\n    :key=\"item.i\"\n    :h=\"item.h\"\n    :i=\"item.i\"\n    :w=\"item.w\"\n    :x=\"item.x\"\n    :y=\"item.y\"\n    :min-w=\"item.minW\"\n    :min-h=\"item.minH\"\n    :max-w=\"item.maxW\"\n    :max-h=\"item.maxH\"\n  >\n    <component\n      :is=\"getWidgetDefinition(item.id)?.component\"\n      :config=\"item.config\"\n    />\n  </grid-item>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/composables/use-dashboard-extension-point.ts",
    "content": "import type { DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { onMounted, shallowRef } from \"vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nconst EXTENSION_POINT_NAME = \"console:dashboard:widgets:create\";\n\nexport function useDashboardExtensionPoint() {\n  const { pluginModuleMap } = usePluginModuleStore();\n\n  const widgetDefinitions = shallowRef<DashboardWidgetDefinition[]>([]);\n\n  onMounted(async () => {\n    const finalDefinitions: DashboardWidgetDefinition[] = [];\n    for (const [name, module] of Object.entries(pluginModuleMap)) {\n      try {\n        const callbackFunction =\n          module?.extensionPoints?.[EXTENSION_POINT_NAME];\n\n        if (typeof callbackFunction !== \"function\") {\n          continue;\n        }\n\n        const definitions = await callbackFunction();\n\n        // Reset id\n        definitions.forEach((definition) => {\n          definition.id = `${name}-${definition.id}`;\n        });\n\n        finalDefinitions.push(...definitions);\n      } catch (error) {\n        console.error(`Error processing plugin module:`, name, error);\n      }\n    }\n    widgetDefinitions.value = finalDefinitions;\n  });\n\n  return { widgetDefinitions };\n}\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/composables/use-dashboard-widgets-fetch.ts",
    "content": "import { ucApiClient } from \"@halo-dev/api-client\";\nimport type {\n  DashboardResponsiveLayout,\n  DashboardWidget,\n} from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { computed, ref, type Ref } from \"vue\";\nimport { DefaultResponsiveLayouts } from \"../widgets/defaults\";\n\nexport function useDashboardWidgetsFetch(breakpoint: Ref<string>) {\n  const layouts = ref<DashboardResponsiveLayout>({});\n  const layout = ref<DashboardWidget[]>([]);\n  const originalLayout = ref<DashboardWidget[]>([]);\n\n  const { isLoading } = useQuery({\n    queryKey: [\"core:dashboard:widgets\", breakpoint],\n    queryFn: async () => {\n      const { data } = await ucApiClient.user.preference.getMyPreference({\n        group: \"dashboard-widgets\",\n      });\n      if (!data) {\n        return null;\n      }\n      return data as DashboardResponsiveLayout;\n    },\n    cacheTime: 0,\n    onSuccess: (data) => {\n      layouts.value = data || DefaultResponsiveLayouts;\n\n      const layoutData =\n        layouts.value[breakpoint.value] || layouts.value[\"lg\"] || [];\n\n      layout.value = layoutData;\n      originalLayout.value = cloneDeep(layoutData);\n    },\n    enabled: computed(() => !!breakpoint.value),\n  });\n\n  return {\n    layouts,\n    layout,\n    originalLayout,\n    isLoading,\n  };\n}\n\nexport function useDashboardWidgetsViewFetch(breakpoint: Ref<string>) {\n  return useQuery({\n    queryKey: [\"core:dashboard:widgets:view\", breakpoint],\n    queryFn: async () => {\n      const { data } = await ucApiClient.user.preference.getMyPreference({\n        group: \"dashboard-widgets\",\n      });\n\n      const layouts = (data ||\n        DefaultResponsiveLayouts) as DashboardResponsiveLayout;\n\n      return {\n        layouts,\n        layout: layouts[breakpoint.value] || layouts.lg || [],\n      };\n    },\n    enabled: computed(() => !!breakpoint.value),\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconDashboard } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport WidgetCard from \"./components/WidgetCard.vue\";\n\ndeclare module \"vue\" {\n  interface GlobalComponents {\n    WidgetCard: (typeof import(\"./components/WidgetCard.vue\"))[\"default\"];\n  }\n}\n\nexport default definePlugin({\n  components: {\n    WidgetCard,\n  },\n  routes: [\n    {\n      path: \"/\",\n      component: BasicLayout,\n      name: \"Root\",\n      redirect: \"/dashboard\",\n      children: [\n        {\n          path: \"dashboard\",\n          name: \"Dashboard\",\n          component: () => import(\"./Dashboard.vue\"),\n          meta: {\n            title: \"core.dashboard.title\",\n            searchable: true,\n            menu: {\n              name: \"core.sidebar.menu.items.dashboard\",\n              group: \"dashboard\",\n              icon: markRaw(IconDashboard),\n              priority: 0,\n              mobile: true,\n            },\n          },\n        },\n        {\n          path: \"dashboard/designer\",\n          name: \"DashboardDesigner\",\n          component: () => import(\"./DashboardDesigner.vue\"),\n          meta: {\n            title: \"core.dashboard_designer.title\",\n            searchable: false,\n          },\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/styles/dashboard.css",
    "content": ".vue-grid-layout {\n  @apply -m-[10px];\n}\n\n.vue-grid-item {\n  transition: none !important;\n}\n\n.vue-grid-item.vue-grid-placeholder {\n  @apply bg-gray-200 !important;\n  @apply opacity-100 !important;\n}\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/defaults.ts",
    "content": "import type { DashboardResponsiveLayout } from \"@halo-dev/ui-shared\";\n\nconst DefaultResponsiveLayouts: DashboardResponsiveLayout = {\n  lg: [\n    {\n      i: \"79187a8d-1c35-497f-a55e-05c0a6258660\",\n      x: 0,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:post:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"96a9ac85-ec47-4c4b-bd02-184ee047f6a5\",\n      x: 6,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:comment:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"ee42743e-06d8-4106-9481-42443bb8b2f0\",\n      x: 3,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:user:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"19a92835-c12d-486b-9546-f16bd7a7da98\",\n      x: 9,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:view:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"e45cd079-a406-42de-a196-e11c64f6d893\",\n      x: 0,\n      y: 3,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:quick-action\",\n      config: {\n        enabled_items: [\n          \"core:user-center\",\n          \"core:theme-preview\",\n          \"core:new-post\",\n          \"core:new-page\",\n          \"core:upload-attachment\",\n          \"core:theme-manage\",\n          \"core:plugin-manage\",\n          \"core:new-user\",\n          \"core:refresh-search-engine\",\n        ],\n      },\n    },\n    {\n      i: \"3bcd12eb-44ee-48e8-a37f-3f5d73470e2d\",\n      x: 6,\n      y: 3,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:notifications\",\n      config: {},\n    },\n  ],\n  xs: [\n    {\n      i: \"2026e492-0832-4a27-b44b-ab076e31ec17\",\n      x: 0,\n      y: 0,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:post:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"7c868acd-10e8-4e92-99f3-ff00b4aa4ec4\",\n      x: 2,\n      y: 0,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:user:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"90b458dc-679e-4b59-b0a9-86b02cf6ae59\",\n      x: 0,\n      y: 3,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:comment:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"bb71aa3a-1170-4e1d-a0f3-290b4ca77ca1\",\n      x: 2,\n      y: 3,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:view:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"c3e467a7-9a15-4732-9ba1-45f36a49d25e\",\n      x: 0,\n      y: 6,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:quick-action\",\n      config: {\n        enabled_items: [\n          \"core:user-center\",\n          \"core:theme-preview\",\n          \"core:new-post\",\n          \"core:new-page\",\n          \"core:upload-attachment\",\n          \"core:theme-manage\",\n          \"core:plugin-manage\",\n          \"core:new-user\",\n          \"core:refresh-search-engine\",\n        ],\n      },\n    },\n    {\n      i: \"24005fc4-e725-4e6d-95ae-f83026ffc178\",\n      x: 0,\n      y: 18,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:notifications\",\n      config: {},\n    },\n  ],\n  xxs: [\n    {\n      i: \"2026e492-0832-4a27-b44b-ab076e31ec17\",\n      x: 0,\n      y: 0,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:post:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"7c868acd-10e8-4e92-99f3-ff00b4aa4ec4\",\n      x: 2,\n      y: 0,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:user:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"90b458dc-679e-4b59-b0a9-86b02cf6ae59\",\n      x: 0,\n      y: 3,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:comment:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"bb71aa3a-1170-4e1d-a0f3-290b4ca77ca1\",\n      x: 2,\n      y: 3,\n      w: 2,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:view:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"c3e467a7-9a15-4732-9ba1-45f36a49d25e\",\n      x: 0,\n      y: 6,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:quick-action\",\n      config: {\n        enabled_items: [\n          \"core:user-center\",\n          \"core:theme-preview\",\n          \"core:new-post\",\n          \"core:new-page\",\n          \"core:upload-attachment\",\n          \"core:theme-manage\",\n          \"core:plugin-manage\",\n          \"core:new-user\",\n          \"core:refresh-search-engine\",\n        ],\n      },\n    },\n    {\n      i: \"24005fc4-e725-4e6d-95ae-f83026ffc178\",\n      x: 0,\n      y: 18,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:notifications\",\n      config: {},\n    },\n  ],\n  md: [\n    {\n      i: \"d86c4b16-9cf5-4d1e-9f46-fc79693c76f2\",\n      x: 0,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:post:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"31a29ecc-e2d6-449c-9850-994e2fd6d9c6\",\n      x: 6,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:comment:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"b3cc217f-5008-49bd-b6a3-b760c1afc4f9\",\n      x: 3,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:user:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"b7afa21f-c24b-4fa2-9c98-de06e3c48687\",\n      x: 9,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:view:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"1f329a9f-d1aa-4337-98b3-34d8732b9143\",\n      x: 0,\n      y: 3,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:quick-action\",\n      config: {\n        enabled_items: [\n          \"core:user-center\",\n          \"core:theme-preview\",\n          \"core:new-post\",\n          \"core:new-page\",\n          \"core:upload-attachment\",\n          \"core:theme-manage\",\n          \"core:plugin-manage\",\n          \"core:new-user\",\n          \"core:refresh-search-engine\",\n        ],\n      },\n    },\n    {\n      i: \"ac9a9984-da16-4d81-adc6-8c64ffcbde5e\",\n      x: 6,\n      y: 3,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:notifications\",\n      config: {},\n    },\n  ],\n  sm: [\n    {\n      i: \"61298416-1b09-426d-914c-ba7664825626\",\n      x: 0,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:post:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"44932f76-7667-4b77-99e7-9778c8f54062\",\n      x: 0,\n      y: 3,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:comment:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"d6508199-b7a3-47dc-aabf-4e3634760b78\",\n      x: 3,\n      y: 0,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:user:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"3525a65d-2c69-4c0f-a378-6c9f1f06ad4c\",\n      x: 3,\n      y: 3,\n      w: 3,\n      h: 3,\n      minW: 2,\n      minH: 2,\n      id: \"core:view:stats\",\n      config: {\n        enable_animation: true,\n      },\n    },\n    {\n      i: \"1bf7eba4-2805-495b-85cd-ebd9ecd9ab82\",\n      x: 0,\n      y: 6,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:quick-action\",\n      config: {\n        enabled_items: [\n          \"core:user-center\",\n          \"core:theme-preview\",\n          \"core:new-post\",\n          \"core:new-page\",\n          \"core:upload-attachment\",\n          \"core:theme-manage\",\n          \"core:plugin-manage\",\n          \"core:new-user\",\n          \"core:refresh-search-engine\",\n        ],\n      },\n    },\n    {\n      i: \"06dc350c-db0e-46ea-9718-cef1e5bca339\",\n      x: 0,\n      y: 18,\n      w: 6,\n      h: 12,\n      minW: 3,\n      minH: 6,\n      id: \"core:notifications\",\n      config: {},\n    },\n  ],\n};\n\nexport { DefaultResponsiveLayouts };\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/index.ts",
    "content": "import type { DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport CommentStatsWidget from \"./presets/comments/CommentStatsWidget.vue\";\nimport PendingCommentsWidget from \"./presets/comments/PendingCommentsWidget.vue\";\nimport IframeWidget from \"./presets/core/iframe/IframeWidget.vue\";\nimport QuickActionWidget from \"./presets/core/quick-action/QuickActionWidget.vue\";\nimport StackWidget from \"./presets/core/stack/StackWidget.vue\";\nimport UpvotesStatsWidget from \"./presets/core/upvotes-stats/UpvotesStatsWidget.vue\";\nimport ViewsStatsWidget from \"./presets/core/view-stats/ViewsStatsWidget.vue\";\nimport PostStatsWidget from \"./presets/posts/PostStatsWidget.vue\";\nimport RecentPublishedWidget from \"./presets/posts/RecentPublishedWidget.vue\";\nimport SinglePageStatsWidget from \"./presets/single-pages/SinglePageStatsWidget.vue\";\nimport NotificationWidget from \"./presets/users/NotificationWidget.vue\";\nimport UserStatsWidget from \"./presets/users/UserStatsWidget.vue\";\n\nexport const internalWidgetDefinitions: DashboardWidgetDefinition[] = [\n  {\n    id: \"core:post:stats\",\n    component: markRaw(PostStatsWidget),\n    group: \"core.dashboard.widgets.groups.post\",\n    configFormKitSchema: () => [\n      {\n        $formkit: \"checkbox\",\n        label: i18n.global.t(\n          \"core.dashboard.widgets.common_form.fields.enable_animation.label\"\n        ),\n        name: \"enable_animation\",\n      },\n    ],\n    defaultConfig: {\n      enable_animation: true,\n    },\n    defaultSize: {\n      w: 3,\n      h: 3,\n      minH: 2,\n      minW: 2,\n    },\n  },\n  {\n    id: \"core:post:recent-published\",\n    component: markRaw(RecentPublishedWidget),\n    group: \"core.dashboard.widgets.groups.post\",\n    defaultConfig: {},\n    defaultSize: {\n      w: 6,\n      h: 12,\n      minH: 6,\n      minW: 3,\n    },\n    permissions: [\"system:posts:view\"],\n  },\n  {\n    id: \"core:singlepage:stats\",\n    component: markRaw(SinglePageStatsWidget),\n    group: \"core.dashboard.widgets.groups.page\",\n    configFormKitSchema: () => [\n      {\n        $formkit: \"checkbox\",\n        label: i18n.global.t(\n          \"core.dashboard.widgets.common_form.fields.enable_animation.label\"\n        ),\n        name: \"enable_animation\",\n      },\n    ],\n    defaultConfig: {\n      enable_animation: true,\n    },\n    defaultSize: {\n      w: 3,\n      h: 3,\n      minH: 2,\n      minW: 2,\n    },\n    permissions: [\"system:singlepages:view\"],\n  },\n  {\n    id: \"core:comment:stats\",\n    component: markRaw(CommentStatsWidget),\n    group: \"core.dashboard.widgets.groups.comment\",\n    configFormKitSchema: () => [\n      {\n        $formkit: \"checkbox\",\n        label: i18n.global.t(\n          \"core.dashboard.widgets.common_form.fields.enable_animation.label\"\n        ),\n        name: \"enable_animation\",\n      },\n    ],\n    defaultConfig: {\n      enable_animation: true,\n    },\n    defaultSize: {\n      w: 3,\n      h: 3,\n      minH: 2,\n      minW: 2,\n    },\n    permissions: [],\n  },\n  {\n    id: \"core:comment:pending\",\n    component: markRaw(PendingCommentsWidget),\n    group: \"core.dashboard.widgets.groups.comment\",\n    defaultConfig: {},\n    defaultSize: {\n      w: 6,\n      h: 12,\n      minH: 6,\n      minW: 3,\n    },\n    permissions: [\"system:comments:view\"],\n  },\n  {\n    id: \"core:user:stats\",\n    component: markRaw(UserStatsWidget),\n    group: \"core.dashboard.widgets.groups.user\",\n    configFormKitSchema: () => [\n      {\n        $formkit: \"checkbox\",\n        label: i18n.global.t(\n          \"core.dashboard.widgets.common_form.fields.enable_animation.label\"\n        ),\n        name: \"enable_animation\",\n      },\n    ],\n    defaultConfig: {\n      enable_animation: true,\n    },\n    defaultSize: {\n      w: 3,\n      h: 3,\n      minH: 2,\n      minW: 2,\n    },\n  },\n  {\n    id: \"core:view:stats\",\n    component: markRaw(ViewsStatsWidget),\n    group: \"core.dashboard.widgets.groups.other\",\n    configFormKitSchema: () => [\n      {\n        $formkit: \"checkbox\",\n        label: i18n.global.t(\n          \"core.dashboard.widgets.common_form.fields.enable_animation.label\"\n        ),\n        name: \"enable_animation\",\n      },\n    ],\n    defaultConfig: {\n      enable_animation: true,\n    },\n    defaultSize: {\n      w: 3,\n      h: 3,\n      minH: 2,\n      minW: 2,\n    },\n  },\n  {\n    id: \"core:upvotes:stats\",\n    component: markRaw(UpvotesStatsWidget),\n    group: \"core.dashboard.widgets.groups.other\",\n    configFormKitSchema: () => [\n      {\n        $formkit: \"checkbox\",\n        label: i18n.global.t(\n          \"core.dashboard.widgets.common_form.fields.enable_animation.label\"\n        ),\n        name: \"enable_animation\",\n      },\n    ],\n    defaultConfig: {\n      enable_animation: true,\n    },\n    defaultSize: {\n      w: 3,\n      h: 3,\n      minH: 2,\n      minW: 2,\n    },\n  },\n  {\n    id: \"core:quick-action\",\n    component: markRaw(QuickActionWidget),\n    group: \"core.dashboard.widgets.groups.other\",\n    defaultConfig: {\n      enabled_items: [\n        \"core:user-center\",\n        \"core:theme-preview\",\n        \"core:new-post\",\n        \"core:new-page\",\n        \"core:upload-attachment\",\n        \"core:theme-manage\",\n        \"core:plugin-manage\",\n        \"core:new-user\",\n        \"core:refresh-search-engine\",\n      ],\n    },\n    defaultSize: {\n      w: 6,\n      h: 12,\n      minH: 6,\n      minW: 3,\n    },\n  },\n  {\n    id: \"core:notifications\",\n    component: markRaw(NotificationWidget),\n    group: \"core.dashboard.widgets.groups.other\",\n    defaultConfig: {},\n    defaultSize: {\n      w: 6,\n      h: 12,\n      minH: 6,\n      minW: 3,\n    },\n  },\n  {\n    id: \"core:stack\",\n    component: markRaw(StackWidget),\n    group: \"core.dashboard.widgets.groups.other\",\n    defaultConfig: {},\n    defaultSize: {\n      w: 6,\n      h: 12,\n      minH: 1,\n      minW: 1,\n    },\n  },\n  {\n    id: \"core:iframe\",\n    component: markRaw(IframeWidget),\n    group: \"core.dashboard.widgets.groups.other\",\n    configFormKitSchema: () => [\n      {\n        $formkit: \"text\",\n        label: i18n.global.t(\n          \"core.dashboard.widgets.presets.iframe.config.fields.title.label\"\n        ),\n        name: \"title\",\n      },\n      {\n        $formkit: \"url\",\n        label: \"URL\",\n        name: \"url\",\n        validation: \"required|url\",\n      },\n    ],\n    defaultConfig: {},\n    defaultSize: {\n      w: 6,\n      h: 12,\n      minH: 2,\n      minW: 2,\n    },\n  },\n];\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/comments/CommentItem.vue",
    "content": "<script setup lang=\"ts\">\nimport CommentDetailModal from \"@console/modules/contents/comments/components/CommentDetailModal.vue\";\nimport OwnerButton from \"@console/modules/contents/comments/components/OwnerButton.vue\";\nimport { useContentProviderExtensionPoint } from \"@console/modules/contents/comments/composables/use-content-provider-extension-point\";\nimport { useSubjectRef } from \"@console/modules/contents/comments/composables/use-subject-ref\";\nimport { coreApiClient, type ListedComment } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconExternalLinkLine,\n  Toast,\n  VEntity,\n  VEntityField,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst props = defineProps<{\n  comment: ListedComment;\n}>();\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\nconst { subjectRefResult } = useSubjectRef(props.comment);\n\nconst detailModalVisible = ref(false);\n\nfunction onCommentDetailModalClose() {\n  detailModalVisible.value = false;\n  queryClient.invalidateQueries({ queryKey: [\"widget-pending-comments\"] });\n}\n\nconst creationTime = computed(() => {\n  return (\n    props.comment?.comment.spec.creationTime ||\n    props.comment?.comment.metadata.creationTimestamp\n  );\n});\n\nconst handleDelete = async () => {\n  Dialog.warning({\n    title: t(\"core.comment.operations.delete_comment.title\"),\n    description: t(\"core.comment.operations.delete_comment.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.content.comment.deleteComment({\n          name: props.comment?.comment?.metadata.name as string,\n        });\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (error) {\n        console.error(\"Failed to delete comment\", error);\n      } finally {\n        queryClient.invalidateQueries({\n          queryKey: [\"widget-pending-comments\"],\n        });\n      }\n    },\n  });\n};\n\nconst { data: contentProvider } = useContentProviderExtensionPoint();\n</script>\n\n<template>\n  <CommentDetailModal\n    v-if=\"detailModalVisible\"\n    :comment=\"comment\"\n    @close=\"onCommentDetailModalClose\"\n  />\n  <VEntity>\n    <template #start>\n      <VEntityField width=\"100%\" max-width=\"100%\">\n        <template #description>\n          <div class=\"flex flex-col gap-2\">\n            <div class=\"mb-1 flex items-center gap-2\">\n              <OwnerButton\n                :owner=\"comment?.owner\"\n                @click=\"detailModalVisible = true\"\n              />\n              <span class=\"whitespace-nowrap text-sm text-gray-900\">\n                {{ $t(\"core.comment.text.commented_on\") }}\n              </span>\n              <RouterLink\n                v-tooltip=\"`${subjectRefResult.label}`\"\n                :to=\"subjectRefResult.route || $route\"\n                class=\"inline-block max-w-md truncate text-sm font-medium text-gray-900 hover:text-gray-600\"\n              >\n                {{ subjectRefResult.title }}\n              </RouterLink>\n              <a\n                v-if=\"subjectRefResult.externalUrl\"\n                :href=\"subjectRefResult.externalUrl\"\n                target=\"_blank\"\n                class=\"invisible text-gray-600 hover:text-gray-900 group-hover:visible\"\n              >\n                <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n              </a>\n            </div>\n            <component\n              :is=\"contentProvider?.component\"\n              :content=\"comment?.comment?.spec.content\"\n            />\n            <HasPermission :permissions=\"['system:comments:manage']\">\n              <div class=\"flex items-center gap-3 text-xs\">\n                <span\n                  class=\"cursor-pointer select-none text-gray-700 hover:text-gray-900\"\n                  @click=\"detailModalVisible = true\"\n                >\n                  {{ $t(\"core.comment.operations.review.button\") }}\n                </span>\n                <span\n                  class=\"cursor-pointer select-none text-gray-700 hover:text-red-600\"\n                  @click=\"handleDelete\"\n                >\n                  {{ $t(\"core.common.buttons.delete\") }}\n                </span>\n              </div>\n            </HasPermission>\n          </div>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"!comment?.comment.spec.approved\">\n        <template #description>\n          <VStatusDot\n            state=\"warning\"\n            animate\n            :text=\"$t('core.comment.list.fields.pending_review')\"\n          />\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"comment?.comment?.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField\n        v-tooltip=\"utils.date.format(creationTime)\"\n        :description=\"utils.date.timeAgo(creationTime)\"\n      />\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/comments/CommentStatsWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useDashboardStats } from \"@console/composables/use-dashboard-stats\";\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport { IconMessage } from \"@halo-dev/components\";\nimport NumberFlow from \"@number-flow/vue\";\n\ndefineProps<{\n  config: {\n    enable_animation: boolean;\n  };\n}>();\n\nconst { data: stats } = useDashboardStats();\n</script>\n<template>\n  <WidgetCard>\n    <div class=\"flex h-full px-4 py-3\">\n      <div class=\"flex items-center gap-4\">\n        <span class=\"rounded-full bg-gray-100 p-2.5 text-gray-600\">\n          <IconMessage class=\"h-5 w-5\" />\n        </span>\n\n        <div class=\"flex flex-col\">\n          <span class=\"text-sm text-gray-500\">\n            {{ $t(\"core.dashboard.widgets.presets.comment_stats.title\") }}\n          </span>\n          <NumberFlow\n            v-if=\"config.enable_animation\"\n            class=\"text-2xl font-medium text-gray-900\"\n            :value=\"stats?.approvedComments || 0\"\n            :format=\"{ notation: 'compact' }\"\n          >\n          </NumberFlow>\n          <span v-else class=\"text-2xl font-medium text-gray-900\">\n            {{ stats?.approvedComments || 0 }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/comments/PendingCommentsWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport type { ListedComment } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport CommentItem from \"./CommentItem.vue\";\n\nconst { data, isLoading, isFetching, refetch } = useQuery<ListedComment[]>({\n  queryKey: [\"widget-pending-comments\"],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.content.comment.listComments({\n      fieldSelector: [\"spec.approved=false\"],\n      page: 1,\n      size: 10,\n    });\n    return data.items;\n  },\n});\n</script>\n<template>\n  <WidgetCard\n    :body-class=\"['!overflow-auto']\"\n    :title=\"\n      $t(\n        'core.dashboard.widgets.presets.pending_comments.title',\n        'Pending Comments'\n      )\n    \"\n  >\n    <VLoading v-if=\"isLoading\" />\n    <VEmpty\n      v-else-if=\"!data?.length\"\n      :title=\"$t('core.dashboard.widgets.presets.pending_comments.empty.title')\"\n    >\n      <template #actions>\n        <VButton :loading=\"isFetching\" @click=\"refetch\">\n          {{ $t(\"core.common.buttons.refresh\") }}\n        </VButton>\n      </template>\n    </VEmpty>\n    <OverlayScrollbarsComponent\n      element=\"div\"\n      :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n      class=\"h-full w-full\"\n      defer\n    >\n      <VEntityContainer>\n        <CommentItem\n          v-for=\"comment in data\"\n          :key=\"comment.comment.metadata.name\"\n          :comment=\"comment\"\n        ></CommentItem>\n      </VEntityContainer>\n    </OverlayScrollbarsComponent>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/iframe/IframeWidget.vue",
    "content": "<script setup lang=\"ts\">\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\n\ndefineProps<{\n  previewMode?: boolean;\n  editMode?: boolean;\n  config: {\n    title?: string;\n    url?: string;\n  };\n}>();\n</script>\n<template>\n  <WidgetCard\n    :title=\"\n      previewMode\n        ? $t('core.dashboard.widgets.presets.iframe.title')\n        : config.title\n    \"\n  >\n    <div\n      v-if=\"!config.url\"\n      class=\"flex h-full w-full items-center justify-center\"\n    >\n      <span class=\"text-sm text-gray-600\">\n        {{ $t(\"core.dashboard.widgets.presets.iframe.empty.title\") }}\n      </span>\n    </div>\n    <iframe\n      v-else\n      :src=\"config.url\"\n      sandbox=\"allow-scripts allow-same-origin\"\n      credentialless\n      referrerpolicy=\"no-referrer\"\n      class=\"h-full w-full border-none\"\n      :class=\"{\n        'pointer-events-none': editMode,\n      }\"\n    ></iframe>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/quick-action/QuickActionItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconArrowRight } from \"@halo-dev/components\";\nimport {\n  utils,\n  type DashboardWidgetQuickActionItem,\n} from \"@halo-dev/ui-shared\";\nimport { useRouter } from \"vue-router\";\n\nconst props = defineProps<{\n  item: DashboardWidgetQuickActionItem;\n}>();\n\nconst router = useRouter();\n\nfunction handleClick() {\n  if (\"action\" in props.item && props.item.action) {\n    props.item.action();\n  } else if (\"route\" in props.item && props.item.route) {\n    router.push(props.item.route);\n  }\n}\n</script>\n\n<template>\n  <template v-if=\"utils.permission.has(item.permissions || [])\">\n    <div\n      class=\"group relative cursor-pointer rounded-lg bg-gray-50 p-4 transition-all hover:bg-gray-100\"\n      @click=\"handleClick\"\n    >\n      <div>\n        <span\n          class=\"inline-flex rounded-lg bg-teal-50 p-3 text-teal-700 ring-4 ring-white\"\n        >\n          <component :is=\"item.icon\"></component>\n        </span>\n      </div>\n      <div class=\"mt-8\">\n        <h3 class=\"text-sm font-semibold\">\n          {{ item.title }}\n        </h3>\n      </div>\n      <span\n        aria-hidden=\"true\"\n        class=\"pointer-events-none absolute right-6 top-6 text-gray-300 transition-all group-hover:translate-x-1 group-hover:text-gray-400\"\n      >\n        <IconArrowRight />\n      </span>\n    </div>\n  </template>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/quick-action/QuickActionWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport type { FormKitOptionsList } from \"@formkit/inputs\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAccountCircleLine,\n  IconBookRead,\n  IconFolder,\n  IconPages,\n  IconPalette,\n  IconPlug,\n  IconSearch,\n  IconSettings,\n  IconUserSettings,\n  IconWindowLine,\n  Toast,\n  VButton,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport type { DashboardWidgetQuickActionItem } from \"@halo-dev/ui-shared\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { computed, markRaw, ref, useTemplateRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useDashboardQuickActionExtensionPoint } from \"./composables/use-dashboard-extension-point\";\nimport QuickActionItem from \"./QuickActionItem.vue\";\nimport ThemePreviewItem from \"./ThemePreviewItem.vue\";\n\nconst props = defineProps<{\n  editMode?: boolean;\n  config?: {\n    enabled_items?: string[];\n  };\n}>();\n\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (e: \"update:config\", config: Record<string, unknown>): void;\n}>();\n\nconst presetItems: DashboardWidgetQuickActionItem[] = [\n  {\n    id: \"core:user-center\",\n    icon: markRaw(IconAccountCircleLine),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.user_center.title\"\n    ),\n    action: () => {\n      window.location.href = \"/uc/profile\";\n    },\n  },\n  {\n    id: \"core:theme-preview\",\n    icon: markRaw(IconWindowLine),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.view_site.title\"\n    ),\n    component: markRaw(ThemePreviewItem),\n    permissions: [\"system:themes:view\"],\n  },\n  {\n    id: \"core:new-post\",\n    icon: markRaw(IconBookRead),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.new_post.title\"\n    ),\n    route: {\n      name: \"PostEditor\",\n    },\n    permissions: [\"system:posts:manage\"],\n  },\n  {\n    id: \"core:new-page\",\n    icon: markRaw(IconPages),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.new_page.title\"\n    ),\n    route: {\n      name: \"SinglePageEditor\",\n    },\n    permissions: [\"system:singlepages:manage\"],\n  },\n  {\n    id: \"core:upload-attachment\",\n    icon: markRaw(IconFolder),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.upload_attachment.title\"\n    ),\n    route: {\n      name: \"Attachments\",\n      query: {\n        action: \"upload\",\n      },\n    },\n    permissions: [\"system:attachments:manage\"],\n  },\n  {\n    id: \"core:theme-manage\",\n    icon: markRaw(IconPalette),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.theme_manage.title\"\n    ),\n    route: {\n      name: \"ThemeDetail\",\n    },\n    permissions: [\"system:themes:view\"],\n  },\n  {\n    id: \"core:plugin-manage\",\n    icon: markRaw(IconPlug),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.plugin_manage.title\"\n    ),\n    route: {\n      name: \"Plugins\",\n    },\n    permissions: [\"system:plugins:view\"],\n  },\n  {\n    id: \"core:new-user\",\n    icon: markRaw(IconUserSettings),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.new_user.title\"\n    ),\n    route: {\n      name: \"Users\",\n      query: {\n        action: \"create\",\n      },\n    },\n    permissions: [\"system:users:manage\"],\n  },\n  {\n    id: \"core:refresh-search-engine\",\n    icon: markRaw(IconSearch),\n    title: t(\n      \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.title\"\n    ),\n    action: () => {\n      Dialog.warning({\n        title: t(\n          \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_title\"\n        ),\n        description: t(\n          \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_content\"\n        ),\n        confirmText: t(\"core.common.buttons.confirm\"),\n        cancelText: t(\"core.common.buttons.cancel\"),\n        onConfirm: async () => {\n          await consoleApiClient.content.indices.rebuildAllIndices();\n          Toast.success(\n            t(\n              \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.success_message\"\n            )\n          );\n        },\n      });\n    },\n    permissions: [\"*\"],\n  },\n];\n\nconst { quickActionItems } = useDashboardQuickActionExtensionPoint();\n\nconst allQuickActionItems = computed(() => {\n  return [...presetItems, ...quickActionItems.value];\n});\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\nconst configVisible = ref(false);\n\nfunction onConfigFormSubmit(data: { enabled_items: string[] }) {\n  emit(\"update:config\", data);\n  configVisible.value = false;\n}\n\nconst itemOptions = computed(() => {\n  return allQuickActionItems.value.map((item) => ({\n    label: item.title,\n    value: item.id,\n  })) as FormKitOptionsList;\n});\n\nconst availableItems = computed(() => {\n  const enabledItems = props.config?.enabled_items;\n\n  if (!enabledItems || enabledItems.length === 0) {\n    return [];\n  }\n\n  const indexMap = new Map();\n  enabledItems.forEach((id, index) => {\n    indexMap.set(id, index);\n  });\n  return allQuickActionItems.value\n    .filter((item) => indexMap.has(item.id))\n    .sort((a, b) => indexMap.get(a.id) - indexMap.get(b.id));\n});\n</script>\n<template>\n  <WidgetCard v-bind=\"$attrs\" :body-class=\"['@container', '!overflow-auto']\">\n    <template #title>\n      <div class=\"inline-flex items-center gap-2\">\n        <div class=\"flex-1 shrink text-base font-medium\">\n          {{ $t(\"core.dashboard.widgets.presets.quickaction.title\") }}\n        </div>\n        <IconSettings\n          v-if=\"editMode\"\n          class=\"cursor-pointer hover:text-gray-600\"\n          @click=\"configVisible = true\"\n        />\n      </div>\n    </template>\n    <OverlayScrollbarsComponent\n      element=\"div\"\n      :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n      class=\"h-full w-full\"\n      style=\"padding: 12px 16px\"\n      defer\n    >\n      <div\n        class=\"grid grid-cols-1 gap-2 overflow-hidden @sm:grid-cols-2 @md:grid-cols-3\"\n      >\n        <template v-for=\"item in availableItems\" :key=\"item.id\">\n          <QuickActionItem v-if=\"!item.component\" :item=\"item\" />\n          <component :is=\"item.component\" v-else :item=\"item\" />\n        </template>\n      </div>\n    </OverlayScrollbarsComponent>\n  </WidgetCard>\n  <VModal\n    v-if=\"configVisible\"\n    ref=\"modal\"\n    mount-to-body\n    :title=\"$t('core.dashboard_designer.config_modal.title')\"\n    @close=\"configVisible = false\"\n  >\n    <div>\n      <FormKit\n        id=\"quick-action-widget-config\"\n        name=\"quick-action-widget-config\"\n        type=\"form\"\n        :preserve=\"true\"\n        @submit=\"onConfigFormSubmit\"\n      >\n        <FormKit\n          type=\"select\"\n          :label=\"\n            $t(\n              'core.dashboard.widgets.presets.quickaction.config.fields.enabled_items.label'\n            )\n          \"\n          name=\"enabled_items\"\n          :options=\"itemOptions\"\n          :value=\"config?.enabled_items || []\"\n          multiple\n          searchable\n          sortable\n        />\n      </FormKit>\n    </div>\n    <template #footer>\n      <VSpace>\n        <VButton\n          type=\"secondary\"\n          @click=\"$formkit.submit('quick-action-widget-config')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/quick-action/ThemePreviewItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport ThemePreviewModal from \"@console/modules/interface/themes/components/preview/ThemePreviewModal.vue\";\nimport type { DashboardWidgetQuickActionItem } from \"@halo-dev/ui-shared\";\nimport { ref } from \"vue\";\nimport QuickActionItem from \"./QuickActionItem.vue\";\n\ndefineProps<{\n  item: DashboardWidgetQuickActionItem;\n}>();\n\nconst visible = ref(false);\n</script>\n\n<template>\n  <QuickActionItem\n    :item=\"{\n      ...item,\n      action: () => {\n        visible = true;\n      },\n    }\"\n  />\n\n  <ThemePreviewModal\n    v-if=\"visible\"\n    :title=\"\n      $t('core.dashboard.widgets.presets.quickaction.actions.view_site.title')\n    \"\n    @close=\"visible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/quick-action/composables/use-dashboard-extension-point.ts",
    "content": "import type { DashboardWidgetQuickActionItem } from \"@halo-dev/ui-shared\";\nimport { onMounted, shallowRef } from \"vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nconst EXTENSION_POINT_NAME =\n  \"console:dashboard:widgets:internal:quick-action:item:create\";\n\nexport function useDashboardQuickActionExtensionPoint() {\n  const { pluginModuleMap } = usePluginModuleStore();\n\n  const quickActionItems = shallowRef<DashboardWidgetQuickActionItem[]>([]);\n\n  onMounted(async () => {\n    const result: DashboardWidgetQuickActionItem[] = [];\n    for (const [name, module] of Object.entries(pluginModuleMap)) {\n      try {\n        const callbackFunction =\n          module?.extensionPoints?.[EXTENSION_POINT_NAME];\n\n        if (typeof callbackFunction !== \"function\") {\n          continue;\n        }\n\n        const definitions = await callbackFunction();\n\n        // Reset id\n        definitions.forEach((definition) => {\n          definition.id = `${name}-${definition.id}`;\n        });\n\n        result.push(...definitions);\n      } catch (error) {\n        console.error(`Error processing plugin module:`, name, error);\n      }\n    }\n    quickActionItems.value = result;\n  });\n\n  return { quickActionItems };\n}\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/stack/StackWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport { IconSettings, VButton } from \"@halo-dev/components\";\nimport { nextTick, onMounted, onUnmounted, ref } from \"vue\";\nimport IndexIndicator from \"./components/IndexIndicator.vue\";\nimport WidgetViewItem from \"./components/WidgetViewItem.vue\";\nimport StackWidgetConfigModal from \"./StackWidgetConfigModal.vue\";\nimport type { StackWidgetConfig } from \"./types\";\n\nconst props = defineProps<{\n  config: StackWidgetConfig;\n  editMode?: boolean;\n  previewMode?: boolean;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"update:config\", config: StackWidgetConfig): void;\n}>();\n\nconst configVisible = ref(false);\n\nconst index = ref(0);\n\nfunction handleNavigate(direction: -1 | 1) {\n  const targetIndex = index.value + direction;\n\n  if (targetIndex < 0) {\n    index.value = props.config.widgets.length - 1;\n  } else if (targetIndex >= props.config.widgets.length) {\n    index.value = 0;\n  } else {\n    index.value = targetIndex;\n  }\n}\n\n// auto play\nconst autoPlayInterval = ref<NodeJS.Timeout | null>(null);\n\nfunction startAutoPlay() {\n  if (!props.config.auto_play || configVisible.value) {\n    return;\n  }\n  if (autoPlayInterval.value) {\n    clearInterval(autoPlayInterval.value);\n  }\n  autoPlayInterval.value = setInterval(() => {\n    handleNavigate(1);\n  }, props.config.auto_play_interval || 3000);\n}\n\nfunction stopAutoPlay() {\n  if (autoPlayInterval.value) {\n    clearInterval(autoPlayInterval.value);\n    autoPlayInterval.value = null;\n  }\n}\n\nonMounted(() => {\n  startAutoPlay();\n});\n\nonUnmounted(() => {\n  stopAutoPlay();\n});\n\nasync function handleSave(config: StackWidgetConfig) {\n  emit(\"update:config\", config);\n  configVisible.value = false;\n\n  await nextTick();\n\n  if (config.auto_play) {\n    startAutoPlay();\n  } else {\n    stopAutoPlay();\n  }\n}\n</script>\n<template>\n  <WidgetCard\n    v-if=\"!config.widgets?.length\"\n    :title=\"$t('core.dashboard.widgets.presets.stack.title')\"\n  >\n    <div class=\"flex h-full w-full items-center justify-center\">\n      <VButton @click=\"configVisible = true\">\n        {{\n          $t(\n            \"core.dashboard.widgets.presets.stack.operations.add_widget.button\"\n          )\n        }}\n      </VButton>\n    </div>\n  </WidgetCard>\n  <div\n    v-else\n    class=\"group/stack-item relative h-full w-full overflow-hidden rounded-lg bg-white\"\n    @mouseenter=\"stopAutoPlay\"\n    @mouseleave=\"startAutoPlay\"\n  >\n    <div\n      v-if=\"editMode || previewMode\"\n      class=\"absolute inset-0 z-10 flex h-10 flex-none items-center justify-between rounded-t-lg border-b border-[#eaecf0] bg-white px-4\"\n    >\n      <div class=\"inline-flex items-center gap-2\">\n        <div class=\"flex-1 shrink text-base font-medium\">\n          {{ $t(\"core.dashboard.widgets.presets.stack.title\") }}\n        </div>\n        <IconSettings\n          v-if=\"editMode\"\n          class=\"cursor-pointer hover:text-gray-600\"\n          @click=\"configVisible = true\"\n        />\n      </div>\n    </div>\n\n    <TransitionGroup name=\"fade\">\n      <WidgetViewItem\n        v-for=\"(widget, i) in config.widgets\"\n        v-show=\"index === i\"\n        :key=\"widget.i\"\n        :item=\"widget\"\n      />\n    </TransitionGroup>\n\n    <div\n      class=\"absolute bottom-2 left-0 right-0 z-10 flex justify-center opacity-0 transition-all duration-200 group-hover/stack-item:opacity-100\"\n    >\n      <IndexIndicator\n        :index=\"index\"\n        :total=\"config.widgets.length\"\n        @prev=\"handleNavigate(-1)\"\n        @next=\"handleNavigate(1)\"\n        @update:index=\"index = $event\"\n      />\n    </div>\n  </div>\n\n  <StackWidgetConfigModal\n    v-if=\"configVisible\"\n    :config=\"config\"\n    @close=\"configVisible = false\"\n    @save=\"handleSave\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/stack/StackWidgetConfigModal.vue",
    "content": "<script setup lang=\"ts\">\nimport ActionButton from \"@console/modules/dashboard/components/ActionButton.vue\";\nimport WidgetHubModal from \"@console/modules/dashboard/components/WidgetHubModal.vue\";\nimport {\n  IconArrowDownLine,\n  IconArrowUpLine,\n  Toast,\n  VButton,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { utils, type DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { onMounted, ref, toRaw, useTemplateRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport WidgetEditableItem from \"./components/WidgetEditableItem.vue\";\nimport type { SimpleWidget, StackWidgetConfig } from \"./types\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n  config: StackWidgetConfig;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"close\"): void;\n  (e: \"save\", config: StackWidgetConfig): void;\n}>();\n\nconst widgets = ref<SimpleWidget[]>();\n\nonMounted(() => {\n  widgets.value = toRaw(props.config.widgets);\n});\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n\nconst widgetsHubModalVisible = ref(false);\n\nfunction handleAddWidget(widgetDefinition: DashboardWidgetDefinition) {\n  if (widgetDefinition.id === \"core:stack\") {\n    Toast.error(\n      t(\"core.dashboard.widgets.presets.stack.config_modal.toast.nest_warning\")\n    );\n    return;\n  }\n\n  widgets.value = [\n    ...(widgets.value || []),\n    {\n      i: utils.id.uuid(),\n      id: widgetDefinition.id,\n      config: widgetDefinition.defaultConfig,\n    },\n  ];\n  widgetsHubModalVisible.value = false;\n}\n\nfunction handleSave(data: { auto_play: boolean; auto_play_interval: number }) {\n  emit(\"save\", {\n    auto_play: data.auto_play,\n    auto_play_interval: data.auto_play_interval,\n    widgets: widgets.value || [],\n  });\n}\n\nfunction handleRemoveWidget(widget: SimpleWidget) {\n  widgets.value = widgets.value?.filter((w) => w.i !== widget.i);\n}\n\nfunction handleUpdateWidgetConfig(\n  widget: SimpleWidget,\n  config: Record<string, unknown>\n) {\n  widgets.value = widgets.value?.map((w) =>\n    w.i === widget.i ? { ...w, config } : w\n  );\n}\n\nfunction handleMoveWidget(widget: SimpleWidget, direction: -1 | 1) {\n  const items = cloneDeep(widgets.value) || [];\n  const currentIndex = items.findIndex((item) => item.i === widget.i);\n\n  if (currentIndex === -1) return;\n\n  const targetIndex = currentIndex + direction;\n\n  if (targetIndex < 0 || targetIndex >= items.length) return;\n\n  [items[currentIndex], items[targetIndex]] = [\n    items[targetIndex],\n    items[currentIndex],\n  ];\n\n  widgets.value = [...items];\n}\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    mount-to-body\n    :title=\"$t('core.dashboard.widgets.presets.stack.config_modal.title')\"\n    :width=\"800\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <div class=\"flex flex-col gap-5\">\n      <FormKit\n        id=\"stack-widget-config-form\"\n        v-slot=\"{ value }\"\n        type=\"form\"\n        name=\"stack-widget-config-form\"\n        :preserve=\"true\"\n        @submit=\"handleSave\"\n      >\n        <FormKit\n          type=\"checkbox\"\n          name=\"auto_play\"\n          :label=\"\n            $t(\n              'core.dashboard.widgets.presets.stack.config_modal.fields.auto_play.label'\n            )\n          \"\n          :value=\"config.auto_play || false\"\n        />\n        <FormKit\n          v-if=\"value?.auto_play\"\n          type=\"number\"\n          number\n          name=\"auto_play_interval\"\n          validation=\"required\"\n          :value=\"config.auto_play_interval || 3000\"\n          :label=\"\n            $t(\n              'core.dashboard.widgets.presets.stack.config_modal.fields.auto_play_interval.label'\n            )\n          \"\n        />\n        <div class=\"flex flex-col gap-4 py-4\">\n          <label\n            class=\"formkit-label block text-sm font-medium text-gray-700 formkit-invalid:text-red-500\"\n          >\n            {{\n              $t(\n                \"core.dashboard.widgets.presets.stack.config_modal.fields.widgets.label\"\n              )\n            }}\n          </label>\n          <div class=\"flex flex-col gap-2 rounded-lg border border-dashed p-2\">\n            <WidgetEditableItem\n              v-for=\"(widget, index) in widgets\"\n              :key=\"widget.id\"\n              :item=\"widget\"\n              @remove=\"handleRemoveWidget(widget)\"\n              @update:config=\"handleUpdateWidgetConfig(widget, $event)\"\n            >\n              <template #actions>\n                <ActionButton\n                  v-if=\"index > 0\"\n                  class=\"bg-gray-200\"\n                  @click=\"handleMoveWidget(widget, -1)\"\n                >\n                  <IconArrowUpLine class=\"text-gray-600\" />\n                </ActionButton>\n                <ActionButton\n                  v-if=\"index < (widgets?.length || 0) - 1\"\n                  class=\"bg-gray-200\"\n                  @click=\"handleMoveWidget(widget, 1)\"\n                >\n                  <IconArrowDownLine class=\"text-gray-600\" />\n                </ActionButton>\n              </template>\n            </WidgetEditableItem>\n\n            <div class=\"justify-left flex\">\n              <VButton @click=\"widgetsHubModalVisible = true\">\n                {{ $t(\"core.common.buttons.add\") }}\n              </VButton>\n            </div>\n          </div>\n        </div>\n      </FormKit>\n    </div>\n    <template #footer>\n      <VSpace>\n        <VButton\n          type=\"secondary\"\n          @click=\"$formkit.submit('stack-widget-config-form')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton @click=\"emit('close')\">\n          {{ $t(\"core.common.buttons.cancel\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n\n  <WidgetHubModal\n    v-if=\"widgetsHubModalVisible\"\n    @close=\"widgetsHubModalVisible = false\"\n    @add-widget=\"handleAddWidget\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/stack/components/IndexIndicator.vue",
    "content": "<script setup lang=\"ts\">\nimport { IconArrowLeft, IconArrowRight } from \"@halo-dev/components\";\n\ndefineProps<{\n  index: number;\n  total: number;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"prev\"): void;\n  (e: \"next\"): void;\n  (e: \"update:index\", index: number): void;\n}>();\n</script>\n\n<template>\n  <div\n    class=\"flex items-center gap-4 rounded-full bg-white p-1 transition-all hover:shadow\"\n  >\n    <button\n      v-if=\"total > 1\"\n      class=\"group flex h-7 w-7 items-center justify-center rounded-full bg-transparent transition-all duration-200 hover:bg-gray-100 focus:outline-none\"\n      @click=\"emit('prev')\"\n    >\n      <IconArrowLeft class=\"text-gray-400 group-hover:text-gray-900\" />\n    </button>\n\n    <div class=\"flex items-center gap-2\">\n      <div\n        v-for=\"i in total\"\n        :key=\"i\"\n        class=\"group cursor-pointer\"\n        @click=\"emit('update:index', i - 1)\"\n      >\n        <div\n          class=\"h-2 w-2 transform rounded-full transition-all duration-200 ease-in-out\"\n          :class=\"{\n            'scale-150 bg-primary': index === i - 1,\n            'bg-gray-300 group-hover:bg-gray-400': index !== i - 1,\n          }\"\n        />\n      </div>\n    </div>\n\n    <button\n      v-if=\"total > 1\"\n      class=\"group flex h-7 w-7 items-center justify-center rounded-full bg-transparent transition-all duration-200 hover:bg-gray-100 focus:outline-none\"\n      @click=\"emit('next')\"\n    >\n      <IconArrowRight class=\"text-gray-400 group-hover:text-gray-900\" />\n    </button>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/stack/components/WidgetEditableItem.vue",
    "content": "<script setup lang=\"ts\">\nimport ActionButton from \"@console/modules/dashboard/components/ActionButton.vue\";\nimport WidgetConfigFormModal from \"@console/modules/dashboard/components/WidgetConfigFormModal.vue\";\nimport { IconCloseCircle, IconSettings } from \"@halo-dev/components\";\nimport { utils, type DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { computed, inject, ref, type ComputedRef } from \"vue\";\nimport type { SimpleWidget } from \"../types\";\n\nconst props = defineProps<{\n  item: SimpleWidget;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"remove\"): void;\n  (e: \"update:config\", config: Record<string, unknown>): void;\n}>();\n\nconst availableWidgetDefinitions = inject<\n  ComputedRef<DashboardWidgetDefinition[]>\n>(\"availableWidgetDefinitions\");\n\nconst widgetDefinition = computed(() => {\n  return availableWidgetDefinitions?.value?.find(\n    (definition) => definition.id === props.item.id\n  );\n});\n\nconst defaultSize = computed(() => {\n  return widgetDefinition.value?.defaultSize || { w: 1, h: 1 };\n});\n\nconst configModalVisible = ref(false);\n\nfunction handleSaveConfig(config: Record<string, unknown>) {\n  emit(\"update:config\", config);\n  configModalVisible.value = false;\n}\n</script>\n<template>\n  <div\n    v-if=\"utils.permission.has(widgetDefinition?.permissions || [])\"\n    class=\"group/grid-item relative\"\n    :style=\"{\n      width: `${defaultSize.w * 100}px`,\n      height: `${defaultSize.h * 36}px`,\n    }\"\n  >\n    <component\n      :is=\"widgetDefinition?.component\"\n      edit-mode\n      :config=\"item.config\"\n      @update:config=\"handleSaveConfig\"\n    />\n    <div\n      class=\"absolute right-0 top-0 hidden h-8 items-center overflow-hidden rounded-tr-lg bg-gray-100 group-hover/grid-item:inline-flex\"\n    >\n      <slot name=\"actions\" />\n      <ActionButton\n        v-if=\"widgetDefinition?.configFormKitSchema\"\n        class=\"bg-black\"\n        @click=\"configModalVisible = true\"\n      >\n        <IconSettings />\n      </ActionButton>\n      <ActionButton class=\"bg-red-500\" @click=\"emit('remove')\">\n        <IconCloseCircle />\n      </ActionButton>\n    </div>\n  </div>\n  <WidgetConfigFormModal\n    v-if=\"\n      widgetDefinition &&\n      widgetDefinition.configFormKitSchema &&\n      configModalVisible\n    \"\n    :widget-definition=\"widgetDefinition\"\n    :config=\"item.config\"\n    @close=\"configModalVisible = false\"\n    @save=\"handleSaveConfig\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/stack/components/WidgetViewItem.vue",
    "content": "<script setup lang=\"ts\">\nimport { utils, type DashboardWidgetDefinition } from \"@halo-dev/ui-shared\";\nimport { computed, inject, type ComputedRef } from \"vue\";\nimport type { SimpleWidget } from \"../types\";\n\nconst props = defineProps<{\n  item: SimpleWidget;\n}>();\n\nconst availableWidgetDefinitions = inject<\n  ComputedRef<DashboardWidgetDefinition[]>\n>(\"availableWidgetDefinitions\");\n\nconst widgetDefinition = computed(() => {\n  return availableWidgetDefinitions?.value?.find(\n    (definition) => definition.id === props.item.id\n  );\n});\n</script>\n\n<template>\n  <div\n    v-if=\"utils.permission.has(widgetDefinition?.permissions || [])\"\n    class=\"relative h-full w-full\"\n  >\n    <component :is=\"widgetDefinition?.component\" :config=\"item.config\" />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/stack/types.ts",
    "content": "export interface SimpleWidget {\n  i: string;\n  id: string;\n  config?: Record<string, unknown>;\n}\n\nexport interface StackWidgetConfig {\n  auto_play?: boolean;\n  auto_play_interval?: number;\n  widgets: SimpleWidget[];\n}\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/upvotes-stats/UpvotesStatsWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useDashboardStats } from \"@console/composables/use-dashboard-stats\";\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport { IconThumbUpLine } from \"@halo-dev/components\";\nimport NumberFlow from \"@number-flow/vue\";\n\ndefineProps<{\n  config: {\n    enable_animation: boolean;\n  };\n}>();\n\nconst { data: stats } = useDashboardStats();\n</script>\n<template>\n  <WidgetCard>\n    <div class=\"flex h-full px-4 py-3\">\n      <div class=\"flex items-center gap-4\">\n        <span class=\"rounded-full bg-gray-100 p-2.5 text-gray-600\">\n          <IconThumbUpLine class=\"h-5 w-5\" />\n        </span>\n\n        <div class=\"flex flex-col\">\n          <span class=\"text-sm text-gray-500\">\n            {{ $t(\"core.dashboard.widgets.presets.upvotes_stats.title\") }}\n          </span>\n          <NumberFlow\n            v-if=\"config.enable_animation\"\n            class=\"text-2xl font-medium text-gray-900\"\n            :value=\"stats?.upvotes || 0\"\n            :format=\"{ notation: 'compact' }\"\n          >\n          </NumberFlow>\n          <span v-else class=\"text-2xl font-medium text-gray-900\">\n            {{ stats?.upvotes || 0 }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/core/view-stats/ViewsStatsWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useDashboardStats } from \"@console/composables/use-dashboard-stats\";\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport { IconEye } from \"@halo-dev/components\";\nimport NumberFlow from \"@number-flow/vue\";\n\ndefineProps<{\n  config: {\n    enable_animation: boolean;\n  };\n}>();\n\nconst { data: stats } = useDashboardStats();\n</script>\n<template>\n  <WidgetCard>\n    <div class=\"flex h-full px-4 py-3\">\n      <div class=\"flex items-center gap-4\">\n        <span class=\"rounded-full bg-gray-100 p-2.5 text-gray-600\">\n          <IconEye class=\"h-5 w-5\" />\n        </span>\n\n        <div class=\"flex flex-col\">\n          <span class=\"text-sm text-gray-500\">\n            {{ $t(\"core.dashboard.widgets.presets.views_stats.title\") }}\n          </span>\n          <NumberFlow\n            v-if=\"config.enable_animation\"\n            class=\"text-2xl font-medium text-gray-900\"\n            :value=\"stats?.visits || 0\"\n            :format=\"{ notation: 'compact' }\"\n          >\n          </NumberFlow>\n          <span v-else class=\"text-2xl font-medium text-gray-900\">\n            {{ stats?.visits || 0 }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/posts/PostStatsWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useDashboardStats } from \"@console/composables/use-dashboard-stats\";\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport { IconBookRead } from \"@halo-dev/components\";\nimport NumberFlow from \"@number-flow/vue\";\n\ndefineProps<{\n  config: {\n    enable_animation: boolean;\n  };\n}>();\n\nconst { data: stats } = useDashboardStats();\n</script>\n<template>\n  <WidgetCard>\n    <div class=\"flex h-full px-4 py-3\">\n      <div class=\"flex items-center gap-4\">\n        <span class=\"rounded-full bg-gray-100 p-2.5 text-gray-600\">\n          <IconBookRead class=\"h-5 w-5\" />\n        </span>\n\n        <div class=\"flex flex-col\">\n          <span class=\"text-sm text-gray-500\">\n            {{ $t(\"core.dashboard.widgets.presets.post_stats.title\") }}\n          </span>\n          <NumberFlow\n            v-if=\"config.enable_animation\"\n            class=\"text-2xl font-medium text-gray-900\"\n            :value=\"stats?.posts || 0\"\n            :format=\"{ notation: 'compact' }\"\n          >\n          </NumberFlow>\n          <span v-else class=\"text-2xl font-medium text-gray-900\">\n            {{ stats?.posts || 0 }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/posts/RecentPublishedWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { postLabels } from \"@/constants/labels\";\nimport PostListItem from \"./components/PostListItem.vue\";\n\nconst { data, isLoading, isFetching, refetch } = useQuery<ListedPost[]>({\n  queryKey: [\"widget-recent-posts\"],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.content.post.listPosts({\n      labelSelector: [`${postLabels.DELETED}=false`],\n      page: 1,\n      size: 10,\n    });\n    return data.items;\n  },\n});\n</script>\n<template>\n  <WidgetCard\n    :body-class=\"['!overflow-auto']\"\n    :title=\"$t('core.dashboard.widgets.presets.recent_published.title')\"\n  >\n    <VLoading v-if=\"isLoading\" />\n    <VEmpty\n      v-else-if=\"!data?.length\"\n      :title=\"$t('core.dashboard.widgets.presets.recent_published.empty.title')\"\n    >\n      <template #actions>\n        <VButton :loading=\"isFetching\" @click=\"refetch\">\n          {{ $t(\"core.common.buttons.refresh\") }}\n        </VButton>\n      </template>\n    </VEmpty>\n    <OverlayScrollbarsComponent\n      v-else\n      element=\"div\"\n      :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n      class=\"h-full w-full\"\n      defer\n    >\n      <VEntityContainer>\n        <PostListItem\n          v-for=\"post in data\"\n          :key=\"post.post.metadata.name\"\n          :post=\"post\"\n        />\n      </VEntityContainer>\n    </OverlayScrollbarsComponent>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/posts/components/PostListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport {\n  IconExternalLinkLine,\n  VEntity,\n  VEntityField,\n  VSpace,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { postLabels } from \"@/constants/labels\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n  post: ListedPost;\n}>();\n\nconst isPublished = computed(() => {\n  return props.post.post.metadata.labels?.[postLabels.PUBLISHED] === \"true\";\n});\n\nconst previewUrl = computed(() => {\n  const { status, metadata } = props.post.post;\n  if (isPublished.value) {\n    return status?.permalink;\n  }\n  return `/preview/posts/${metadata.name}`;\n});\n\nconst datetime = computed(() => {\n  return (\n    props.post.post.spec.publishTime ||\n    props.post.post.metadata.creationTimestamp\n  );\n});\n\nconst commentSubjectRefKey = `content.halo.run/Post/${props.post.post.metadata.name}`;\nconst commentListVisible = ref(false);\n\nconst commentText = computed(() => {\n  const { totalComment, approvedComment } = props.post.stats || {};\n\n  let text = t(\"core.dashboard.widgets.presets.recent_published.comments\", {\n    comments: totalComment || 0,\n  });\n\n  const pendingComments = (totalComment || 0) - (approvedComment || 0);\n\n  if (pendingComments > 0) {\n    text += t(\n      \"core.dashboard.widgets.presets.recent_published.comments-with-pending\",\n      {\n        count: pendingComments,\n      }\n    );\n  }\n  return text;\n});\n</script>\n\n<template>\n  <VEntity>\n    <template #start>\n      <VEntityField\n        :title=\"post.post.spec.title\"\n        :route=\"{\n          name: 'PostEditor',\n          query: { name: post.post.metadata.name },\n        }\"\n      >\n        <template v-if=\"isPublished\" #description>\n          <VSpace>\n            <span class=\"text-xs text-gray-500\">\n              {{\n                $t(\"core.dashboard.widgets.presets.recent_published.visits\", {\n                  visits: post.stats.visit || 0,\n                })\n              }}\n            </span>\n            <span\n              class=\"cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline\"\n              @click=\"commentListVisible = true\"\n            >\n              {{ commentText }}\n            </span>\n          </VSpace>\n          <SubjectQueryCommentListModal\n            v-if=\"commentListVisible\"\n            :subject-ref-key=\"commentSubjectRefKey\"\n            @close=\"commentListVisible = false\"\n          />\n        </template>\n        <template #extra>\n          <VSpace>\n            <VTag v-if=\"!isPublished\">\n              {{ $t(\"core.post.filters.status.items.draft\") }}\n            </VTag>\n            <a\n              v-if=\"previewUrl\"\n              target=\"_blank\"\n              :href=\"previewUrl\"\n              :title=\"previewUrl\"\n              class=\"text-gray-600 opacity-0 transition-all hover:text-gray-900 group-hover:opacity-100\"\n            >\n              <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n            </a>\n          </VSpace>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField\n        v-tooltip=\"utils.date.format(datetime)\"\n        :description=\"utils.date.timeAgo(datetime)\"\n      ></VEntityField>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/single-pages/SinglePageStatsWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { IconPages } from \"@halo-dev/components\";\nimport NumberFlow from \"@number-flow/vue\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { singlePageLabels } from \"@/constants/labels\";\n\ndefineProps<{\n  config: {\n    enable_animation: boolean;\n  };\n}>();\n\nconst { data: total } = useQuery({\n  queryKey: [\"widget-singlePage-count\"],\n  queryFn: async () => {\n    const { data } = await coreApiClient.content.singlePage.listSinglePage({\n      labelSelector: [\n        `${singlePageLabels.DELETED}=false`,\n        `${singlePageLabels.PUBLISHED}=true`,\n      ],\n      page: 1,\n      size: 1,\n    });\n    return data.total;\n  },\n});\n</script>\n<template>\n  <WidgetCard>\n    <div class=\"flex h-full px-4 py-3\">\n      <div class=\"flex items-center gap-4\">\n        <span class=\"rounded-full bg-gray-100 p-2.5 text-gray-600\">\n          <IconPages class=\"h-5 w-5\" />\n        </span>\n\n        <div class=\"flex flex-col\">\n          <span class=\"text-sm text-gray-500\">\n            {{ $t(\"core.dashboard.widgets.presets.page_stats.title\") }}\n          </span>\n          <NumberFlow\n            v-if=\"config.enable_animation\"\n            class=\"text-2xl font-medium text-gray-900\"\n            :value=\"total || 0\"\n            :format=\"{ notation: 'compact' }\"\n          >\n          </NumberFlow>\n          <span v-else class=\"text-2xl font-medium text-gray-900\">\n            {{ total || 0 }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/users/NotificationWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport type { Notification } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  VButton,\n  VEmpty,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n} from \"@halo-dev/components\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport sanitize from \"sanitize-html\";\n\nconst { currentUser } = stores.currentUser();\n\nconst {\n  data: notifications,\n  isLoading,\n  refetch,\n  isFetching,\n} = useQuery({\n  queryKey: [\"user-notifications\"],\n  queryFn: async () => {\n    const { data } =\n      await ucApiClient.notification.notification.listUserNotifications({\n        username: currentUser?.user.metadata.name as string,\n        page: 1,\n        size: 20,\n        fieldSelector: [\"spec.unread=true\"],\n      });\n\n    return data.items;\n  },\n});\n\nfunction handleRouteToNotification(notification: Notification) {\n  window.location.href = `/uc/notifications?name=${notification.metadata.name}`;\n}\n</script>\n\n<template>\n  <WidgetCard\n    :body-class=\"['@container', '!overflow-auto']\"\n    :title=\"$t('core.dashboard.widgets.presets.notification.title')\"\n  >\n    <template #actions>\n      <a\n        class=\"text-sm text-gray-600 hover:text-gray-900\"\n        href=\"/uc/notifications\"\n      >\n        {{ $t(\"core.common.buttons.view_all\") }}\n      </a>\n    </template>\n    <VLoading v-if=\"isLoading\" />\n    <VEmpty\n      v-else-if=\"!notifications?.length\"\n      :title=\"$t('core.dashboard.widgets.presets.notification.empty.title')\"\n    >\n      <template #actions>\n        <VButton :loading=\"isFetching\" @click=\"refetch\">\n          {{ $t(\"core.common.buttons.refresh\") }}\n        </VButton>\n      </template>\n    </VEmpty>\n    <OverlayScrollbarsComponent\n      v-else\n      element=\"div\"\n      :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n      class=\"h-full w-full\"\n      defer\n    >\n      <VEntityContainer>\n        <VEntity\n          v-for=\"notification in notifications\"\n          :key=\"notification.metadata.name\"\n        >\n          <template #start>\n            <VEntityField\n              :title=\"notification.spec?.title\"\n              :description=\"\n                sanitize(notification.spec?.htmlContent || '', {\n                  allowedTags: [],\n                  allowedAttributes: {},\n                })\n              \"\n              @click=\"handleRouteToNotification(notification)\"\n            />\n          </template>\n          <template #end>\n            <VEntityField>\n              <template #description>\n                <span class=\"truncate text-xs tabular-nums text-gray-500\">\n                  {{\n                    utils.date.timeAgo(notification.metadata.creationTimestamp)\n                  }}\n                </span>\n              </template>\n            </VEntityField>\n          </template>\n        </VEntity>\n      </VEntityContainer>\n    </OverlayScrollbarsComponent>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/dashboard/widgets/presets/users/UserStatsWidget.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useDashboardStats } from \"@console/composables/use-dashboard-stats\";\nimport WidgetCard from \"@console/modules/dashboard/components/WidgetCard.vue\";\nimport { IconUserSettings } from \"@halo-dev/components\";\nimport NumberFlow from \"@number-flow/vue\";\n\ndefineProps<{\n  config: {\n    enable_animation: boolean;\n  };\n}>();\n\nconst { data: stats } = useDashboardStats();\n</script>\n<template>\n  <WidgetCard>\n    <div class=\"flex h-full px-4 py-3\">\n      <div class=\"flex items-center gap-4\">\n        <span class=\"rounded-full bg-gray-100 p-2.5 text-gray-600\">\n          <IconUserSettings class=\"h-5 w-5\" />\n        </span>\n\n        <div class=\"flex flex-col\">\n          <span class=\"text-sm text-gray-500\">\n            {{ $t(\"core.dashboard.widgets.presets.user_stats.title\") }}\n          </span>\n          <NumberFlow\n            v-if=\"config.enable_animation\"\n            class=\"text-2xl font-medium text-gray-900\"\n            :value=\"stats?.users || 0\"\n            :format=\"{ notation: 'compact' }\"\n          >\n          </NumberFlow>\n          <span v-else class=\"text-2xl font-medium text-gray-900\">\n            {{ stats?.users || 0 }}\n          </span>\n        </div>\n      </div>\n    </div>\n  </WidgetCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/index.ts",
    "content": "import type { PluginModule } from \"@halo-dev/ui-shared\";\n\nconst modules = import.meta.glob(\"./**/module.ts\", {\n  eager: true,\n  import: \"default\",\n}) as Record<string, PluginModule>;\n\nexport default modules;\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/Menus.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  Menu,\n  MenuItem,\n  MenuItemV1alpha1ApiListMenuItemRequest,\n} from \"@halo-dev/api-client\";\nimport { coreApiClient, paginate } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconList,\n  IconListSettings,\n  IconMore,\n  Toast,\n  VButton,\n  VCard,\n  VDropdown,\n  VDropdownItem,\n  VEmpty,\n  VLoading,\n  VPageHeader,\n  VSpace,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { Draggable } from \"@he-tree/vue\";\nimport \"@he-tree/vue/style/default.css\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport MenuItemEditingModal from \"./components/MenuItemEditingModal.vue\";\nimport MenuList from \"./components/MenuList.vue\";\nimport type { MenuTreeItem } from \"./utils\";\nimport {\n  buildMenuItemsTree,\n  convertMenuTreeItemToMenuItem,\n  convertTreeToMenuItems,\n  getChildrenNames,\n  resetMenuItemsTreePriority,\n} from \"./utils\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]);\nconst selectedMenu = ref<Menu>();\nconst selectedMenuItem = ref<MenuItem>();\nconst selectedParentMenuItem = ref<MenuItem>();\nconst menuItemEditingModal = ref();\n\nconst {\n  data: menuItems,\n  isLoading,\n  refetch,\n} = useQuery<MenuItem[]>({\n  queryKey: [\"menu-items\", selectedMenu],\n  queryFn: async () => {\n    if (!selectedMenu.value?.spec.menuItems) {\n      return [];\n    }\n\n    const menuItemNames = selectedMenu.value.spec.menuItems.filter(Boolean);\n\n    return await paginate<MenuItemV1alpha1ApiListMenuItemRequest, MenuItem>(\n      (params) => coreApiClient.menuItem.listMenuItem(params),\n      {\n        fieldSelector: [`name=(${menuItemNames.join(\",\")})`],\n        size: 1000,\n      }\n    );\n  },\n  onSuccess(data) {\n    menuTreeItems.value = buildMenuItemsTree(data);\n  },\n  refetchInterval(data) {\n    const deletingMenuItems = data?.filter(\n      (menuItem) => !!menuItem.metadata.deletionTimestamp\n    );\n    return deletingMenuItems?.length ? 1000 : false;\n  },\n  enabled: computed(() => !!selectedMenu.value),\n});\n\nconst handleOpenEditingModal = (menuItem: MenuTreeItem) => {\n  coreApiClient.menuItem\n    .getMenuItem({\n      name: menuItem.metadata.name,\n    })\n    .then((response) => {\n      selectedMenuItem.value = response.data;\n      menuItemEditingModal.value = true;\n    });\n};\n\nconst handleOpenCreateByParentModal = (menuItem: MenuTreeItem) => {\n  selectedParentMenuItem.value = convertMenuTreeItemToMenuItem(menuItem);\n  menuItemEditingModal.value = true;\n};\n\nconst onMenuItemEditingModalClose = () => {\n  selectedParentMenuItem.value = undefined;\n  selectedMenuItem.value = undefined;\n  menuItemEditingModal.value = false;\n};\n\nconst onMenuItemSaved = async (menuItem: MenuItem) => {\n  if (!selectedMenu.value) {\n    return;\n  }\n\n  // update menu items\n  await coreApiClient.menu.patchMenu({\n    name: selectedMenu.value.metadata.name,\n    jsonPatchInner: [\n      {\n        op: \"add\",\n        path: \"/spec/menuItems\",\n        value: Array.from(\n          new Set([\n            ...(selectedMenu.value.spec.menuItems || []),\n            menuItem.metadata.name,\n          ])\n        ),\n      },\n    ],\n  });\n\n  await queryClient.invalidateQueries({ queryKey: [\"menus\"] });\n  await refetch();\n};\n\nconst batchUpdating = ref(false);\n\nasync function handleUpdateInBatch() {\n  if (batchUpdating.value) {\n    return;\n  }\n\n  const menuTreeItemsToUpdate = resetMenuItemsTreePriority(menuTreeItems.value);\n  const menuItemsToUpdate = convertTreeToMenuItems(menuTreeItemsToUpdate);\n  try {\n    batchUpdating.value = true;\n    const promises = menuItemsToUpdate.map((menuItem) =>\n      coreApiClient.menuItem.patchMenuItem({\n        name: menuItem.metadata.name,\n        jsonPatchInner: [\n          {\n            op: \"add\",\n            path: \"/spec/priority\",\n            value: menuItem.spec.priority || 0,\n          },\n          {\n            op: \"add\",\n            path: \"/spec/children\",\n            value: menuItem.spec.children || [],\n          },\n        ],\n      })\n    );\n    await Promise.all(promises);\n  } catch (e) {\n    console.error(\"Failed to update menu items\", e);\n  } finally {\n    await queryClient.invalidateQueries({ queryKey: [\"menus\"] });\n    await refetch();\n    batchUpdating.value = false;\n  }\n}\n\nconst handleDelete = async (menuItem: MenuTreeItem) => {\n  Dialog.info({\n    title: t(\"core.menu.operations.delete_menu_item.title\"),\n    description: t(\"core.menu.operations.delete_menu_item.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await coreApiClient.menuItem.deleteMenuItem({\n        name: menuItem.metadata.name,\n      });\n\n      const childrenNames = getChildrenNames(menuItem);\n\n      if (childrenNames.length) {\n        const deleteChildrenRequests = childrenNames.map((name) =>\n          coreApiClient.menuItem.deleteMenuItem({\n            name,\n          })\n        );\n        await Promise.all(deleteChildrenRequests);\n      }\n\n      await refetch();\n\n      // update items under menu\n      await coreApiClient.menu.patchMenu({\n        name: selectedMenu.value?.metadata.name as string,\n        jsonPatchInner: [\n          {\n            op: \"add\",\n            path: \"/spec/menuItems\",\n            value:\n              selectedMenu.value?.spec.menuItems?.filter(\n                (name) =>\n                  ![menuItem.metadata.name, ...childrenNames].includes(name)\n              ) || [],\n          },\n        ],\n      });\n\n      await queryClient.invalidateQueries({ queryKey: [\"menus\"] });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nconst TargetRef = {\n  Post: t(\"core.menu.menu_item_editing_modal.fields.ref_kind.options.post\"),\n  SinglePage: t(\n    \"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page\"\n  ),\n  Category: t(\n    \"core.menu.menu_item_editing_modal.fields.ref_kind.options.category\"\n  ),\n  Tag: t(\"core.menu.menu_item_editing_modal.fields.ref_kind.options.tag\"),\n};\n\nfunction getMenuItemRefDisplayName(menuItem: MenuTreeItem) {\n  const { kind } = menuItem.spec.targetRef || {};\n\n  if (kind && TargetRef[kind]) {\n    return TargetRef[kind];\n  }\n\n  return undefined;\n}\n</script>\n<template>\n  <MenuItemEditingModal\n    v-if=\"menuItemEditingModal && selectedMenu\"\n    :menu-item=\"selectedMenuItem\"\n    :parent-menu-item=\"selectedParentMenuItem\"\n    :menu=\"selectedMenu\"\n    @close=\"onMenuItemEditingModalClose\"\n    @saved=\"onMenuItemSaved\"\n  />\n  <VPageHeader :title=\"$t('core.menu.title')\">\n    <template #icon>\n      <IconListSettings />\n    </template>\n  </VPageHeader>\n  <div class=\"m-0 md:m-4\">\n    <div class=\"flex flex-col gap-4 sm:flex-row\">\n      <div class=\"w-96 flex-none\">\n        <MenuList v-model:selected-menu=\"selectedMenu\" />\n      </div>\n      <div class=\"min-w-0 flex-1 shrink\">\n        <VCard :body-class=\"['!p-0']\">\n          <template #header>\n            <div class=\"block w-full bg-gray-50 px-4 py-3\">\n              <div\n                class=\"relative flex flex-col items-start sm:flex-row sm:items-center\"\n              >\n                <div class=\"flex w-full flex-1 sm:w-auto\">\n                  <span class=\"text-base font-medium\">\n                    {{ selectedMenu?.spec.displayName }}\n                  </span>\n                </div>\n                <div class=\"mt-4 flex sm:mt-0\">\n                  <VSpace>\n                    <VButton\n                      v-permission=\"['system:menus:manage']\"\n                      size=\"xs\"\n                      type=\"default\"\n                      @click=\"menuItemEditingModal = true\"\n                    >\n                      {{ $t(\"core.common.buttons.new\") }}\n                    </VButton>\n                  </VSpace>\n                </div>\n              </div>\n            </div>\n          </template>\n          <VLoading v-if=\"isLoading\" />\n          <Transition v-else-if=\"!menuItems?.length\" appear name=\"fade\">\n            <VEmpty\n              :message=\"$t('core.menu.menu_item_empty.message')\"\n              :title=\"$t('core.menu.menu_item_empty.title')\"\n            >\n              <template #actions>\n                <VSpace>\n                  <VButton @click=\"refetch()\">\n                    {{ $t(\"core.common.buttons.refresh\") }}\n                  </VButton>\n                  <VButton\n                    v-permission=\"['system:menus:manage']\"\n                    type=\"secondary\"\n                    @click=\"menuItemEditingModal = true\"\n                  >\n                    <template #icon>\n                      <IconAddCircle />\n                    </template>\n                    {{ $t(\"core.common.buttons.new\") }}\n                  </VButton>\n                </VSpace>\n              </template>\n            </VEmpty>\n          </Transition>\n          <Transition v-else appear name=\"fade\">\n            <Draggable\n              v-model=\"menuTreeItems\"\n              :class=\"{\n                'cursor-progress opacity-60': batchUpdating,\n              }\"\n              :disable-drag=\"batchUpdating\"\n              trigger-class=\"drag-element\"\n              :indent=\"40\"\n              @after-drop=\"handleUpdateInBatch\"\n            >\n              <template #default=\"{ node }\">\n                <div\n                  class=\"group relative flex w-full items-center justify-between px-4 py-3 hover:bg-gray-50\"\n                >\n                  <div class=\"min-w-0 flex-1 shrink\">\n                    <div\n                      v-permission=\"['system:menus:manage']\"\n                      class=\"drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex\"\n                    >\n                      <IconList class=\"h-3.5 w-3.5\" />\n                    </div>\n                    <div class=\"flex flex-col gap-1\">\n                      <div class=\"inline-flex items-center gap-2\">\n                        <span\n                          class=\"truncate text-sm font-medium text-gray-900\"\n                        >\n                          {{ node.status.displayName }}\n                        </span>\n                        <VTag v-if=\"getMenuItemRefDisplayName(node)\">\n                          {{ getMenuItemRefDisplayName(node) }}\n                        </VTag>\n                      </div>\n                      <a\n                        v-if=\"node.status?.href\"\n                        :href=\"node.status?.href\"\n                        :title=\"node.status?.href\"\n                        target=\"_blank\"\n                        class=\"truncate text-xs text-gray-500 group-hover:text-gray-900\"\n                      >\n                        {{ node.status.href }}\n                      </a>\n                    </div>\n                  </div>\n                  <div class=\"flex flex-none items-center gap-6\">\n                    <VStatusDot\n                      v-if=\"node.metadata.deletionTimestamp\"\n                      v-tooltip=\"$t('core.common.status.deleting')\"\n                      state=\"warning\"\n                      animate\n                    />\n                    <VDropdown\n                      v-if=\"utils.permission.has(['system:menus:manage'])\"\n                    >\n                      <div\n                        class=\"cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-200/60\"\n                        @click.stop\n                      >\n                        <IconMore />\n                      </div>\n                      <template #popper>\n                        <VDropdownItem @click=\"handleOpenEditingModal(node)\">\n                          {{ $t(\"core.common.buttons.edit\") }}\n                        </VDropdownItem>\n                        <VDropdownItem\n                          @click=\"handleOpenCreateByParentModal(node)\"\n                        >\n                          {{\n                            $t(\"core.menu.operations.add_sub_menu_item.button\")\n                          }}\n                        </VDropdownItem>\n                        <VDropdownItem\n                          type=\"danger\"\n                          @click=\"handleDelete(node)\"\n                        >\n                          {{ $t(\"core.common.buttons.delete\") }}\n                        </VDropdownItem>\n                      </template>\n                    </VDropdown>\n                  </div>\n                </div>\n              </template>\n            </Draggable>\n          </Transition>\n        </VCard>\n      </div>\n    </div>\n  </div>\n</template>\n<style scoped>\n:deep(.vtlist-inner) {\n  @apply divide-y divide-gray-100;\n}\n:deep(.he-tree-drag-placeholder) {\n  height: 60px;\n}\n</style>\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/components/MenuEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Menu } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    menu?: Menu;\n  }>(),\n  {\n    menu: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"created\", menu: Menu): void;\n}>();\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst formState = ref<Menu>({\n  spec: {\n    displayName: \"\",\n    menuItems: [],\n  },\n  apiVersion: \"v1alpha1\",\n  kind: \"Menu\",\n  metadata: {\n    name: \"\",\n    generateName: \"menu-\",\n  },\n});\n\nconst saving = ref(false);\n\nconst modalTitle = props.menu\n  ? t(\"core.menu.menu_editing_modal.titles.update\")\n  : t(\"core.menu.menu_editing_modal.titles.create\");\n\nconst handleSaveMenu = async () => {\n  try {\n    saving.value = true;\n    if (props.menu) {\n      await coreApiClient.menu.updateMenu({\n        name: formState.value.metadata.name,\n        menu: formState.value,\n      });\n    } else {\n      const { data } = await coreApiClient.menu.createMenu({\n        menu: formState.value,\n      });\n      emit(\"created\", data);\n    }\n\n    queryClient.invalidateQueries({ queryKey: [\"menus\"] });\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (e) {\n    console.error(\"Failed to create menu\", e);\n  } finally {\n    saving.value = false;\n  }\n};\n\nonMounted(() => {\n  if (props.menu) {\n    formState.value = cloneDeep(props.menu);\n  }\n  setFocus(\"menuDisplayNameInput\");\n});\n</script>\n<template>\n  <VModal ref=\"modal\" :width=\"500\" :title=\"modalTitle\" @close=\"emit('close')\">\n    <FormKit\n      id=\"menu-form\"\n      name=\"menu-form\"\n      type=\"form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleSaveMenu\"\n    >\n      <FormKit\n        id=\"menuDisplayNameInput\"\n        v-model=\"formState.spec.displayName\"\n        :label=\"$t('core.menu.menu_editing_modal.fields.display_name.label')\"\n        type=\"text\"\n        name=\"displayName\"\n        validation=\"required|length:0,100\"\n      ></FormKit>\n    </FormKit>\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"saving\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('menu-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/components/MenuItemEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Menu, MenuItem, Ref } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { computed, nextTick, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    menu: Menu;\n    parentMenuItem?: MenuItem;\n    menuItem?: MenuItem;\n  }>(),\n  {\n    parentMenuItem: undefined,\n    menuItem: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"saved\", menuItem: MenuItem): void;\n}>();\n\nconst { t } = useI18n();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst selectedParentMenuItem = ref<string>(\"\");\nconst formState = ref<MenuItem>({\n  spec: {\n    displayName: \"\",\n    href: \"\",\n    target: \"_self\",\n    children: [],\n    priority: 0,\n  },\n  apiVersion: \"v1alpha1\",\n  kind: \"MenuItem\",\n  metadata: {\n    name: \"\",\n    generateName: \"menu-item-\",\n  },\n});\nconst saving = ref(false);\n\nconst isUpdateMode = !!props.menuItem;\n\nconst modalTitle = props.menuItem\n  ? t(\"core.menu.menu_item_editing_modal.titles.update\")\n  : t(\"core.menu.menu_item_editing_modal.titles.create\");\n\nconst annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();\n\nconst handleSaveMenuItem = async () => {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  formState.value.metadata.annotations = {\n    ...annotations,\n    ...customAnnotations,\n  };\n\n  try {\n    saving.value = true;\n\n    const menuItemRef = menuItemRefs.find(\n      (ref) => ref.ref?.kind === selectedRefKind.value\n    );\n\n    if (menuItemRef?.ref) {\n      formState.value.spec.targetRef = {\n        ...menuItemRef.ref,\n        name: selectedRefName.value,\n      };\n      formState.value.spec.displayName = undefined;\n      formState.value.spec.href = undefined;\n    }\n\n    if (isUpdateMode) {\n      const { data } = await coreApiClient.menuItem.updateMenuItem({\n        name: formState.value.metadata.name,\n        menuItem: formState.value,\n      });\n\n      emit(\"saved\", data);\n    } else {\n      const { data } = await coreApiClient.menuItem.createMenuItem({\n        menuItem: formState.value,\n      });\n\n      // if parent menu item is selected, add the new menu item to the parent menu item\n      if (selectedParentMenuItem.value) {\n        const { data: parentMenuItem } =\n          await coreApiClient.menuItem.getMenuItem({\n            name: selectedParentMenuItem.value,\n          });\n\n        await coreApiClient.menuItem.patchMenuItem({\n          name: selectedParentMenuItem.value,\n          jsonPatchInner: [\n            {\n              op: \"add\",\n              path: \"/spec/children\",\n              value: [\n                ...(parentMenuItem.spec.children || []),\n                data.metadata.name,\n              ],\n            },\n          ],\n        });\n      }\n\n      emit(\"saved\", data);\n    }\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (e) {\n    console.error(\"Failed to create menu item\", e);\n  } finally {\n    saving.value = false;\n  }\n};\n\ninterface MenuItemRef {\n  label: string;\n  inputType?: string;\n  ref?: Ref;\n}\n\nconst baseRef: Omit<Ref, \"kind\"> = {\n  group: \"content.halo.run\",\n  version: \"v1alpha1\",\n  name: \"\",\n};\n\nconst menuItemRefs: MenuItemRef[] = [\n  {\n    label: t(\n      \"core.menu.menu_item_editing_modal.fields.ref_kind.options.custom\"\n    ),\n  },\n  {\n    label: t(\"core.menu.menu_item_editing_modal.fields.ref_kind.options.post\"),\n    inputType: \"postSelect\",\n    ref: {\n      ...baseRef,\n      kind: \"Post\",\n    },\n  },\n  {\n    label: t(\n      \"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page\"\n    ),\n    inputType: \"singlePageSelect\",\n    ref: {\n      ...baseRef,\n      kind: \"SinglePage\",\n    },\n  },\n  {\n    label: t(\n      \"core.menu.menu_item_editing_modal.fields.ref_kind.options.category\"\n    ),\n    inputType: \"categorySelect\",\n    ref: {\n      ...baseRef,\n      kind: \"Category\",\n    },\n  },\n  {\n    label: t(\"core.menu.menu_item_editing_modal.fields.ref_kind.options.tag\"),\n    inputType: \"tagSelect\",\n    ref: {\n      ...baseRef,\n      kind: \"Tag\",\n    },\n  },\n];\n\nconst menuItemRefsMap = menuItemRefs.map((menuItemRef) => {\n  return {\n    label: menuItemRef.label,\n    value: menuItemRef.ref?.kind,\n  };\n});\n\nconst selectedRef = computed(() => {\n  return menuItemRefs.find(\n    (menuItemRef) => menuItemRef.ref?.kind === selectedRefKind.value\n  );\n});\n\nconst selectedRefKind = ref<string>();\nconst selectedRefName = ref<string>(\"\");\n\nconst onMenuItemSourceChange = () => {\n  selectedRefName.value = \"\";\n};\n\nonMounted(() => {\n  if (props.menuItem) {\n    formState.value = cloneDeep(props.menuItem);\n\n    // Set Ref related\n    const { targetRef } = formState.value.spec;\n\n    if (targetRef) {\n      selectedRefName.value = targetRef.name;\n      selectedRefKind.value = targetRef.kind as string;\n    }\n  }\n\n  selectedParentMenuItem.value = props.parentMenuItem?.metadata.name || \"\";\n\n  setFocus(\"displayNameInput\");\n});\n</script>\n<template>\n  <VModal ref=\"modal\" :width=\"700\" :title=\"modalTitle\" @close=\"emit('close')\">\n    <FormKit\n      id=\"menuitem-form\"\n      name=\"menuitem-form\"\n      type=\"form\"\n      :preserve=\"true\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleSaveMenuItem\"\n    >\n      <div>\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.menu.menu_item_editing_modal.groups.general\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              v-if=\"!isUpdateMode\"\n              v-model=\"selectedParentMenuItem\"\n              :label=\"\n                $t('core.menu.menu_item_editing_modal.fields.parent.label')\n              \"\n              :placeholder=\"\n                $t(\n                  'core.menu.menu_item_editing_modal.fields.parent.placeholder'\n                )\n              \"\n              type=\"menuItemSelect\"\n              :clearable=\"true\"\n              :menu-items=\"menu.spec.menuItems || []\"\n            />\n\n            <FormKit\n              v-model=\"selectedRefKind\"\n              :options=\"menuItemRefsMap\"\n              :disabled=\"isUpdateMode\"\n              :label=\"\n                $t('core.menu.menu_item_editing_modal.fields.ref_kind.label')\n              \"\n              type=\"select\"\n              @change=\"onMenuItemSourceChange\"\n            />\n\n            <FormKit\n              v-if=\"!selectedRefKind\"\n              id=\"displayNameInput\"\n              v-model=\"formState.spec.displayName\"\n              :label=\"\n                $t(\n                  'core.menu.menu_item_editing_modal.fields.display_name.label'\n                )\n              \"\n              type=\"text\"\n              name=\"displayName\"\n              validation=\"required|length:0,100\"\n            />\n\n            <FormKit\n              v-if=\"!selectedRefKind\"\n              v-model=\"formState.spec.href\"\n              :label=\"$t('core.menu.menu_item_editing_modal.fields.href.label')\"\n              type=\"text\"\n              name=\"href\"\n              validation=\"required|length:0,1024\"\n            />\n\n            <FormKit\n              v-if=\"selectedRef?.ref\"\n              :id=\"selectedRef.inputType\"\n              :key=\"selectedRef.inputType\"\n              v-model=\"selectedRefName\"\n              :placeholder=\"\n                $t(\n                  'core.menu.menu_item_editing_modal.fields.ref_kind.placeholder',\n                  { label: selectedRef.label }\n                )\n              \"\n              :label=\"selectedRef.label\"\n              :type=\"selectedRef.inputType as any\"\n              validation=\"required\"\n            />\n\n            <FormKit\n              v-model=\"formState.spec.target\"\n              :label=\"\n                $t('core.menu.menu_item_editing_modal.fields.target.label')\n              \"\n              type=\"select\"\n              name=\"target\"\n              :options=\"[\n                {\n                  label: $t(\n                    'core.menu.menu_item_editing_modal.fields.target.options.self'\n                  ),\n                  value: '_self',\n                },\n                {\n                  label: $t(\n                    'core.menu.menu_item_editing_modal.fields.target.options.blank'\n                  ),\n                  value: '_blank',\n                },\n                {\n                  label: $t(\n                    'core.menu.menu_item_editing_modal.fields.target.options.parent'\n                  ),\n                  value: '_parent',\n                },\n                {\n                  label: $t(\n                    'core.menu.menu_item_editing_modal.fields.target.options.top'\n                  ),\n                  value: '_top',\n                },\n              ]\"\n            />\n          </div>\n        </div>\n      </div>\n    </FormKit>\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.menu.menu_item_editing_modal.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"formState.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :value=\"formState.metadata.annotations\"\n          kind=\"MenuItem\"\n          :form-data=\"formState\"\n          group=\"\"\n        />\n      </div>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"saving\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('menuitem-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/components/MenuList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  Menu,\n  MenuV1alpha1ApiListMenuRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  paginate,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VButton,\n  VCard,\n  VDropdownItem,\n  VEmpty,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport MenuEditingModal from \"./MenuEditingModal.vue\";\n\ninterface SystemMenuConfig {\n  primary: string;\n}\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    selectedMenu?: Menu;\n  }>(),\n  {\n    selectedMenu: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:selectedMenu\", menu: Menu): void;\n}>();\n\nconst selectedMenuToUpdate = ref<Menu>();\nconst menuEditingModal = ref<boolean>(false);\n\nconst {\n  data: menus,\n  isLoading,\n  refetch,\n} = useQuery<Menu[]>({\n  queryKey: [\"menus\"],\n  queryFn: async () => {\n    return await paginate<MenuV1alpha1ApiListMenuRequest, Menu>(\n      (params) => coreApiClient.menu.listMenu(params),\n      {\n        size: 1000,\n      }\n    );\n  },\n  onSuccess(data) {\n    if (props.selectedMenu) {\n      const updatedMenu = data?.find(\n        (menu) => menu.metadata.name === props.selectedMenu?.metadata.name\n      );\n      if (updatedMenu) {\n        emit(\"update:selectedMenu\", updatedMenu);\n      }\n    }\n  },\n  refetchInterval(data) {\n    const hasDeletingMenu = data?.some(\n      (menu) => !!menu.metadata.deletionTimestamp\n    );\n    return hasDeletingMenu ? 1000 : false;\n  },\n});\n\nconst menuQuery = useRouteQuery(\"menu\");\nconst handleSelect = (menu: Menu) => {\n  emit(\"update:selectedMenu\", menu);\n  menuQuery.value = menu.metadata.name;\n};\n\nconst handleDeleteMenu = async (menu: Menu) => {\n  Dialog.warning({\n    title: t(\"core.menu.operations.delete_menu.title\"),\n    description: t(\"core.menu.operations.delete_menu.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.menu.deleteMenu({\n          name: menu.metadata.name,\n        });\n\n        const deleteItemsPromises = menu.spec.menuItems?.map((item) =>\n          coreApiClient.menuItem.deleteMenuItem({\n            name: item,\n          })\n        );\n\n        if (!deleteItemsPromises) {\n          return;\n        }\n\n        await Promise.all(deleteItemsPromises);\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (e) {\n        console.error(\"Failed to delete menu\", e);\n      } finally {\n        await refetch();\n      }\n    },\n  });\n};\n\nconst handleOpenEditingModal = (menu?: Menu) => {\n  selectedMenuToUpdate.value = menu;\n  menuEditingModal.value = true;\n};\n\nonMounted(async () => {\n  await refetch();\n\n  if (menuQuery.value) {\n    const menu = menus.value?.find((m) => m.metadata.name === menuQuery.value);\n    if (menu) {\n      handleSelect(menu);\n    }\n    return;\n  }\n\n  if (menus.value?.length) {\n    handleSelect(menus.value[0]);\n  }\n});\n\n// primary menu\nconst { data: primaryMenuName, refetch: refetchPrimaryMenuName } = useQuery({\n  queryKey: [\"primary-menu-name\"],\n  queryFn: async () => {\n    const { data } =\n      await consoleApiClient.configMap.system.getSystemConfigByGroup({\n        group: \"menu\",\n      });\n\n    const { primary } = (data as SystemMenuConfig) || {};\n\n    return primary;\n  },\n});\n\nconst handleSetPrimaryMenu = async (menu: Menu) => {\n  await consoleApiClient.configMap.system.updateSystemConfigByGroup({\n    group: \"menu\",\n    body: {\n      primary: menu.metadata.name,\n    },\n  });\n\n  await refetchPrimaryMenuName();\n\n  Toast.success(t(\"core.menu.operations.set_primary.toast_success\"));\n};\n</script>\n<template>\n  <MenuEditingModal\n    v-if=\"menuEditingModal\"\n    :menu=\"selectedMenuToUpdate\"\n    @close=\"menuEditingModal = false\"\n    @created=\"handleSelect\"\n  />\n  <VCard :body-class=\"['!p-0']\" :title=\"$t('core.menu.title')\">\n    <VLoading v-if=\"isLoading\" />\n    <Transition v-else-if=\"!menus?.length\" appear name=\"fade\">\n      <VEmpty\n        :message=\"$t('core.menu.empty.message')\"\n        :title=\"$t('core.menu.empty.title')\"\n      >\n        <template #actions>\n          <VButton size=\"sm\" @click=\"refetch()\">\n            {{ $t(\"core.common.buttons.refresh\") }}\n          </VButton>\n        </template>\n      </VEmpty>\n    </Transition>\n    <Transition v-else appear name=\"fade\">\n      <VEntityContainer>\n        <VEntity\n          v-for=\"menu in menus\"\n          :key=\"menu.metadata.name\"\n          :is-selected=\"selectedMenu?.metadata.name === menu.metadata.name\"\n          @click=\"handleSelect(menu)\"\n        >\n          <template #start>\n            <VEntityField\n              :title=\"menu.spec?.displayName\"\n              :description=\"\n                $t('core.menu.list.fields.items_count', {\n                  count: menu.spec.menuItems?.length || 0,\n                })\n              \"\n            >\n              <template v-if=\"menu.metadata.name === primaryMenuName\" #extra>\n                <VTag>\n                  {{ $t(\"core.menu.list.fields.primary\") }}\n                </VTag>\n              </template>\n            </VEntityField>\n          </template>\n          <template #end>\n            <VEntityField v-if=\"menu.metadata.deletionTimestamp\">\n              <template #description>\n                <VStatusDot\n                  v-tooltip=\"$t('core.common.status.deleting')\"\n                  state=\"warning\"\n                  animate\n                />\n              </template>\n            </VEntityField>\n          </template>\n          <template\n            v-if=\"utils.permission.has(['system:menus:manage'])\"\n            #dropdownItems\n          >\n            <VDropdownItem\n              v-if=\"primaryMenuName !== menu.metadata.name\"\n              @click=\"handleSetPrimaryMenu(menu)\"\n            >\n              {{ $t(\"core.menu.operations.set_primary.button\") }}\n            </VDropdownItem>\n            <VDropdownItem @click=\"handleOpenEditingModal(menu)\">\n              {{ $t(\"core.common.buttons.edit\") }}\n            </VDropdownItem>\n            <VDropdownItem\n              :disabled=\"primaryMenuName === menu.metadata.name\"\n              type=\"danger\"\n              @click=\"handleDeleteMenu(menu)\"\n            >\n              {{ $t(\"core.common.buttons.delete\") }}\n            </VDropdownItem>\n          </template>\n        </VEntity>\n      </VEntityContainer>\n    </Transition>\n    <template v-if=\"utils.permission.has(['system:menus:manage'])\" #footer>\n      <VButton block type=\"secondary\" @click=\"handleOpenEditingModal()\">\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconListSettings } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  components: {},\n  routes: [\n    {\n      path: \"/menus\",\n      name: \"MenusRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.menu.title\",\n        searchable: true,\n        permissions: [\"system:menus:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.menus\",\n          group: \"interface\",\n          icon: markRaw(IconListSettings),\n          priority: 1,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Menus\",\n          component: () => import(\"./Menus.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/utils/__tests__/__snapshots__/index.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`buildMenuItemsTree > should match snapshot 1`] = `\n[\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n      \"name\": \"411a3639-bf0d-4266-9cfb-14184259dab5\",\n      \"version\": 1,\n    },\n    \"spec\": {\n      \"children\": [],\n      \"displayName\": \"首页\",\n      \"href\": \"https://ryanc.cc/\",\n      \"priority\": 0,\n    },\n  },\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-07-28T06:50:32.777556Z\",\n          \"name\": \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n          \"version\": 4,\n        },\n        \"spec\": {\n          \"children\": [],\n          \"displayName\": \"Halo\",\n          \"href\": \"https://ryanc.cc/categories/halo\",\n          \"priority\": 0,\n        },\n      },\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [\n          {\n            \"apiVersion\": \"v1alpha1\",\n            \"children\": [],\n            \"kind\": \"MenuItem\",\n            \"metadata\": {\n              \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n              \"name\": \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n              \"version\": 1,\n            },\n            \"spec\": {\n              \"children\": [],\n              \"displayName\": \"Spring Boot\",\n              \"href\": \"https://ryanc.cc/categories/spring-boot\",\n              \"priority\": 0,\n            },\n          },\n        ],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n          \"name\": \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n          \"version\": 1,\n        },\n        \"spec\": {\n          \"children\": [\n            \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n          ],\n          \"displayName\": \"Java\",\n          \"href\": \"https://ryanc.cc/categories/java\",\n          \"priority\": 1,\n        },\n      },\n    ],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:19:37.252228Z\",\n      \"name\": \"40e4ba86-5c7e-43c2-b4c0-cee376d26564\",\n      \"version\": 12,\n    },\n    \"spec\": {\n      \"children\": [\n        \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n        \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n      ],\n      \"displayName\": \"文章分类\",\n      \"href\": \"https://ryanc.cc/categories\",\n      \"priority\": 1,\n    },\n  },\n]\n`;\n\nexports[`convertMenuTreeItemToMenuItem > should handle node with empty children 1`] = `\n{\n  \"apiVersion\": \"v1alpha1\",\n  \"children\": [],\n  \"kind\": \"MenuItem\",\n  \"metadata\": {\n    \"creationTimestamp\": \"2022-01-01T00:00:00Z\",\n    \"name\": \"test\",\n    \"version\": 1,\n  },\n  \"spec\": {\n    \"children\": [],\n    \"displayName\": \"test\",\n    \"href\": \"#\",\n    \"priority\": 0,\n  },\n}\n`;\n\nexports[`convertMenuTreeItemToMenuItem > should match snapshot 1`] = `\n{\n  \"apiVersion\": \"v1alpha1\",\n  \"children\": [\n    {\n      \"apiVersion\": \"v1alpha1\",\n      \"children\": [],\n      \"kind\": \"MenuItem\",\n      \"metadata\": {\n        \"creationTimestamp\": \"2022-07-28T06:50:32.777556Z\",\n        \"name\": \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n        \"version\": 4,\n      },\n      \"spec\": {\n        \"children\": [],\n        \"displayName\": \"Halo\",\n        \"href\": \"https://ryanc.cc/categories/halo\",\n        \"priority\": 0,\n      },\n    },\n    {\n      \"apiVersion\": \"v1alpha1\",\n      \"children\": [\n        {\n          \"apiVersion\": \"v1alpha1\",\n          \"children\": [],\n          \"kind\": \"MenuItem\",\n          \"metadata\": {\n            \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n            \"name\": \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n            \"version\": 1,\n          },\n          \"spec\": {\n            \"children\": [],\n            \"displayName\": \"Spring Boot\",\n            \"href\": \"https://ryanc.cc/categories/spring-boot\",\n            \"priority\": 0,\n          },\n        },\n      ],\n      \"kind\": \"MenuItem\",\n      \"metadata\": {\n        \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n        \"name\": \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n        \"version\": 1,\n      },\n      \"spec\": {\n        \"children\": [\n          \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n        ],\n        \"displayName\": \"Java\",\n        \"href\": \"https://ryanc.cc/categories/java\",\n        \"priority\": 1,\n      },\n    },\n  ],\n  \"kind\": \"MenuItem\",\n  \"metadata\": {\n    \"creationTimestamp\": \"2022-08-05T04:19:37.252228Z\",\n    \"name\": \"40e4ba86-5c7e-43c2-b4c0-cee376d26564\",\n    \"version\": 12,\n  },\n  \"spec\": {\n    \"children\": [\n      \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n      \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n    ],\n    \"displayName\": \"文章分类\",\n    \"href\": \"https://ryanc.cc/categories\",\n    \"priority\": 1,\n  },\n}\n`;\n\nexports[`convertMenuTreeItemToMenuItem > should match snapshot 2`] = `\n{\n  \"apiVersion\": \"v1alpha1\",\n  \"children\": [\n    {\n      \"apiVersion\": \"v1alpha1\",\n      \"children\": [],\n      \"kind\": \"MenuItem\",\n      \"metadata\": {\n        \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n        \"name\": \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n        \"version\": 1,\n      },\n      \"spec\": {\n        \"children\": [],\n        \"displayName\": \"Spring Boot\",\n        \"href\": \"https://ryanc.cc/categories/spring-boot\",\n        \"priority\": 0,\n      },\n    },\n  ],\n  \"kind\": \"MenuItem\",\n  \"metadata\": {\n    \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n    \"name\": \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n    \"version\": 1,\n  },\n  \"spec\": {\n    \"children\": [\n      \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n    ],\n    \"displayName\": \"Java\",\n    \"href\": \"https://ryanc.cc/categories/java\",\n    \"priority\": 1,\n  },\n}\n`;\n\nexports[`convertTreeToMenuItems > will match snapshot 1`] = `\n[\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n      \"name\": \"411a3639-bf0d-4266-9cfb-14184259dab5\",\n      \"version\": 1,\n    },\n    \"spec\": {\n      \"children\": [],\n      \"displayName\": \"首页\",\n      \"href\": \"https://ryanc.cc/\",\n      \"priority\": 0,\n    },\n  },\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-07-28T06:50:32.777556Z\",\n          \"name\": \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n          \"version\": 4,\n        },\n        \"spec\": {\n          \"children\": [],\n          \"displayName\": \"Halo\",\n          \"href\": \"https://ryanc.cc/categories/halo\",\n          \"priority\": 0,\n        },\n      },\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [\n          {\n            \"apiVersion\": \"v1alpha1\",\n            \"children\": [],\n            \"kind\": \"MenuItem\",\n            \"metadata\": {\n              \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n              \"name\": \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n              \"version\": 1,\n            },\n            \"spec\": {\n              \"children\": [],\n              \"displayName\": \"Spring Boot\",\n              \"href\": \"https://ryanc.cc/categories/spring-boot\",\n              \"priority\": 0,\n            },\n          },\n        ],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n          \"name\": \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n          \"version\": 1,\n        },\n        \"spec\": {\n          \"children\": [\n            \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n          ],\n          \"displayName\": \"Java\",\n          \"href\": \"https://ryanc.cc/categories/java\",\n          \"priority\": 1,\n        },\n      },\n    ],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:19:37.252228Z\",\n      \"name\": \"40e4ba86-5c7e-43c2-b4c0-cee376d26564\",\n      \"version\": 12,\n    },\n    \"spec\": {\n      \"children\": [\n        \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n        \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n      ],\n      \"displayName\": \"文章分类\",\n      \"href\": \"https://ryanc.cc/categories\",\n      \"priority\": 1,\n    },\n  },\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-07-28T06:50:32.777556Z\",\n      \"name\": \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n      \"version\": 4,\n    },\n    \"spec\": {\n      \"children\": [],\n      \"displayName\": \"Halo\",\n      \"href\": \"https://ryanc.cc/categories/halo\",\n      \"priority\": 0,\n    },\n  },\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n          \"name\": \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n          \"version\": 1,\n        },\n        \"spec\": {\n          \"children\": [],\n          \"displayName\": \"Spring Boot\",\n          \"href\": \"https://ryanc.cc/categories/spring-boot\",\n          \"priority\": 0,\n        },\n      },\n    ],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n      \"name\": \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n      \"version\": 1,\n    },\n    \"spec\": {\n      \"children\": [\n        \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n      ],\n      \"displayName\": \"Java\",\n      \"href\": \"https://ryanc.cc/categories/java\",\n      \"priority\": 1,\n    },\n  },\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n      \"name\": \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n      \"version\": 1,\n    },\n    \"spec\": {\n      \"children\": [],\n      \"displayName\": \"Spring Boot\",\n      \"href\": \"https://ryanc.cc/categories/spring-boot\",\n      \"priority\": 0,\n    },\n  },\n]\n`;\n\nexports[`resetMenuItemsTreePriority > should match snapshot 1`] = `\n[\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n      \"name\": \"411a3639-bf0d-4266-9cfb-14184259dab5\",\n      \"version\": 1,\n    },\n    \"spec\": {\n      \"children\": [],\n      \"displayName\": \"首页\",\n      \"href\": \"https://ryanc.cc/\",\n      \"priority\": 0,\n    },\n  },\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-07-28T06:50:32.777556Z\",\n          \"name\": \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n          \"version\": 4,\n        },\n        \"spec\": {\n          \"children\": [],\n          \"displayName\": \"Halo\",\n          \"href\": \"https://ryanc.cc/categories/halo\",\n          \"priority\": 0,\n        },\n      },\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [\n          {\n            \"apiVersion\": \"v1alpha1\",\n            \"children\": [],\n            \"kind\": \"MenuItem\",\n            \"metadata\": {\n              \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n              \"name\": \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n              \"version\": 1,\n            },\n            \"spec\": {\n              \"children\": [],\n              \"displayName\": \"Spring Boot\",\n              \"href\": \"https://ryanc.cc/categories/spring-boot\",\n              \"priority\": 0,\n            },\n          },\n        ],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n          \"name\": \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n          \"version\": 1,\n        },\n        \"spec\": {\n          \"children\": [\n            \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n          ],\n          \"displayName\": \"Java\",\n          \"href\": \"https://ryanc.cc/categories/java\",\n          \"priority\": 1,\n        },\n      },\n    ],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:19:37.252228Z\",\n      \"name\": \"40e4ba86-5c7e-43c2-b4c0-cee376d26564\",\n      \"version\": 12,\n    },\n    \"spec\": {\n      \"children\": [\n        \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n        \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n      ],\n      \"displayName\": \"文章分类\",\n      \"href\": \"https://ryanc.cc/categories\",\n      \"priority\": 1,\n    },\n  },\n]\n`;\n\nexports[`sortMenuItemsTree > will match snapshot 1`] = `\n[\n  {\n    \"apiVersion\": \"v1alpha1\",\n    \"children\": [\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-08-05T04:22:03.377364Z\",\n          \"name\": \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n          \"version\": 0,\n        },\n        \"spec\": {\n          \"displayName\": \"Java\",\n          \"href\": \"https://ryanc.cc/categories/java\",\n          \"priority\": 0,\n        },\n      },\n      {\n        \"apiVersion\": \"v1alpha1\",\n        \"children\": [],\n        \"kind\": \"MenuItem\",\n        \"metadata\": {\n          \"creationTimestamp\": \"2022-07-28T06:50:32.777556Z\",\n          \"name\": \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n          \"version\": 4,\n        },\n        \"spec\": {\n          \"displayName\": \"Halo\",\n          \"href\": \"https://ryanc.cc/categories/halo\",\n          \"priority\": 1,\n        },\n      },\n    ],\n    \"kind\": \"MenuItem\",\n    \"metadata\": {\n      \"creationTimestamp\": \"2022-08-05T04:19:37.252228Z\",\n      \"name\": \"40e4ba86-5c7e-43c2-b4c0-cee376d26564\",\n      \"version\": 12,\n    },\n    \"spec\": {\n      \"displayName\": \"文章分类\",\n      \"href\": \"https://ryanc.cc/categories\",\n      \"priority\": 0,\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/utils/__tests__/index.spec.ts",
    "content": "import type { MenuItem } from \"@halo-dev/api-client\";\nimport { describe, expect, it } from \"vitest\";\nimport type { MenuTreeItem } from \"../index\";\nimport {\n  buildMenuItemsTree,\n  convertMenuTreeItemToMenuItem,\n  convertTreeToMenuItems,\n  getChildrenNames,\n  resetMenuItemsTreePriority,\n  sortMenuItemsTree,\n} from \"../index\";\n\nconst rawMenuItems: MenuItem[] = [\n  {\n    spec: {\n      displayName: \"文章分类\",\n      href: \"https://ryanc.cc/categories\",\n      children: [\n        \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n        \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n      ],\n      priority: 1,\n    },\n    apiVersion: \"v1alpha1\",\n    kind: \"MenuItem\",\n    metadata: {\n      name: \"40e4ba86-5c7e-43c2-b4c0-cee376d26564\",\n      version: 12,\n      creationTimestamp: \"2022-08-05T04:19:37.252228Z\",\n    },\n  },\n  {\n    spec: {\n      displayName: \"Halo\",\n      href: \"https://ryanc.cc/categories/halo\",\n      children: [],\n      priority: 0,\n    },\n    apiVersion: \"v1alpha1\",\n    kind: \"MenuItem\",\n    metadata: {\n      name: \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n      version: 4,\n      creationTimestamp: \"2022-07-28T06:50:32.777556Z\",\n    },\n  },\n  {\n    spec: {\n      displayName: \"Java\",\n      href: \"https://ryanc.cc/categories/java\",\n      children: [\"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\"],\n      priority: 1,\n    },\n    apiVersion: \"v1alpha1\",\n    kind: \"MenuItem\",\n    metadata: {\n      name: \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n      version: 1,\n      creationTimestamp: \"2022-08-05T04:22:03.377364Z\",\n    },\n  },\n  {\n    spec: {\n      displayName: \"Spring Boot\",\n      href: \"https://ryanc.cc/categories/spring-boot\",\n      children: [],\n      priority: 0,\n    },\n    apiVersion: \"v1alpha1\",\n    kind: \"MenuItem\",\n    metadata: {\n      name: \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n      version: 1,\n      creationTimestamp: \"2022-08-05T04:22:03.377364Z\",\n    },\n  },\n  {\n    spec: {\n      displayName: \"首页\",\n      href: \"https://ryanc.cc/\",\n      children: [],\n      priority: 0,\n    },\n    apiVersion: \"v1alpha1\",\n    kind: \"MenuItem\",\n    metadata: {\n      name: \"411a3639-bf0d-4266-9cfb-14184259dab5\",\n      version: 1,\n      creationTimestamp: \"2022-08-05T04:22:03.377364Z\",\n    },\n  },\n];\n\ndescribe(\"buildMenuItemsTree\", () => {\n  it(\"should match snapshot\", () => {\n    const tree = buildMenuItemsTree(rawMenuItems);\n    expect(tree).toMatchSnapshot();\n  });\n\n  it(\"should be sorted correctly and children at top level\", () => {\n    const menuItems = buildMenuItemsTree(rawMenuItems);\n    expect(menuItems[0].spec.priority).toBe(0);\n    expect(menuItems[1].spec.priority).toBe(1);\n    expect(menuItems[1].children[0].spec.priority).toBe(0);\n    expect(menuItems[1].children[1].spec.priority).toBe(1);\n    expect(menuItems[1].children[1].children[0].spec.priority).toBe(0);\n    expect(menuItems[0].spec.displayName).toBe(\"首页\");\n    expect(menuItems[1].spec.displayName).toBe(\"文章分类\");\n    expect(menuItems[1].children[0].spec.displayName).toBe(\"Halo\");\n    expect(menuItems[1].children[1].spec.displayName).toBe(\"Java\");\n    expect(menuItems[1].children[1].children[0].spec.displayName).toBe(\n      \"Spring Boot\"\n    );\n  });\n\n  it(\"should handle empty input\", () => {\n    expect(buildMenuItemsTree([])).toEqual([]);\n  });\n});\n\ndescribe(\"convertTreeToMenuItems\", () => {\n  it(\"will match snapshot\", function () {\n    const menuTreeItems = buildMenuItemsTree(rawMenuItems);\n    expect(convertTreeToMenuItems(menuTreeItems)).toMatchSnapshot();\n  });\n  it(\"should handle empty input\", () => {\n    expect(convertTreeToMenuItems([])).toEqual([]);\n  });\n});\n\ndescribe(\"sortMenuItemsTree\", () => {\n  it(\"will match snapshot\", () => {\n    const tree: MenuTreeItem[] = [\n      {\n        apiVersion: \"v1alpha1\",\n        kind: \"MenuItem\",\n        metadata: {\n          creationTimestamp: \"2022-08-05T04:19:37.252228Z\",\n          name: \"40e4ba86-5c7e-43c2-b4c0-cee376d26564\",\n          version: 12,\n        },\n        spec: {\n          priority: 0,\n          displayName: \"文章分类\",\n          href: \"https://ryanc.cc/categories\",\n        },\n        children: [\n          {\n            apiVersion: \"v1alpha1\",\n            kind: \"MenuItem\",\n            metadata: {\n              creationTimestamp: \"2022-07-28T06:50:32.777556Z\",\n              name: \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n              version: 4,\n            },\n            spec: {\n              priority: 1,\n              displayName: \"Halo\",\n              href: \"https://ryanc.cc/categories/halo\",\n            },\n            children: [],\n          },\n          {\n            apiVersion: \"v1alpha1\",\n            kind: \"MenuItem\",\n            metadata: {\n              creationTimestamp: \"2022-08-05T04:22:03.377364Z\",\n              name: \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n              version: 0,\n            },\n            spec: {\n              priority: 0,\n              displayName: \"Java\",\n              href: \"https://ryanc.cc/categories/java\",\n            },\n            children: [],\n          },\n        ],\n      },\n    ];\n    expect(sortMenuItemsTree(tree)).toMatchSnapshot();\n  });\n});\n\ndescribe(\"resetMenuItemsTreePriority\", () => {\n  it(\"should match snapshot\", function () {\n    expect(\n      resetMenuItemsTreePriority(buildMenuItemsTree(rawMenuItems))\n    ).toMatchSnapshot();\n  });\n});\n\ndescribe(\"getChildrenNames\", () => {\n  it(\"should return correct names\", () => {\n    const menuTreeItems = buildMenuItemsTree(rawMenuItems);\n    expect(getChildrenNames(menuTreeItems[0])).toEqual([]);\n    expect(getChildrenNames(menuTreeItems[1])).toEqual([\n      \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n      \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n      \"96b636bb-3e4a-44d1-8ea7-f9da9e876f45\",\n    ]);\n    expect(getChildrenNames(menuTreeItems[1].children[0])).toEqual([]);\n  });\n  it(\"should handle empty children\", () => {\n    const node: MenuTreeItem = {\n      apiVersion: \"v1alpha1\",\n      kind: \"MenuItem\",\n      metadata: {\n        name: \"test\",\n        version: 1,\n        creationTimestamp: \"2022-01-01T00:00:00Z\",\n      },\n      spec: {\n        displayName: \"test\",\n        href: \"#\",\n        children: [],\n        priority: 0,\n      },\n      children: [],\n    };\n    expect(getChildrenNames(node)).toEqual([]);\n  });\n});\n\ndescribe(\"convertMenuTreeItemToMenuItem\", () => {\n  it(\"should match snapshot\", () => {\n    const menuTreeItems = buildMenuItemsTree(rawMenuItems);\n    expect(convertMenuTreeItemToMenuItem(menuTreeItems[1])).toMatchSnapshot();\n    expect(\n      convertMenuTreeItemToMenuItem(menuTreeItems[1].children[1])\n    ).toMatchSnapshot();\n  });\n  it(\"should return correct MenuItem\", () => {\n    const menuTreeItems = buildMenuItemsTree(rawMenuItems);\n    expect(\n      convertMenuTreeItemToMenuItem(menuTreeItems[1]).spec.displayName\n    ).toBe(\"文章分类\");\n    expect(\n      convertMenuTreeItemToMenuItem(menuTreeItems[1]).spec.children\n    ).toStrictEqual([\n      \"caeef383-3828-4039-9114-6f9ad3b4a37e\",\n      \"ded1943d-9fdb-4563-83ee-2f04364872e0\",\n    ]);\n  });\n  it(\"should handle node with empty children\", () => {\n    const node: MenuTreeItem = {\n      apiVersion: \"v1alpha1\",\n      kind: \"MenuItem\",\n      metadata: {\n        name: \"test\",\n        version: 1,\n        creationTimestamp: \"2022-01-01T00:00:00Z\",\n      },\n      spec: {\n        displayName: \"test\",\n        href: \"#\",\n        children: [],\n        priority: 0,\n      },\n      children: [],\n    };\n    expect(convertMenuTreeItemToMenuItem(node)).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "ui/console-src/modules/interface/menus/utils/index.ts",
    "content": "import type { MenuItem } from \"@halo-dev/api-client\";\nimport { cloneDeep } from \"es-toolkit\";\n\nexport interface MenuTreeItem extends MenuItem {\n  children: MenuTreeItem[];\n}\n\n/**\n * Convert a flat array of menu items into a menu tree with children at the top level.\n *\n * @param menuItems\n */\nexport function buildMenuItemsTree(menuItems: MenuItem[]): MenuTreeItem[] {\n  const menuItemsToUpdate = cloneDeep(menuItems);\n\n  const menuItemsMap: Record<string, MenuTreeItem> = {};\n  const parentMap: Record<string, string> = {};\n\n  menuItemsToUpdate.forEach((menuItem) => {\n    menuItemsMap[menuItem.metadata.name] = {\n      ...menuItem,\n      children: [],\n    };\n    (menuItem.spec.children as string[]).forEach((child) => {\n      parentMap[child] = menuItem.metadata.name;\n    });\n  });\n\n  Object.values(menuItemsMap).forEach((menuTreeItem) => {\n    const parentName = parentMap[menuTreeItem.metadata.name];\n    if (parentName && menuItemsMap[parentName]) {\n      menuItemsMap[parentName].children.push(menuTreeItem);\n    }\n  });\n\n  const menuTreeItems = Object.values(menuItemsMap).filter(\n    (node) => parentMap[node.metadata.name] === undefined\n  );\n\n  return sortMenuItemsTree(menuTreeItems);\n}\n\n/**\n * Sort a menu tree by priority.\n *\n * @param menuTreeItems\n */\nexport function sortMenuItemsTree(\n  menuTreeItems: MenuTreeItem[]\n): MenuTreeItem[] {\n  return menuTreeItems\n    .sort((a, b) => {\n      const aPriority = a.spec.priority ?? 0;\n      const bPriority = b.spec.priority ?? 0;\n      if (aPriority < bPriority) {\n        return -1;\n      }\n      if (aPriority > bPriority) {\n        return 1;\n      }\n      return 0;\n    })\n    .map((menuTreeItem) => {\n      if (menuTreeItem.children.length) {\n        return {\n          ...menuTreeItem,\n          children: sortMenuItemsTree(menuTreeItem.children),\n        };\n      }\n      return menuTreeItem;\n    });\n}\n\n/**\n * Reset the menu tree item's priority.\n *\n * @param menuItems\n */\nexport function resetMenuItemsTreePriority(\n  menuItems: MenuTreeItem[]\n): MenuTreeItem[] {\n  for (let i = 0; i < menuItems.length; i++) {\n    menuItems[i].spec.priority = i;\n    if (menuItems[i].children) {\n      resetMenuItemsTreePriority(menuItems[i].children);\n    }\n  }\n  return menuItems;\n}\n\n/**\n * Convert a menu tree items into a flat array of menu.\n *\n * @param menuTreeItems\n */\nexport function convertTreeToMenuItems(menuTreeItems: MenuTreeItem[]) {\n  const menuItems: MenuItem[] = [];\n  const menuItemsMap = new Map<string, MenuItem>();\n  const convertMenuItem = (node: MenuTreeItem | undefined) => {\n    if (!node) {\n      return;\n    }\n    const children = node.children || [];\n    const { ...rest } = node;\n    menuItemsMap.set(node.metadata.name, {\n      ...rest,\n      spec: {\n        ...node.spec,\n        children: children.map((child) => child.metadata.name),\n      },\n    });\n    children.forEach((child) => {\n      convertMenuItem(child);\n    });\n  };\n  menuTreeItems.forEach((node) => {\n    convertMenuItem(node);\n  });\n  menuItemsMap.forEach((node) => {\n    menuItems.push(node);\n  });\n  return menuItems;\n}\n\nexport function getChildrenNames(menuTreeItem: MenuTreeItem): string[] {\n  const childrenNames: string[] = [];\n\n  function getChildrenNamesRecursive(menuTreeItem: MenuTreeItem) {\n    if (menuTreeItem.children) {\n      menuTreeItem.children.forEach((child) => {\n        childrenNames.push(child.metadata.name);\n        getChildrenNamesRecursive(child);\n      });\n    }\n  }\n\n  getChildrenNamesRecursive(menuTreeItem);\n\n  return childrenNames;\n}\n\n/**\n * Convert {@link MenuTreeItem} to {@link MenuItem} with flat children name array.\n *\n * @param menuTreeItem\n */\nexport function convertMenuTreeItemToMenuItem(\n  menuTreeItem: MenuTreeItem\n): MenuItem {\n  const childNames = (menuTreeItem.children || []).map(\n    (child) => child.metadata.name\n  );\n  const { ...rest } = menuTreeItem;\n  return {\n    ...rest,\n    spec: {\n      ...menuTreeItem.spec,\n      children: childNames,\n    },\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/ThemeDetail.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Theme } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconMore,\n  Toast,\n  VAvatar,\n  VDescription,\n  VDescriptionItem,\n  VDropdown,\n  VDropdownDivider,\n  VDropdownItem,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport type { Ref } from \"vue\";\nimport { inject, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useThemeConfigFile, useThemeLifeCycle } from \"./composables/use-theme\";\n\nconst { t } = useI18n();\n\nconst selectedTheme = inject<Ref<Theme | undefined>>(\"selectedTheme\", ref());\nconst themesModal = inject<Ref<boolean>>(\"themesModal\");\n\nconst { isActivated, getFailedMessage, handleResetSettingConfig } =\n  useThemeLifeCycle(selectedTheme);\n\nasync function handleClearCache() {\n  Dialog.warning({\n    title: t(\"core.theme.operations.clear_templates_cache.title\"),\n    description: t(\"core.theme.operations.clear_templates_cache.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      if (!selectedTheme.value) {\n        console.error(\"No selected or activated theme\");\n        return;\n      }\n\n      await consoleApiClient.theme.theme.invalidateCache({\n        name: selectedTheme.value?.metadata.name,\n      });\n\n      Toast.success(t(\"core.common.toast.operation_success\"));\n    },\n  });\n}\n\nconst handleReloadTheme = async () => {\n  Dialog.warning({\n    title: t(\"core.theme.operations.reload.title\"),\n    description: t(\"core.theme.operations.reload.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        if (!selectedTheme?.value) {\n          return;\n        }\n\n        await consoleApiClient.theme.theme.reload({\n          name: selectedTheme.value.metadata.name as string,\n        });\n\n        Toast.success(t(\"core.theme.operations.reload.toast_success\"));\n\n        window.location.reload();\n      } catch (e) {\n        console.error(\"Failed to reload theme setting\", e);\n      }\n    },\n  });\n};\n\nconst { handleExportThemeConfiguration, openSelectImportFileDialog } =\n  useThemeConfigFile(selectedTheme);\n</script>\n\n<template>\n  <div class=\"overflow-hidden rounded-b-base\">\n    <div class=\"bg-white px-4 py-4 sm:px-6\">\n      <div class=\"group flex items-center justify-between\">\n        <div class=\"flex flex-row items-center gap-3\">\n          <VAvatar\n            :key=\"selectedTheme?.metadata.name\"\n            :alt=\"selectedTheme?.spec.displayName\"\n            :src=\"selectedTheme?.spec.logo\"\n            size=\"lg\"\n          />\n          <div>\n            <h3 class=\"text-lg font-medium leading-6 text-gray-900\">\n              {{ selectedTheme?.spec.displayName }}\n            </h3>\n            <p class=\"mt-1 flex max-w-2xl items-center gap-2\">\n              <span class=\"text-sm text-gray-500\">\n                {{ selectedTheme?.spec.version }}\n              </span>\n              <VTag>\n                {{\n                  isActivated\n                    ? t(\"core.common.status.activated\")\n                    : t(\"core.common.status.not_activated\")\n                }}\n              </VTag>\n              <VStatusDot\n                v-if=\"getFailedMessage()\"\n                v-tooltip=\"getFailedMessage()\"\n                state=\"warning\"\n                animate\n              />\n            </p>\n          </div>\n        </div>\n        <VDropdown v-permission=\"['system:themes:manage']\">\n          <div\n            class=\"cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-100\"\n          >\n            <IconMore />\n          </div>\n          <template #popper>\n            <VDropdownItem @click=\"themesModal = true\">\n              {{ $t(\"core.common.buttons.upgrade\") }}\n            </VDropdownItem>\n            <VDropdownItem @click=\"handleExportThemeConfiguration\">\n              {{ $t(\"core.theme.operations.export_configuration.button\") }}\n            </VDropdownItem>\n            <VDropdownItem @click=\"openSelectImportFileDialog()\">\n              {{ $t(\"core.theme.operations.import_configuration.button\") }}\n            </VDropdownItem>\n            <VDropdownDivider />\n            <VDropdownItem type=\"danger\" @click=\"handleReloadTheme\">\n              {{ $t(\"core.theme.operations.reload.button\") }}\n            </VDropdownItem>\n            <VDropdownItem type=\"danger\" @click=\"handleClearCache\">\n              {{ $t(\"core.theme.operations.clear_templates_cache.button\") }}\n            </VDropdownItem>\n            <VDropdownItem type=\"danger\" @click=\"handleResetSettingConfig\">\n              {{ $t(\"core.common.buttons.reset\") }}\n            </VDropdownItem>\n          </template>\n        </VDropdown>\n      </div>\n    </div>\n    <div class=\"border-t border-gray-200\">\n      <VDescription>\n        <VDescriptionItem label=\"ID\" :content=\"selectedTheme?.metadata.name\" />\n        <VDescriptionItem\n          :label=\"$t('core.theme.detail.fields.description')\"\n          :content=\"\n            selectedTheme?.spec.description || $t('core.common.text.none')\n          \"\n        ></VDescriptionItem>\n        <VDescriptionItem :label=\"$t('core.theme.detail.fields.author')\">\n          <a\n            v-if=\"selectedTheme?.spec.author\"\n            :href=\"selectedTheme.spec.author.website || '#'\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ selectedTheme.spec.author.name }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.theme.detail.fields.version')\"\n          :content=\"selectedTheme?.spec.version\"\n        />\n        <VDescriptionItem\n          :label=\"$t('core.theme.detail.fields.requires')\"\n          :content=\"selectedTheme?.spec.requires\"\n        />\n        <VDescriptionItem :label=\"$t('core.theme.detail.fields.homepage')\">\n          <a\n            v-if=\"selectedTheme?.spec.homepage\"\n            :href=\"selectedTheme.spec.homepage\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ selectedTheme.spec.homepage }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem :label=\"$t('core.theme.detail.fields.repo')\">\n          <a\n            v-if=\"selectedTheme?.spec.repo\"\n            :href=\"selectedTheme.spec.repo\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ selectedTheme.spec.repo }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem :label=\"$t('core.theme.detail.fields.issues')\">\n          <a\n            v-if=\"selectedTheme?.spec.issues\"\n            :href=\"selectedTheme.spec.issues\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ selectedTheme.spec.issues }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem :label=\"$t('core.theme.detail.fields.license')\">\n          <ul\n            v-if=\"\n              selectedTheme?.spec.license && selectedTheme?.spec.license.length\n            \"\n            class=\"list-inside\"\n            :class=\"{ 'list-disc': selectedTheme?.spec.license.length > 1 }\"\n          >\n            <li\n              v-for=\"(license, index) in selectedTheme.spec.license\"\n              :key=\"index\"\n            >\n              <a v-if=\"license.url\" :href=\"license.url\" target=\"_blank\">\n                {{ license.name }}\n              </a>\n              <span v-else>\n                {{ license.name }}\n              </span>\n            </li>\n          </ul>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.theme.detail.fields.storage_location')\"\n          :content=\"selectedTheme?.status?.location\"\n        />\n      </VDescription>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/ThemeSetting.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitSchemaCondition, FormKitSchemaNode } from \"@formkit/core\";\nimport type { Setting, Theme } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton } from \"@halo-dev/components\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { set } from \"es-toolkit/compat\";\nimport type { Ref } from \"vue\";\nimport { computed, inject, ref, toRaw } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport BackToTop from \"@/components/back-to-top/BackToTop.vue\";\nimport StickyBlock from \"@/components/sticky-block/StickyBlock.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst group = inject<Ref<string>>(\"activeTab\", ref(\"\"));\n\nconst selectedTheme = inject<Ref<Theme | undefined>>(\"selectedTheme\");\nconst setting = inject<Ref<Setting | undefined>>(\"setting\", ref());\n\nconst saving = ref(false);\n\nconst { data: configMapData, suspense } = useQuery({\n  queryKey: [\"core:theme:configMap:data\", selectedTheme],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.theme.theme.fetchThemeJsonConfig({\n      name: selectedTheme?.value?.metadata.name as string,\n    });\n    return data;\n  },\n  enabled: computed(() => {\n    return !!setting.value && !!selectedTheme?.value;\n  }),\n});\n\nconst currentConfigMapGroupData = computed(() => {\n  return configMapData.value?.[group.value];\n});\n\nconst formSchema = computed(() => {\n  if (!setting.value) {\n    return;\n  }\n  const { forms } = setting.value.spec;\n  return forms.find((item) => item.group === group?.value)?.formSchema as (\n    | FormKitSchemaCondition\n    | FormKitSchemaNode\n  )[];\n});\n\nconst handleSaveConfigMap = async (data: object) => {\n  saving.value = true;\n\n  if (!selectedTheme?.value) {\n    saving.value = false;\n    return;\n  }\n\n  await consoleApiClient.theme.theme.updateThemeJsonConfig({\n    name: selectedTheme?.value?.metadata.name,\n    body: set(cloneDeep(configMapData.value) || {}, group.value, data),\n  });\n\n  Toast.success(t(\"core.common.toast.save_success\"));\n\n  queryClient.invalidateQueries({ queryKey: [\"core:theme:configMap:data\"] });\n\n  saving.value = false;\n};\n\nawait suspense();\n</script>\n<template>\n  <div class=\"p-4\">\n    <FormKit\n      v-if=\"group && formSchema && currentConfigMapGroupData\"\n      :id=\"group\"\n      :value=\"currentConfigMapGroupData || {}\"\n      :name=\"group\"\n      :preserve=\"true\"\n      type=\"form\"\n      @submit=\"handleSaveConfigMap\"\n    >\n      <FormKitSchema\n        :schema=\"toRaw(formSchema)\"\n        :data=\"toRaw(currentConfigMapGroupData)\"\n      />\n    </FormKit>\n    <StickyBlock\n      v-permission=\"['system:themes:manage']\"\n      class=\"-mx-4 -mb-4 rounded-b-base rounded-t-lg bg-white p-4 pt-5\"\n      position=\"bottom\"\n    >\n      <VButton\n        :loading=\"saving\"\n        type=\"secondary\"\n        @click=\"$formkit.submit(group || '')\"\n      >\n        {{ $t(\"core.common.buttons.save\") }}\n      </VButton>\n    </StickyBlock>\n\n    <BackToTop />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/ThemeListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport type { Theme } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Toast,\n  VButton,\n  VDropdown,\n  VDropdownDivider,\n  VDropdownItem,\n  VSpace,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils, type OperationItem } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, inject, markRaw, ref, toRefs, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useThemeLifeCycle } from \"../composables/use-theme\";\nimport MoreOperationItem from \"./operation/MoreOperationItem.vue\";\nimport UninstallOperationItem from \"./operation/UninstallOperationItem.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    theme: Theme;\n    installed?: boolean;\n    isSelected?: boolean;\n  }>(),\n  {\n    installed: true,\n    isSelected: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"upgrade\"): void;\n  (event: \"preview\"): void;\n  (event: \"select\", theme: Theme): void;\n}>();\n\nconst { theme } = toRefs(props);\n\nconst activeTabId = inject<Ref<string>>(\"activeTabId\", ref(\"\"));\n\nconst {\n  isActivated,\n  getFailedMessage,\n  handleActiveTheme,\n  handleResetSettingConfig,\n} = useThemeLifeCycle(theme);\n\n// Creating theme\nconst creating = ref(false);\n\nconst handleCreateTheme = async () => {\n  try {\n    creating.value = true;\n\n    const { data } = await coreApiClient.theme.theme.createTheme({\n      theme: props.theme,\n    });\n\n    // create theme settings\n    consoleApiClient.theme.theme.reload({ name: data.metadata.name });\n\n    activeTabId.value = \"installed\";\n\n    Toast.success(t(\"core.common.toast.install_success\"));\n  } catch (error) {\n    console.error(\"Failed to create theme\", error);\n  } finally {\n    creating.value = false;\n    queryClient.invalidateQueries({ queryKey: [\"installed-themes\"] });\n    queryClient.invalidateQueries({ queryKey: [\"not-installed-themes\"] });\n  }\n};\n\nconst { data: operationItems } = useOperationItemExtensionPoint<Theme>(\n  \"theme:list-item:operation:create\",\n  theme,\n  computed((): OperationItem<Theme>[] => [\n    {\n      priority: 10,\n      component: markRaw(VButton),\n      props: {\n        size: \"sm\",\n      },\n      action: () => handleActiveTheme(true),\n      label: t(\"core.common.buttons.activate\"),\n      hidden: isActivated.value,\n      permissions: [\"system:themes:manage\"],\n    },\n    {\n      priority: 20,\n      component: markRaw(VButton),\n      props: {\n        size: \"sm\",\n      },\n      action: () => {\n        emit(\"select\", props.theme);\n      },\n      label: t(\"core.common.buttons.select\"),\n    },\n    {\n      priority: 30,\n      component: markRaw(MoreOperationItem),\n      permissions: [\"system:themes:manage\"],\n      children: [\n        {\n          priority: 10,\n          component: markRaw(VDropdownItem),\n          action: () => {\n            emit(\"preview\");\n          },\n          label: t(\"core.common.buttons.preview\"),\n        },\n        {\n          priority: 20,\n          component: markRaw(VDropdownDivider),\n        },\n        {\n          priority: 30,\n          component: markRaw(UninstallOperationItem),\n          props: {\n            theme: props.theme,\n          },\n        },\n        {\n          priority: 40,\n          component: markRaw(VDropdownItem),\n          props: {\n            type: \"danger\",\n          },\n          action: () => handleResetSettingConfig(),\n          label: t(\"core.common.buttons.reset\"),\n        },\n      ],\n    },\n  ])\n);\n</script>\n\n<template>\n  <div\n    class=\"group relative flex grid-cols-1 flex-col overflow-hidden rounded bg-white p-0 shadow-sm transition-all duration-500 hover:shadow-md hover:ring-1 sm:grid sm:grid-cols-7 sm:p-2\"\n    :class=\"{ 'ring-1': isSelected }\"\n  >\n    <div class=\"col-span-2\">\n      <div class=\"relative block\">\n        <div class=\"aspect-h-9 aspect-w-16\">\n          <div\n            class=\"transform-gpu rounded-none bg-cover bg-center bg-no-repeat sm:rounded\"\n            :style=\"{\n              backgroundImage: `url(${theme.spec.logo})`,\n            }\"\n          >\n            <div\n              class=\"flex h-full w-full items-center justify-center rounded-none backdrop-blur-3xl sm:rounded\"\n            >\n              <img class=\"h-16 w-16 rounded\" :src=\"theme.spec.logo\" />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"relative col-span-5 grid grid-cols-1 content-between p-2 sm:px-4 sm:py-1\"\n    >\n      <div>\n        <div class=\"flex flex-wrap items-center justify-between gap-2\">\n          <div class=\"inline-flex items-center gap-2\">\n            <div\n              class=\"relative block cursor-pointer text-sm font-medium text-black transition-all hover:text-gray-600 hover:underline sm:text-base\"\n              @click=\"emit('select', theme)\"\n            >\n              {{ theme.spec.displayName }}\n            </div>\n            <span class=\"text-xs text-gray-500 sm:text-sm\">\n              {{ theme.spec.version }}\n            </span>\n            <VTag v-if=\"isActivated\" theme=\"primary\">\n              {{ $t(\"core.common.status.activated\") }}\n            </VTag>\n          </div>\n          <div>\n            <VStatusDot\n              v-if=\"getFailedMessage()\"\n              v-tooltip=\"getFailedMessage()\"\n              state=\"warning\"\n              animate\n            />\n            <VStatusDot\n              v-if=\"theme.metadata.deletionTimestamp\"\n              v-tooltip=\"$t('core.common.status.deleting')\"\n              state=\"warning\"\n              animate\n            />\n          </div>\n        </div>\n        <p\n          class=\"mt-2 line-clamp-1 text-xs font-normal text-gray-500 sm:text-sm\"\n        >\n          {{ theme.spec.description }}\n        </p>\n      </div>\n      <div\n        class=\"mt-4 flex w-full flex-1 items-center justify-between gap-2 sm:mt-0\"\n      >\n        <div v-if=\"theme.spec.author\" class=\"inline-flex items-center gap-1.5\">\n          <a\n            v-if=\"theme.spec.author.website\"\n            class=\"text-xs text-gray-700 hover:text-gray-900\"\n            :href=\"theme.spec.author.website\"\n            target=\"_blank\"\n          >\n            {{ theme.spec.author.name }}\n          </a>\n          <span v-else class=\"text-xs text-gray-700\">\n            {{ theme.spec.author.name }}\n          </span>\n        </div>\n        <div>\n          <VSpace v-if=\"installed\">\n            <template v-for=\"item in operationItems\">\n              <template v-if=\"!item.children?.length\">\n                <component\n                  :is=\"item.component\"\n                  v-if=\"\n                    !item.hidden && utils.permission.has(item.permissions || [])\n                  \"\n                  :key=\"`${theme.metadata.name}-${item.label}-${item.priority}`\"\n                  v-bind=\"item.props\"\n                  @click=\"item.action?.(theme)\"\n                >\n                  {{ item.label }}\n                </component>\n              </template>\n              <template v-else>\n                <VDropdown\n                  v-if=\"\n                    !item.hidden && utils.permission.has(item.permissions || [])\n                  \"\n                  :key=\"`${theme.metadata.name}-${item.label}-${item.priority}`\"\n                >\n                  <component\n                    :is=\"item.component\"\n                    v-bind=\"item.props\"\n                    @click=\"item.action?.(theme)\"\n                  >\n                    {{ item.label }}\n                  </component>\n                  <template #popper>\n                    <template v-for=\"childItem in item.children\">\n                      <component\n                        :is=\"childItem.component\"\n                        v-if=\"\n                          !childItem.hidden &&\n                          utils.permission.has(childItem.permissions || [])\n                        \"\n                        :key=\"`${theme.metadata.name}-${childItem.label}-${childItem.priority}`\"\n                        v-bind=\"childItem.props\"\n                        @click=\"childItem.action?.(theme)\"\n                      >\n                        {{ childItem.label }}\n                      </component>\n                    </template>\n                  </template>\n                </VDropdown>\n              </template>\n            </template>\n          </VSpace>\n          <VButton\n            v-if=\"!installed && utils.permission.has(['system:themes:manage'])\"\n            size=\"sm\"\n            :loading=\"creating\"\n            @click=\"handleCreateTheme\"\n          >\n            {{ $t(\"core.common.buttons.install\") }}\n          </VButton>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/ThemeListModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Theme } from \"@halo-dev/api-client\";\nimport { VButton, VLoading, VModal, VTabbar } from \"@halo-dev/components\";\nimport { utils, type ThemeListTab } from \"@halo-dev/ui-shared\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport {\n  computed,\n  defineAsyncComponent,\n  inject,\n  nextTick,\n  onMounted,\n  provide,\n  ref,\n  shallowRef,\n  watch,\n  type Ref,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nconst { t } = useI18n();\n\nconst selectedTheme = inject<Ref<Theme | undefined>>(\"selectedTheme\", ref());\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"select\", theme: Theme | undefined): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst tabs = shallowRef<ThemeListTab[]>([\n  {\n    id: \"installed\",\n    label: t(\"core.theme.list_modal.tabs.installed\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./list-tabs/InstalledThemes.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 10,\n  },\n  {\n    id: \"local-upload\",\n    label: t(\"core.theme.list_modal.tabs.local_upload\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./list-tabs/LocalUpload.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 20,\n  },\n  {\n    id: \"remote-download\",\n    label: t(\"core.theme.list_modal.tabs.remote_download.label\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./list-tabs/RemoteDownload.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 30,\n  },\n  {\n    id: \"not_installed\",\n    label: t(\"core.theme.list_modal.tabs.not_installed\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./list-tabs/NotInstalledThemes.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 40,\n  },\n]);\n\nwatch(\n  () => selectedTheme.value,\n  (value, oldValue) => {\n    if (value && oldValue) {\n      emit(\"select\", value);\n      modal.value?.close();\n    }\n  }\n);\n\nconst activeTabId = ref();\n\nprovide<Ref<string>>(\"activeTabId\", activeTabId);\n\nconst modalTitle = computed(() => {\n  const tab = tabs.value.find((tab) => tab.id === activeTabId.value);\n  return tab?.label;\n});\n\n// handle remote wordpress url from route\nconst remoteDownloadUrl = useRouteQuery<string>(\"remote-download-url\");\n\nonMounted(() => {\n  if (remoteDownloadUrl.value) {\n    nextTick(() => {\n      activeTabId.value = \"remote-download\";\n    });\n  }\n});\n\nconst { pluginModules } = usePluginModuleStore();\n\nonMounted(async () => {\n  const tabsFromPlugins: ThemeListTab[] = [];\n\n  for (const pluginModule of pluginModules) {\n    try {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"theme:list:tabs:create\"];\n\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const items = await callbackFunction();\n\n      tabsFromPlugins.push(\n        ...items.filter((item) => {\n          return utils.permission.has(item.permissions || []);\n        })\n      );\n    } catch (error) {\n      console.error(`Error processing plugin module:`, pluginModule, error);\n    }\n  }\n\n  tabs.value = tabs.value.concat(tabsFromPlugins).sort((a, b) => {\n    return a.priority - b.priority;\n  });\n\n  activeTabId.value = tabs.value[0].id;\n});\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"920\"\n    height=\"calc(100vh - 20px)\"\n    :title=\"modalTitle\"\n    @close=\"emit('close')\"\n  >\n    <VTabbar\n      v-model:active-id=\"activeTabId\"\n      :items=\"\n        tabs.map((tab) => {\n          return { label: tab.label, id: tab.id };\n        })\n      \"\n      type=\"outline\"\n    />\n\n    <div class=\"mt-2\">\n      <template v-for=\"tab in tabs\" :key=\"tab.id\">\n        <component\n          :is=\"tab.component\"\n          v-bind=\"tab.props\"\n          v-if=\"tab.id === activeTabId\"\n        />\n      </template>\n    </div>\n\n    <template #footer>\n      <VButton @click=\"modal?.close()\">\n        {{ $t(\"core.common.buttons.close\") }}\n      </VButton>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/list-tabs/InstalledThemes.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useThemeStore } from \"@console/stores/theme\";\nimport type {\n  Theme,\n  ThemeV1alpha1ConsoleApiListThemesRequest,\n} from \"@halo-dev/api-client\";\nimport { consoleApiClient, paginate } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  VButton,\n  VEmpty,\n  VLoading,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { inject, ref, type Ref } from \"vue\";\nimport ThemePreviewModal from \"../preview/ThemePreviewModal.vue\";\nimport ThemeListItem from \"../ThemeListItem.vue\";\n\nconst themeStore = useThemeStore();\n\nconst selectedTheme = inject<Ref<Theme | undefined>>(\"selectedTheme\", ref());\nconst activeTabId = inject<Ref<string>>(\"activeTabId\", ref(\"\"));\n\nfunction handleSelectTheme(theme: Theme) {\n  selectedTheme.value = theme;\n}\n\nconst {\n  data: themes,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery<Theme[]>({\n  queryKey: [\"installed-themes\"],\n  queryFn: async () => {\n    const themes = await paginate<\n      ThemeV1alpha1ConsoleApiListThemesRequest,\n      Theme\n    >((params) => consoleApiClient.theme.theme.listThemes(params), {\n      uninstalled: false,\n      size: 1000,\n    });\n\n    return themes.sort((a, b) => {\n      const activatedThemeName = themeStore.activatedTheme?.metadata.name;\n      if (a.metadata.name === activatedThemeName) {\n        return -1;\n      }\n      if (b.metadata.name === activatedThemeName) {\n        return 1;\n      }\n      return 0;\n    });\n  },\n  refetchInterval(data) {\n    const hasDeletingTheme = data?.some(\n      (theme) => !!theme.metadata.deletionTimestamp\n    );\n\n    return hasDeletingTheme ? 1000 : false;\n  },\n});\n\n// preview\nconst previewVisible = ref(false);\nconst selectedPreviewTheme = ref<Theme>();\n\nconst handleOpenPreview = (theme: Theme) => {\n  selectedPreviewTheme.value = theme;\n  previewVisible.value = true;\n};\n</script>\n\n<template>\n  <div id=\"installed-themes-wrapper\">\n    <VLoading v-if=\"isLoading\" />\n    <Transition v-else-if=\"!themes?.length\" appear name=\"fade\">\n      <VEmpty\n        :message=\"$t('core.theme.list_modal.empty.message')\"\n        :title=\"$t('core.theme.list_modal.empty.title')\"\n      >\n        <template #actions>\n          <VSpace>\n            <VButton :loading=\"isFetching\" @click=\"refetch()\">\n              {{ $t(\"core.common.buttons.refresh\") }}\n            </VButton>\n            <VButton\n              v-permission=\"['system:themes:manage']\"\n              type=\"secondary\"\n              @click=\"activeTabId = 'local-upload'\"\n            >\n              <template #icon>\n                <IconAddCircle />\n              </template>\n              {{ $t(\"core.theme.common.buttons.install\") }}\n            </VButton>\n          </VSpace>\n        </template>\n      </VEmpty>\n    </Transition>\n    <Transition v-else appear name=\"fade\">\n      <ul class=\"box-border h-full w-full space-y-3\" role=\"list\">\n        <li v-for=\"theme in themes\" :key=\"theme.metadata.name\">\n          <ThemeListItem\n            :theme=\"theme\"\n            :is-selected=\"theme.metadata.name === selectedTheme?.metadata?.name\"\n            @select=\"handleSelectTheme\"\n            @preview=\"handleOpenPreview(theme)\"\n          />\n        </li>\n      </ul>\n    </Transition>\n    <ThemePreviewModal\n      v-if=\"previewVisible\"\n      :theme=\"selectedPreviewTheme\"\n      @close=\"previewVisible = false\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/list-tabs/LocalUpload.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useThemeStore } from \"@console/stores/theme\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport type { ErrorResponse, UppyFile } from \"@uppy/core\";\nimport type { Ref } from \"vue\";\nimport { inject, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport AppDownloadAlert from \"@/components/common/AppDownloadAlert.vue\";\nimport { THEME_ALREADY_EXISTS_TYPE } from \"../../constants\";\nimport type { ThemeInstallationErrorResponse } from \"../../types\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\nconst themeStore = useThemeStore();\n\nconst activeTabId = inject<Ref<string>>(\"activeTabId\", ref(\"\"));\n\nconst endpoint = \"/apis/api.console.halo.run/v1alpha1/themes/install\";\n\nconst onUploaded = () => {\n  Toast.success(t(\"core.common.toast.install_success\"));\n\n  queryClient.invalidateQueries({ queryKey: [\"themes\"] });\n  themeStore.fetchActivatedTheme();\n\n  activeTabId.value = \"installed\";\n};\n\nconst onError = (file: UppyFile, response: ErrorResponse) => {\n  const body = response.body as ThemeInstallationErrorResponse;\n\n  if (body.type === THEME_ALREADY_EXISTS_TYPE) {\n    handleCatchExistsException(body, file.data as File);\n  }\n};\n\nconst handleCatchExistsException = async (\n  error: ThemeInstallationErrorResponse,\n  file?: File\n) => {\n  Dialog.info({\n    title: t(\"core.theme.operations.existed_during_installation.title\"),\n    description: t(\n      \"core.theme.operations.existed_during_installation.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      if (!file) {\n        throw new Error(\"File is required\");\n      }\n\n      await consoleApiClient.theme.theme.upgradeTheme({\n        name: error.themeName,\n        file: file,\n      });\n\n      Toast.success(t(\"core.common.toast.upgrade_success\"));\n\n      queryClient.invalidateQueries({ queryKey: [\"themes\"] });\n      themeStore.fetchActivatedTheme();\n\n      activeTabId.value = \"installed\";\n    },\n  });\n};\n</script>\n\n<template>\n  <div class=\"pb-3\">\n    <AppDownloadAlert />\n  </div>\n  <UppyUpload\n    :restrictions=\"{\n      maxNumberOfFiles: 1,\n      allowedFileTypes: ['.zip'],\n    }\"\n    :endpoint=\"endpoint\"\n    width=\"100%\"\n    auto-proceed\n    @uploaded=\"onUploaded\"\n    @error=\"onError\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/list-tabs/NotInstalledThemes.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  Theme,\n  ThemeV1alpha1ConsoleApiListThemesRequest,\n} from \"@halo-dev/api-client\";\nimport { consoleApiClient, paginate } from \"@halo-dev/api-client\";\nimport { VButton, VEmpty, VLoading } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport ThemeListItem from \"../ThemeListItem.vue\";\n\nconst {\n  data: themes,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery<Theme[]>({\n  queryKey: [\"not-installed-themes\"],\n  queryFn: async () => {\n    return await paginate<ThemeV1alpha1ConsoleApiListThemesRequest, Theme>(\n      (params) => consoleApiClient.theme.theme.listThemes(params),\n      {\n        uninstalled: true,\n        size: 1000,\n      }\n    );\n  },\n});\n</script>\n\n<template>\n  <div id=\"not-installed-themes-wrapper\">\n    <VLoading v-if=\"isLoading\" />\n    <Transition v-else-if=\"!themes?.length\" appear name=\"fade\">\n      <VEmpty :title=\"$t('core.theme.list_modal.not_installed_empty.title')\">\n        <template #actions>\n          <VButton :loading=\"isFetching\" @click=\"refetch\">\n            {{ $t(\"core.common.buttons.refresh\") }}\n          </VButton>\n        </template>\n      </VEmpty>\n    </Transition>\n    <Transition v-else appear name=\"fade\">\n      <ul class=\"box-border h-full w-full space-y-3\" role=\"list\">\n        <li v-for=\"(theme, index) in themes\" :key=\"index\">\n          <ThemeListItem :theme=\"theme\" :installed=\"false\" />\n        </li>\n      </ul>\n    </Transition>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/list-tabs/RemoteDownload.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useThemeStore } from \"@console/stores/theme\";\nimport { submitForm } from \"@formkit/core\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VButton } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { Ref } from \"vue\";\nimport { inject, nextTick, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { THEME_ALREADY_EXISTS_TYPE } from \"../../constants\";\nimport type { ThemeInstallationErrorResponse } from \"../../types\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\nconst themeStore = useThemeStore();\n\nconst activeTabId = inject<Ref<string>>(\"activeTabId\", ref(\"\"));\nconst remoteDownloadUrl = ref(\"\");\nconst downloading = ref(false);\n\nconst handleDownloadTheme = async () => {\n  try {\n    downloading.value = true;\n\n    await consoleApiClient.theme.theme.installThemeFromUri({\n      installFromUriRequest: {\n        uri: remoteDownloadUrl.value,\n      },\n    });\n\n    Toast.success(t(\"core.common.toast.install_success\"));\n\n    queryClient.invalidateQueries({ queryKey: [\"themes\"] });\n    themeStore.fetchActivatedTheme();\n\n    activeTabId.value = \"installed\";\n\n    // eslint-disable-next-line\n  } catch (error: any) {\n    const data = error?.response.data as ThemeInstallationErrorResponse;\n    if (data?.type === THEME_ALREADY_EXISTS_TYPE) {\n      handleCatchExistsException(data);\n    }\n\n    console.error(\"Failed to download theme\", error);\n  } finally {\n    routeRemoteDownloadUrl.value = null;\n    downloading.value = false;\n  }\n};\n\nconst handleCatchExistsException = async (\n  error: ThemeInstallationErrorResponse\n) => {\n  Dialog.info({\n    title: t(\"core.theme.operations.existed_during_installation.title\"),\n    description: t(\n      \"core.theme.operations.existed_during_installation.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await consoleApiClient.theme.theme.upgradeThemeFromUri({\n        name: error.themeName,\n        upgradeFromUriRequest: {\n          uri: remoteDownloadUrl.value,\n        },\n      });\n\n      Toast.success(t(\"core.common.toast.upgrade_success\"));\n\n      queryClient.invalidateQueries({ queryKey: [\"themes\"] });\n      themeStore.fetchActivatedTheme();\n\n      activeTabId.value = \"installed\";\n    },\n  });\n};\n\n// handle remote download url from route\nconst routeRemoteDownloadUrl = useRouteQuery<string | null>(\n  \"remote-download-url\"\n);\n\nonMounted(() => {\n  if (routeRemoteDownloadUrl.value) {\n    remoteDownloadUrl.value = routeRemoteDownloadUrl.value;\n    nextTick(() => {\n      submitForm(\"theme-remote-download-form\");\n    });\n  }\n});\n</script>\n\n<template>\n  <FormKit\n    id=\"theme-remote-download-form\"\n    name=\"theme-remote-download-form\"\n    type=\"form\"\n    :preserve=\"true\"\n    @submit=\"handleDownloadTheme\"\n  >\n    <FormKit\n      v-model=\"remoteDownloadUrl\"\n      :label=\"$t('core.theme.list_modal.tabs.remote_download.fields.url')\"\n      type=\"text\"\n      validation=\"required\"\n    ></FormKit>\n  </FormKit>\n\n  <div class=\"pt-5\">\n    <VButton\n      :loading=\"downloading\"\n      type=\"secondary\"\n      @click=\"$formkit.submit('theme-remote-download-form')\"\n    >\n      {{ $t(\"core.common.buttons.download\") }}\n    </VButton>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/operation/MoreOperationItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconMore, VButton } from \"@halo-dev/components\";\n</script>\n\n<template>\n  <VButton size=\"sm\"><IconMore /></VButton>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/operation/UninstallOperationItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Theme } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VDropdown, VDropdownItem } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    theme: Theme;\n  }>(),\n  {}\n);\n\nconst handleUninstall = async (deleteExtensions?: boolean) => {\n  Dialog.warning({\n    title: `${\n      deleteExtensions\n        ? t(\"core.theme.operations.uninstall_and_delete_config.title\")\n        : t(\"core.theme.operations.uninstall.title\")\n    }`,\n    description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    confirmType: \"danger\",\n    onConfirm: async () => {\n      try {\n        await coreApiClient.theme.theme.deleteTheme({\n          name: props.theme.metadata.name,\n        });\n\n        // delete theme setting and configMap\n        if (deleteExtensions) {\n          const { settingName, configMapName } = props.theme.spec;\n\n          if (settingName) {\n            await coreApiClient.setting.deleteSetting(\n              {\n                name: settingName,\n              },\n              {\n                mute: true,\n              }\n            );\n          }\n\n          if (configMapName) {\n            await coreApiClient.configMap.deleteConfigMap(\n              {\n                name: configMapName,\n              },\n              {\n                mute: true,\n              }\n            );\n          }\n        }\n\n        Toast.success(t(\"core.common.toast.uninstall_success\"));\n      } catch (e) {\n        console.error(\"Failed to uninstall theme\", e);\n      } finally {\n        queryClient.invalidateQueries({ queryKey: [\"installed-themes\"] });\n      }\n    },\n  });\n};\n</script>\n\n<template>\n  <VDropdown placement=\"right\" :triggers=\"['click']\">\n    <VDropdownItem type=\"danger\">\n      {{ $t(\"core.common.buttons.uninstall\") }}\n    </VDropdownItem>\n    <template #popper>\n      <VDropdownItem type=\"danger\" @click=\"handleUninstall()\">\n        {{ $t(\"core.common.buttons.uninstall\") }}\n      </VDropdownItem>\n      <VDropdownItem type=\"danger\" @click=\"handleUninstall(true)\">\n        {{ $t(\"core.theme.operations.uninstall_and_delete_config.button\") }}\n      </VDropdownItem>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/preview/ThemePreviewListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Theme } from \"@halo-dev/api-client\";\nimport {\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VTag,\n} from \"@halo-dev/components\";\nimport { UseImage } from \"@vueuse/components\";\nimport { toRefs } from \"vue\";\nimport { useThemeLifeCycle } from \"../../composables/use-theme\";\n\nconst props = withDefaults(\n  defineProps<{\n    theme: Theme;\n    isSelected?: boolean;\n  }>(),\n  {\n    isSelected: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"open-settings\"): void;\n}>();\n\nconst { theme } = toRefs(props);\n\nconst { isActivated, handleActiveTheme } = useThemeLifeCycle(theme);\n</script>\n\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template #start>\n      <VEntityField v-if=\"theme.spec.logo\">\n        <template #description>\n          <div class=\"w-20\">\n            <div\n              class=\"group aspect-h-3 aspect-w-4 block w-full overflow-hidden rounded border bg-gray-100\"\n            >\n              <UseImage\n                :src=\"theme.spec.logo\"\n                :alt=\"theme.spec.displayName\"\n                class=\"pointer-events-none object-cover group-hover:opacity-75\"\n              >\n                <template #loading>\n                  <div\n                    class=\"flex h-full items-center justify-center object-cover\"\n                  >\n                    <span class=\"text-xs text-gray-400\">\n                      {{ $t(\"core.common.status.loading\") }}...\n                    </span>\n                  </div>\n                </template>\n                <template #error>\n                  <div\n                    class=\"flex h-full items-center justify-center object-cover\"\n                  >\n                    <span class=\"text-xs text-red-400\">\n                      {{ $t(\"core.common.status.loading_error\") }}\n                    </span>\n                  </div>\n                </template>\n              </UseImage>\n            </div>\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"theme.spec.displayName\"\n        :description=\"theme.spec.version\"\n      >\n        <template #extra>\n          <VTag v-if=\"isActivated\">\n            {{ $t(\"core.common.status.activated\") }}\n          </VTag>\n        </template>\n      </VEntityField>\n    </template>\n\n    <template #dropdownItems>\n      <VDropdownItem v-if=\"!isActivated\" @click=\"handleActiveTheme()\">\n        {{ $t(\"core.common.buttons.activate\") }}\n      </VDropdownItem>\n      <VDropdownItem @click=\"emit('open-settings')\">\n        {{ $t(\"core.common.buttons.setting\") }}\n      </VDropdownItem>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/components/preview/ThemePreviewModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useThemeStore } from \"@console/stores/theme\";\nimport type { FormKitSchemaCondition, FormKitSchemaNode } from \"@formkit/core\";\nimport type {\n  Setting,\n  SettingForm,\n  Theme,\n  ThemeV1alpha1ConsoleApiListThemesRequest,\n} from \"@halo-dev/api-client\";\nimport { consoleApiClient, paginate } from \"@halo-dev/api-client\";\nimport {\n  IconComputer,\n  IconLink,\n  IconPalette,\n  IconPhone,\n  IconRefreshLine,\n  IconSettings,\n  IconTablet,\n  Toast,\n  VButton,\n  VEntityContainer,\n  VLoading,\n  VModal,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { set } from \"es-toolkit/compat\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { storeToRefs } from \"pinia\";\nimport { computed, markRaw, onMounted, ref, toRaw } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport StickyBlock from \"@/components/sticky-block/StickyBlock.vue\";\nimport ThemePreviewListItem from \"./ThemePreviewListItem.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    title?: string;\n    theme?: Theme;\n  }>(),\n  {\n    title: undefined,\n    theme: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\ninterface SettingTab {\n  id: string;\n  label: string;\n}\n\nconst { activatedTheme } = storeToRefs(useThemeStore());\n\nconst previewFrame = ref<HTMLIFrameElement | null>(null);\nconst themesVisible = ref(false);\nconst switching = ref(false);\nconst selectedTheme = ref<Theme>();\n\nconst { data: themes } = useQuery<Theme[]>({\n  queryKey: [\"themes\"],\n  queryFn: async () => {\n    return await paginate<ThemeV1alpha1ConsoleApiListThemesRequest, Theme>(\n      (params) => consoleApiClient.theme.theme.listThemes(params),\n      {\n        size: 1000,\n      }\n    );\n  },\n});\n\nonMounted(() => {\n  selectedTheme.value = toRaw(props.theme) || toRaw(activatedTheme?.value);\n});\n\nconst handleOpenThemes = () => {\n  settingsVisible.value = false;\n  themesVisible.value = !themesVisible.value;\n};\n\nconst handleSelect = (theme: Theme) => {\n  selectedTheme.value = theme;\n};\n\nconst previewUrl = computed(() => {\n  if (!selectedTheme.value) {\n    return \"#\";\n  }\n  return `/?preview-theme=${selectedTheme.value.metadata.name}`;\n});\n\nconst modalTitle = computed(() => {\n  if (props.title) {\n    return props.title;\n  }\n  return t(\"core.theme.preview_model.title\", {\n    display_name: selectedTheme.value?.spec.displayName,\n  });\n});\n\n// theme settings\nconst saving = ref(false);\nconst settingTabs = ref<SettingTab[]>([] as SettingTab[]);\nconst activeSettingTab = ref(\"\");\nconst settingsVisible = ref(false);\n\nconst { data: setting } = useQuery<Setting>({\n  queryKey: [\"theme-setting\", selectedTheme],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.theme.theme.fetchThemeSetting({\n      name: selectedTheme?.value?.metadata.name as string,\n    });\n\n    return data;\n  },\n  onSuccess(data) {\n    if (data) {\n      const { forms } = data.spec;\n      settingTabs.value = forms.map((item: SettingForm) => {\n        return {\n          id: item.group,\n          label: item.label || \"\",\n        };\n      });\n    }\n\n    activeSettingTab.value = settingTabs.value[0].id;\n  },\n  enabled: computed(() => !!selectedTheme.value?.spec.settingName),\n});\n\nconst { data: configMapData } = useQuery({\n  queryKey: [\"core:theme:configMap:data\", selectedTheme],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.theme.theme.fetchThemeJsonConfig({\n      name: selectedTheme?.value?.metadata.name as string,\n    });\n    return data;\n  },\n  enabled: computed(\n    () => !!setting.value && !!selectedTheme.value?.spec.configMapName\n  ),\n});\n\nconst currentConfigMapGroupData = computed(() => {\n  return configMapData.value?.[activeSettingTab.value];\n});\n\nconst formSchema = computed(() => {\n  if (!setting.value) {\n    return;\n  }\n  const { forms } = setting.value.spec;\n  return forms.find((item) => item.group === activeSettingTab.value)\n    ?.formSchema as (FormKitSchemaCondition | FormKitSchemaNode)[];\n});\n\nconst handleRefresh = () => {\n  previewFrame.value?.contentWindow?.location.reload();\n};\n\nconst handleSaveConfigMap = async (data: object) => {\n  saving.value = true;\n\n  if (!selectedTheme?.value) {\n    saving.value = false;\n    return;\n  }\n\n  await consoleApiClient.theme.theme.updateThemeJsonConfig({\n    name: selectedTheme?.value?.metadata.name,\n    body: set(\n      cloneDeep(configMapData.value) || {},\n      activeSettingTab.value,\n      data\n    ),\n  });\n\n  Toast.success(t(\"core.common.toast.save_success\"));\n\n  queryClient.invalidateQueries({ queryKey: [\"core:theme:configMap:data\"] });\n\n  saving.value = false;\n\n  handleRefresh();\n};\n\nconst handleOpenSettings = (theme?: Theme) => {\n  if (theme) {\n    selectedTheme.value = theme;\n  }\n  themesVisible.value = false;\n  settingsVisible.value = !settingsVisible.value;\n};\n\n// mock devices\nconst mockDevices = [\n  {\n    id: \"desktop\",\n    icon: markRaw(IconComputer),\n  },\n  {\n    id: \"tablet\",\n    icon: markRaw(IconTablet),\n  },\n  {\n    id: \"phone\",\n    icon: markRaw(IconPhone),\n  },\n];\n\nconst deviceActiveId = ref(mockDevices[0].id);\n\nconst iframeClasses = computed(() => {\n  if (deviceActiveId.value === \"desktop\") {\n    return \"w-full h-full\";\n  }\n  if (deviceActiveId.value === \"tablet\") {\n    return \"w-2/3 h-2/3 ring-2 rounded ring-gray-300\";\n  }\n  return \"w-96 h-[50rem] ring-2 rounded ring-gray-300\";\n});\n</script>\n<template>\n  <VModal\n    :body-class=\"['!p-0']\"\n    fullscreen\n    :title=\"modalTitle\"\n    :mount-to-body=\"true\"\n    @close=\"emit('close')\"\n  >\n    <template #center>\n      <!-- TODO: Reactor VTabbar component to support icon prop -->\n      <VTabbar\n        v-model:active-id=\"deviceActiveId\"\n        :items=\"mockDevices as any\"\n        type=\"outline\"\n      ></VTabbar>\n    </template>\n    <template #actions>\n      <span\n        v-tooltip=\"{\n          content: $t('core.theme.empty.actions.switch'),\n          delay: 300,\n        }\"\n        :class=\"{ 'bg-gray-200': themesVisible }\"\n        @click=\"handleOpenThemes\"\n      >\n        <IconPalette />\n      </span>\n      <span\n        v-tooltip=\"{\n          content: $t('core.theme.preview_model.actions.setting'),\n          delay: 300,\n        }\"\n        :class=\"{ 'bg-gray-200': settingsVisible }\"\n        @click=\"handleOpenSettings(undefined)\"\n      >\n        <IconSettings />\n      </span>\n      <span\n        v-tooltip=\"{\n          content: $t('core.common.buttons.refresh'),\n          delay: 300,\n        }\"\n        @click=\"handleRefresh()\"\n      >\n        <IconRefreshLine />\n      </span>\n      <span\n        v-tooltip=\"{\n          content: $t('core.theme.preview_model.actions.open'),\n          delay: 300,\n        }\"\n      >\n        <a :href=\"previewUrl\" target=\"_blank\">\n          <IconLink />\n        </a>\n      </span>\n    </template>\n    <div\n      class=\"flex h-full items-center justify-center divide-x divide-gray-100 transition-all\"\n    >\n      <transition\n        enter-active-class=\"transform transition ease-in-out duration-300\"\n        enter-from-class=\"-translate-x-full\"\n        enter-to-class=\"translate-x-0\"\n        leave-active-class=\"transform transition ease-in-out duration-300\"\n        leave-from-class=\"translate-x-0\"\n        leave-to-class=\"-translate-x-full\"\n        appear\n      >\n        <OverlayScrollbarsComponent\n          v-if=\"themesVisible || settingsVisible\"\n          element=\"div\"\n          :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n          class=\"relative h-full w-96\"\n          :class=\"{ '!overflow-hidden': switching }\"\n          defer\n        >\n          <transition\n            enter-active-class=\"transform transition ease-in-out duration-300 delay-150\"\n            enter-from-class=\"translate-x-full\"\n            enter-to-class=\"-translate-x-0\"\n            leave-active-class=\"transform transition ease-in-out duration-300\"\n            leave-from-class=\"-translate-x-0\"\n            leave-to-class=\"translate-x-full\"\n            @before-enter=\"switching = true\"\n            @after-enter=\"switching = false\"\n            @before-leave=\"switching = true\"\n            @after-leave=\"switching = false\"\n          >\n            <div v-show=\"settingsVisible\" class=\"mb-20\">\n              <!-- @vue-skip -->\n              <VTabbar\n                v-model:active-id=\"activeSettingTab\"\n                :items=\"settingTabs\"\n                class=\"w-full !rounded-none\"\n                type=\"outline\"\n              ></VTabbar>\n              <div class=\"bg-white p-3\">\n                <FormKit\n                  v-if=\"\n                    activeSettingTab && formSchema && currentConfigMapGroupData\n                  \"\n                  :id=\"activeSettingTab\"\n                  :key=\"activeSettingTab\"\n                  :value=\"currentConfigMapGroupData || {}\"\n                  :name=\"activeSettingTab\"\n                  :preserve=\"true\"\n                  type=\"form\"\n                  @submit=\"handleSaveConfigMap\"\n                >\n                  <FormKitSchema\n                    :schema=\"toRaw(formSchema)\"\n                    :data=\"toRaw(currentConfigMapGroupData)\"\n                  />\n                </FormKit>\n                <StickyBlock\n                  v-permission=\"['system:themes:manage']\"\n                  class=\"-mx-4 -mb-4 -mr-3 rounded-b-base rounded-t-lg bg-white p-4 pt-5\"\n                  position=\"bottom\"\n                >\n                  <VButton\n                    :loading=\"saving\"\n                    type=\"secondary\"\n                    @click=\"$formkit.submit(activeSettingTab)\"\n                  >\n                    {{ $t(\"core.common.buttons.save\") }}\n                  </VButton>\n                </StickyBlock>\n              </div>\n            </div>\n          </transition>\n          <transition\n            enter-active-class=\"transform transition ease-in-out duration-300 delay-150\"\n            enter-from-class=\"-translate-x-full\"\n            enter-to-class=\"translate-x-0\"\n            leave-active-class=\"transform transition ease-in-out duration-300\"\n            leave-from-class=\"translate-x-0\"\n            leave-to-class=\"-translate-x-full\"\n            @before-enter=\"switching = true\"\n            @after-enter=\"switching = false\"\n            @before-leave=\"switching = true\"\n            @after-leave=\"switching = false\"\n          >\n            <VEntityContainer v-show=\"themesVisible\">\n              <ThemePreviewListItem\n                v-for=\"item in themes\"\n                :key=\"item.metadata.name\"\n                :theme=\"item\"\n                :is-selected=\"\n                  selectedTheme?.metadata.name === item.metadata.name\n                \"\n                @click=\"handleSelect(item)\"\n                @open-settings=\"handleOpenSettings(item)\"\n              />\n            </VEntityContainer>\n          </transition>\n        </OverlayScrollbarsComponent>\n      </transition>\n      <div\n        class=\"flex h-full flex-1 items-center justify-center transition-all duration-300\"\n      >\n        <VLoading v-if=\"!previewUrl\" />\n        <iframe\n          v-else\n          ref=\"previewFrame\"\n          class=\"border-none transition-all duration-500\"\n          :class=\"iframeClasses\"\n          :src=\"previewUrl\"\n        ></iframe>\n      </div>\n    </div>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/composables/use-theme.ts",
    "content": "import { useThemeStore } from \"@console/stores/theme\";\nimport type { Theme } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { useFileDialog } from \"@vueuse/core\";\nimport { merge } from \"es-toolkit\";\nimport { storeToRefs } from \"pinia\";\nimport type { ComputedRef, Ref } from \"vue\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\ninterface useThemeLifeCycleReturn {\n  loading: Ref<boolean>;\n  isActivated: ComputedRef<boolean>;\n  getFailedMessage: () => string | undefined;\n  handleActiveTheme: (reload?: boolean) => void;\n  handleResetSettingConfig: () => void;\n}\n\nexport function useThemeLifeCycle(\n  theme: Ref<Theme | undefined>\n): useThemeLifeCycleReturn {\n  const { t } = useI18n();\n\n  const loading = ref(false);\n\n  const themeStore = useThemeStore();\n\n  const { activatedTheme } = storeToRefs(themeStore);\n\n  const isActivated = computed(() => {\n    return activatedTheme?.value?.metadata.name === theme.value?.metadata.name;\n  });\n\n  const getFailedMessage = (): string | undefined => {\n    if (!(theme.value?.status?.phase === \"FAILED\")) {\n      return;\n    }\n\n    const condition = theme.value.status.conditions?.[0];\n\n    if (condition) {\n      return [condition.type, condition.message].join(\"：\");\n    }\n  };\n\n  const handleActiveTheme = async (reload?: boolean) => {\n    Dialog.info({\n      title: t(\"core.theme.operations.active.title\"),\n      description: theme.value?.spec.displayName,\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        try {\n          if (!theme.value) return;\n\n          await consoleApiClient.theme.theme.activateTheme({\n            name: theme.value?.metadata.name,\n          });\n\n          Toast.success(t(\"core.theme.operations.active.toast_success\"));\n\n          if (reload) {\n            window.location.reload();\n          }\n        } catch (e) {\n          console.error(\"Failed to active theme\", e);\n        } finally {\n          themeStore.fetchActivatedTheme();\n        }\n      },\n    });\n  };\n\n  const handleResetSettingConfig = async () => {\n    Dialog.warning({\n      title: t(\"core.theme.operations.reset.title\"),\n      description: t(\"core.theme.operations.reset.description\"),\n      confirmType: \"danger\",\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        try {\n          if (!theme?.value) {\n            return;\n          }\n\n          await consoleApiClient.theme.theme.resetThemeConfig({\n            name: theme.value.metadata.name as string,\n          });\n\n          Toast.success(t(\"core.theme.operations.reset.toast_success\"));\n        } catch (e) {\n          console.error(\"Failed to reset theme setting config\", e);\n        }\n      },\n    });\n  };\n\n  return {\n    loading,\n    isActivated,\n    getFailedMessage,\n    handleActiveTheme,\n    handleResetSettingConfig,\n  };\n}\n\nexport function useThemeCustomTemplates(type: \"post\" | \"page\" | \"category\") {\n  const themeStore = useThemeStore();\n  const { t } = useI18n();\n\n  const templates = computed(() => {\n    const defaultTemplate = [\n      {\n        label: t(\"core.theme.custom_templates.default\"),\n        value: \"\",\n      },\n    ];\n\n    if (!themeStore.activatedTheme) {\n      return defaultTemplate;\n    }\n    const { customTemplates } = themeStore.activatedTheme.spec;\n    if (!customTemplates?.[type]) {\n      return defaultTemplate;\n    }\n    return [\n      ...defaultTemplate,\n      ...(customTemplates[type]?.map((template) => {\n        return {\n          value: template.file,\n          label: template.name || template.file,\n        };\n      }) || []),\n    ];\n  });\n\n  return {\n    templates,\n  };\n}\n\ninterface ExportData {\n  themeName: string;\n  version: string;\n  settingName: string;\n  configMapName: string;\n  configs: Record<string, unknown>;\n}\n\nexport function useThemeConfigFile(theme: Ref<Theme | undefined>) {\n  const { t } = useI18n();\n\n  const handleExportThemeConfiguration = async () => {\n    if (!theme.value) {\n      console.error(\"No selected or activated theme\");\n      return;\n    }\n\n    const { data } = await consoleApiClient.theme.theme.fetchThemeJsonConfig({\n      name: theme?.value?.metadata.name as string,\n    });\n\n    if (!data) {\n      console.error(\"Failed to fetch theme config\");\n      return;\n    }\n\n    const themeName = theme.value.metadata.name;\n\n    const exportData: ExportData = {\n      themeName: themeName,\n      version: theme.value.spec.version || \"\",\n      settingName: theme.value.spec.settingName || \"\",\n      configMapName: theme.value.spec.configMapName || \"\",\n      configs: data as Record<string, unknown>,\n    };\n\n    const exportStr = JSON.stringify(exportData, null, 2);\n    const blob = new Blob([exportStr], { type: \"application/json\" });\n    const temporaryExportUrl = URL.createObjectURL(blob);\n    const temporaryLinkTag = document.createElement(\"a\");\n\n    temporaryLinkTag.href = temporaryExportUrl;\n    temporaryLinkTag.download = `export-${themeName}-config-${Date.now().toString()}.json`;\n\n    document.body.appendChild(temporaryLinkTag);\n    temporaryLinkTag.click();\n\n    document.body.removeChild(temporaryLinkTag);\n    URL.revokeObjectURL(temporaryExportUrl);\n  };\n\n  const {\n    open: openSelectImportFileDialog,\n    onChange: handleImportThemeConfiguration,\n  } = useFileDialog({\n    accept: \"application/json\",\n    multiple: false,\n    directory: false,\n    reset: true,\n  });\n\n  handleImportThemeConfiguration(async (files) => {\n    if (files === null || files.length === 0) {\n      return;\n    }\n    const configText = await files[0].text();\n    const configJson = JSON.parse(configText || \"{}\") as ExportData;\n    if (!configJson.configs) {\n      return;\n    }\n    if (!configJson.themeName || !configJson.version) {\n      Toast.error(\n        t(\"core.theme.operations.import_configuration.invalid_format\")\n      );\n      return;\n    }\n    if (!theme.value) {\n      console.error(\"No selected or activated theme\");\n      return;\n    }\n    if (configJson.themeName !== theme.value.metadata.name) {\n      Toast.error(\n        t(\"core.theme.operations.import_configuration.mismatched_theme\")\n      );\n      return;\n    }\n\n    if (configJson.version !== theme.value.spec.version) {\n      Dialog.warning({\n        title: t(\n          \"core.theme.operations.import_configuration.version_mismatch.title\"\n        ),\n        description: t(\n          \"core.theme.operations.import_configuration.version_mismatch.description\"\n        ),\n        confirmText: t(\"core.common.buttons.confirm\"),\n        cancelText: t(\"core.common.buttons.cancel\"),\n        onConfirm: () => {\n          handleSaveConfigMap(configJson.configs);\n        },\n        onCancel() {\n          return;\n        },\n      });\n      return;\n    }\n    handleSaveConfigMap(configJson.configs);\n  });\n\n  const handleSaveConfigMap = async (importData: Record<string, unknown>) => {\n    if (!theme.value) {\n      return;\n    }\n    const { data: originalData } =\n      await consoleApiClient.theme.theme.fetchThemeJsonConfig({\n        name: theme.value.metadata.name as string,\n      });\n\n    if (!originalData) {\n      return;\n    }\n\n    await consoleApiClient.theme.theme.updateThemeJsonConfig({\n      name: theme.value.metadata.name,\n      body: merge(originalData, importData),\n    });\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  };\n\n  return {\n    handleExportThemeConfiguration,\n    openSelectImportFileDialog,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/constants/index.ts",
    "content": "export const THEME_ALREADY_EXISTS_TYPE =\n  \"https://halo.run/probs/theme-alreay-exists\";\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/layouts/ThemeLayout.vue",
    "content": "<script lang=\"ts\" setup>\nimport BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { useThemeStore } from \"@console/stores/theme\";\nimport type { Setting, SettingForm, Theme } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconExchange,\n  IconEye,\n  IconListSettings,\n  IconPalette,\n  VButton,\n  VCard,\n  VEmpty,\n  VLoading,\n  VPageHeader,\n  VSpace,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { storeToRefs } from \"pinia\";\nimport {\n  computed,\n  nextTick,\n  onMounted,\n  provide,\n  ref,\n  shallowRef,\n  watch,\n  type Ref,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRoute, useRouter } from \"vue-router\";\nimport ThemePreviewModal from \"../components/preview/ThemePreviewModal.vue\";\nimport ThemeListModal from \"../components/ThemeListModal.vue\";\nimport { useThemeLifeCycle } from \"../composables/use-theme\";\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst router = useRouter();\n\ninterface ThemeTab {\n  id: string;\n  label: string;\n  route: {\n    name: string;\n    params?: Record<string, string>;\n  };\n}\n\nconst initialTabs: ThemeTab[] = [\n  {\n    id: \"detail\",\n    label: t(\"core.theme.tabs.detail\"),\n    route: {\n      name: \"ThemeDetail\",\n    },\n  },\n];\n\nconst tabs = shallowRef<ThemeTab[]>(initialTabs);\nconst selectedTheme = ref<Theme>();\nconst themesModal = ref(false);\nconst previewModal = ref(false);\nconst activeTab = ref(tabs.value[0].id);\nprovide<Ref<string>>(\"activeTab\", activeTab);\nprovide<Ref<boolean>>(\"themesModal\", themesModal);\n\nconst { loading, isActivated, handleActiveTheme } =\n  useThemeLifeCycle(selectedTheme);\n\nprovide<Ref<Theme | undefined>>(\"selectedTheme\", selectedTheme);\n\nconst { data: setting } = useQuery<Setting>({\n  queryKey: [\"theme-setting\", selectedTheme],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.theme.theme.fetchThemeSetting({\n      name: selectedTheme.value?.metadata.name as string,\n    });\n    return data;\n  },\n  enabled: computed(() => {\n    return (\n      !!selectedTheme.value &&\n      !!selectedTheme.value.spec.settingName &&\n      utils.permission.has([\"system:themes:view\"])\n    );\n  }),\n  async onSuccess(data) {\n    if (data) {\n      const { forms } = data.spec;\n      tabs.value = [\n        ...initialTabs,\n        ...forms.map((item: SettingForm) => {\n          return {\n            id: item.group,\n            label: item.label || \"\",\n            route: {\n              name: \"ThemeSetting\",\n              params: {\n                group: item.group,\n              },\n            },\n          };\n        }),\n      ] as ThemeTab[];\n    }\n\n    await nextTick();\n\n    handleTriggerTabChange();\n  },\n});\n\nprovide<Ref<Setting | undefined>>(\"setting\", setting);\n\nconst handleTabChange = (id: string | number) => {\n  const tab = tabs.value.find((item) => item.id === id);\n  if (tab) {\n    activeTab.value = tab.id;\n    router.push(tab.route);\n  }\n};\n\nconst handleTriggerTabChange = () => {\n  if (route.name === \"ThemeSetting\") {\n    const tab = tabs.value.find((tab) => {\n      return (\n        tab.route.name === route.name &&\n        tab.route.params?.group === route.params.group\n      );\n    });\n    if (tab) {\n      activeTab.value = tab.id;\n      return;\n    }\n    handleTabChange(tabs.value[0].id);\n    return;\n  }\n\n  const tab = tabs.value.find((tab) => tab.route.name === route.name);\n  activeTab.value = tab ? tab.id : tabs.value[0].id;\n};\n\nconst onSelectTheme = () => {\n  tabs.value = cloneDeep(initialTabs);\n  handleTabChange(tabs.value[0].id);\n};\n\nonMounted(() => {\n  const themeStore = useThemeStore();\n\n  const { activatedTheme } = storeToRefs(themeStore);\n\n  selectedTheme.value = activatedTheme?.value;\n});\n\nwatch([() => route.name, () => route.params], async () => {\n  handleTriggerTabChange();\n});\n\n// handle remote download url from route\nconst remoteDownloadUrl = useRouteQuery<string | null>(\"remote-download-url\");\nonMounted(() => {\n  if (remoteDownloadUrl.value) {\n    Dialog.warning({\n      title: t(\"core.theme.operations.remote_download.title\"),\n      description: t(\"core.theme.operations.remote_download.description\", {\n        url: remoteDownloadUrl.value,\n      }),\n      confirmText: t(\"core.common.buttons.download\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm() {\n        themesModal.value = true;\n      },\n      onCancel() {\n        remoteDownloadUrl.value = null;\n      },\n    });\n  }\n});\n</script>\n<template>\n  <BasicLayout>\n    <VPageHeader :title=\"selectedTheme?.spec.displayName\">\n      <template #icon>\n        <IconPalette />\n      </template>\n      <template #actions>\n        <VButton\n          v-show=\"!isActivated\"\n          v-permission=\"['system:themes:manage']\"\n          size=\"sm\"\n          type=\"primary\"\n          @click=\"handleActiveTheme()\"\n        >\n          {{ $t(\"core.common.buttons.activate\") }}\n        </VButton>\n        <VButton type=\"default\" size=\"sm\" @click=\"previewModal = true\">\n          <template #icon>\n            <IconEye />\n          </template>\n          {{ $t(\"core.common.buttons.preview\") }}\n        </VButton>\n        <VButton type=\"secondary\" @click=\"themesModal = true\">\n          <template #icon>\n            <IconListSettings />\n          </template>\n          {{ $t(\"core.theme.actions.management\") }}\n        </VButton>\n      </template>\n    </VPageHeader>\n\n    <div class=\"m-0 md:m-4\">\n      <VEmpty\n        v-if=\"!selectedTheme && !loading\"\n        :message=\"$t('core.theme.empty.message')\"\n        :title=\"$t('core.theme.empty.title')\"\n      >\n        <template #actions>\n          <VSpace>\n            <VButton @click=\"themesModal = true\">\n              {{ $t(\"core.theme.common.buttons.install\") }}\n            </VButton>\n            <VButton type=\"secondary\" @click=\"themesModal = true\">\n              <template #icon>\n                <IconExchange />\n              </template>\n              {{ $t(\"core.theme.empty.actions.switch\") }}\n            </VButton>\n          </VSpace>\n        </template>\n      </VEmpty>\n\n      <div v-else>\n        <VCard :body-class=\"['!p-0', '!overflow-visible']\">\n          <template #header>\n            <VTabbar\n              v-model:active-id=\"activeTab\"\n              :items=\"tabs.map((item) => ({ id: item.id, label: item.label }))\"\n              class=\"w-full !rounded-none\"\n              type=\"outline\"\n              @change=\"handleTabChange\"\n            ></VTabbar>\n          </template>\n          <div class=\"rounded-b-base bg-white\">\n            <RouterView\n              :key=\"`${selectedTheme?.metadata.name}-${activeTab}`\"\n              v-slot=\"{ Component }\"\n            >\n              <template v-if=\"Component\">\n                <Suspense>\n                  <component :is=\"Component\"></component>\n                  <template #fallback>\n                    <VLoading />\n                  </template>\n                </Suspense>\n              </template>\n            </RouterView>\n          </div>\n        </VCard>\n      </div>\n    </div>\n\n    <ThemeListModal\n      v-if=\"themesModal\"\n      @close=\"themesModal = false\"\n      @select=\"onSelectTheme\"\n    />\n    <ThemePreviewModal\n      v-if=\"previewModal\"\n      :theme=\"selectedTheme\"\n      @close=\"previewModal = false\"\n    />\n  </BasicLayout>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/module.ts",
    "content": "import { IconPalette } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  components: {},\n  routes: [\n    {\n      path: \"/theme\",\n      name: \"ThemeRoot\",\n      component: () => import(\"./layouts/ThemeLayout.vue\"),\n      meta: {\n        title: \"core.theme.title\",\n        searchable: true,\n        permissions: [\"system:themes:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.themes\",\n          group: \"interface\",\n          icon: markRaw(IconPalette),\n          priority: 0,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"ThemeDetail\",\n          component: () => import(\"./ThemeDetail.vue\"),\n        },\n        {\n          path: \"settings/:group\",\n          name: \"ThemeSetting\",\n          component: () => import(\"./ThemeSetting.vue\"),\n          meta: {\n            title: \"core.theme.settings.title\",\n            permissions: [\"system:themes:view\"],\n          },\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/interface/themes/types/index.ts",
    "content": "export interface ThemeInstallationErrorResponse {\n  detail: string;\n  instance: string;\n  themeName: string;\n  requestId: string;\n  status: number;\n  timestamp: string;\n  title: string;\n  type: string;\n}\n"
  },
  {
    "path": "ui/console-src/modules/system/auth-providers/AuthProviderDetail.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitSchemaCondition, FormKitSchemaNode } from \"@formkit/core\";\nimport type { AuthProvider, Setting } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Toast,\n  VAvatar,\n  VButton,\n  VCard,\n  VDescription,\n  VDescriptionItem,\n  VPageHeader,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed, ref, shallowRef, toRaw, toRefs, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRoute } from \"vue-router\";\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst { name } = toRefs(route.params);\n\nconst tabs = shallowRef<{ id: string; label: string }[]>([\n  {\n    id: \"detail\",\n    label: t(\"core.identity_authentication.tabs.detail\"),\n  },\n]);\n\nconst activeTab = ref<string>(\"detail\");\n\nconst { data: authProvider } = useQuery<AuthProvider>({\n  queryKey: [\"auth-provider\", name],\n  queryFn: async () => {\n    const { data } = await coreApiClient.auth.authProvider.getAuthProvider({\n      name: name.value as string,\n    });\n    return data;\n  },\n  enabled: computed(() => !!name.value),\n});\n\nwatch(\n  () => authProvider.value,\n  () => {\n    if (authProvider.value?.spec.settingRef?.name) {\n      tabs.value = [\n        ...tabs.value,\n        {\n          id: \"setting\",\n          label: t(\"core.identity_authentication.tabs.setting\"),\n        },\n      ];\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\n// setting\nconst isSubmitting = ref(false);\nconst settingName = computed(() => authProvider.value?.spec.settingRef?.name);\nconst settingGroup = computed(\n  () => authProvider.value?.spec.settingRef?.group as string\n);\nconst configMapName = computed(\n  () => authProvider.value?.spec.configMapRef?.name\n);\n\nconst { data: setting, refetch: refetchSettings } = useQuery<Setting>({\n  queryKey: [\"auth-provider-setting\", settingName.value],\n  queryFn: async () => {\n    const { data } = await coreApiClient.setting.getSetting(\n      {\n        name: settingName.value as string,\n      },\n      {\n        mute: true,\n      }\n    );\n    return data;\n  },\n  enabled: computed(() => !!settingName.value),\n});\n\nconst formSchema = computed(() => {\n  if (!setting.value) {\n    return;\n  }\n  const { forms } = setting.value.spec;\n  return forms.find((item) => item.group === settingGroup.value)\n    ?.formSchema as (FormKitSchemaCondition | FormKitSchemaNode)[];\n});\n\nconst { data: configMap, refetch: refetchConfigMap } = useQuery({\n  queryKey: [\"auth-provider-configMap\", configMapName],\n  queryFn: async () => {\n    const { data } = await coreApiClient.configMap.getConfigMap(\n      {\n        name: configMapName.value as string,\n      },\n      {\n        mute: true,\n      }\n    );\n    return data;\n  },\n  retry: 0,\n  onError: async () => {\n    const data = {};\n    data[settingGroup.value] = \"{}\";\n    await coreApiClient.configMap.createConfigMap({\n      configMap: {\n        apiVersion: \"v1alpha1\",\n        data: data,\n        kind: \"ConfigMap\",\n        metadata: {\n          name: authProvider.value?.spec.configMapRef?.name as string,\n        },\n      },\n    });\n\n    await refetchConfigMap();\n  },\n  enabled: computed(() => !!configMapName.value && !!setting.value),\n});\n\nconst configMapData = computed(() => {\n  if (!configMap.value) {\n    return {};\n  }\n  return JSON.parse(configMap.value.data?.[settingGroup.value] || \"{}\");\n});\n\nconst onSubmit = async (data: Record<string, unknown>) => {\n  try {\n    isSubmitting.value = true;\n\n    const { data: configMapToUpdate } =\n      await coreApiClient.configMap.getConfigMap({\n        name: configMapName.value as string,\n      });\n\n    await coreApiClient.configMap.updateConfigMap({\n      name: configMapName.value as string,\n      configMap: {\n        ...configMapToUpdate,\n        data: {\n          ...(configMapToUpdate?.data || {}),\n          [settingGroup.value]: JSON.stringify(data),\n        },\n      },\n    });\n    await refetchSettings();\n    await refetchConfigMap();\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (error) {\n    console.error(error);\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n\nconst displayName = computed(() => {\n  if (!authProvider.value) {\n    return t(\"core.common.status.loading\");\n  }\n\n  return t(\n    `core.identity_authentication.fields.display_name.${authProvider.value?.metadata.name}`,\n    authProvider.value?.spec.displayName || \"\"\n  );\n});\n\nconst description = computed(() => {\n  if (!authProvider.value) {\n    return t(\"core.common.status.loading\");\n  }\n\n  return t(\n    `core.identity_authentication.fields.description.${authProvider.value?.metadata.name}`,\n    authProvider.value?.spec.description || \"\"\n  );\n});\n</script>\n\n<template>\n  <VPageHeader :title=\"displayName\">\n    <template #icon>\n      <VAvatar\n        :src=\"authProvider?.spec.logo\"\n        :alt=\"authProvider?.spec.displayName\"\n        size=\"sm\"\n      />\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <VTabbar\n          v-model:active-id=\"activeTab\"\n          :items=\"tabs\"\n          class=\"w-full !rounded-none\"\n          type=\"outline\"\n        ></VTabbar>\n      </template>\n      <div class=\"bg-white\">\n        <div v-if=\"activeTab === 'detail'\">\n          <VDescription>\n            <VDescriptionItem\n              :label=\"\n                $t('core.identity_authentication.detail.fields.display_name')\n              \"\n              :content=\"displayName\"\n            />\n            <VDescriptionItem\n              :label=\"\n                $t('core.identity_authentication.detail.fields.description')\n              \"\n              :content=\"description\"\n            />\n            <VDescriptionItem\n              :label=\"$t('core.identity_authentication.detail.fields.website')\"\n            >\n              <a\n                v-if=\"authProvider?.spec.website\"\n                :href=\"authProvider?.spec.website\"\n                target=\"_blank\"\n              >\n                {{ authProvider.spec.website }}\n              </a>\n              <span v-else>\n                {{ $t(\"core.common.text.none\") }}\n              </span>\n            </VDescriptionItem>\n            <VDescriptionItem\n              :label=\"\n                $t('core.identity_authentication.detail.fields.help_page')\n              \"\n            >\n              <a\n                v-if=\"authProvider?.spec.helpPage\"\n                :href=\"authProvider?.spec.helpPage\"\n                target=\"_blank\"\n              >\n                {{ authProvider.spec.helpPage }}\n              </a>\n              <span v-else>{{ $t(\"core.common.text.none\") }}</span>\n            </VDescriptionItem>\n            <VDescriptionItem\n              :label=\"\n                $t(\n                  'core.identity_authentication.detail.fields.authentication_url'\n                )\n              \"\n              :content=\"authProvider?.spec.authenticationUrl\"\n            />\n          </VDescription>\n        </div>\n        <div v-if=\"activeTab === 'setting'\" class=\"bg-white p-4\">\n          <div>\n            <FormKit\n              v-if=\"settingGroup && formSchema && configMapData\"\n              :id=\"settingGroup\"\n              :model-value=\"configMapData\"\n              :name=\"settingGroup\"\n              :preserve=\"true\"\n              type=\"form\"\n              @submit=\"onSubmit\"\n            >\n              <FormKitSchema\n                :schema=\"toRaw(formSchema)\"\n                :data=\"configMapData\"\n              />\n            </FormKit>\n          </div>\n          <div class=\"pt-5\">\n            <div class=\"flex justify-start\">\n              <VButton\n                :loading=\"isSubmitting\"\n                type=\"secondary\"\n                @click=\"$formkit.submit(settingGroup || '')\"\n              >\n                {{ $t(\"core.common.buttons.save\") }}\n              </VButton>\n            </div>\n          </div>\n        </div>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/auth-providers/AuthProviders.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedAuthProvider } from \"@halo-dev/api-client\";\nimport {\n  AuthProviderSpecAuthTypeEnum,\n  consoleApiClient,\n} from \"@halo-dev/api-client\";\nimport {\n  IconLockPasswordLine,\n  VLoading,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport AuthProvidersSection from \"./components/AuthProvidersSection.vue\";\n\nconst SYSTEM_CONFIGMAP_AUTH_PROVIDER = \"authProvider\";\n\nconst formAuthProviders = ref<ListedAuthProvider[]>([]);\nconst oauth2AuthProviders = ref<ListedAuthProvider[]>([]);\n\nconst { isLoading, refetch } = useQuery<ListedAuthProvider[]>({\n  queryKey: [\"auth-providers\"],\n  queryFn: async () => {\n    const { data } =\n      await consoleApiClient.auth.authProvider.listAuthProviders();\n    return data;\n  },\n  onSuccess(data) {\n    formAuthProviders.value = data.filter(\n      (authProvider) =>\n        authProvider.authType === AuthProviderSpecAuthTypeEnum.Form\n    );\n    oauth2AuthProviders.value = data.filter(\n      (authProvider) =>\n        authProvider.authType === AuthProviderSpecAuthTypeEnum.Oauth2\n    );\n  },\n});\n\n// Drag and drop\nconst updating = ref(false);\n\nasync function onSortUpdate() {\n  try {\n    updating.value = true;\n\n    const allAuthProviders = [\n      ...formAuthProviders.value,\n      ...oauth2AuthProviders.value,\n    ].filter(Boolean);\n\n    await consoleApiClient.configMap.system.updateSystemConfigByGroup({\n      group: SYSTEM_CONFIGMAP_AUTH_PROVIDER,\n      body: {\n        states: allAuthProviders.map((authProvider, index) => {\n          return {\n            name: authProvider.name,\n            enabled: authProvider.enabled,\n            priority: index,\n          };\n        }),\n      },\n    });\n  } finally {\n    await refetch();\n    updating.value = false;\n  }\n}\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.identity_authentication.title')\">\n    <template #icon>\n      <IconLockPasswordLine />\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 space-y-5 md:m-4\">\n    <VLoading v-if=\"isLoading\" />\n    <TransitionGroup v-else appear name=\"fade\">\n      <AuthProvidersSection\n        :key=\"AuthProviderSpecAuthTypeEnum.Form\"\n        v-model=\"formAuthProviders\"\n        :title=\"$t('core.identity_authentication.list.types.form')\"\n        :loading=\"updating\"\n        @update=\"onSortUpdate\"\n      />\n\n      <AuthProvidersSection\n        v-if=\"oauth2AuthProviders.length\"\n        :key=\"AuthProviderSpecAuthTypeEnum.Oauth2\"\n        v-model=\"oauth2AuthProviders\"\n        :title=\"$t('core.identity_authentication.list.types.oauth2')\"\n        :loading=\"updating\"\n        @update=\"onSortUpdate\"\n      />\n    </TransitionGroup>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/auth-providers/components/AuthProviderListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedAuthProvider } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconList,\n  IconSettings,\n  Toast,\n  VAvatar,\n  VEntity,\n  VEntityField,\n  VSwitch,\n} from \"@halo-dev/components\";\nimport { useI18n } from \"vue-i18n\";\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n  authProvider: ListedAuthProvider;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"reload\"): void;\n}>();\n\nconst handleChangeStatus = async () => {\n  Dialog.info({\n    title: props.authProvider.enabled\n      ? t(\"core.identity_authentication.operations.disable.title\")\n      : t(\"core.identity_authentication.operations.enable.title\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        if (props.authProvider.enabled) {\n          await consoleApiClient.auth.authProvider.disableAuthProvider({\n            name: props.authProvider.name,\n          });\n\n          Toast.success(t(\"core.common.toast.inactive_success\"));\n        } else {\n          await consoleApiClient.auth.authProvider.enableAuthProvider({\n            name: props.authProvider.name,\n          });\n          Toast.success(t(\"core.common.toast.active_success\"));\n        }\n\n        emit(\"reload\");\n      } catch (e) {\n        console.error(e);\n      }\n    },\n  });\n};\n</script>\n\n<template>\n  <VEntity>\n    <template #prepend>\n      <div\n        class=\"drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex\"\n      >\n        <IconList class=\"h-3.5 w-3.5\" />\n      </div>\n    </template>\n    <template #start>\n      <VEntityField>\n        <template #description>\n          <VAvatar\n            :alt=\"authProvider.displayName\"\n            :src=\"authProvider.logo\"\n            size=\"md\"\n          ></VAvatar>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"\n          $t(\n            `core.identity_authentication.fields.display_name.${authProvider.name}`,\n            authProvider.displayName\n          )\n        \"\n        :description=\"\n          $t(\n            `core.identity_authentication.fields.description.${authProvider.name}`,\n            authProvider.description || ''\n          )\n        \"\n        :route=\"{\n          name: 'AuthProviderDetail',\n          params: { name: authProvider.name },\n        }\"\n        width=\"27rem\"\n      >\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField>\n        <template #description>\n          <div class=\"flex items-center\">\n            <VSwitch\n              v-tooltip=\"{\n                disabled: !authProvider.privileged,\n                content: $t(\n                  'core.identity_authentication.operations.disable_privileged.tooltip'\n                ),\n              }\"\n              :model-value=\"authProvider.enabled\"\n              :disabled=\"authProvider.privileged\"\n              @click=\"handleChangeStatus\"\n            />\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <RouterLink\n            class=\"cursor-pointer rounded p-1 text-gray-600 transition-all hover:text-blue-600 group-hover:bg-gray-200/60\"\n            :to=\"{\n              name: 'AuthProviderDetail',\n              params: { name: authProvider.name },\n            }\"\n          >\n            <IconSettings />\n          </RouterLink>\n        </template>\n      </VEntityField>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/auth-providers/components/AuthProvidersSection.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ListedAuthProvider } from \"@halo-dev/api-client\";\nimport { VCard } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport AuthProviderListItem from \"./AuthProviderListItem.vue\";\n\nconst queryClient = useQueryClient();\n\nconst modelValue = defineModel<ListedAuthProvider[]>({ default: [] });\n\nconst { loading = false } = defineProps<{\n  loading?: boolean;\n  title: string;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"update\"): void;\n}>();\n\nfunction onReload() {\n  queryClient.invalidateQueries({ queryKey: [\"auth-providers\"] });\n}\n</script>\n<template>\n  <VCard :title=\"title\" :body-class=\"['!p-0']\">\n    <div class=\"w-full overflow-x-auto\">\n      <table class=\"w-full border-spacing-0\">\n        <VueDraggable\n          v-model=\"modelValue\"\n          ghost-class=\"opacity-50\"\n          handle=\".drag-element\"\n          class=\"divide-y divide-gray-100\"\n          :class=\"{\n            'cursor-progress opacity-60': loading,\n          }\"\n          tag=\"tbody\"\n          :disabled=\"loading\"\n          @update=\"emit('update')\"\n        >\n          <AuthProviderListItem\n            v-for=\"authProvider in modelValue\"\n            :key=\"authProvider.name\"\n            :auth-provider=\"authProvider\"\n            @reload=\"onReload\"\n          />\n        </VueDraggable>\n      </table>\n    </div>\n  </VCard>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/auth-providers/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\n\nexport default definePlugin({\n  routes: [\n    {\n      path: \"/users/auth-providers\",\n      component: BasicLayout,\n      children: [\n        {\n          path: \"\",\n          name: \"AuthProviders\",\n          component: () => import(\"./AuthProviders.vue\"),\n          meta: {\n            title: \"core.identity_authentication.title\",\n            searchable: true,\n            permissions: [\"*\"],\n          },\n        },\n        {\n          path: \":name\",\n          name: \"AuthProviderDetail\",\n          component: () => import(\"./AuthProviderDetail.vue\"),\n          meta: {\n            title: \"core.identity_authentication.detail.title\",\n            permissions: [\"*\"],\n          },\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/backup/Backups.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconAddCircle,\n  IconServerLine,\n  VButton,\n  VCard,\n  VLoading,\n  VPageHeader,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport type { BackupTab } from \"@halo-dev/ui-shared\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { defineAsyncComponent, onMounted, shallowRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport { useBackup } from \"./composables/use-backup\";\n\nconst { t } = useI18n();\n\nconst tabs = shallowRef<BackupTab[]>([\n  {\n    id: \"backups\",\n    label: t(\"core.backup.tabs.backup_list\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/List.vue\"),\n      loadingComponent: VLoading,\n    }),\n  },\n  {\n    id: \"restore\",\n    label: t(\"core.backup.tabs.restore\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/Restore.vue\"),\n      loadingComponent: VLoading,\n    }),\n  },\n]);\n\nconst activeTab = useRouteQuery<string>(\"tab\", tabs.value[0].id);\n\nconst { handleCreate } = useBackup();\n\nconst { pluginModules } = usePluginModuleStore();\n\nonMounted(async () => {\n  for (const pluginModule of pluginModules) {\n    try {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"backup:tabs:create\"];\n\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const backupTabs = await callbackFunction();\n\n      if (backupTabs) {\n        tabs.value = tabs.value.concat(backupTabs);\n      }\n    } catch (error) {\n      console.error(`Error processing plugin module:`, pluginModule, error);\n    }\n  }\n});\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.backup.title')\">\n    <template #icon>\n      <IconServerLine />\n    </template>\n    <template #actions>\n      <VButton type=\"secondary\" @click=\"handleCreate\">\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.backup.operations.create.button\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <VTabbar\n          v-model:active-id=\"activeTab\"\n          :items=\"tabs.map((item) => ({ id: item.id, label: item.label }))\"\n          class=\"w-full !rounded-none\"\n          type=\"outline\"\n        ></VTabbar>\n      </template>\n      <div class=\"bg-white\">\n        <template v-for=\"tab in tabs\" :key=\"tab.id\">\n          <component :is=\"tab.component\" v-if=\"activeTab === tab.id\" />\n        </template>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/backup/components/BackupListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport type { Backup } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n  type StatusDotState,\n} from \"@halo-dev/components\";\nimport { utils, type OperationItem } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport prettyBytes from \"pretty-bytes\";\nimport { computed, markRaw, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport EntityDropdownItems from \"@/components/entity/EntityDropdownItems.vue\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    backup: Backup;\n    showOperations?: boolean;\n  }>(),\n  {\n    showOperations: true,\n  }\n);\n\nconst { backup } = toRefs(props);\n\ntype Phase = {\n  text: string;\n  state: StatusDotState;\n  animate: boolean;\n  value: \"PENDING\" | \"RUNNING\" | \"SUCCEEDED\" | \"FAILED\";\n};\n\nconst phases: Phase[] = [\n  {\n    text: t(\"core.backup.list.phases.pending\"),\n    state: \"default\",\n    animate: false,\n    value: \"PENDING\",\n  },\n  {\n    text: t(\"core.backup.list.phases.running\"),\n    state: \"warning\",\n    animate: true,\n    value: \"RUNNING\",\n  },\n  {\n    text: t(\"core.backup.list.phases.succeeded\"),\n    state: \"success\",\n    animate: false,\n    value: \"SUCCEEDED\",\n  },\n  {\n    text: t(\"core.backup.list.phases.failed\"),\n    state: \"error\",\n    animate: false,\n    value: \"FAILED\",\n  },\n];\n\nconst getPhase = computed(() => {\n  if (!props.backup.status?.phase) {\n    return undefined;\n  }\n  return phases.find((phase) => phase.value === props.backup.status?.phase);\n});\n\nconst getFailureMessage = computed(() => {\n  const { phase, failureMessage } = props.backup.status || {};\n  return phase === \"FAILED\" ? failureMessage : undefined;\n});\n\nfunction handleDownload() {\n  window.open(\n    `/apis/console.api.migration.halo.run/v1alpha1/backups/${props.backup.metadata.name}/files/${props.backup.status?.filename}`,\n    \"_blank\"\n  );\n}\n\nfunction handleDelete() {\n  Dialog.warning({\n    title: t(\"core.backup.operations.delete.title\"),\n    description: t(\"core.backup.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      await coreApiClient.migration.backup.deleteBackup({\n        name: props.backup.metadata.name,\n      });\n\n      queryClient.invalidateQueries({ queryKey: [\"backups\"] });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n}\n\nconst { data: operationItems } = useOperationItemExtensionPoint<Backup>(\n  \"backup:list-item:operation:create\",\n  backup,\n  computed((): OperationItem<Backup>[] => [\n    {\n      priority: 10,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.download\"),\n      hidden: props.backup.status?.phase !== \"SUCCEEDED\",\n      permissions: [],\n      action: () => handleDownload(),\n    },\n    {\n      priority: 20,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.delete\"),\n      action: () => handleDelete(),\n    },\n  ])\n);\n</script>\n\n<template>\n  <VEntity>\n    <template #start>\n      <VEntityField\n        :title=\"backup.metadata.name\"\n        :description=\"backup.status?.filename\"\n      >\n        <template v-if=\"backup.status?.filename\" #description>\n          <VSpace class=\"flex-wrap\">\n            <span class=\"text-xs text-gray-500\">\n              {{ backup.status?.filename }}\n            </span>\n            <span class=\"text-xs text-gray-500\">\n              {{ prettyBytes(backup.status?.size || 0) }}\n            </span>\n          </VSpace>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"getPhase\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"{ content: getFailureMessage }\"\n            :state=\"getPhase.state\"\n            :text=\"getPhase.text\"\n            :animate=\"getPhase.animate\"\n          />\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"backup.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField\n        v-if=\"backup.spec?.expiresAt && backup.status?.phase === 'SUCCEEDED'\"\n      >\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{\n              $t(\"core.backup.list.fields.expiresAt\", {\n                expiresAt: utils.date.timeAgo(backup.spec?.expiresAt),\n              })\n            }}\n          </span>\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"backup.metadata.creationTimestamp\">\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{ utils.date.format(backup.metadata.creationTimestamp) }}\n          </span>\n        </template>\n      </VEntityField>\n      <slot name=\"end\"></slot>\n    </template>\n    <template v-if=\"showOperations\" #dropdownItems>\n      <EntityDropdownItems\n        :dropdown-items=\"operationItems || []\"\n        :item=\"backup\"\n      />\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/backup/composables/use-backup.ts",
    "content": "import {\n  BackupStatusPhaseEnum,\n  coreApiClient,\n  paginate,\n  type Backup,\n  type BackupV1alpha1ApiListBackupRequest,\n} from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nexport function useBackupFetch() {\n  return useQuery({\n    queryKey: [\"backups\"],\n    queryFn: async () => {\n      return await paginate<BackupV1alpha1ApiListBackupRequest, Backup>(\n        (params) => coreApiClient.migration.backup.listBackup(params),\n        {\n          size: 1000,\n        }\n      );\n    },\n    refetchInterval(data) {\n      const deletingBackups = data?.filter((backup) => {\n        return !!backup.metadata.deletionTimestamp;\n      });\n\n      if (deletingBackups?.length) {\n        return 1000;\n      }\n\n      const pendingBackups = data?.filter((backup) => {\n        return (\n          backup.status?.phase === BackupStatusPhaseEnum.Pending ||\n          backup.status?.phase === BackupStatusPhaseEnum.Running\n        );\n      });\n\n      if (pendingBackups?.length) {\n        return 3000;\n      }\n\n      return false;\n    },\n  });\n}\n\nexport function useBackup() {\n  const { t } = useI18n();\n  const queryClient = useQueryClient();\n\n  const handleCreate = async () => {\n    Dialog.info({\n      title: t(\"core.backup.operations.create.title\"),\n      description: t(\"core.backup.operations.create.description\"),\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      async onConfirm() {\n        await coreApiClient.migration.backup.createBackup({\n          backup: {\n            apiVersion: \"migration.halo.run/v1alpha1\",\n            kind: \"Backup\",\n            metadata: {\n              generateName: \"backup-\",\n              name: \"\",\n            },\n            spec: {\n              expiresAt: utils.date.dayjs().add(7, \"day\").toISOString(),\n            },\n          },\n        });\n\n        queryClient.invalidateQueries({ queryKey: [\"backups\"] });\n\n        Toast.success(t(\"core.backup.operations.create.toast_success\"));\n      },\n    });\n  };\n\n  return { handleCreate };\n}\n"
  },
  {
    "path": "ui/console-src/modules/system/backup/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconServerLine } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  components: {},\n  routes: [\n    {\n      path: \"/backup\",\n      name: \"BackupRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.backup.title\",\n        searchable: true,\n        permissions: [\"system:migrations:manage\"],\n        menu: {\n          name: \"core.sidebar.menu.items.backup\",\n          group: \"system\",\n          icon: markRaw(IconServerLine),\n          priority: 4,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Backup\",\n          component: () => import(\"./Backups.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/backup/tabs/List.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n} from \"@halo-dev/components\";\nimport BackupListItem from \"../components/BackupListItem.vue\";\nimport { useBackupFetch } from \"../composables/use-backup\";\n\nconst { data: backups, isLoading, isFetching, refetch } = useBackupFetch();\n</script>\n\n<template>\n  <VLoading v-if=\"isLoading\" />\n  <Transition v-else-if=\"!backups?.length\" appear name=\"fade\">\n    <VEmpty\n      :message=\"$t('core.backup.empty.message')\"\n      :title=\"$t('core.backup.empty.title')\"\n    >\n      <template #actions>\n        <VButton :loading=\"isFetching\" @click=\"refetch()\">\n          {{ $t(\"core.common.buttons.refresh\") }}\n        </VButton>\n      </template>\n    </VEmpty>\n  </Transition>\n  <Transition v-else appear name=\"fade\">\n    <VEntityContainer>\n      <BackupListItem\n        v-for=\"backup in backups\"\n        :key=\"backup.metadata.name\"\n        :backup=\"backup\"\n      />\n    </VEntityContainer>\n  </Transition>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/backup/tabs/Restore.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { BackupFile } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VAlert,\n  VButton,\n  VEmpty,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n  VTabItem,\n  VTabs,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useMutation, useQuery } from \"@tanstack/vue-query\";\nimport axios from \"axios\";\nimport prettyBytes from \"pretty-bytes\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst { t } = useI18n();\n\nconst complete = ref(false);\nconst showUploader = ref(false);\nconst activeTabId = ref<\"local\" | \"remote\" | \"backups\">(\"local\");\n\nconst onProcessCompleted = () => {\n  Dialog.success({\n    title: t(\"core.backup.operations.restore.title\"),\n    description: t(\"core.backup.operations.restore.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    showCancel: false,\n    async onConfirm() {\n      await handleShutdown();\n    },\n  });\n};\n\nasync function handleShutdown() {\n  await axios.post(`/actuator/restart`);\n  Toast.success(t(\"core.backup.operations.restart.toast_success\"));\n\n  setTimeout(() => {\n    complete.value = true;\n  }, 1000);\n}\n\n// Remote download to restore\nconst remoteDownloadUrl = ref(\"\");\nconst { isLoading: downloading, mutate: handleRemoteDownload } = useMutation({\n  mutationKey: [\"remote-download-restore\"],\n  mutationFn: async () => {\n    return await consoleApiClient.migration.restoreBackup({\n      downloadUrl: remoteDownloadUrl.value,\n    });\n  },\n  retry: false,\n  onSuccess() {\n    onProcessCompleted();\n  },\n});\n\nconst {\n  data: backupFiles,\n  refetch: refetchBackupFiles,\n  isLoading: isLoadingBackupFiles,\n  isFetching: isFetchingBackupFiles,\n} = useQuery({\n  queryKey: [\"backup-files\", activeTabId],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.migration.getBackupFiles();\n    return data;\n  },\n  enabled: computed(() => activeTabId.value === \"backups\"),\n});\n\nfunction handleRestoreFromBackup(backupFile: BackupFile) {\n  Dialog.info({\n    title: t(\"core.backup.operations.restore_by_backup.title\"),\n    description: t(\"core.backup.operations.restore_by_backup.description\", {\n      filename: backupFile.filename,\n    }),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      await consoleApiClient.migration.restoreBackup({\n        filename: backupFile.filename,\n      });\n      setTimeout(() => {\n        onProcessCompleted();\n      }, 200);\n    },\n  });\n}\n\nuseQuery({\n  queryKey: [\"check-health\"],\n  queryFn: async () => {\n    const { data } = await axios.get(\"/actuator/health\");\n    return data;\n  },\n  onSuccess(data) {\n    if (data.status === \"UP\") {\n      window.location.reload();\n    }\n  },\n  retry: true,\n  retryDelay: 2000,\n  enabled: computed(() => complete.value),\n});\n</script>\n\n<template>\n  <div v-if=\"!complete\">\n    <div class=\"px-4 py-3\">\n      <VAlert :title=\"$t('core.common.text.tip')\" :closable=\"false\">\n        <template #description>\n          <ul>\n            <li>{{ $t(\"core.backup.restore.tips.first\") }}</li>\n            <li>\n              <strong>\n                {{ $t(\"core.backup.restore.tips.second\") }}\n              </strong>\n            </li>\n            <li>\n              {{ $t(\"core.backup.restore.tips.third\") }}\n            </li>\n          </ul>\n        </template>\n        <template v-if=\"!showUploader\" #actions>\n          <VButton @click=\"showUploader = true\">\n            {{ $t(\"core.backup.restore.start\") }}\n          </VButton>\n        </template>\n      </VAlert>\n    </div>\n    <div v-if=\"showUploader\" class=\"flex flex-col px-4 pb-3\">\n      <VTabs v-model:active-id=\"activeTabId\" type=\"pills\">\n        <VTabItem\n          id=\"local\"\n          :label=\"$t('core.backup.restore.tabs.local.label')\"\n        >\n          <UppyUpload\n            :restrictions=\"{\n              maxNumberOfFiles: 1,\n              allowedFileTypes: ['.zip'],\n            }\"\n            endpoint=\"/apis/console.api.migration.halo.run/v1alpha1/restorations\"\n            width=\"100%\"\n            @uploaded=\"onProcessCompleted\"\n          />\n        </VTabItem>\n        <VTabItem\n          id=\"remote\"\n          :label=\"$t('core.backup.restore.tabs.remote.label')\"\n        >\n          <FormKit\n            id=\"restore-remote-download-form\"\n            name=\"restore-remote-download-form\"\n            type=\"form\"\n            :preserve=\"true\"\n            @submit=\"handleRemoteDownload()\"\n          >\n            <FormKit\n              v-model=\"remoteDownloadUrl\"\n              :label=\"$t('core.backup.restore.tabs.remote.fields.url')\"\n              type=\"text\"\n              validation=\"required\"\n            ></FormKit>\n          </FormKit>\n\n          <div class=\"pt-5\">\n            <VButton\n              :loading=\"downloading\"\n              type=\"secondary\"\n              @click=\"$formkit.submit('restore-remote-download-form')\"\n            >\n              {{ $t(\"core.backup.operations.remote_download.button\") }}\n            </VButton>\n          </div>\n        </VTabItem>\n        <VTabItem\n          id=\"backups\"\n          :label=\"$t('core.backup.restore.tabs.backup.label')\"\n        >\n          <VLoading v-if=\"isLoadingBackupFiles\" />\n          <VEntityContainer v-else-if=\"backupFiles?.length\">\n            <VEntity\n              v-for=\"backupFile in backupFiles\"\n              :key=\"backupFile.filename\"\n            >\n              <template #start>\n                <VEntityField\n                  :title=\"backupFile.filename\"\n                  :description=\"prettyBytes(backupFile.size || 0)\"\n                >\n                </VEntityField>\n              </template>\n              <template #end>\n                <VEntityField v-if=\"backupFile.lastModifiedTime\">\n                  <template #description>\n                    <span class=\"truncate text-xs tabular-nums text-gray-500\">\n                      {{ utils.date.format(backupFile.lastModifiedTime) }}\n                    </span>\n                  </template>\n                </VEntityField>\n                <VEntityField v-permission=\"['system:migrations:manage']\">\n                  <template #description>\n                    <VButton\n                      size=\"sm\"\n                      @click=\"handleRestoreFromBackup(backupFile)\"\n                    >\n                      {{\n                        $t(\"core.backup.operations.restore_by_backup.button\")\n                      }}\n                    </VButton>\n                  </template>\n                </VEntityField>\n              </template>\n            </VEntity>\n          </VEntityContainer>\n\n          <VEmpty\n            v-else\n            :title=\"$t('core.backup.restore.tabs.backup.empty.title')\"\n            :message=\"$t('core.backup.restore.tabs.backup.empty.message')\"\n          >\n            <template #actions>\n              <VButton\n                :loading=\"isFetchingBackupFiles\"\n                @click=\"refetchBackupFiles\"\n              >\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n            </template>\n          </VEmpty>\n        </VTabItem>\n      </VTabs>\n    </div>\n  </div>\n\n  <div v-else class=\"flex h-72 flex-col items-center justify-center\">\n    <VLoading />\n    <div class=\"text-xs text-gray-600\">\n      {{ $t(\"core.backup.restore.tips.complete\") }}\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/overview/Overview.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useThemeStore } from \"@console/stores/theme\";\nimport type {\n  Plugin,\n  PluginV1alpha1ConsoleApiListPluginsRequest,\n} from \"@halo-dev/api-client\";\nimport { consoleApiClient, paginate } from \"@halo-dev/api-client\";\nimport {\n  IconClipboardLine,\n  IconTerminalBoxLine,\n  Toast,\n  VButton,\n  VCard,\n  VDescription,\n  VDescriptionItem,\n  VLoading,\n  VPageHeader,\n  VTag,\n} from \"@halo-dev/components\";\nimport type { Info, Startup } from \"@halo-dev/ui-shared\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useClipboard } from \"@vueuse/core\";\nimport axios from \"axios\";\nimport { storeToRefs } from \"pinia\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport H2WarningAlert from \"@/components/alerts/H2WarningAlert.vue\";\nimport ExternalUrlItem from \"./components/ExternalUrlItem.vue\";\n\nconst { t } = useI18n();\nconst themeStore = useThemeStore();\n\nconst { data: info } = useQuery<Info>({\n  queryKey: [\"system-info\"],\n  queryFn: async () => {\n    const { data } = await axios.get<Info>(`/actuator/info`, {\n      withCredentials: true,\n    });\n    return data;\n  },\n  retry: 0,\n});\n\nconst { globalInfo } = storeToRefs(stores.globalInfo());\n\nconst { data: startup } = useQuery<Startup>({\n  queryKey: [\"system-startup-info\"],\n  queryFn: async () => {\n    const { data } = await axios.get<Startup>(`/actuator/startup`, {\n      withCredentials: true,\n    });\n    return data;\n  },\n  retry: 0,\n});\n\nconst { data: plugins, isLoading: isPluginsLoading } = useQuery<Plugin[]>({\n  queryKey: [\"enabled-plugins\"],\n  queryFn: async () => {\n    return await paginate<PluginV1alpha1ConsoleApiListPluginsRequest, Plugin>(\n      (params) => consoleApiClient.plugin.plugin.listPlugins(params),\n      {\n        size: 1000,\n        enabled: true,\n      }\n    );\n  },\n  enabled: computed(() => utils.permission.has([\"system:plugins:view\"])),\n});\n\n// copy system information to clipboard\nconst { copy, isSupported } = useClipboard({ legacy: true });\n\ninterface CopyItem {\n  label: string;\n  value?: string;\n  href?: string;\n  children?: CopyItem[];\n}\n\nconst handleCopy = () => {\n  if (!isSupported.value) {\n    Toast.warning(t(\"core.overview.actions.copy.toast_browser_not_supported\"));\n    return;\n  }\n\n  const copyItems: CopyItem[] = [\n    {\n      label: t(\"core.overview.fields.external_url\"),\n      value: globalInfo.value?.externalUrl || \"\",\n    },\n    {\n      label: t(\"core.overview.fields.start_time\"),\n      value: utils.date.format(startup.value?.timeline.startTime) || \"\",\n    },\n    {\n      label: t(\"core.overview.fields.version\"),\n      value: info.value?.build?.version || \"\",\n    },\n    {\n      label: t(\"core.overview.fields.build_time\"),\n      value: utils.date.format(info.value?.build?.time) || \"\",\n    },\n    {\n      label: \"Git Commit\",\n      value: info.value?.git?.commit.id || \"\",\n    },\n    {\n      label: \"Java\",\n      value:\n        [info.value?.java.runtime.name, info.value?.java.runtime.version]\n          .filter(Boolean)\n          .join(\" / \")\n          .trim() || \"\",\n    },\n    {\n      label: t(\"core.overview.fields.database\"),\n      value:\n        [info.value?.database.name, info.value?.database.version]\n          .filter(Boolean)\n          .join(\" / \") || \"\",\n    },\n    {\n      label: t(\"core.overview.fields.os\"),\n      value:\n        [info.value?.os.name, info.value?.os.version]\n          .filter(Boolean)\n          .join(\" / \") || \"\",\n    },\n    {\n      label: t(\"core.overview.fields.activated_theme\"),\n      value: `${themeStore.activatedTheme?.spec.displayName} ${themeStore.activatedTheme?.spec.version}`,\n      href:\n        themeStore.activatedTheme?.spec.repo ||\n        themeStore.activatedTheme?.spec.homepage,\n    },\n    {\n      label: t(\"core.overview.fields.enabled_plugins\"),\n      children: plugins.value?.map((plugin) => ({\n        value: `${plugin.spec.displayName} ${plugin.spec.version}`,\n        href: plugin.spec.repo || plugin.spec.homepage,\n      })) as CopyItem[],\n    },\n  ];\n\n  const text = copyItems\n    .map((item) => {\n      if (item.children?.length) {\n        const childrenText = item.children\n          .map(\n            (child) =>\n              `  - ${\n                child.href ? `[${child.value}](${child.href})` : child.value\n              }`\n          )\n          .filter(Boolean)\n          .join(\"\\n\");\n        return `- ${item.label}:\\n${childrenText}`;\n      } else {\n        return `- ${item.label}: ${\n          item.href ? `[${item.value}](${item.href})` : item.value || \"\"\n        }`;\n      }\n    })\n    .join(\"\\n\");\n\n  copy(text);\n\n  Toast.success(t(\"core.common.toast.copy_success\"));\n};\n\nconst handleDownloadLogfile = () => {\n  axios\n    .get(`/actuator/logfile`)\n    .then((response) => {\n      const blob = new Blob([response.data]);\n      const downloadElement = document.createElement(\"a\");\n      const href = window.URL.createObjectURL(blob);\n      downloadElement.href = href;\n      downloadElement.download = `halo-log-${utils.date.format(new Date())}.log`;\n      document.body.appendChild(downloadElement);\n      downloadElement.click();\n      document.body.removeChild(downloadElement);\n      window.URL.revokeObjectURL(href);\n\n      Toast.success(t(\"core.common.toast.download_success\"));\n    })\n    .catch((e) => {\n      Toast.error(t(\"core.common.toast.download_failed\"));\n      console.log(\"Failed to download log file.\", e);\n    });\n};\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.overview.title')\">\n    <template #icon>\n      <IconTerminalBoxLine />\n    </template>\n    <template #actions>\n      <VButton size=\"sm\" @click=\"handleCopy\">\n        <template #icon>\n          <IconClipboardLine />\n        </template>\n        {{ $t(\"core.common.buttons.copy\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 flex flex-col gap-4 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <div class=\"bg-white\">\n        <div\n          class=\"flex items-center justify-between bg-white px-4 py-4 sm:px-6\"\n        >\n          <div>\n            <h3 class=\"text-lg font-medium leading-6 text-gray-900\">\n              {{ $t(\"core.overview.header.titles.general\") }}\n            </h3>\n          </div>\n        </div>\n        <div class=\"border-t border-gray-200\">\n          <VDescription>\n            <ExternalUrlItem />\n            <VDescriptionItem\n              v-if=\"startup?.timeline.startTime\"\n              :label=\"$t('core.overview.fields.start_time')\"\n              :content=\"utils.date.format(startup?.timeline.startTime)\"\n            />\n            <VDescriptionItem\n              v-if=\"themeStore.activatedTheme\"\n              :label=\"$t('core.overview.fields.activated_theme')\"\n            >\n              <VTag @click=\"$router.push({ name: 'ThemeDetail' })\">\n                <template v-if=\"themeStore.activatedTheme.spec.logo\" #leftIcon>\n                  <img\n                    class=\"h-3.5 w-3.5 rounded-sm\"\n                    :src=\"themeStore.activatedTheme.spec.logo\"\n                    :alt=\"themeStore.activatedTheme.spec.displayName\"\n                  />\n                </template>\n                {{ themeStore.activatedTheme.spec.displayName }}\n              </VTag>\n            </VDescriptionItem>\n            <VDescriptionItem\n              v-permission=\"['system:plugins:view']\"\n              :label=\"$t('core.overview.fields.enabled_plugins')\"\n            >\n              <VLoading v-if=\"isPluginsLoading\" />\n              <span v-else-if=\"!plugins?.length\">\n                {{ $t(\"core.common.text.none\") }}\n              </span>\n              <div v-else class=\"flex flex-wrap gap-1.5\">\n                <VTag\n                  v-for=\"plugin in plugins\"\n                  :key=\"plugin.metadata.name\"\n                  @click=\"\n                    $router.push({\n                      name: 'PluginDetail',\n                      params: { name: plugin.metadata.name },\n                    })\n                  \"\n                >\n                  <template v-if=\"plugin.status?.logo\" #leftIcon>\n                    <img\n                      class=\"h-3.5 w-3.5 rounded-sm\"\n                      :src=\"plugin.status?.logo\"\n                      :alt=\"plugin.spec.displayName\"\n                    />\n                  </template>\n                  {{ plugin.spec.displayName }}\n                </VTag>\n              </div>\n            </VDescriptionItem>\n          </VDescription>\n        </div>\n      </div>\n    </VCard>\n    <VCard v-if=\"info\" :body-class=\"['!p-0']\">\n      <div class=\"bg-white\">\n        <div\n          class=\"flex items-center justify-between bg-white px-4 py-4 sm:px-6\"\n        >\n          <div>\n            <h3 class=\"text-lg font-medium leading-6 text-gray-900\">\n              {{ $t(\"core.overview.header.titles.environment\") }}\n            </h3>\n          </div>\n        </div>\n        <div class=\"border-t border-gray-200\">\n          <VDescription>\n            <VDescriptionItem\n              v-if=\"info.build\"\n              :label=\"$t('core.overview.fields.version')\"\n            >\n              <a\n                :href=\"`https://github.com/halo-dev/halo/releases/tag/v${info.build.version}`\"\n                class=\"hover:text-gray-600\"\n                target=\"_blank\"\n              >\n                {{ info.build.version }}\n              </a>\n            </VDescriptionItem>\n            <VDescriptionItem\n              v-if=\"info.build\"\n              :label=\"$t('core.overview.fields.build_time')\"\n              :content=\"utils.date.format(info.build.time)\"\n            />\n            <VDescriptionItem v-if=\"info.git\" label=\"Git Commit\">\n              <a\n                :href=\"`https://github.com/halo-dev/halo/commit/${info.git.commit.id}`\"\n                class=\"hover:text-gray-600\"\n                target=\"_blank\"\n              >\n                {{ info.git.commit.id }}\n              </a>\n            </VDescriptionItem>\n            <VDescriptionItem\n              label=\"Java\"\n              :content=\"\n                [info.java.runtime.name, info.java.runtime.version].join(' / ')\n              \"\n            />\n            <VDescriptionItem :label=\"$t('core.overview.fields.database')\">\n              <span>\n                {{ [info.database.name, info.database.version].join(\" / \") }}\n              </span>\n              <H2WarningAlert class=\"mt-3\" />\n            </VDescriptionItem>\n            <VDescriptionItem :label=\"$t('core.overview.fields.os')\">\n              {{ info.os.name }} {{ info.os.version }} / {{ info.os.arch }}\n            </VDescriptionItem>\n            <VDescriptionItem\n              :label=\"$t('core.overview.fields.timezone')\"\n              :content=\"globalInfo?.timeZone\"\n            />\n            <VDescriptionItem\n              :label=\"$t('core.overview.fields.locale')\"\n              :content=\"globalInfo?.locale\"\n            />\n            <VDescriptionItem\n              :label=\"$t('core.overview.fields.log')\"\n              vertical-center\n            >\n              <VButton size=\"sm\" @click=\"handleDownloadLogfile()\">\n                {{ $t(\"core.common.buttons.download\") }}\n              </VButton>\n            </VDescriptionItem>\n          </VDescription>\n        </div>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/overview/components/ExternalUrlForm.vue",
    "content": "<script setup lang=\"ts\">\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VButton, VLoading, VSpace } from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport axios from \"axios\";\nimport { storeToRefs } from \"pinia\";\nimport { computed, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst BASIC_GROUP = \"basic\";\n\nconst { t } = useI18n();\nconst { globalInfo } = storeToRefs(stores.globalInfo());\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst isRestarting = ref(false);\n\nfunction onSubmit({ externalUrl }: { externalUrl: string }) {\n  Dialog.warning({\n    title: t(\"core.overview.external_url_form.operations.save.title\"),\n    description: t(\n      \"core.overview.external_url_form.operations.save.description\"\n    ),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const { data: basicConfig } =\n        await consoleApiClient.configMap.system.getSystemConfigByGroup({\n          group: BASIC_GROUP,\n        });\n\n      await consoleApiClient.configMap.system.updateSystemConfigByGroup({\n        group: BASIC_GROUP,\n        body: {\n          ...basicConfig,\n          externalUrl: externalUrl.trim(),\n        },\n      });\n\n      await axios.post(`/actuator/restart`);\n\n      isRestarting.value = true;\n\n      Toast.success(t(\"core.common.toast.save_success\"));\n    },\n  });\n}\n\nonMounted(() => {\n  setFocus(\"externalUrl\");\n});\n\nuseQuery({\n  queryKey: [\"check-health\"],\n  queryFn: async () => {\n    const { data } = await axios.get(\"/actuator/health\");\n    return data;\n  },\n  onSuccess(data) {\n    if (data.status === \"UP\") {\n      window.location.reload();\n    }\n  },\n  retry: true,\n  retryDelay: 2000,\n  enabled: computed(() => isRestarting.value),\n});\n</script>\n\n<template>\n  <template v-if=\"!isRestarting\">\n    <FormKit\n      id=\"external-url-form\"\n      type=\"form\"\n      name=\"external-url-form\"\n      @submit=\"onSubmit\"\n    >\n      <FormKit\n        id=\"externalUrl\"\n        :model-value=\"globalInfo?.externalUrl\"\n        type=\"url\"\n        name=\"externalUrl\"\n        validation=\"url|required\"\n        :validation-label=\"$t('core.overview.fields.external_url')\"\n        :classes=\"{ outer: '!pb-0' }\"\n      ></FormKit>\n    </FormKit>\n    <VSpace class=\"mt-4\">\n      <VButton type=\"secondary\" @click=\"$formkit.submit('external-url-form')\">\n        {{ $t(\"core.common.buttons.save\") }}\n      </VButton>\n      <VButton @click=\"emit('close')\">\n        {{ $t(\"core.common.buttons.cancel\") }}\n      </VButton>\n    </VSpace>\n  </template>\n  <template v-else>\n    <div class=\"flex items-center gap-2\">\n      <VLoading class=\"!inline-flex !py-0\" />\n      <div class=\"text-xs text-gray-600\">\n        {{ $t(\"core.overview.external_url_form.tips.restarting\") }}\n      </div>\n    </div>\n  </template>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/overview/components/ExternalUrlItem.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  IconRiPencilFill,\n  VAlert,\n  VDescriptionItem,\n} from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { storeToRefs } from \"pinia\";\nimport { computed, ref } from \"vue\";\nimport ExternalUrlForm from \"./ExternalUrlForm.vue\";\n\nconst { globalInfo } = storeToRefs(stores.globalInfo());\n\nconst isExternalUrlValid = computed(() => {\n  if (!globalInfo.value) {\n    return true;\n  }\n\n  if (!globalInfo.value?.externalUrl) {\n    return false;\n  }\n\n  const url = new URL(globalInfo.value.externalUrl);\n  const { host: currentHost, protocol: currentProtocol } = window.location;\n  return url.host === currentHost && url.protocol === currentProtocol;\n});\n\nconst showExternalUrlForm = ref(false);\n</script>\n\n<template>\n  <VDescriptionItem :label=\"$t('core.overview.fields.external_url')\">\n    <div v-if=\"!showExternalUrlForm\" class=\"flex items-center gap-3\">\n      <span v-if=\"globalInfo?.externalUrl\">\n        {{ globalInfo?.externalUrl }}\n      </span>\n      <span v-else>\n        {{ $t(\"core.overview.fields_values.external_url.not_setup\") }}\n      </span>\n      <IconRiPencilFill\n        class=\"cursor-pointer text-sm text-gray-600 hover:text-gray-900\"\n        @click=\"showExternalUrlForm = true\"\n      />\n    </div>\n    <ExternalUrlForm v-else @close=\"showExternalUrlForm = false\" />\n    <VAlert\n      v-if=\"!isExternalUrlValid && !showExternalUrlForm\"\n      class=\"mt-3\"\n      type=\"warning\"\n      :title=\"$t('core.common.text.warning')\"\n      :closable=\"false\"\n    >\n      <template #description>\n        {{ $t(\"core.overview.alert.external_url_invalid\") }}\n      </template>\n    </VAlert>\n  </VDescriptionItem>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/overview/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconTerminalBoxLine } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  components: {},\n  routes: [\n    {\n      path: \"/overview\",\n      name: \"OverviewRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.overview.title\",\n        searchable: true,\n        permissions: [\"system:actuator:manage\"],\n        menu: {\n          name: \"core.sidebar.menu.items.overview\",\n          group: \"system\",\n          icon: markRaw(IconTerminalBoxLine),\n          priority: 3,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Overview\",\n          component: () => import(\"./Overview.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/PluginDetail.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin, Setting } from \"@halo-dev/api-client\";\nimport { VAvatar, VCard, VPageHeader, VTabbar } from \"@halo-dev/components\";\nimport type { Ref } from \"vue\";\nimport { provide, toRefs } from \"vue\";\nimport { useRoute } from \"vue-router\";\nimport { usePluginDetailTabs } from \"./composables/use-plugin\";\n\nconst route = useRoute();\n\nconst { name } = toRefs(route.params);\n\nconst { plugin, setting, activeTab, tabs } = usePluginDetailTabs(\n  name as Ref<string | undefined>,\n  true\n);\n\nprovide<Ref<string>>(\"activeTab\", activeTab);\nprovide<Ref<Plugin | undefined>>(\"plugin\", plugin);\nprovide<Ref<Setting | undefined>>(\"setting\", setting);\n</script>\n<template>\n  <VPageHeader :title=\"plugin?.spec?.displayName\">\n    <template #icon>\n      <VAvatar\n        v-if=\"plugin\"\n        :src=\"plugin.status?.logo\"\n        :alt=\"plugin.spec.displayName\"\n        size=\"sm\"\n      />\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0', '!overflow-visible']\">\n      <template #header>\n        <VTabbar\n          v-model:active-id=\"activeTab\"\n          :items=\"tabs.map((item) => ({ id: item.id, label: item.label }))\"\n          class=\"w-full !rounded-none\"\n          type=\"outline\"\n        ></VTabbar>\n      </template>\n      <div class=\"rounded-b-base bg-white\">\n        <template v-for=\"tab in tabs\" :key=\"tab.id\">\n          <component :is=\"tab.component\" v-if=\"activeTab === tab.id\" />\n        </template>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/PluginExtensionPointSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  coreApiClient,\n  paginate,\n  type ExtensionPointDefinition,\n  type ExtensionPointDefinitionV1alpha1ApiListExtensionPointDefinitionRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  IconSettings,\n  VButton,\n  VCard,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { computed, watch } from \"vue\";\nimport ExtensionDefinitionMultiInstanceView from \"./components/extension-points/ExtensionDefinitionMultiInstanceView.vue\";\nimport ExtensionDefinitionSingletonView from \"./components/extension-points/ExtensionDefinitionSingletonView.vue\";\n\nconst { data: extensionPointDefinitions } = useQuery({\n  queryKey: [\"extension-point-definitions\"],\n  queryFn: async () => {\n    return await paginate<\n      ExtensionPointDefinitionV1alpha1ApiListExtensionPointDefinitionRequest,\n      ExtensionPointDefinition\n    >(\n      (params) =>\n        coreApiClient.plugin.extensionPointDefinition.listExtensionPointDefinition(\n          params\n        ),\n      {\n        size: 1000,\n      }\n    );\n  },\n});\n\nconst selectedExtensionPointDefinitionName = useRouteQuery<string | undefined>(\n  \"extension-point-definition-name\"\n);\n\nconst selectedExtensionPointDefinition = computed(() => {\n  return extensionPointDefinitions.value?.find(\n    (item) => item.metadata.name === selectedExtensionPointDefinitionName.value\n  );\n});\n\nwatch(\n  () => extensionPointDefinitions.value,\n  (value) => {\n    if (value?.length && !selectedExtensionPointDefinitionName.value) {\n      selectedExtensionPointDefinitionName.value = value?.[0].metadata.name;\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.plugin.extension-settings.title')\">\n    <template #icon>\n      <IconSettings />\n    </template>\n    <template #actions>\n      <VButton size=\"sm\" @click=\"$router.back()\">\n        {{ $t(\"core.common.buttons.back\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard\n      style=\"height: calc(100vh - 5.5rem)\"\n      :body-class=\"['h-full', '!p-0']\"\n    >\n      <div class=\"flex h-full divide-x\">\n        <div class=\"w-72 flex-none\">\n          <div\n            class=\"sticky top-0 z-10 flex h-12 items-center border-b bg-white px-4\"\n          >\n            <h2 class=\"font-semibold text-gray-900\">\n              {{\n                $t(\n                  \"core.plugin.extension-settings.extension-point-definition.title\"\n                )\n              }}\n            </h2>\n          </div>\n          <ul\n            class=\"box-border h-full w-full divide-y divide-gray-100 overflow-auto pb-12\"\n            role=\"list\"\n          >\n            <li\n              v-for=\"extensionPointDefinition in extensionPointDefinitions\"\n              :key=\"extensionPointDefinition.metadata.name\"\n              class=\"relative cursor-pointer\"\n              @click=\"\n                selectedExtensionPointDefinitionName =\n                  extensionPointDefinition.metadata.name\n              \"\n            >\n              <div\n                v-show=\"\n                  selectedExtensionPointDefinitionName ===\n                  extensionPointDefinition.metadata.name\n                \"\n                class=\"absolute inset-y-0 left-0 w-0.5 bg-primary\"\n              ></div>\n              <div\n                class=\"flex flex-col space-y-1.5 px-4 py-2.5 hover:bg-gray-50\"\n              >\n                <h3\n                  class=\"line-clamp-1 break-words text-sm font-medium text-gray-900\"\n                >\n                  {{ extensionPointDefinition.spec.displayName }}\n                </h3>\n                <p class=\"line-clamp-2 text-xs text-gray-600\">\n                  {{ extensionPointDefinition.spec.description }}\n                </p>\n              </div>\n            </li>\n          </ul>\n        </div>\n        <div class=\"flex min-w-0 flex-1 shrink flex-col overflow-auto\">\n          <div\n            class=\"sticky top-0 z-10 flex h-12 items-center space-x-3 border-b bg-white px-4\"\n          >\n            <h2 class=\"font-semibold text-gray-900\">\n              {{ selectedExtensionPointDefinition?.spec.displayName }}\n            </h2>\n            <small class=\"line-clamp-1 text-gray-600\">\n              {{ selectedExtensionPointDefinition?.spec.description }}\n            </small>\n          </div>\n          <ExtensionDefinitionSingletonView\n            v-if=\"selectedExtensionPointDefinition?.spec.type === 'SINGLETON'\"\n            :extension-point-definition=\"selectedExtensionPointDefinition\"\n          />\n          <ExtensionDefinitionMultiInstanceView\n            v-else-if=\"\n              selectedExtensionPointDefinition?.spec.type === 'MULTI_INSTANCE'\n            \"\n            :extension-point-definition=\"selectedExtensionPointDefinition\"\n          />\n        </div>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/PluginList.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  PluginStatusPhaseEnum,\n  consoleApiClient,\n  paginate,\n  type Plugin,\n  type PluginV1alpha1ConsoleApiListPluginsRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconPlug,\n  IconRefreshLine,\n  IconSettings,\n  VButton,\n  VCard,\n  VDropdown,\n  VDropdownItem,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { Ref } from \"vue\";\nimport { computed, onMounted, provide, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport PluginInstallationModal from \"./components/PluginInstallationModal.vue\";\nimport PluginListItem from \"./components/PluginListItem.vue\";\nimport { usePluginBatchOperations } from \"./composables/use-plugin\";\n\nconst { t } = useI18n();\n\nconst pluginInstallationModalVisible = ref(false);\n\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\n\nconst selectedEnabledValue = useRouteQuery<string | undefined>(\"enabled\");\nconst selectedSortValue = useRouteQuery<string | undefined>(\"sort\");\n\nconst hasFilters = computed(() => {\n  return selectedEnabledValue.value !== undefined || selectedSortValue.value;\n});\n\nfunction handleClearFilters() {\n  selectedSortValue.value = undefined;\n  selectedEnabledValue.value = undefined;\n}\n\nconst { data, isLoading, isFetching, refetch } = useQuery({\n  queryKey: [\"plugins\", keyword, selectedEnabledValue, selectedSortValue],\n  queryFn: async () => {\n    return await paginate<PluginV1alpha1ConsoleApiListPluginsRequest, Plugin>(\n      (params) => consoleApiClient.plugin.plugin.listPlugins(params),\n      {\n        size: 1000,\n        keyword: keyword.value,\n        enabled: selectedEnabledValue.value\n          ? JSON.parse(selectedEnabledValue.value)\n          : undefined,\n        sort: [selectedSortValue.value].filter(Boolean) as string[],\n      }\n    );\n  },\n  keepPreviousData: true,\n  refetchInterval: (data) => {\n    const hasDeletingData = data?.some(\n      (plugin) => !!plugin.metadata.deletionTimestamp\n    );\n\n    if (hasDeletingData) {\n      return 1000;\n    }\n\n    const hasStartingData = data?.some(\n      (plugin) =>\n        plugin.spec.enabled &&\n        plugin.status?.phase !==\n          (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)\n    );\n\n    if (hasStartingData) {\n      return 3000;\n    }\n\n    return false;\n  },\n});\n\n// selection\nconst selectedNames = ref<string[]>([]);\nprovide<Ref<string[]>>(\"selectedNames\", selectedNames);\nconst checkedAll = ref(false);\n\nwatch(\n  () => selectedNames.value,\n  (value) => {\n    checkedAll.value = value.length === data.value?.length;\n  }\n);\n\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n  if (checked) {\n    selectedNames.value =\n      data.value?.map((plugin) => {\n        return plugin.metadata.name;\n      }) || [];\n  } else {\n    selectedNames.value.length = 0;\n  }\n};\n\nconst { handleChangeStatusInBatch, handleUninstallInBatch } =\n  usePluginBatchOperations(selectedNames);\n\n// handle remote download url from route\nconst routeRemoteDownloadUrl = useRouteQuery<string | null>(\n  \"remote-download-url\"\n);\nonMounted(() => {\n  if (routeRemoteDownloadUrl.value) {\n    Dialog.warning({\n      title: t(\"core.plugin.operations.remote_download.title\"),\n      description: t(\"core.plugin.operations.remote_download.description\", {\n        url: routeRemoteDownloadUrl.value,\n      }),\n      confirmText: t(\"core.common.buttons.download\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm() {\n        pluginInstallationModalVisible.value = true;\n      },\n      onCancel() {\n        routeRemoteDownloadUrl.value = null;\n      },\n    });\n  }\n});\n</script>\n<template>\n  <PluginInstallationModal\n    v-if=\"\n      pluginInstallationModalVisible &&\n      utils.permission.has(['system:plugins:manage'])\n    \"\n    @close=\"pluginInstallationModalVisible = false\"\n  />\n\n  <VPageHeader :title=\"$t('core.plugin.title')\">\n    <template #icon>\n      <IconPlug />\n    </template>\n    <template #actions>\n      <HasPermission :permissions=\"['*']\">\n        <VButton\n          size=\"sm\"\n          @click=\"$router.push({ name: 'PluginExtensionPointSettings' })\"\n        >\n          <template #icon>\n            <IconSettings />\n          </template>\n          {{ $t(\"core.plugin.actions.extension-point-settings\") }}\n        </VButton>\n      </HasPermission>\n\n      <VButton\n        v-permission=\"['system:plugins:manage']\"\n        type=\"secondary\"\n        @click=\"pluginInstallationModalVisible = true\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.install\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <div\n              v-permission=\"['system:posts:manage']\"\n              class=\"hidden items-center sm:flex\"\n            >\n              <input\n                v-model=\"checkedAll\"\n                type=\"checkbox\"\n                @change=\"handleCheckAllChange\"\n              />\n            </div>\n            <div class=\"flex w-full flex-1 items-center gap-2 sm:w-auto\">\n              <SearchInput v-if=\"!selectedNames.length\" v-model=\"keyword\" />\n              <VSpace v-else>\n                <VButton @click=\"handleChangeStatusInBatch(true)\">\n                  {{ $t(\"core.common.buttons.activate\") }}\n                </VButton>\n                <VButton @click=\"handleChangeStatusInBatch(false)\">\n                  {{ $t(\"core.common.buttons.inactivate\") }}\n                </VButton>\n                <VDropdown>\n                  <VButton type=\"danger\">\n                    {{ $t(\"core.common.buttons.uninstall\") }}\n                  </VButton>\n                  <template #popper>\n                    <VDropdownItem\n                      type=\"danger\"\n                      @click=\"handleUninstallInBatch(false)\"\n                    >\n                      {{ $t(\"core.common.buttons.uninstall\") }}\n                    </VDropdownItem>\n                    <VDropdownItem\n                      type=\"danger\"\n                      @click=\"handleUninstallInBatch(true)\"\n                    >\n                      {{\n                        $t(\n                          \"core.plugin.operations.uninstall_and_delete_config.button\"\n                        )\n                      }}\n                    </VDropdownItem>\n                  </template>\n                </VDropdown>\n              </VSpace>\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <FilterCleanButton\n                v-if=\"hasFilters\"\n                @click=\"handleClearFilters\"\n              />\n              <FilterDropdown\n                v-model=\"selectedEnabledValue\"\n                :label=\"$t('core.common.filters.labels.status')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.all'),\n                  },\n                  {\n                    label: t('core.plugin.filters.status.items.active'),\n                    value: 'true',\n                  },\n                  {\n                    label: t('core.plugin.filters.status.items.inactive'),\n                    value: 'false',\n                  },\n                ]\"\n              />\n              <FilterDropdown\n                v-model=\"selectedSortValue\"\n                :label=\"$t('core.common.filters.labels.sort')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.default'),\n                  },\n                  {\n                    label: t('core.plugin.filters.sort.items.create_time_desc'),\n                    value: 'creationTimestamp,desc',\n                  },\n                  {\n                    label: t('core.plugin.filters.sort.items.create_time_asc'),\n                    value: 'creationTimestamp,asc',\n                  },\n                ]\"\n              />\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"refetch()\"\n                >\n                  <IconRefreshLine\n                    v-tooltip=\"$t('core.common.buttons.refresh')\"\n                    :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n\n      <VLoading v-if=\"isLoading\" />\n\n      <Transition v-else-if=\"!data?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.plugin.empty.message')\"\n          :title=\"$t('core.plugin.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton :loading=\"isFetching\" @click=\"refetch()\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton\n                v-permission=\"['system:plugins:manage']\"\n                type=\"secondary\"\n                @click=\"pluginInstallationModalVisible = true\"\n              >\n                <template #icon>\n                  <IconAddCircle />\n                </template>\n                {{ $t(\"core.plugin.empty.actions.install\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <PluginListItem\n            v-for=\"plugin in data\"\n            :key=\"plugin.metadata.name\"\n            :plugin=\"plugin\"\n            :is-selected=\"selectedNames.includes(plugin.metadata.name)\"\n          />\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <div class=\"flex h-8 items-center\">\n          <span class=\"text-sm text-gray-500\">\n            {{\n              $t(\"core.components.pagination.total_label\", {\n                total: data?.length,\n              })\n            }}\n          </span>\n        </div>\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/PluginConditionsModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { VButton, VModal } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { ref } from \"vue\";\n\nwithDefaults(defineProps<{ plugin: Plugin }>(), {});\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref();\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :title=\"$t('core.plugin.conditions_modal.title')\"\n    :width=\"900\"\n    layer-closable\n    @close=\"emit('close')\"\n  >\n    <table class=\"min-w-full divide-y divide-gray-100\">\n      <thead class=\"bg-gray-50\">\n        <tr>\n          <th\n            class=\"px-4 py-3 text-left text-sm font-semibold text-gray-900 sm:w-96\"\n            scope=\"col\"\n          >\n            {{ $t(\"core.plugin.conditions_modal.fields.type\") }}\n          </th>\n          <th\n            scope=\"col\"\n            class=\"px-4 py-3 text-left text-sm font-semibold text-gray-900\"\n          >\n            {{ $t(\"core.plugin.conditions_modal.fields.status\") }}\n          </th>\n          <th\n            scope=\"col\"\n            class=\"px-4 py-3 text-left text-sm font-semibold text-gray-900\"\n          >\n            {{ $t(\"core.plugin.conditions_modal.fields.reason\") }}\n          </th>\n          <th\n            scope=\"col\"\n            class=\"px-4 py-3 text-left text-sm font-semibold text-gray-900\"\n          >\n            {{ $t(\"core.plugin.conditions_modal.fields.message\") }}\n          </th>\n          <th\n            scope=\"col\"\n            class=\"px-4 py-3 text-left text-sm font-semibold text-gray-900\"\n          >\n            {{ $t(\"core.plugin.conditions_modal.fields.last_transition_time\") }}\n          </th>\n        </tr>\n      </thead>\n      <tbody class=\"divide-y divide-gray-100 bg-white\">\n        <tr\n          v-for=\"(condition, index) in plugin?.status?.conditions\"\n          :key=\"index\"\n        >\n          <td class=\"whitespace-nowrap px-4 py-3 text-sm text-gray-900\">\n            {{ condition.type }}\n          </td>\n          <td class=\"whitespace-nowrap px-4 py-3 text-sm text-gray-500\">\n            {{ condition.status }}\n          </td>\n          <td class=\"whitespace-nowrap px-4 py-3 text-sm text-gray-500\">\n            {{ condition.reason || \"-\" }}\n          </td>\n          <td class=\"whitespace-nowrap px-4 py-3 text-sm text-gray-500\">\n            <pre>{{ condition.message || \"-\" }}</pre>\n          </td>\n          <td\n            v-tooltip=\"utils.date.format(condition.lastTransitionTime)\"\n            class=\"whitespace-nowrap px-4 py-3 text-sm text-gray-500\"\n          >\n            {{ utils.date.timeAgo(condition.lastTransitionTime) }}\n          </td>\n        </tr>\n      </tbody>\n    </table>\n\n    <template #footer>\n      <VButton @click=\"modal.close()\">\n        {{ $t(\"core.common.buttons.close\") }}\n      </VButton>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/PluginDetailModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin, Setting } from \"@halo-dev/api-client\";\nimport { IconLink, VButton, VModal, VTabbar } from \"@halo-dev/components\";\nimport { provide, ref, toRefs, type Ref } from \"vue\";\nimport { usePluginDetailTabs } from \"../composables/use-plugin\";\n\nconst props = withDefaults(defineProps<{ name: string }>(), {});\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { name } = toRefs(props);\n\nconst { plugin, setting, tabs, activeTab } = usePluginDetailTabs(name, false);\n\nprovide<Ref<string>>(\"activeTab\", activeTab);\nprovide<Ref<Plugin | undefined>>(\"plugin\", plugin);\nprovide<Ref<Setting | undefined>>(\"setting\", setting);\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"plugin?.spec.displayName\"\n    :centered=\"true\"\n    :width=\"920\"\n    height=\"calc(100vh - 20px)\"\n    mount-to-body\n    @close=\"emit('close')\"\n  >\n    <template #actions>\n      <span>\n        <RouterLink\n          :to=\"{\n            name: 'PluginDetail',\n            params: { name },\n          }\"\n        >\n          <IconLink />\n        </RouterLink>\n      </span>\n    </template>\n    <VTabbar\n      v-model:active-id=\"activeTab\"\n      :items=\"\n        tabs.map((tab) => {\n          return { label: tab.label, id: tab.id };\n        })\n      \"\n      type=\"outline\"\n    />\n    <div class=\"-m-4 mt-2\">\n      <template v-for=\"tab in tabs\" :key=\"tab.id\">\n        <component :is=\"tab.component\" v-if=\"activeTab === tab.id\" />\n      </template>\n    </div>\n    <template #footer>\n      <VButton @click=\"modal?.close()\">\n        {{ $t(\"core.common.buttons.close\") }}\n      </VButton>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/PluginInstallationModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { VButton, VLoading, VModal, VTabbar } from \"@halo-dev/components\";\nimport { utils, type PluginInstallationTab } from \"@halo-dev/ui-shared\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport {\n  computed,\n  defineAsyncComponent,\n  nextTick,\n  onMounted,\n  provide,\n  ref,\n  shallowRef,\n  toRefs,\n  type Ref,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    pluginToUpgrade?: Plugin;\n  }>(),\n  {\n    pluginToUpgrade: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst { pluginToUpgrade } = toRefs(props);\nprovide<Ref<Plugin | undefined>>(\"pluginToUpgrade\", pluginToUpgrade);\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst tabs = shallowRef<PluginInstallationTab[]>([\n  {\n    id: \"local\",\n    label: t(\"core.plugin.upload_modal.tabs.local\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./installation-tabs/LocalUpload.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 10,\n  },\n  {\n    id: \"remote\",\n    label: t(\"core.plugin.upload_modal.tabs.remote.title\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./installation-tabs/RemoteDownload.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 20,\n  },\n]);\n\nconst activeTabId = ref();\n\nconst modalTitle = computed(() => {\n  return props.pluginToUpgrade\n    ? t(\"core.plugin.upload_modal.titles.upgrade\", {\n        display_name: props.pluginToUpgrade.spec.displayName,\n      })\n    : t(\"core.plugin.upload_modal.titles.install\");\n});\n\n// handle remote download url from route\nconst routeRemoteDownloadUrl = useRouteQuery<string | null>(\n  \"remote-download-url\"\n);\n\nonMounted(() => {\n  if (routeRemoteDownloadUrl.value) {\n    nextTick(() => {\n      activeTabId.value = \"remote\";\n    });\n  }\n});\n\nconst { pluginModules } = usePluginModuleStore();\n\nonMounted(async () => {\n  for (const pluginModule of pluginModules) {\n    try {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"plugin:installation:tabs:create\"];\n\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const items = await callbackFunction();\n\n      tabs.value.push(\n        ...items.filter((item) => {\n          return utils.permission.has(item.permissions || []);\n        })\n      );\n    } catch (error) {\n      console.error(`Error processing plugin module:`, pluginModule, error);\n    }\n  }\n\n  tabs.value.sort((a, b) => {\n    return a.priority - b.priority;\n  });\n\n  activeTabId.value = tabs.value[0].id;\n});\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"modalTitle\"\n    :centered=\"true\"\n    :width=\"920\"\n    height=\"calc(100vh - 20px)\"\n    @close=\"emit('close')\"\n  >\n    <VTabbar\n      v-model:active-id=\"activeTabId\"\n      :items=\"\n        tabs.map((tab) => {\n          return { label: tab.label, id: tab.id };\n        })\n      \"\n      type=\"outline\"\n    />\n    <div class=\"mt-2\">\n      <template v-for=\"tab in tabs\" :key=\"tab.id\">\n        <component\n          :is=\"tab.component\"\n          v-bind=\"tab.props\"\n          v-if=\"tab.id === activeTabId\"\n          @close-modal=\"modal?.close()\"\n        />\n      </template>\n    </div>\n    <template #footer>\n      <VButton @click=\"modal?.close()\">\n        {{ $t(\"core.common.buttons.close\") }}\n      </VButton>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/PluginListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useEntityFieldItemExtensionPoint } from \"@console/composables/use-entity-extension-points\";\nimport { useOperationItemExtensionPoint } from \"@console/composables/use-operation-extension-points\";\nimport PluginInstallationModal from \"@console/modules/system/plugins/components/PluginInstallationModal.vue\";\nimport {\n  PluginStatusPhaseEnum,\n  consoleApiClient,\n  type Plugin,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n} from \"@halo-dev/components\";\nimport {\n  utils,\n  type EntityFieldItem,\n  type OperationItem,\n} from \"@halo-dev/ui-shared\";\nimport type { Ref } from \"vue\";\nimport { computed, inject, markRaw, ref, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRouter } from \"vue-router\";\nimport EntityFieldItems from \"@/components/entity-fields/EntityFieldItems.vue\";\nimport StatusDotField from \"@/components/entity-fields/StatusDotField.vue\";\nimport EntityDropdownItems from \"@/components/entity/EntityDropdownItems.vue\";\nimport { pluginLabels } from \"@/constants/labels\";\nimport { usePluginLifeCycle } from \"../composables/use-plugin\";\nimport AuthorField from \"./entity-fields/AuthorField.vue\";\nimport LogoField from \"./entity-fields/LogoField.vue\";\nimport ReloadField from \"./entity-fields/ReloadField.vue\";\nimport SwitchField from \"./entity-fields/SwitchField.vue\";\nimport TitleField from \"./entity-fields/TitleField.vue\";\n\nconst { t } = useI18n();\nconst router = useRouter();\n\nconst props = withDefaults(\n  defineProps<{\n    plugin: Plugin;\n    isSelected?: boolean;\n  }>(),\n  { isSelected: false }\n);\n\nconst { plugin } = toRefs(props);\n\nconst selectedNames = inject<Ref<string[]>>(\"selectedNames\", ref([]));\n\nconst { getStatusDotState, getStatusMessage, uninstall } =\n  usePluginLifeCycle(plugin);\n\nconst pluginUpgradeModalVisible = ref(false);\n\nconst handleResetSettingConfig = async () => {\n  Dialog.warning({\n    title: t(\"core.plugin.operations.reset.title\"),\n    description: t(\"core.plugin.operations.reset.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        if (!plugin?.value) {\n          return;\n        }\n\n        await consoleApiClient.plugin.plugin.resetPluginConfig({\n          name: plugin.value.metadata.name as string,\n        });\n\n        Toast.success(t(\"core.plugin.operations.reset.toast_success\"));\n      } catch (e) {\n        console.error(\"Failed to reset plugin setting config\", e);\n      }\n    },\n  });\n};\n\nconst { data: operationItems } = useOperationItemExtensionPoint<Plugin>(\n  \"plugin:list-item:operation:create\",\n  plugin,\n  computed((): OperationItem<Plugin>[] => [\n    {\n      priority: 10,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.detail\"),\n      permissions: [],\n      action: () => {\n        router.push({\n          name: \"PluginDetail\",\n          params: { name: props.plugin?.metadata.name },\n        });\n      },\n    },\n    {\n      priority: 20,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.upgrade\"),\n      permissions: [],\n      action: () => {\n        pluginUpgradeModalVisible.value = true;\n      },\n      // System reserved plugins cannot be upgraded\n      hidden:\n        plugin.value.metadata.labels?.[pluginLabels.SYSTEM_RESERVED] === \"true\",\n    },\n    {\n      priority: 30,\n      component: markRaw(VDropdownDivider),\n    },\n    {\n      priority: 40,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.uninstall\"),\n      children: [\n        {\n          priority: 10,\n          component: markRaw(VDropdownItem),\n          props: {\n            type: \"danger\",\n          },\n          label: t(\"core.common.buttons.uninstall\"),\n          action: () => uninstall(),\n        },\n        {\n          priority: 20,\n          component: markRaw(VDropdownItem),\n          props: {\n            type: \"danger\",\n          },\n          label: t(\"core.plugin.operations.uninstall_and_delete_config.button\"),\n          action: () => uninstall(true),\n        },\n      ],\n      // System reserved plugins cannot be uninstalled\n      hidden:\n        plugin.value.metadata.labels?.[pluginLabels.SYSTEM_RESERVED] === \"true\",\n    },\n    {\n      priority: 50,\n      component: markRaw(VDropdownItem),\n      props: {\n        type: \"danger\",\n      },\n      label: t(\"core.common.buttons.reset\"),\n      action: () => {\n        handleResetSettingConfig();\n      },\n    },\n  ])\n);\n\nconst { data: fields } = useEntityFieldItemExtensionPoint<Plugin>(\n  \"plugin:list-item:field:create\",\n  plugin,\n  computed((): EntityFieldItem[] => {\n    const { phase } = props.plugin.status || {};\n\n    const shouldHideStatusDot =\n      phase === PluginStatusPhaseEnum.Started ||\n      phase === PluginStatusPhaseEnum.Disabled;\n\n    return [\n      {\n        position: \"start\",\n        priority: 10,\n        component: markRaw(LogoField),\n        props: {\n          plugin: props.plugin,\n        },\n      },\n      {\n        position: \"start\",\n        priority: 20,\n        component: markRaw(TitleField),\n        props: {\n          plugin: props.plugin,\n        },\n      },\n      {\n        position: \"end\",\n        priority: 10,\n        component: markRaw(StatusDotField),\n        props: {\n          tooltip: getStatusMessage(),\n          state: getStatusDotState(),\n          animate: true,\n        },\n        hidden: shouldHideStatusDot,\n      },\n      {\n        position: \"end\",\n        priority: 20,\n        component: markRaw(StatusDotField),\n        props: {\n          tooltip: t(\"core.common.status.deleting\"),\n          state: \"warning\",\n          animate: true,\n        },\n        hidden: !props.plugin.metadata.deletionTimestamp,\n      },\n      {\n        position: \"end\",\n        priority: 30,\n        component: markRaw(AuthorField),\n        props: {\n          plugin: props.plugin,\n        },\n        hidden: !props.plugin.spec.author,\n      },\n      {\n        position: \"end\",\n        priority: 40,\n        component: markRaw(VEntityField),\n        props: {\n          description: props.plugin.spec.version,\n        },\n      },\n      {\n        position: \"end\",\n        priority: 41,\n        component: markRaw(ReloadField),\n        props: {\n          plugin: props.plugin,\n        },\n      },\n      {\n        position: \"end\",\n        priority: 60,\n        component: markRaw(SwitchField),\n        props: {\n          plugin: props.plugin,\n        },\n        permissions: [\"system:plugins:manage\"],\n      },\n    ];\n  })\n);\n</script>\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template v-if=\"utils.permission.has(['system:plugins:manage'])\" #checkbox>\n      <input\n        v-model=\"selectedNames\"\n        :value=\"plugin.metadata.name\"\n        name=\"post-checkbox\"\n        type=\"checkbox\"\n      />\n    </template>\n    <template #start>\n      <EntityFieldItems :fields=\"fields?.start || []\" />\n    </template>\n    <template #end>\n      <EntityFieldItems :fields=\"fields?.end || []\" />\n    </template>\n    <template\n      v-if=\"utils.permission.has(['system:plugins:manage'])\"\n      #dropdownItems\n    >\n      <EntityDropdownItems\n        :dropdown-items=\"operationItems || []\"\n        :item=\"plugin\"\n      />\n    </template>\n  </VEntity>\n\n  <PluginInstallationModal\n    v-if=\"\n      pluginUpgradeModalVisible &&\n      utils.permission.has(['system:plugins:manage'])\n    \"\n    :plugin-to-upgrade=\"plugin\"\n    @close=\"pluginUpgradeModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/entity-fields/AuthorField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { VEntityField } from \"@halo-dev/components\";\nwithDefaults(\n  defineProps<{\n    plugin: Plugin;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField v-if=\"plugin.spec.author\">\n    <template #description>\n      <a\n        :href=\"plugin.spec.author.website\"\n        class=\"hidden text-sm text-gray-500 hover:text-gray-900 sm:block\"\n        target=\"_blank\"\n      >\n        @{{ plugin.spec.author.name }}\n      </a>\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/entity-fields/LogoField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { VAvatar, VEntityField } from \"@halo-dev/components\";\nwithDefaults(\n  defineProps<{\n    plugin: Plugin;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField>\n    <template #description>\n      <VAvatar\n        :alt=\"plugin.spec.displayName\"\n        :src=\"plugin.status?.logo\"\n        size=\"md\"\n      ></VAvatar>\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/entity-fields/ReloadField.vue",
    "content": "<script setup lang=\"ts\">\nimport { PluginStatusPhaseEnum, type Plugin } from \"@halo-dev/api-client\";\nimport { IconInformation, VButton } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    plugin: Plugin;\n  }>(),\n  {}\n);\n\nconst enabledJsModulesInfo =\n  (window[\"enabledPlugins\"] as { name: string; version: string }[]) || [];\n\nconst currentJsModuleInfo = enabledJsModulesInfo.find((jsModuleInfo) => {\n  return jsModuleInfo.name === props.plugin.metadata.name;\n});\n\nconst needsReloadWindow = computed(() => {\n  if (!currentJsModuleInfo) {\n    return false;\n  }\n\n  const { version } = props.plugin.spec;\n  const { phase } = props.plugin.status || {};\n\n  const isStarted = PluginStatusPhaseEnum.Started === phase;\n\n  return isStarted && version !== currentJsModuleInfo?.version;\n});\n\nfunction handleReloadWindow() {\n  window.location.reload();\n}\n</script>\n\n<template>\n  <VButton v-if=\"needsReloadWindow\" size=\"xs\" @click=\"handleReloadWindow\">\n    <template #icon>\n      <IconInformation />\n    </template>\n    {{ $t(\"core.plugin.operations.reload_window.button\") }}\n  </VButton>\n</template>\n\n<style lang=\"scss\" scoped></style>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/entity-fields/SwitchField.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { VEntityField, VSwitch } from \"@halo-dev/components\";\nimport { toRefs } from \"vue\";\nimport { usePluginLifeCycle } from \"../../composables/use-plugin\";\nconst props = withDefaults(\n  defineProps<{\n    plugin: Plugin;\n  }>(),\n  {}\n);\n\nconst { plugin } = toRefs(props);\n\nconst { changingStatus, changeStatus } = usePluginLifeCycle(plugin);\n</script>\n\n<template>\n  <VEntityField v-permission=\"['system:plugins:manage']\">\n    <template #description>\n      <div class=\"flex items-center\">\n        <VSwitch\n          :model-value=\"plugin.spec.enabled\"\n          :loading=\"changingStatus\"\n          @click=\"changeStatus\"\n        />\n      </div>\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/entity-fields/TitleField.vue",
    "content": "<script lang=\"ts\" setup>\nimport { type Plugin } from \"@halo-dev/api-client\";\nimport { VEntityField, VTag } from \"@halo-dev/components\";\nimport { pluginLabels } from \"@/constants/labels\";\n\nwithDefaults(\n  defineProps<{\n    plugin: Plugin;\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <VEntityField\n    :title=\"plugin.spec.displayName\"\n    :description=\"plugin.spec.description\"\n    :route=\"{\n      name: 'PluginDetail',\n      params: { name: plugin.metadata.name },\n    }\"\n  >\n    <template\n      v-if=\"plugin.metadata.labels?.[pluginLabels.SYSTEM_RESERVED] === 'true'\"\n      #extra\n    >\n      <VTag>\n        {{ $t(\"core.plugin.list.fields.system_reserved\") }}\n      </VTag>\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/extension-points/ExtensionDefinitionListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  coreApiClient,\n  paginate,\n  type ExtensionDefinition,\n  type Plugin,\n  type PluginV1alpha1ApiListPluginRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  IconSettings,\n  VAvatar,\n  VEntity,\n  VEntityField,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed, ref } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{ extensionDefinition: ExtensionDefinition }>(),\n  {}\n);\n\nconst { data: plugins } = useQuery<Plugin[]>({\n  queryKey: [\"extension-definition-related-plugins\"],\n  queryFn: async () => {\n    return await paginate<PluginV1alpha1ApiListPluginRequest, Plugin>(\n      (params) => coreApiClient.plugin.plugin.listPlugin(params),\n      {\n        size: 1000,\n      }\n    );\n  },\n});\n\nconst matchedPlugin = computed(() => {\n  return plugins.value?.find(\n    (plugin) =>\n      plugin.metadata.name ===\n      props.extensionDefinition.metadata.labels?.[\"plugin.halo.run/plugin-name\"]\n  );\n});\n\nconst pluginDetailModalVisible = ref(false);\n</script>\n\n<template>\n  <PluginDetailModal\n    v-if=\"pluginDetailModalVisible && matchedPlugin\"\n    :name=\"matchedPlugin.metadata.name\"\n    @close=\"pluginDetailModalVisible = false\"\n  />\n  <VEntity>\n    <template v-if=\"$slots['selection-indicator']\" #checkbox>\n      <slot name=\"selection-indicator\" />\n    </template>\n    <template #start>\n      <VEntityField>\n        <template #description>\n          <VAvatar\n            :alt=\"extensionDefinition.spec.displayName\"\n            :src=\"extensionDefinition.spec.icon || matchedPlugin?.status?.logo\"\n            size=\"md\"\n          ></VAvatar>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"extensionDefinition.spec.displayName\"\n        :description=\"extensionDefinition.spec.description\"\n      ></VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"matchedPlugin\">\n        <template #description>\n          <div\n            class=\"cursor-pointer rounded p-1 text-gray-600 transition-all hover:text-blue-600 group-hover:bg-gray-200/60\"\n            @click.prevent=\"pluginDetailModalVisible = true\"\n          >\n            <IconSettings />\n          </div>\n        </template>\n      </VEntityField>\n      <slot name=\"end\" />\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/extension-points/ExtensionDefinitionMultiInstanceView.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ExtensionPointDefinition } from \"@halo-dev/api-client\";\nimport { VEmpty, VEntityContainer, VLoading } from \"@halo-dev/components\";\nimport { toRefs } from \"vue\";\nimport { useExtensionDefinitionFetch } from \"../../composables/use-extension-definition-fetch\";\nimport ExtensionDefinitionListItem from \"./ExtensionDefinitionListItem.vue\";\n\nconst props = withDefaults(\n  defineProps<{ extensionPointDefinition?: ExtensionPointDefinition }>(),\n  { extensionPointDefinition: undefined }\n);\n\nconst { extensionPointDefinition } = toRefs(props);\n\nconst { data: extensionDefinitions, isLoading } = useExtensionDefinitionFetch(\n  extensionPointDefinition\n);\n</script>\n\n<template>\n  <div class=\"p-4\">\n    <VLoading v-if=\"isLoading\"></VLoading>\n    <Transition v-else-if=\"!extensionDefinitions?.length\" appear name=\"fade\">\n      <VEmpty\n        :title=\"\n          $t('core.plugin.extension-settings.extension-definition.empty.title')\n        \"\n      ></VEmpty>\n    </Transition>\n    <Transition v-else name=\"fade\" appear>\n      <div class=\"overflow-hidden rounded-base border\">\n        <VEntityContainer>\n          <ExtensionDefinitionListItem\n            v-for=\"item in extensionDefinitions\"\n            :key=\"item.metadata.name\"\n            :extension-definition=\"item\"\n          />\n        </VEntityContainer>\n      </div>\n    </Transition>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/extension-points/ExtensionDefinitionSingletonView.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  consoleApiClient,\n  type ExtensionPointDefinition,\n} from \"@halo-dev/api-client\";\nimport {\n  Toast,\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n} from \"@halo-dev/components\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, ref, toRefs, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useExtensionDefinitionFetch } from \"../../composables/use-extension-definition-fetch\";\nimport ExtensionDefinitionListItem from \"./ExtensionDefinitionListItem.vue\";\n\nconst EXTENSION_POINT_ENABLED_GROUP = \"extensionPointEnabled\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst Q_KEY = (name?: Ref<string | undefined>) => [\n  \"extension-point-value\",\n  name,\n];\n\nconst props = withDefaults(\n  defineProps<{ extensionPointDefinition?: ExtensionPointDefinition }>(),\n  { extensionPointDefinition: undefined }\n);\n\nconst extensionPointDefinitionName = computed(() => {\n  return extensionPointDefinition.value?.metadata.name;\n});\n\nconst { extensionPointDefinition } = toRefs(props);\n\nconst { data: extensionDefinitions, isLoading } = useExtensionDefinitionFetch(\n  extensionPointDefinition\n);\n\nconst { data: value } = useQuery({\n  queryKey: Q_KEY(extensionPointDefinitionName),\n  queryFn: async () => {\n    if (!extensionPointDefinition.value) return null;\n\n    const { data: extensionPointEnabled } =\n      await consoleApiClient.configMap.system.getSystemConfigByGroup({\n        group: EXTENSION_POINT_ENABLED_GROUP,\n      });\n\n    const extensionPointValue =\n      extensionPointEnabled?.[extensionPointDefinition.value?.metadata.name];\n\n    // check is array\n    if (Array.isArray(extensionPointValue)) {\n      return extensionPointValue[0];\n    }\n\n    return null;\n  },\n  enabled: computed(() => !!extensionPointDefinition.value),\n});\n\nconst isSubmitting = ref(false);\n\nasync function onExtensionChange(e: Event) {\n  const value = (e.target as HTMLInputElement).value;\n  await handleChange(value);\n}\n\nasync function handleChange(value: string) {\n  if (!extensionPointDefinition.value) return;\n\n  isSubmitting.value = true;\n\n  try {\n    const { data: extensionPointEnabled } =\n      await consoleApiClient.configMap.system.getSystemConfigByGroup({\n        group: EXTENSION_POINT_ENABLED_GROUP,\n      });\n\n    await consoleApiClient.configMap.system.updateSystemConfigByGroup({\n      group: EXTENSION_POINT_ENABLED_GROUP,\n      body: {\n        ...extensionPointEnabled,\n        [extensionPointDefinition.value?.metadata.name]: [value],\n      },\n    });\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n\n    queryClient.invalidateQueries({\n      queryKey: Q_KEY(extensionPointDefinitionName),\n    });\n  } catch (_) {\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  } finally {\n    isSubmitting.value = false;\n  }\n}\n</script>\n\n<template>\n  <div class=\"p-4\">\n    <VLoading v-if=\"isLoading\"></VLoading>\n    <Transition v-else-if=\"!extensionDefinitions?.length\" appear name=\"fade\">\n      <VEmpty\n        :title=\"\n          $t('core.plugin.extension-settings.extension-definition.empty.title')\n        \"\n      ></VEmpty>\n    </Transition>\n    <Transition v-else name=\"fade\" appear>\n      <div\n        class=\"overflow-hidden rounded-base border\"\n        :class=\"{ 'pointer-events-none opacity-50': isSubmitting }\"\n      >\n        <VEntityContainer>\n          <ExtensionDefinitionListItem\n            v-for=\"item in extensionDefinitions\"\n            :key=\"item.metadata.name\"\n            :extension-definition=\"item\"\n          >\n            <template #selection-indicator>\n              <input\n                :value=\"item.metadata.name\"\n                type=\"radio\"\n                name=\"activated-extension\"\n                :checked=\"item.metadata.name === value\"\n                :disabled=\"isSubmitting\"\n                @change=\"onExtensionChange\"\n              />\n            </template>\n            <template #end>\n              <VEntityField v-if=\"item.metadata.name !== value\">\n                <template #description>\n                  <VButton size=\"sm\" @click=\"handleChange(item.metadata.name)\">\n                    {{\n                      $t(\n                        \"core.plugin.extension-settings.extension-definition.operation.use.button\"\n                      )\n                    }}\n                  </VButton>\n                </template>\n              </VEntityField>\n            </template>\n          </ExtensionDefinitionListItem>\n        </VEntityContainer>\n      </div>\n    </Transition>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/installation-tabs/LocalUpload.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport type { ErrorResponse, SuccessResponse, UppyFile } from \"@uppy/core\";\nimport { computed, inject, ref, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport AppDownloadAlert from \"@/components/common/AppDownloadAlert.vue\";\nimport { PLUGIN_ALREADY_EXISTS_TYPE } from \"../../constants\";\nimport type { PluginInstallationErrorResponse } from \"../../types\";\n\nconst emit = defineEmits<{\n  (event: \"close-modal\"): void;\n}>();\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst pluginToUpgrade = inject<Ref<Plugin | undefined>>(\n  \"pluginToUpgrade\",\n  ref()\n);\n\nconst endpoint = computed(() => {\n  if (pluginToUpgrade.value) {\n    return `/apis/api.console.halo.run/v1alpha1/plugins/${pluginToUpgrade.value.metadata.name}/upgrade`;\n  }\n  return \"/apis/api.console.halo.run/v1alpha1/plugins/install\";\n});\n\nconst onUploaded = async (response: SuccessResponse) => {\n  if (pluginToUpgrade.value) {\n    Toast.success(t(\"core.common.toast.upgrade_success\"));\n    window.location.reload();\n    return;\n  }\n\n  emit(\"close-modal\");\n\n  queryClient.invalidateQueries({ queryKey: [\"plugins\"] });\n\n  handleShowActiveModalAfterInstall(response.body as Plugin);\n};\n\nconst onError = (file: UppyFile, response: ErrorResponse) => {\n  const body = response.body as PluginInstallationErrorResponse;\n\n  if (body.type === PLUGIN_ALREADY_EXISTS_TYPE) {\n    handleCatchExistsException(body, file.data as File);\n  }\n};\n\nconst handleShowActiveModalAfterInstall = (plugin: Plugin) => {\n  Dialog.success({\n    title: t(\"core.plugin.upload_modal.operations.active_after_install.title\"),\n    description: t(\n      \"core.plugin.upload_modal.operations.active_after_install.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await consoleApiClient.plugin.plugin.changePluginRunningState({\n          name: plugin.metadata.name,\n          pluginRunningStateRequest: {\n            enable: true,\n          },\n        });\n\n        window.location.reload();\n      } catch (e) {\n        console.error(e);\n      }\n    },\n  });\n};\n\nconst handleCatchExistsException = async (\n  error: PluginInstallationErrorResponse,\n  file?: File\n) => {\n  Dialog.info({\n    title: t(\n      \"core.plugin.upload_modal.operations.existed_during_installation.title\"\n    ),\n    description: t(\n      \"core.plugin.upload_modal.operations.existed_during_installation.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await consoleApiClient.plugin.plugin.upgradePlugin({\n        name: error.pluginName,\n        file: file,\n      });\n\n      Toast.success(t(\"core.common.toast.upgrade_success\"));\n\n      window.location.reload();\n    },\n  });\n};\n</script>\n\n<template>\n  <div class=\"mb-3\">\n    <AppDownloadAlert />\n  </div>\n\n  <UppyUpload\n    :restrictions=\"{\n      maxNumberOfFiles: 1,\n      allowedFileTypes: ['.jar'],\n    }\"\n    :endpoint=\"endpoint\"\n    width=\"100%\"\n    auto-proceed\n    @uploaded=\"onUploaded\"\n    @error=\"onError\"\n  />\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/installation-tabs/RemoteDownload.vue",
    "content": "<script lang=\"ts\" setup>\nimport { submitForm } from \"@formkit/core\";\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VButton } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { Ref } from \"vue\";\nimport { inject, nextTick, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { PLUGIN_ALREADY_EXISTS_TYPE } from \"../../constants\";\nimport type { PluginInstallationErrorResponse } from \"../../types\";\n\nconst emit = defineEmits<{\n  (event: \"close-modal\"): void;\n}>();\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst pluginToUpgrade = inject<Ref<Plugin | undefined>>(\n  \"pluginToUpgrade\",\n  ref()\n);\n\nconst remoteDownloadUrl = ref(\"\");\nconst downloading = ref(false);\n\nconst handleDownloadPlugin = async () => {\n  try {\n    downloading.value = true;\n    if (pluginToUpgrade.value) {\n      await consoleApiClient.plugin.plugin.upgradePluginFromUri({\n        name: pluginToUpgrade.value.metadata.name,\n        upgradeFromUriRequest: {\n          uri: remoteDownloadUrl.value,\n        },\n      });\n\n      Toast.success(t(\"core.common.toast.upgrade_success\"));\n      window.location.reload();\n      return;\n    }\n\n    const { data: plugin } =\n      await consoleApiClient.plugin.plugin.installPluginFromUri({\n        installFromUriRequest: {\n          uri: remoteDownloadUrl.value,\n        },\n      });\n\n    emit(\"close-modal\");\n    queryClient.invalidateQueries({ queryKey: [\"plugins\"] });\n\n    handleShowActiveModalAfterInstall(plugin);\n\n    // eslint-disable-next-line\n  } catch (error: any) {\n    const data = error?.response.data as PluginInstallationErrorResponse;\n    if (data?.type === PLUGIN_ALREADY_EXISTS_TYPE) {\n      handleCatchExistsException(data);\n    }\n\n    console.error(\"Failed to download plugin\", error);\n  } finally {\n    routeRemoteDownloadUrl.value = null;\n    downloading.value = false;\n  }\n};\n\nconst handleShowActiveModalAfterInstall = (plugin: Plugin) => {\n  Dialog.success({\n    title: t(\"core.plugin.upload_modal.operations.active_after_install.title\"),\n    description: t(\n      \"core.plugin.upload_modal.operations.active_after_install.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await consoleApiClient.plugin.plugin.changePluginRunningState({\n          name: plugin.metadata.name,\n          pluginRunningStateRequest: {\n            enable: true,\n          },\n        });\n\n        window.location.reload();\n      } catch (e) {\n        console.error(e);\n      }\n    },\n  });\n};\n\nconst handleCatchExistsException = async (\n  error: PluginInstallationErrorResponse\n) => {\n  Dialog.info({\n    title: t(\n      \"core.plugin.upload_modal.operations.existed_during_installation.title\"\n    ),\n    description: t(\n      \"core.plugin.upload_modal.operations.existed_during_installation.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      await consoleApiClient.plugin.plugin.upgradePluginFromUri({\n        name: error.pluginName,\n        upgradeFromUriRequest: {\n          uri: remoteDownloadUrl.value,\n        },\n      });\n\n      Toast.success(t(\"core.common.toast.upgrade_success\"));\n\n      window.location.reload();\n    },\n  });\n};\n\n// handle remote download url from route\nconst routeRemoteDownloadUrl = useRouteQuery<string | null>(\n  \"remote-download-url\"\n);\n\nonMounted(() => {\n  if (routeRemoteDownloadUrl.value) {\n    remoteDownloadUrl.value = routeRemoteDownloadUrl.value;\n    nextTick(() => {\n      submitForm(\"plugin-remote-download-form\");\n    });\n  }\n});\n</script>\n\n<template>\n  <FormKit\n    id=\"plugin-remote-download-form\"\n    name=\"plugin-remote-download-form\"\n    type=\"form\"\n    :preserve=\"true\"\n    @submit=\"handleDownloadPlugin\"\n  >\n    <FormKit\n      v-model=\"remoteDownloadUrl\"\n      :label=\"$t('core.plugin.upload_modal.tabs.remote.fields.url')\"\n      type=\"text\"\n      validation=\"required\"\n    ></FormKit>\n  </FormKit>\n\n  <div class=\"pt-5\">\n    <VButton\n      :loading=\"downloading\"\n      type=\"secondary\"\n      @click=\"$formkit.submit('plugin-remote-download-form')\"\n    >\n      {{ $t(\"core.common.buttons.download\") }}\n    </VButton>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/tabs/Detail.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  PluginStatusPhaseEnum,\n  coreApiClient,\n  paginate,\n  type Plugin,\n  type Role,\n  type RoleV1alpha1ApiListRoleRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  VAlert,\n  VButton,\n  VDescription,\n  VDescriptionItem,\n  VSpace,\n  VSwitch,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useClipboard } from \"@vueuse/core\";\nimport type { Ref } from \"vue\";\nimport { computed, inject, ref } from \"vue\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { pluginLabels, roleLabels } from \"@/constants/labels\";\nimport { usePluginLifeCycle } from \"../../composables/use-plugin\";\nimport PluginConditionsModal from \"../PluginConditionsModal.vue\";\n\nconst plugin = inject<Ref<Plugin | undefined>>(\"plugin\");\nconst { changeStatus, changingStatus } = usePluginLifeCycle(plugin);\n\ninterface RoleTemplateGroup {\n  module: string | null | undefined;\n  roles: Role[];\n}\n\nconst { data: pluginRoleTemplates } = useQuery({\n  queryKey: [\"plugin-roles\", plugin?.value?.metadata.name],\n  queryFn: async () => {\n    return await paginate<RoleV1alpha1ApiListRoleRequest, Role>(\n      (params) => coreApiClient.role.listRole(params),\n      {\n        size: 1000,\n        labelSelector: [\n          `${pluginLabels.NAME}=${plugin?.value?.metadata.name}`,\n          `${roleLabels.TEMPLATE}=true`,\n          \"!halo.run/hidden\",\n        ],\n      }\n    );\n  },\n  cacheTime: 0,\n  enabled: computed(\n    () =>\n      !!plugin?.value?.metadata.name &&\n      utils.permission.has([\"system:roles:view\"])\n  ),\n});\n\nconst pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {\n  const groups: RoleTemplateGroup[] = [];\n  pluginRoleTemplates.value?.forEach((role) => {\n    const group = groups.find(\n      (group) =>\n        group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]\n    );\n    if (group) {\n      group.roles.push(role);\n    } else {\n      groups.push({\n        module: role.metadata.annotations?.[rbacAnnotations.MODULE],\n        roles: [role],\n      });\n    }\n  });\n  return groups;\n});\n\n// Error alert\nconst conditionsModalVisible = ref(false);\n\nconst errorAlertVisible = computed(() => {\n  const { phase } = plugin?.value?.status || {};\n\n  return (\n    phase !== PluginStatusPhaseEnum.Started &&\n    phase !== PluginStatusPhaseEnum.Disabled\n  );\n});\n\nconst lastCondition = computed(() => {\n  return plugin?.value?.status?.conditions?.[0];\n});\n\nconst { copy, copied } = useClipboard({\n  legacy: true,\n});\n</script>\n\n<template>\n  <PluginConditionsModal\n    v-if=\"conditionsModalVisible && plugin\"\n    :plugin=\"plugin\"\n    @close=\"conditionsModalVisible = false\"\n  />\n  <div class=\"overflow-hidden rounded-b-base\">\n    <div class=\"flex items-center justify-between bg-white px-4 py-4 sm:px-6\">\n      <div>\n        <h3 class=\"text-lg font-medium leading-6 text-gray-900\">\n          {{ $t(\"core.plugin.detail.header.title\") }}\n        </h3>\n      </div>\n      <div v-permission=\"['system:plugins:manage']\">\n        <VSwitch\n          :loading=\"changingStatus\"\n          :model-value=\"plugin?.spec.enabled\"\n          @change=\"changeStatus\"\n        />\n      </div>\n    </div>\n    <div\n      v-if=\"errorAlertVisible && lastCondition\"\n      class=\"w-full px-4 pb-2 sm:px-6\"\n    >\n      <VAlert type=\"error\" :title=\"lastCondition.reason\" :closable=\"false\">\n        <template #description>\n          <div class=\"overflow-x-auto\">\n            <pre>{{ lastCondition.message }}</pre>\n          </div>\n        </template>\n        <template #actions>\n          <VSpace>\n            <VButton size=\"sm\" @click=\"conditionsModalVisible = true\">\n              {{ $t(\"core.plugin.detail.operations.view_conditions.button\") }}\n            </VButton>\n            <VButton size=\"sm\" @click=\"copy(lastCondition.message || '')\">\n              {{\n                copied\n                  ? $t(\n                      \"core.plugin.detail.operations.copy_error_message.copied\"\n                    )\n                  : $t(\n                      \"core.plugin.detail.operations.copy_error_message.button\"\n                    )\n              }}\n            </VButton>\n          </VSpace>\n        </template>\n      </VAlert>\n    </div>\n    <div class=\"border-t border-gray-200\">\n      <VDescription>\n        <VDescriptionItem label=\"ID\" :content=\"plugin?.metadata.name\" />\n        <VDescriptionItem\n          :label=\"$t('core.plugin.detail.fields.description')\"\n          :content=\"plugin?.spec.description || $t('core.common.text.none')\"\n        />\n        <VDescriptionItem :label=\"$t('core.plugin.detail.fields.author')\">\n          <a\n            v-if=\"plugin?.spec.author\"\n            :href=\"plugin?.spec.author.website\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ plugin?.spec.author.name }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.plugin.detail.fields.version')\"\n          :content=\"plugin?.spec.version\"\n        />\n        <VDescriptionItem\n          :label=\"$t('core.plugin.detail.fields.requires')\"\n          :content=\"plugin?.spec.requires\"\n        />\n        <VDescriptionItem :label=\"$t('core.plugin.detail.fields.homepage')\">\n          <a\n            v-if=\"plugin?.spec.homepage\"\n            :href=\"plugin?.spec.homepage\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ plugin?.spec.homepage }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem :label=\"$t('core.plugin.detail.fields.repo')\">\n          <a\n            v-if=\"plugin?.spec.repo\"\n            :href=\"plugin.spec.repo\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ plugin.spec.repo }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem :label=\"$t('core.plugin.detail.fields.issues')\">\n          <a\n            v-if=\"plugin?.spec.issues\"\n            :href=\"plugin.spec.issues\"\n            class=\"hover:text-gray-600\"\n            target=\"_blank\"\n          >\n            {{ plugin.spec.issues }}\n          </a>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem :label=\"$t('core.plugin.detail.fields.license')\">\n          <ul\n            v-if=\"plugin?.spec.license && plugin?.spec.license.length\"\n            class=\"list-inside\"\n            :class=\"{ 'list-disc': plugin?.spec.license.length > 1 }\"\n          >\n            <li v-for=\"(license, index) in plugin.spec.license\" :key=\"index\">\n              <a v-if=\"license.url\" :href=\"license.url\" target=\"_blank\">\n                {{ license.name }}\n              </a>\n              <span v-else>\n                {{ license.name }}\n              </span>\n            </li>\n          </ul>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.plugin.detail.fields.role_templates')\"\n        >\n          <dl\n            v-if=\"pluginRoleTemplateGroups.length\"\n            class=\"divide-y divide-gray-100\"\n          >\n            <div\n              v-for=\"(group, groupIndex) in pluginRoleTemplateGroups\"\n              :key=\"groupIndex\"\n              class=\"rounded bg-gray-50 px-4 py-5 hover:bg-gray-100 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6\"\n            >\n              <dt class=\"text-sm font-medium text-gray-900\">\n                {{ group.module }}\n              </dt>\n              <dd class=\"mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0\">\n                <ul class=\"space-y-2\">\n                  <li v-for=\"(role, index) in group.roles\" :key=\"index\">\n                    <div\n                      class=\"inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-primary\"\n                    >\n                      <div class=\"inline-flex flex-col gap-y-3\">\n                        <span class=\"font-medium text-gray-900\">\n                          {{\n                            role.metadata.annotations?.[\n                              rbacAnnotations.DISPLAY_NAME\n                            ]\n                          }}\n                        </span>\n                        <span\n                          v-if=\"\n                            role.metadata.annotations?.[\n                              rbacAnnotations.DEPENDENCIES\n                            ]\n                          \"\n                          class=\"text-xs text-gray-400\"\n                        >\n                          {{\n                            $t(\"core.role.common.text.dependent_on\", {\n                              roles: JSON.parse(\n                                role.metadata.annotations?.[\n                                  rbacAnnotations.DEPENDENCIES\n                                ]\n                              ).join(\", \"),\n                            })\n                          }}\n                        </span>\n                      </div>\n                    </div>\n                  </li>\n                </ul>\n              </dd>\n            </div>\n          </dl>\n          <span v-else>\n            {{ $t(\"core.common.text.none\") }}\n          </span>\n        </VDescriptionItem>\n        <VDescriptionItem\n          :label=\"$t('core.plugin.detail.fields.creation_time')\"\n          :content=\"utils.date.format(plugin?.metadata.creationTimestamp)\"\n        />\n        <VDescriptionItem\n          :label=\"$t('core.plugin.detail.fields.last_starttime')\"\n          :content=\"utils.date.format(plugin?.status?.lastStartTime)\"\n        />\n        <VDescriptionItem\n          :label=\"$t('core.plugin.detail.fields.load_location')\"\n          :content=\"plugin?.status?.loadLocation\"\n        ></VDescriptionItem>\n      </VDescription>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/components/tabs/Setting.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitSchemaCondition, FormKitSchemaNode } from \"@formkit/core\";\nimport type { Plugin, Setting } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton } from \"@halo-dev/components\";\nimport { events } from \"@halo-dev/ui-shared\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { set } from \"es-toolkit/compat\";\nimport { computed, inject, ref, toRaw, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport BackToTop from \"@/components/back-to-top/BackToTop.vue\";\nimport StickyBlock from \"@/components/sticky-block/StickyBlock.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst group = inject<Ref<string>>(\"activeTab\", ref(\"basic\"));\nconst plugin = inject<Ref<Plugin | undefined>>(\"plugin\");\nconst setting = inject<Ref<Setting | undefined>>(\"setting\", ref());\nconst saving = ref(false);\n\nconst { data: configMapData } = useQuery({\n  queryKey: [\"core:plugin:configMap:data\", plugin],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.plugin.plugin.fetchPluginJsonConfig(\n      {\n        name: plugin?.value?.metadata.name as string,\n      }\n    );\n    return data;\n  },\n  enabled: computed(() => {\n    return !!setting.value && !!plugin?.value;\n  }),\n});\n\nconst currentConfigMapGroupData = computed(() => {\n  return configMapData.value?.[group.value];\n});\n\nconst formSchema = computed(() => {\n  if (!setting.value) {\n    return;\n  }\n  const { forms } = setting.value.spec;\n  return forms.find((item) => item.group === group?.value)?.formSchema as (\n    | FormKitSchemaCondition\n    | FormKitSchemaNode\n  )[];\n});\n\nconst handleSaveConfigMap = async (data: object) => {\n  saving.value = true;\n\n  if (!plugin?.value) {\n    saving.value = false;\n    return;\n  }\n\n  await consoleApiClient.plugin.plugin.updatePluginJsonConfig({\n    name: plugin.value.metadata.name,\n    body: set(cloneDeep(configMapData.value) || {}, group.value, data),\n  });\n\n  Toast.success(t(\"core.common.toast.save_success\"));\n\n  queryClient.invalidateQueries({ queryKey: [\"core:plugin:configMap:data\"] });\n\n  saving.value = false;\n\n  // Push a custom event to notify other components to refresh data\n  events.emit(\"core:plugin:configMap:updated\", {\n    pluginName: plugin.value.metadata.name,\n    group: group.value,\n  });\n};\n</script>\n<template>\n  <div class=\"rounded-b-base bg-white p-4\">\n    <div>\n      <FormKit\n        v-if=\"group && formSchema && currentConfigMapGroupData\"\n        :id=\"group\"\n        :value=\"currentConfigMapGroupData\"\n        :name=\"group\"\n        :preserve=\"true\"\n        type=\"form\"\n        @submit=\"handleSaveConfigMap\"\n      >\n        <FormKitSchema\n          :schema=\"toRaw(formSchema)\"\n          :data=\"toRaw(currentConfigMapGroupData)\"\n        />\n      </FormKit>\n    </div>\n\n    <StickyBlock\n      v-permission=\"['system:plugins:manage']\"\n      class=\"-mx-4 -mb-4 rounded-b-base rounded-t-lg bg-white p-4 pt-5\"\n      position=\"bottom\"\n    >\n      <VButton\n        :loading=\"saving\"\n        type=\"secondary\"\n        @click=\"$formkit.submit(group || '')\"\n      >\n        {{ $t(\"core.common.buttons.save\") }}\n      </VButton>\n    </StickyBlock>\n\n    <BackToTop />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/composables/use-extension-definition-fetch.ts",
    "content": "import type {\n  ExtensionDefinition,\n  ExtensionDefinitionV1alpha1ApiListExtensionDefinitionRequest,\n  ExtensionPointDefinition,\n} from \"@halo-dev/api-client\";\nimport { coreApiClient, paginate } from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed, type Ref } from \"vue\";\n\nexport function useExtensionDefinitionFetch(\n  extensionPointDefinition: Ref<ExtensionPointDefinition | undefined>\n) {\n  return useQuery({\n    queryKey: [\"extension-definitions\", extensionPointDefinition],\n    queryFn: async () => {\n      return await paginate<\n        ExtensionDefinitionV1alpha1ApiListExtensionDefinitionRequest,\n        ExtensionDefinition\n      >(\n        (params) =>\n          coreApiClient.plugin.extensionDefinition.listExtensionDefinition(\n            params\n          ),\n        {\n          size: 1000,\n          fieldSelector: [\n            `spec.extensionPointName=${extensionPointDefinition.value?.metadata.name}`,\n          ],\n        }\n      );\n    },\n    enabled: computed(() => !!extensionPointDefinition.value),\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/composables/use-plugin.ts",
    "content": "import {\n  PluginStatusPhaseEnum,\n  consoleApiClient,\n  coreApiClient,\n  type Plugin,\n  type SettingForm,\n} from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VLoading } from \"@halo-dev/components\";\nimport { utils, type PluginTab } from \"@halo-dev/ui-shared\";\nimport { useMutation, useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { ComputedRef, Ref } from \"vue\";\nimport { computed, defineAsyncComponent, ref, shallowRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\ninterface usePluginLifeCycleReturn {\n  isStarted: ComputedRef<boolean | undefined>;\n  getStatusDotState: () => string;\n  getStatusMessage: () => string | undefined;\n  changeStatus: () => void;\n  changingStatus: Ref<boolean>;\n  uninstall: (deleteExtensions?: boolean) => void;\n}\n\nexport function usePluginLifeCycle(\n  plugin?: Ref<Plugin | undefined>\n): usePluginLifeCycleReturn {\n  const { t } = useI18n();\n\n  const isStarted = computed(() => {\n    return (\n      plugin?.value?.status?.phase === PluginStatusPhaseEnum.Started &&\n      plugin.value?.spec.enabled\n    );\n  });\n\n  const getStatusDotState = () => {\n    const { phase } = plugin?.value?.status || {};\n    const { enabled } = plugin?.value?.spec || {};\n\n    if (enabled && phase === PluginStatusPhaseEnum.Failed) {\n      return \"error\";\n    }\n\n    if (phase === PluginStatusPhaseEnum.Disabling) {\n      return \"warning\";\n    }\n\n    return \"default\";\n  };\n\n  const getStatusMessage = () => {\n    if (!plugin?.value) return;\n\n    const { phase } = plugin.value.status || {};\n\n    if (\n      phase === PluginStatusPhaseEnum.Failed ||\n      phase === PluginStatusPhaseEnum.Disabling\n    ) {\n      const lastCondition = plugin.value.status?.conditions?.[0];\n\n      return (\n        [lastCondition?.reason, lastCondition?.message]\n          .filter(Boolean)\n          .join(\": \") || \"Unknown\"\n      );\n    }\n\n    // Starting up\n    if (\n      phase !== (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)\n    ) {\n      return t(\"core.common.status.starting_up\");\n    }\n  };\n\n  const { isLoading: changingStatus, mutate: changeStatus } = useMutation({\n    mutationKey: [\"change-plugin-status\"],\n    mutationFn: async () => {\n      if (!plugin?.value) return;\n\n      const { enabled } = plugin.value.spec;\n\n      return await consoleApiClient.plugin.plugin.changePluginRunningState({\n        name: plugin.value.metadata.name,\n        pluginRunningStateRequest: {\n          enable: !enabled,\n        },\n      });\n    },\n    retry: 3,\n    retryDelay: 1000,\n    onSuccess() {\n      window.location.reload();\n    },\n  });\n\n  const uninstall = (deleteExtensions?: boolean) => {\n    if (!plugin?.value) return;\n\n    const { enabled } = plugin.value.spec;\n\n    Dialog.warning({\n      title: `${\n        deleteExtensions\n          ? t(\"core.plugin.operations.uninstall_and_delete_config.title\")\n          : t(\"core.plugin.operations.uninstall.title\")\n      }`,\n      description: `${\n        enabled\n          ? t(\"core.plugin.operations.uninstall_when_enabled.description\")\n          : t(\"core.common.dialog.descriptions.cannot_be_recovered\")\n      }`,\n      confirmType: \"danger\",\n      confirmText: `${\n        enabled\n          ? t(\"core.plugin.operations.uninstall_when_enabled.confirm_text\")\n          : t(\"core.common.buttons.uninstall\")\n      }`,\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        if (!plugin.value) return;\n\n        try {\n          await consoleApiClient.plugin.plugin.changePluginRunningState({\n            name: plugin.value.metadata.name,\n            pluginRunningStateRequest: {\n              enable: false,\n            },\n          });\n\n          await coreApiClient.plugin.plugin.deletePlugin({\n            name: plugin.value.metadata.name,\n          });\n\n          // delete plugin setting and configMap\n          if (deleteExtensions) {\n            const { settingName, configMapName } = plugin.value.spec;\n\n            if (settingName) {\n              await coreApiClient.setting.deleteSetting(\n                {\n                  name: settingName,\n                },\n                {\n                  mute: true,\n                }\n              );\n            }\n\n            if (configMapName) {\n              await coreApiClient.configMap.deleteConfigMap(\n                {\n                  name: configMapName,\n                },\n                {\n                  mute: true,\n                }\n              );\n            }\n          }\n\n          Toast.success(t(\"core.common.toast.uninstall_success\"));\n        } catch (e) {\n          console.error(\"Failed to uninstall plugin\", e);\n        } finally {\n          window.location.reload();\n        }\n      },\n    });\n  };\n\n  return {\n    isStarted,\n    getStatusDotState,\n    getStatusMessage,\n    changeStatus,\n    changingStatus,\n    uninstall,\n  };\n}\n\nexport function usePluginBatchOperations(names: Ref<string[]>) {\n  const { t } = useI18n();\n\n  function handleUninstallInBatch(deleteExtensions: boolean) {\n    Dialog.warning({\n      title: `${\n        deleteExtensions\n          ? t(\n              \"core.plugin.operations.uninstall_and_delete_config_in_batch.title\"\n            )\n          : t(\"core.plugin.operations.uninstall_in_batch.title\")\n      }`,\n      description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n      confirmType: \"danger\",\n      confirmText: t(\"core.common.buttons.uninstall\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        try {\n          for (let i = 0; i < names.value.length; i++) {\n            await coreApiClient.plugin.plugin.deletePlugin({\n              name: names.value[i],\n            });\n\n            if (deleteExtensions) {\n              const { data: plugin } =\n                await coreApiClient.plugin.plugin.getPlugin({\n                  name: names.value[i],\n                });\n\n              const { settingName, configMapName } = plugin.spec;\n\n              if (settingName) {\n                await coreApiClient.setting.deleteSetting(\n                  {\n                    name: settingName,\n                  },\n                  {\n                    mute: true,\n                  }\n                );\n              }\n\n              if (configMapName) {\n                await coreApiClient.configMap.deleteConfigMap(\n                  {\n                    name: configMapName,\n                  },\n                  {\n                    mute: true,\n                  }\n                );\n              }\n            }\n          }\n\n          window.location.reload();\n        } catch (e) {\n          console.error(\"Failed to uninstall plugin in batch\", e);\n        }\n      },\n    });\n  }\n\n  function handleChangeStatusInBatch(enabled: boolean) {\n    Dialog.info({\n      title: enabled\n        ? t(\"core.plugin.operations.change_status_in_batch.activate_title\")\n        : t(\"core.plugin.operations.change_status_in_batch.inactivate_title\"),\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        try {\n          for (let i = 0; i < names.value.length; i++) {\n            await consoleApiClient.plugin.plugin.changePluginRunningState({\n              name: names.value[i],\n              pluginRunningStateRequest: {\n                enable: enabled,\n              },\n            });\n          }\n\n          window.location.reload();\n        } catch (e) {\n          console.error(\"Failed to change plugin status in batch\", e);\n        }\n      },\n    });\n  }\n\n  return { handleUninstallInBatch, handleChangeStatusInBatch };\n}\n\nexport function usePluginDetailTabs(\n  pluginName: Ref<string | undefined>,\n  recordsActiveTab: boolean\n) {\n  const { t } = useI18n();\n\n  const initialTabs = [\n    {\n      id: \"detail\",\n      label: t(\"core.plugin.tabs.detail\"),\n      component: defineAsyncComponent({\n        loader: () => import(\"../components/tabs/Detail.vue\"),\n        loadingComponent: VLoading,\n      }),\n    },\n  ];\n\n  const tabs = shallowRef<PluginTab[]>(initialTabs);\n  const activeTab = recordsActiveTab\n    ? useRouteQuery<string>(\"tab\", tabs.value[0].id)\n    : ref(tabs.value[0].id);\n\n  const { data: plugin } = useQuery({\n    queryKey: [\"plugin\", pluginName],\n    queryFn: async () => {\n      const { data } = await coreApiClient.plugin.plugin.getPlugin({\n        name: pluginName.value as string,\n      });\n      return data;\n    },\n    async onSuccess(data) {\n      if (\n        !data.spec.settingName ||\n        !utils.permission.has([\"system:plugins:manage\"])\n      ) {\n        tabs.value = [...initialTabs, ...(await getTabsFromExtensions())];\n      }\n    },\n  });\n\n  const { data: setting } = useQuery({\n    queryKey: [\"plugin-setting\", plugin],\n    queryFn: async () => {\n      const { data } = await consoleApiClient.plugin.plugin.fetchPluginSetting({\n        name: plugin.value?.metadata.name as string,\n      });\n      return data;\n    },\n    enabled: computed(() => {\n      return (\n        !!plugin.value &&\n        !!plugin.value.spec.settingName &&\n        utils.permission.has([\"system:plugins:manage\"])\n      );\n    }),\n    async onSuccess(data) {\n      if (data) {\n        const { forms } = data.spec;\n        tabs.value = [\n          ...initialTabs,\n          ...(await getTabsFromExtensions()),\n          ...forms.map((item: SettingForm) => {\n            return {\n              id: item.group,\n              label: item.label || \"\",\n              component: defineAsyncComponent({\n                loader: () => import(\"../components/tabs/Setting.vue\"),\n                loadingComponent: VLoading,\n              }),\n            };\n          }),\n        ] as PluginTab[];\n      }\n    },\n  });\n\n  async function getTabsFromExtensions() {\n    const { pluginModuleMap } = usePluginModuleStore();\n\n    const currentPluginModule = pluginModuleMap[pluginName.value as string];\n\n    if (!currentPluginModule) {\n      return [];\n    }\n\n    const callbackFunction =\n      currentPluginModule?.extensionPoints?.[\"plugin:self:tabs:create\"];\n\n    if (typeof callbackFunction !== \"function\") {\n      return [];\n    }\n\n    const pluginTabs = await callbackFunction();\n\n    return pluginTabs.filter((tab) => {\n      return utils.permission.has(tab.permissions || []);\n    });\n  }\n\n  return {\n    plugin,\n    setting,\n    tabs,\n    activeTab,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/constants/index.ts",
    "content": "export const PLUGIN_ALREADY_EXISTS_TYPE =\n  \"https://halo.run/probs/plugin-alreay-exists\";\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconPlug } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport type { RouteRecordRaw } from \"vue-router\";\nimport PluginDetailModal from \"./components/PluginDetailModal.vue\";\n\ndeclare module \"vue\" {\n  interface GlobalComponents {\n    PluginDetailModal: (typeof import(\"./components/PluginDetailModal.vue\"))[\"default\"];\n  }\n}\n\nexport default definePlugin({\n  components: {\n    PluginDetailModal,\n  },\n  routes: [\n    {\n      path: \"/plugins\",\n      name: \"PluginsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.plugin.title\",\n        searchable: true,\n        permissions: [\"system:plugins:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.plugins\",\n          group: \"system\",\n          icon: markRaw(IconPlug),\n          priority: 0,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Plugins\",\n          component: () => import(\"./PluginList.vue\"),\n        },\n        {\n          path: \"extension-point-settings\",\n          name: \"PluginExtensionPointSettings\",\n          component: () => import(\"./PluginExtensionPointSettings.vue\"),\n          meta: {\n            title: \"core.plugin.extension-settings.title\",\n            hideFooter: true,\n            permissions: [\"*\"],\n          },\n        },\n        {\n          path: \":name\",\n          name: \"PluginDetail\",\n          component: () => import(\"./PluginDetail.vue\"),\n          meta: {\n            title: \"core.plugin.detail.title\",\n            permissions: [\"system:plugins:view\"],\n          },\n        },\n      ],\n    } as RouteRecordRaw,\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/plugins/types/index.ts",
    "content": "export interface PluginInstallationErrorResponse {\n  detail: string;\n  instance: string;\n  pluginName: string;\n  requestId: string;\n  status: number;\n  timestamp: string;\n  title: string;\n  type: string;\n}\n"
  },
  {
    "path": "ui/console-src/modules/system/roles/RoleDetail.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Role } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconShieldUser,\n  VAlert,\n  VButton,\n  VCard,\n  VDescription,\n  VDescriptionItem,\n  VPageHeader,\n  VTabbar,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRoute } from \"vue-router\";\nimport { useRoleForm, useRoleTemplateSelection } from \"@/composables/use-role\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { SUPER_ROLE_NAME } from \"@/constants/constants\";\nimport { pluginLabels, roleLabels } from \"@/constants/labels\";\nimport { resolveDeepDependencies } from \"@/utils/role\";\nimport { useFetchRoleTemplates } from \"../users/composables/use-role\";\n\nconst route = useRoute();\nconst { t } = useI18n();\n\nconst tabActiveId = ref(\"detail\");\n\nconst { data: roleTemplates } = useFetchRoleTemplates();\n\nconst { roleTemplateGroups, handleRoleTemplateSelect, selectedRoleTemplates } =\n  useRoleTemplateSelection(roleTemplates);\n\nconst { formState, isSubmitting, handleCreateOrUpdate } = useRoleForm();\n\nconst isSystemReserved = computed(() => {\n  return (\n    formState.value.metadata.labels?.[roleLabels.SYSTEM_RESERVED] === \"true\"\n  );\n});\n\nconst isSuperRole = computed(() => {\n  return formState.value.metadata.name === SUPER_ROLE_NAME;\n});\n\nconst getRoleCountText = computed(() => {\n  if (formState.value.metadata.name === SUPER_ROLE_NAME) {\n    return t(\"core.role.common.text.contains_all_permissions\");\n  }\n\n  const dependencies = new Set<string>(\n    resolveDeepDependencies(formState.value, roleTemplates.value || [])\n  );\n\n  return t(\"core.role.common.text.contains_n_permissions\", {\n    count: dependencies.size || 0,\n  });\n});\n\nwatch(\n  () => selectedRoleTemplates.value,\n  (newValue) => {\n    if (formState.value.metadata.annotations) {\n      formState.value.metadata.annotations[rbacAnnotations.DEPENDENCIES] =\n        JSON.stringify(Array.from(newValue));\n    }\n  }\n);\n\nconst { refetch } = useQuery<Role>({\n  queryKey: [\"role\", route.params.name],\n  queryFn: async () => {\n    const { data } = await coreApiClient.role.getRole({\n      name: route.params.name as string,\n    });\n    return data;\n  },\n  onSuccess(data) {\n    formState.value = data;\n    selectedRoleTemplates.value = new Set<string>(\n      resolveDeepDependencies(data, roleTemplates.value || [])\n    );\n  },\n  enabled: computed(() => !!roleTemplates.value),\n});\n\nconst handleUpdateRole = async () => {\n  await handleCreateOrUpdate();\n  await refetch();\n};\n</script>\n<template>\n  <VPageHeader :title=\"$t('core.role.detail.title')\">\n    <template #icon>\n      <IconShieldUser />\n    </template>\n  </VPageHeader>\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <VTabbar\n          v-model:active-id=\"tabActiveId\"\n          :items=\"[\n            { id: 'detail', label: $t('core.role.detail.tabs.detail') },\n            {\n              id: 'permissions',\n              label: $t('core.role.detail.tabs.permissions'),\n            },\n          ]\"\n          class=\"w-full !rounded-none\"\n          type=\"outline\"\n        ></VTabbar>\n      </template>\n      <div v-if=\"tabActiveId === 'detail'\">\n        <div class=\"px-4 py-4 sm:px-6\">\n          <h3 class=\"text-lg font-medium leading-6 text-gray-900\">\n            {{ $t(\"core.role.detail.header.title\") }}\n          </h3>\n          <p\n            class=\"mt-1 flex max-w-2xl items-center gap-2 text-sm text-gray-500\"\n          >\n            <span>{{ getRoleCountText }}</span>\n          </p>\n        </div>\n        <div class=\"border-t border-gray-200\">\n          <VDescription>\n            <VDescriptionItem\n              :label=\"$t('core.role.detail.fields.display_name')\"\n            >\n              {{\n                formState.metadata?.annotations?.[\n                  rbacAnnotations.DISPLAY_NAME\n                ] || formState.metadata?.name\n              }}\n            </VDescriptionItem>\n            <VDescriptionItem\n              :label=\"$t('core.role.detail.fields.name')\"\n              :content=\"formState.metadata?.name\"\n            />\n            <VDescriptionItem :label=\"$t('core.role.detail.fields.type')\">\n              <VTag>\n                {{\n                  isSystemReserved\n                    ? t(\"core.role.common.text.system_reserved\")\n                    : t(\"core.role.common.text.custom\")\n                }}\n              </VTag>\n            </VDescriptionItem>\n            <VDescriptionItem\n              :label=\"$t('core.role.detail.fields.creation_time')\"\n              :content=\"utils.date.format(formState.metadata.creationTimestamp)\"\n            />\n          </VDescription>\n        </div>\n      </div>\n\n      <div v-if=\"tabActiveId === 'permissions'\">\n        <div v-if=\"isSystemReserved\" class=\"px-4 py-5\">\n          <VAlert\n            :title=\"$t('core.common.text.tip')\"\n            :description=\"\n              $t(\n                'core.role.permissions_detail.system_reserved_alert.description'\n              )\n            \"\n            class=\"w-full sm:w-1/4\"\n            :closable=\"false\"\n          />\n        </div>\n\n        <div>\n          <dl class=\"divide-y divide-gray-100\">\n            <div\n              v-for=\"(group, index) in roleTemplateGroups\"\n              :key=\"index\"\n              class=\"bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6\"\n            >\n              <dt class=\"text-sm font-medium text-gray-900\">\n                <div>\n                  {{ $t(`core.rbac.${group.module}`, group.module as string) }}\n                </div>\n                <div\n                  v-if=\"\n                    group.roles.length &&\n                    group.roles[0].metadata.labels?.[pluginLabels.NAME]\n                  \"\n                  class=\"mt-3 text-xs text-gray-500\"\n                >\n                  <i18n-t\n                    keypath=\"core.role.common.text.provided_by_plugin\"\n                    tag=\"div\"\n                  >\n                    <template #plugin>\n                      <RouterLink\n                        :to=\"{\n                          name: 'PluginDetail',\n                          params: {\n                            name: group.roles[0].metadata.labels?.[\n                              pluginLabels.NAME\n                            ],\n                          },\n                        }\"\n                        class=\"hover:text-blue-600\"\n                      >\n                        {{\n                          group.roles[0].metadata.labels?.[pluginLabels.NAME]\n                        }}\n                      </RouterLink>\n                    </template>\n                  </i18n-t>\n                </div>\n              </dt>\n              <dd class=\"mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0\">\n                <ul class=\"space-y-2\">\n                  <li v-for=\"role in group.roles\" :key=\"role.metadata.name\">\n                    <label\n                      class=\"inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary\"\n                    >\n                      <input\n                        v-if=\"!isSuperRole\"\n                        v-model=\"selectedRoleTemplates\"\n                        :value=\"role.metadata.name\"\n                        type=\"checkbox\"\n                        :disabled=\"isSystemReserved\"\n                        @change=\"handleRoleTemplateSelect\"\n                      />\n                      <input v-else type=\"checkbox\" checked disabled />\n                      <div class=\"flex flex-1 flex-col gap-y-3\">\n                        <span class=\"font-medium text-gray-900\">\n                          {{\n                            $t(\n                              `core.rbac.${\n                                role.metadata.annotations?.[\n                                  rbacAnnotations.DISPLAY_NAME\n                                ]\n                              }`,\n                              role.metadata.annotations?.[\n                                rbacAnnotations.DISPLAY_NAME\n                              ] as string\n                            )\n                          }}\n                        </span>\n                        <span\n                          v-if=\"\n                            role.metadata.annotations?.[\n                              rbacAnnotations.DEPENDENCIES\n                            ]\n                          \"\n                          class=\"text-xs text-gray-400\"\n                        >\n                          {{\n                            $t(\"core.role.common.text.dependent_on\", {\n                              roles: JSON.parse(\n                                role.metadata.annotations?.[\n                                  rbacAnnotations.DEPENDENCIES\n                                ]\n                              )\n                                .map((item: string) =>\n                                  $t(`core.rbac.${item}`, item as string)\n                                )\n                                .join(\"，\"),\n                            })\n                          }}\n                        </span>\n                      </div>\n                    </label>\n                  </li>\n                </ul>\n              </dd>\n            </div>\n          </dl>\n          <div v-permission=\"['system:roles:manage']\" class=\"p-4\">\n            <VButton\n              :loading=\"isSubmitting\"\n              type=\"secondary\"\n              :disabled=\"isSystemReserved\"\n              @click=\"handleUpdateRole\"\n            >\n              {{ $t(\"core.common.buttons.save\") }}\n            </VButton>\n          </div>\n        </div>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/roles/RoleList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  Role,\n  RoleV1alpha1ApiListRoleRequest,\n} from \"@halo-dev/api-client\";\nimport { coreApiClient, paginate } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconShieldUser,\n  Toast,\n  VButton,\n  VCard,\n  VDropdownItem,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VLoading,\n  VPageHeader,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport Fuse from \"fuse.js\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { SUPER_ROLE_NAME } from \"@/constants/constants\";\nimport { roleLabels } from \"@/constants/labels\";\nimport { resolveDeepDependencies } from \"@/utils/role\";\nimport { useFetchRoleTemplates } from \"../users/composables/use-role\";\nimport RoleEditingModal from \"./components/RoleEditingModal.vue\";\n\nconst { t } = useI18n();\n\nconst editingModal = ref<boolean>(false);\nconst selectedRole = ref<Role>();\n\nlet fuse: Fuse<Role> | undefined = undefined;\n\nconst { data: roleTemplates } = useFetchRoleTemplates();\n\nconst {\n  data: roles,\n  isLoading,\n  refetch,\n} = useQuery<Role[]>({\n  queryKey: [\"roles\"],\n  queryFn: async () => {\n    return await paginate<RoleV1alpha1ApiListRoleRequest, Role>(\n      (params) => coreApiClient.role.listRole(params),\n      {\n        size: 1000,\n        labelSelector: [`!${roleLabels.TEMPLATE}`],\n      }\n    );\n  },\n  refetchInterval(data) {\n    const hasDeletingRole = data?.some(\n      (item) => !!item.metadata.deletionTimestamp\n    );\n    return hasDeletingRole ? 1000 : false;\n  },\n  onSuccess(data) {\n    fuse = new Fuse(data, {\n      keys: [\n        {\n          name: \"displayName\",\n          getFn: (role) => {\n            return (\n              role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] || \"\"\n            );\n          },\n        },\n        \"metadata.name\",\n      ],\n      useExtendedSearch: true,\n      threshold: 0.2,\n    });\n  },\n  enabled: computed(() => !!roleTemplates.value),\n});\n\nconst keyword = ref(\"\");\n\nconst searchResults = computed(() => {\n  if (!fuse || !keyword.value) {\n    return roles.value || [];\n  }\n\n  return fuse?.search(keyword.value).map((item) => item.item);\n});\n\nconst isSystemReserved = (role: Role) => {\n  return role.metadata.labels?.[roleLabels.SYSTEM_RESERVED] === \"true\";\n};\n\nconst getRoleCountText = (role: Role) => {\n  if (role.metadata.name === SUPER_ROLE_NAME) {\n    return t(\"core.role.common.text.contains_all_permissions\");\n  }\n\n  const dependencies = new Set<string>(\n    resolveDeepDependencies(role, roleTemplates.value || [])\n  );\n\n  return t(\"core.role.common.text.contains_n_permissions\", {\n    count: dependencies.size || 0,\n  });\n};\n\nconst handleOpenEditingModal = (role: Role) => {\n  selectedRole.value = role;\n  editingModal.value = true;\n};\n\nconst onEditingModalClose = () => {\n  selectedRole.value = undefined;\n  editingModal.value = false;\n  refetch();\n};\n\nconst handleCloneRole = async (role: Role) => {\n  const roleToCreate: Role = {\n    apiVersion: \"v1alpha1\",\n    kind: \"Role\",\n    metadata: {\n      name: \"\",\n      generateName: \"role-\",\n      labels: {},\n      annotations: {\n        [rbacAnnotations.DEPENDENCIES]:\n          role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] || \"\",\n        [rbacAnnotations.DISPLAY_NAME]:\n          role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] || \"\",\n      },\n    },\n    rules: [],\n  };\n\n  // 如果是超级管理员角色，那么需要获取到所有角色模板并填充到表单\n  if (role.metadata.name === SUPER_ROLE_NAME) {\n    const allRoleTemplates = await paginate<\n      RoleV1alpha1ApiListRoleRequest,\n      Role\n    >((params) => coreApiClient.role.listRole(params), {\n      size: 1000,\n      labelSelector: [`${roleLabels.TEMPLATE}=true`, \"!halo.run/hidden\"],\n    });\n\n    const roleTemplateNames = allRoleTemplates.map(\n      (item) => item.metadata.name\n    );\n    if (roleToCreate.metadata.annotations) {\n      roleToCreate.metadata.annotations[rbacAnnotations.DEPENDENCIES] =\n        JSON.stringify(roleTemplateNames);\n    } else {\n      roleToCreate.metadata.annotations = {\n        [rbacAnnotations.DEPENDENCIES]: JSON.stringify(roleTemplateNames),\n      };\n    }\n  }\n\n  selectedRole.value = roleToCreate;\n  editingModal.value = true;\n};\n\nconst handleDelete = async (role: Role) => {\n  Dialog.warning({\n    title: t(\"core.role.operations.delete.title\"),\n    description: t(\"core.role.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.role.deleteRole({\n          name: role.metadata.name,\n        });\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (e) {\n        console.error(\"Failed to delete role\", e);\n      } finally {\n        refetch();\n      }\n    },\n  });\n};\n</script>\n<template>\n  <RoleEditingModal\n    v-if=\"editingModal\"\n    :role=\"selectedRole\"\n    @close=\"onEditingModalClose\"\n  />\n\n  <VPageHeader :title=\"$t('core.role.title')\">\n    <template #icon>\n      <IconShieldUser />\n    </template>\n    <template #actions>\n      <VButton\n        v-permission=\"['system:roles:manage']\"\n        type=\"secondary\"\n        @click=\"editingModal = true\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col items-start sm:flex-row sm:items-center\"\n          >\n            <div class=\"flex w-full flex-1 sm:w-auto\">\n              <FormKit\n                v-model=\"keyword\"\n                :placeholder=\"$t('core.common.placeholder.search')\"\n                type=\"text\"\n              ></FormKit>\n            </div>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <VEntity v-for=\"role in searchResults\" :key=\"role.metadata.name\">\n            <template #start>\n              <VEntityField\n                :title=\"\n                  role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||\n                  role.metadata.name\n                \"\n                :description=\"getRoleCountText(role)\"\n                :route=\"{\n                  name: 'RoleDetail',\n                  params: {\n                    name: role.metadata.name,\n                  },\n                }\"\n              ></VEntityField>\n            </template>\n            <template #end>\n              <!-- TODO: 支持显示用户数量 -->\n              <VEntityField v-if=\"false\" description=\"0 个用户\" />\n              <VEntityField>\n                <template #description>\n                  <VTag>\n                    {{\n                      isSystemReserved(role)\n                        ? t(\"core.role.common.text.system_reserved\")\n                        : t(\"core.role.common.text.custom\")\n                    }}\n                  </VTag>\n                </template>\n              </VEntityField>\n              <VEntityField v-if=\"role.metadata.deletionTimestamp\">\n                <template #description>\n                  <VStatusDot\n                    v-tooltip=\"$t('core.common.status.deleting')\"\n                    state=\"warning\"\n                    animate\n                  />\n                </template>\n              </VEntityField>\n              <VEntityField>\n                <template #description>\n                  <span class=\"truncate text-xs tabular-nums text-gray-500\">\n                    {{ utils.date.format(role.metadata.creationTimestamp) }}\n                  </span>\n                </template>\n              </VEntityField>\n            </template>\n            <template\n              v-if=\"utils.permission.has(['system:roles:manage'])\"\n              #dropdownItems\n            >\n              <VDropdownItem\n                v-if=\"!isSystemReserved(role)\"\n                @click=\"handleOpenEditingModal(role)\"\n              >\n                {{ $t(\"core.common.buttons.edit\") }}\n              </VDropdownItem>\n              <VDropdownItem\n                v-if=\"!isSystemReserved(role)\"\n                type=\"danger\"\n                @click=\"handleDelete(role)\"\n              >\n                {{ $t(\"core.common.buttons.delete\") }}\n              </VDropdownItem>\n              <VDropdownItem @click=\"handleCloneRole(role)\">\n                {{\n                  $t(\"core.role.operations.create_based_on_this_role.button\")\n                }}\n              </VDropdownItem>\n            </template>\n          </VEntity>\n        </VEntityContainer>\n      </Transition>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/roles/components/RoleEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Role } from \"@halo-dev/api-client\";\nimport { VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { onMounted, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { useRoleForm, useRoleTemplateSelection } from \"@/composables/use-role\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { pluginLabels } from \"@/constants/labels\";\nimport { setFocus } from \"@/formkit/utils/focus\";\nimport { resolveDeepDependencies } from \"@/utils/role\";\nimport { useFetchRoleTemplates } from \"../../users/composables/use-role\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    role?: Role;\n  }>(),\n  {\n    role: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { data: roleTemplates } = useFetchRoleTemplates();\n\nconst { roleTemplateGroups, handleRoleTemplateSelect, selectedRoleTemplates } =\n  useRoleTemplateSelection(roleTemplates);\n\nconst { formState, isSubmitting, handleCreateOrUpdate } = useRoleForm();\n\nwatch(\n  () => selectedRoleTemplates.value,\n  (newValue) => {\n    if (formState.value.metadata.annotations) {\n      formState.value.metadata.annotations[rbacAnnotations.DEPENDENCIES] =\n        JSON.stringify(Array.from(newValue));\n    }\n  }\n);\n\nonMounted(() => {\n  setFocus(\"displayNameInput\");\n\n  if (props.role) {\n    formState.value = cloneDeep(props.role);\n    selectedRoleTemplates.value = new Set<string>(\n      resolveDeepDependencies(props.role, roleTemplates.value || [])\n    );\n  }\n});\n\nconst editingModalTitle = props.role\n  ? t(\"core.role.editing_modal.titles.update\")\n  : t(\"core.role.editing_modal.titles.create\");\n\nconst handleCreateOrUpdateRole = async () => {\n  await handleCreateOrUpdate();\n  modal.value?.close();\n};\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"editingModalTitle\"\n    :width=\"700\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n        <div class=\"md:col-span-1\">\n          <div class=\"sticky top-0\">\n            <span class=\"text-base font-medium text-gray-900\">\n              {{ $t(\"core.role.editing_modal.groups.general\") }}\n            </span>\n          </div>\n        </div>\n        <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n          <FormKit\n            v-if=\"formState.metadata.annotations\"\n            id=\"role-form\"\n            name=\"role-form\"\n            :actions=\"false\"\n            type=\"form\"\n            :config=\"{ validationVisibility: 'submit' }\"\n            @submit=\"handleCreateOrUpdateRole\"\n          >\n            <FormKit\n              id=\"displayNameInput\"\n              v-model=\"\n                formState.metadata.annotations[rbacAnnotations.DISPLAY_NAME]\n              \"\n              :label=\"$t('core.role.editing_modal.fields.display_name')\"\n              type=\"text\"\n              validation=\"required|length:0,50\"\n            ></FormKit>\n            <FormKit\n              v-model=\"\n                formState.metadata.annotations[\n                  rbacAnnotations.DISALLOW_ACCESS_CONSOLE\n                ]\n              \"\n              on-value=\"true\"\n              off-value=\"false\"\n              type=\"checkbox\"\n              :label=\"\n                $t(\n                  'core.role.editing_modal.fields.disallow_access_console.label'\n                )\n              \"\n              :help=\"\n                $t(\n                  'core.role.editing_modal.fields.disallow_access_console.help'\n                )\n              \"\n            ></FormKit>\n          </FormKit>\n        </div>\n      </div>\n      <div class=\"py-5\">\n        <div class=\"border-t border-gray-200\"></div>\n      </div>\n      <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n        <div class=\"md:col-span-1\">\n          <div class=\"sticky top-0\">\n            <span class=\"text-base font-medium text-gray-900\">\n              {{ $t(\"core.role.editing_modal.groups.permissions\") }}\n            </span>\n          </div>\n        </div>\n        <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n          <dl class=\"divide-y divide-gray-100\">\n            <div\n              v-for=\"(group, groupIndex) in roleTemplateGroups\"\n              :key=\"groupIndex\"\n              class=\"flex flex-col gap-3 bg-white py-5 first:pt-0\"\n            >\n              <dt class=\"text-sm font-medium text-gray-900\">\n                <div>\n                  {{ $t(`core.rbac.${group.module}`, group.module as string) }}\n                </div>\n                <div\n                  v-if=\"\n                    group.roles.length &&\n                    group.roles[0].metadata.labels?.[pluginLabels.NAME]\n                  \"\n                  class=\"mt-3 text-xs text-gray-500\"\n                >\n                  <i18n-t\n                    keypath=\"core.role.common.text.provided_by_plugin\"\n                    tag=\"div\"\n                  >\n                    <template #plugin>\n                      <RouterLink\n                        :to=\"{\n                          name: 'PluginDetail',\n                          params: {\n                            name: group.roles[0].metadata.labels?.[\n                              pluginLabels.NAME\n                            ],\n                          },\n                        }\"\n                        class=\"hover:text-blue-600\"\n                      >\n                        {{\n                          group.roles[0].metadata.labels?.[pluginLabels.NAME]\n                        }}\n                      </RouterLink>\n                    </template>\n                  </i18n-t>\n                </div>\n              </dt>\n              <dd class=\"text-sm text-gray-900\">\n                <ul class=\"space-y-2\">\n                  <li\n                    v-for=\"roleTemplate in group.roles\"\n                    :key=\"roleTemplate.metadata.name\"\n                  >\n                    <label\n                      class=\"inline-flex w-full cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary\"\n                    >\n                      <input\n                        v-model=\"selectedRoleTemplates\"\n                        :value=\"roleTemplate.metadata.name\"\n                        type=\"checkbox\"\n                        @change=\"handleRoleTemplateSelect\"\n                      />\n                      <div class=\"flex flex-1 flex-col gap-y-3\">\n                        <span class=\"font-medium text-gray-900\">\n                          {{\n                            $t(\n                              `core.rbac.${\n                                roleTemplate.metadata.annotations?.[\n                                  rbacAnnotations.DISPLAY_NAME\n                                ]\n                              }`,\n                              roleTemplate.metadata.annotations?.[\n                                rbacAnnotations.DISPLAY_NAME\n                              ] as string\n                            )\n                          }}\n                        </span>\n                        <span\n                          v-if=\"\n                            roleTemplate.metadata.annotations?.[\n                              rbacAnnotations.DEPENDENCIES\n                            ]\n                          \"\n                          class=\"text-xs text-gray-400\"\n                        >\n                          {{\n                            $t(\"core.role.common.text.dependent_on\", {\n                              roles: JSON.parse(\n                                roleTemplate.metadata.annotations?.[\n                                  rbacAnnotations.DEPENDENCIES\n                                ]\n                              )\n                                .map((item: string) =>\n                                  $t(`core.rbac.${item}`, item as string)\n                                )\n                                .join(\"，\"),\n                            })\n                          }}\n                        </span>\n                      </div>\n                    </label>\n                  </li>\n                </ul>\n              </dd>\n            </div>\n          </dl>\n        </div>\n      </div>\n    </div>\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('role-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/roles/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\n\nexport default definePlugin({\n  components: {},\n  routes: [\n    {\n      path: \"/users/roles\",\n      component: BasicLayout,\n      children: [\n        {\n          path: \"\",\n          name: \"Roles\",\n          component: () => import(\"./RoleList.vue\"),\n          meta: {\n            title: \"core.role.title\",\n            searchable: true,\n            permissions: [\"system:roles:view\"],\n          },\n        },\n        {\n          path: \":name\",\n          name: \"RoleDetail\",\n          component: () => import(\"./RoleDetail.vue\"),\n          meta: {\n            title: \"core.role.detail.title\",\n            permissions: [\"system:roles:view\"],\n          },\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/settings/SystemSettings.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Setting, SettingForm } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconSettings,\n  VCard,\n  VLoading,\n  VPageHeader,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport type { Component, Raw } from \"vue\";\nimport {\n  defineAsyncComponent,\n  markRaw,\n  provide,\n  shallowRef,\n  type Ref,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SettingTab from \"./tabs/Setting.vue\";\n\nconst { t } = useI18n();\n\ninterface Tab {\n  id: string;\n  label: string;\n  component: Raw<Component>;\n}\n\nconst tabs = shallowRef<Tab[]>([\n  {\n    id: \"loading\",\n    label: t(\"core.common.status.loading\"),\n    component: markRaw(SettingTab),\n  },\n]);\n\nconst activeTab = useRouteQuery<string>(\"tab\", \"basic\");\nprovide<Ref<string>>(\"activeTab\", activeTab);\n\nconst { data: setting } = useQuery({\n  queryKey: [\"system-setting\"],\n  queryFn: async () => {\n    const { data } = await coreApiClient.setting.getSetting({\n      name: \"system\",\n    });\n    return data;\n  },\n  async onSuccess(data) {\n    if (data) {\n      const { forms } = data.spec;\n      tabs.value = forms.map((item: SettingForm) => {\n        return {\n          id: item.group,\n          label: item.label || \"\",\n          component: markRaw(SettingTab),\n        };\n      });\n\n      if (!activeTab.value) {\n        activeTab.value = tabs.value[0].id;\n      }\n\n      // TODO: use integrations center to refactor this\n      if (utils.permission.has([\"system:notifier:configuration\"])) {\n        tabs.value = [\n          ...tabs.value,\n          {\n            id: \"notification\",\n            label: \"通知设置\",\n            component: defineAsyncComponent({\n              loader: () => import(\"./tabs/Notifications.vue\"),\n              loadingComponent: VLoading,\n            }),\n          },\n        ];\n      }\n    }\n  },\n});\n\nprovide<Ref<Setting | undefined>>(\"setting\", setting);\n</script>\n<template>\n  <VPageHeader :title=\"$t('core.setting.title')\">\n    <template #icon>\n      <IconSettings />\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0', '!overflow-visible']\">\n      <template #header>\n        <VTabbar\n          v-model:active-id=\"activeTab\"\n          :items=\"tabs.map((item) => ({ id: item.id, label: item.label }))\"\n          class=\"w-full !rounded-none\"\n          type=\"outline\"\n        ></VTabbar>\n      </template>\n      <div class=\"rounded-b-base bg-white\">\n        <template v-for=\"tab in tabs\" :key=\"tab.id\">\n          <component :is=\"tab.component\" v-if=\"activeTab === tab.id\" />\n        </template>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/settings/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconSettings } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  components: {},\n  routes: [\n    {\n      path: \"/settings\",\n      name: \"SettingsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.setting.title\",\n        permissions: [\"system:settings:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.settings\",\n          group: \"system\",\n          icon: markRaw(IconSettings),\n          priority: 2,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"SystemSetting\",\n          component: () => import(\"./SystemSettings.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/settings/tabs/NotificationSetting.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitSchemaCondition, FormKitSchemaNode } from \"@formkit/core\";\nimport type { NotifierDescriptor, Setting } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton } from \"@halo-dev/components\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport type { Ref } from \"vue\";\nimport { computed, inject, ref, toRaw } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport StickyBlock from \"@/components/sticky-block/StickyBlock.vue\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst notifierDescriptor = inject<Ref<NotifierDescriptor | undefined>>(\n  \"notifierDescriptor\",\n  ref()\n);\n\nconst name = computed(\n  (): string => notifierDescriptor.value?.metadata.name as string\n);\n\nconst { data: setting } = useQuery<Setting | undefined>({\n  queryKey: [\"notifier-setting\", notifierDescriptor],\n  queryFn: async () => {\n    const { data } = await coreApiClient.setting.getSetting({\n      name: notifierDescriptor.value?.spec?.senderSettingRef?.name as string,\n    });\n    return data;\n  },\n  enabled: computed(() => !!notifierDescriptor.value),\n});\n\nconst configMapData = ref<Record<string, unknown>>({});\n\nuseQuery<Record<string, unknown>>({\n  queryKey: [\"notifier-configMap\", notifierDescriptor],\n  queryFn: async () => {\n    const { data } =\n      await consoleApiClient.notification.notifier.fetchSenderConfig({\n        name: name.value,\n      });\n    return data as Record<string, unknown>;\n  },\n  onSuccess(data) {\n    configMapData.value = data;\n  },\n  enabled: computed(() => !!notifierDescriptor.value),\n});\n\nconst formSchema = computed(() => {\n  return setting.value?.spec.forms.find(\n    (form) =>\n      form.group === notifierDescriptor.value?.spec?.senderSettingRef?.group\n  )?.formSchema as (FormKitSchemaCondition | FormKitSchemaNode)[];\n});\n\nconst { isLoading: isMutating, mutate } = useMutation({\n  mutationKey: [\"save-notifier-configMap\", notifierDescriptor],\n  mutationFn: async () => {\n    const { data } =\n      await consoleApiClient.notification.notifier.saveSenderConfig({\n        name: name.value,\n        body: configMapData.value,\n      });\n    return data;\n  },\n  onSuccess() {\n    queryClient.invalidateQueries({ queryKey: [\"notifier-configMap\"] });\n    Toast.success(t(\"core.common.toast.save_success\"));\n  },\n});\n</script>\n\n<template>\n  <Transition v-if=\"formSchema\" mode=\"out-in\" appear name=\"fade\">\n    <div>\n      <FormKit\n        :id=\"name\"\n        v-model=\"configMapData\"\n        :name=\"name\"\n        :preserve=\"true\"\n        type=\"form\"\n        @submit=\"mutate()\"\n      >\n        <FormKitSchema :schema=\"toRaw(formSchema)\" :data=\"configMapData\" />\n      </FormKit>\n\n      <StickyBlock\n        v-permission=\"['system:configmaps:manage']\"\n        class=\"-mx-4 -mb-4 rounded-b-base rounded-t-lg bg-white p-4 pt-5\"\n        position=\"bottom\"\n      >\n        <VButton\n          :loading=\"isMutating\"\n          type=\"secondary\"\n          @click=\"$formkit.submit(name)\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n      </StickyBlock>\n    </div>\n  </Transition>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/settings/tabs/Notifications.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  NotifierDescriptor,\n  NotifierDescriptorV1alpha1ApiListNotifierDescriptorRequest,\n} from \"@halo-dev/api-client\";\nimport { coreApiClient, paginate } from \"@halo-dev/api-client\";\nimport { VTabbar } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport type { Component, ComputedRef, Raw } from \"vue\";\nimport { computed, markRaw, provide, ref, shallowRef } from \"vue\";\nimport NotificationSetting from \"./NotificationSetting.vue\";\n\ninterface Tab {\n  id: string;\n  label: string;\n  component: Raw<Component>;\n}\n\nconst tabs = shallowRef<Tab[]>();\n\nconst activeTab = ref();\n\nconst { data: notifierDescriptors } = useQuery({\n  queryKey: [\"notifier-descriptors\"],\n  queryFn: async () => {\n    return await paginate<\n      NotifierDescriptorV1alpha1ApiListNotifierDescriptorRequest,\n      NotifierDescriptor\n    >(\n      (params) =>\n        coreApiClient.notification.notifierDescriptor.listNotifierDescriptor(\n          params\n        ),\n      {\n        size: 1000,\n      }\n    );\n  },\n  onSuccess(data) {\n    if (data) {\n      tabs.value = data.map((descriptor) => {\n        return {\n          id: descriptor.metadata.name,\n          label: descriptor.spec?.displayName,\n          component: markRaw(NotificationSetting),\n        };\n      }) as Tab[];\n\n      if (!activeTab.value) {\n        activeTab.value = tabs.value[0].id;\n      }\n    }\n  },\n});\n\nconst notifierDescriptor = computed(() => {\n  return notifierDescriptors.value?.find(\n    (item) => item.metadata.name === activeTab.value\n  );\n});\n\nprovide<ComputedRef<NotifierDescriptor | undefined>>(\n  \"notifierDescriptor\",\n  notifierDescriptor\n);\n</script>\n\n<template>\n  <div class=\"p-4\">\n    <VTabbar\n      v-model:active-id=\"activeTab\"\n      :items=\"tabs?.map((item) => ({ id: item.id, label: item.label }))\"\n      class=\"w-full\"\n      type=\"pills\"\n    ></VTabbar>\n    <div class=\"mt-4\">\n      <template v-for=\"tab in tabs\" :key=\"tab.id\">\n        <component :is=\"tab.component\" v-if=\"activeTab === tab.id\" />\n      </template>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/settings/tabs/Setting.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitSchemaCondition, FormKitSchemaNode } from \"@formkit/core\";\nimport type { Setting } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VLoading } from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, inject, ref, toRaw, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport StickyBlock from \"@/components/sticky-block/StickyBlock.vue\";\nimport { setLanguage } from \"@/locales\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst group = inject<Ref<string>>(\"activeTab\", ref(\"basic\"));\nconst setting = inject<Ref<Setting | undefined>>(\"setting\", ref());\nconst isSubmitting = ref(false);\n\nconst { data: configMapGroupData, isLoading } = useQuery({\n  queryKey: [\"core:system:configMap:group-data\", group],\n  queryFn: async () => {\n    const { data } =\n      await consoleApiClient.configMap.system.getSystemConfigByGroup({\n        group: group.value,\n      });\n    return data as Record<string, unknown>;\n  },\n  enabled: computed(() => !!group.value),\n});\n\nconst formSchema = computed(() => {\n  if (!setting.value) {\n    return;\n  }\n  const { forms } = setting.value.spec;\n  return forms.find((item) => item.group === group?.value)?.formSchema as (\n    | FormKitSchemaCondition\n    | FormKitSchemaNode\n  )[];\n});\n\nconst handleSaveConfigMap = async (data: Record<string, unknown>) => {\n  try {\n    isSubmitting.value = true;\n    await consoleApiClient.configMap.system.updateSystemConfigByGroup({\n      group: group.value,\n      body: data,\n    });\n\n    queryClient.invalidateQueries({\n      queryKey: [\"core:system:configMap:group-data\"],\n    });\n\n    await stores.globalInfo().fetchGlobalInfo();\n\n    if (group.value === \"basic\") {\n      const language = data.language;\n      document.cookie = `language=${language}; path=/; SameSite=Lax; Secure`;\n      await setLanguage(language as string);\n    }\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (error) {\n    console.error(error);\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n</script>\n<template>\n  <div class=\"p-4\">\n    <VLoading v-if=\"isLoading\" />\n    <FormKit\n      v-else-if=\"group && formSchema && configMapGroupData\"\n      :id=\"group\"\n      :value=\"configMapGroupData\"\n      :name=\"group\"\n      :preserve=\"true\"\n      type=\"form\"\n      @submit=\"handleSaveConfigMap\"\n    >\n      <FormKitSchema\n        :schema=\"toRaw(formSchema)\"\n        :data=\"toRaw(configMapGroupData)\"\n      />\n    </FormKit>\n\n    <HasPermission\n      v-if=\"!isLoading\"\n      :permissions=\"['system:configmaps:manage']\"\n    >\n      <StickyBlock\n        class=\"-mx-4 -mb-4 rounded-b-base rounded-t-lg bg-white p-4 pt-5\"\n        position=\"bottom\"\n      >\n        <VButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          @click=\"$formkit.submit(group || '')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n      </StickyBlock>\n    </HasPermission>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/tools/Tools.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconToolsFill,\n  VButton,\n  VCard,\n  VEmpty,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { onMounted, ref } from \"vue\";\nimport type { RouteRecordRaw } from \"vue-router\";\nimport { useRouter } from \"vue-router\";\n\nconst router = useRouter();\nconst routes = ref<RouteRecordRaw[]>([]);\n\nasync function isRouteValid(route?: RouteRecordRaw) {\n  if (!route) return false;\n  const { meta } = route;\n  if (!meta?.menu) return false;\n\n  // If permissions doesn't exist or is empty\n  if (!meta.permissions) return true;\n\n  // Check if permissions is a function\n  if (typeof meta.permissions === \"function\") {\n    try {\n      return await meta.permissions(\n        utils.permission.getUserPermissions() || []\n      );\n    } catch (e) {\n      console.error(\n        `Error checking permissions for route ${String(route.name)}:`,\n        e\n      );\n      return false;\n    }\n  }\n\n  // Default behavior for array of permissions\n  return utils.permission.has(meta?.permissions as string[], true);\n}\n\n// Use async function to set routes\nconst fetchRoutes = async () => {\n  const matchedRoute = router.currentRoute.value.matched[0];\n  const childRoutes =\n    router\n      .getRoutes()\n      .find((route) => route.name === matchedRoute.name)\n      ?.children.filter((route) => route.path !== \"\") || [];\n\n  const validRoutes: RouteRecordRaw[] = [];\n  for (const route of childRoutes) {\n    if (await isRouteValid(route)) {\n      validRoutes.push(route);\n    }\n  }\n\n  routes.value = validRoutes;\n};\n\n// Fetch routes on component mount\nonMounted(() => {\n  fetchRoutes();\n});\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.tool.title')\">\n    <template #icon>\n      <IconToolsFill />\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <VEmpty\n        v-if=\"!routes?.length\"\n        :title=\"$t('core.tool.empty.title')\"\n        :message=\"$t('core.tool.empty.message')\"\n      ></VEmpty>\n      <VEntityContainer v-else>\n        <VEntity v-for=\"route in routes\" :key=\"route.name\">\n          <template #start>\n            <VEntityField>\n              <template #description>\n                <component\n                  :is=\"route.meta?.menu?.icon\"\n                  v-if=\"route.meta?.menu?.icon\"\n                  class=\"text-lg\"\n                />\n                <IconToolsFill v-else class=\"text-lg\" />\n              </template>\n            </VEntityField>\n            <VEntityField\n              :route=\"{ name: route.name }\"\n              :title=\"route.meta?.menu?.name\"\n              :description=\"route.meta?.description\"\n            ></VEntityField>\n          </template>\n\n          <template #end>\n            <VEntityField>\n              <template #description>\n                <VButton size=\"sm\" @click=\"$router.push({ name: route.name })\">\n                  {{ $t(\"core.common.buttons.access\") }}\n                </VButton>\n              </template>\n            </VEntityField>\n          </template>\n        </VEntity>\n      </VEntityContainer>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/tools/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconToolsFill } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  components: {},\n  routes: [\n    {\n      path: \"/tools\",\n      name: \"ToolsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.tool.title\",\n        menu: {\n          name: \"core.sidebar.menu.items.tools\",\n          group: \"system\",\n          icon: markRaw(IconToolsFill),\n          priority: 5,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Tools\",\n          component: () => import(\"./Tools.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/users/UserDetail.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { User } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VButton,\n  VDropdown,\n  VDropdownDivider,\n  VDropdownItem,\n  VLoading,\n  VTabbar,\n  VTag,\n} from \"@halo-dev/components\";\nimport { stores, utils, type UserTab } from \"@halo-dev/ui-shared\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport {\n  computed,\n  defineAsyncComponent,\n  onMounted,\n  provide,\n  ref,\n  shallowRef,\n  type Ref,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRoute, useRouter } from \"vue-router\";\nimport UserAvatar from \"@/components/user-avatar/UserAvatar.vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport GrantPermissionModal from \"./components/GrantPermissionModal.vue\";\nimport UserEditingModal from \"./components/UserEditingModal.vue\";\nimport UserPasswordChangeModal from \"./components/UserPasswordChangeModal.vue\";\nimport { useUserEnableDisable } from \"./composables/use-user\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\nconst { currentUser } = stores.currentUser();\n\nconst editingModal = ref(false);\nconst passwordChangeModal = ref(false);\nconst grantPermissionModal = ref<boolean>(false);\nconst { params } = useRoute();\nconst router = useRouter();\nconst {\n  data: user,\n  isLoading,\n  refetch,\n} = useQuery({\n  queryKey: [\"user-detail\", params.name],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.user.getUserDetail({\n      name: params.name as string,\n    });\n    return data;\n  },\n  enabled: computed(() => !!params.name),\n});\n\nconst tabs = shallowRef<UserTab[]>([\n  {\n    id: \"detail\",\n    label: t(\"core.user.detail.tabs.detail\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/Detail.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 10,\n  },\n]);\n\n// Collect user:detail:tabs:create extension points\nconst { pluginModules } = usePluginModuleStore();\n\nonMounted(async () => {\n  for (const pluginModule of pluginModules) {\n    try {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"user:detail:tabs:create\"];\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const providers = await callbackFunction();\n\n      tabs.value = [...tabs.value, ...providers].sort(\n        (a, b) => a.priority - b.priority\n      );\n    } catch (error) {\n      console.error(`Error processing plugin module:`, pluginModule, error);\n    }\n  }\n});\n\nconst activeTab = useRouteQuery<string>(\"tab\", tabs.value[0].id, {\n  mode: \"push\",\n});\n\nprovide<Ref<string>>(\"activeTab\", activeTab);\n\nconst tabbarItems = computed(() => {\n  return tabs.value.map((tab) => ({\n    id: tab.id,\n    label: tab.label,\n  }));\n});\n\nconst handleDelete = async (user: User) => {\n  Dialog.warning({\n    title: t(\"core.user.operations.delete.title\"),\n    description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.user.deleteUser({\n          name: user.metadata.name,\n        });\n        Toast.success(t(\"core.common.toast.delete_success\"));\n        router.push({ name: \"Users\" });\n      } catch (e) {\n        console.error(\"Failed to delete user\", e);\n      }\n    },\n  });\n};\n\nfunction handleRouteToUC() {\n  window.location.href = \"/uc\";\n}\n\nfunction onPasswordChangeModalClose() {\n  passwordChangeModal.value = false;\n  refetch();\n}\nfunction onGrantPermissionModalClose() {\n  grantPermissionModal.value = false;\n  refetch();\n}\n\nconst { handleEnableOrDisableUser } = useUserEnableDisable();\n</script>\n<template>\n  <UserEditingModal\n    v-if=\"editingModal && user?.user\"\n    :user=\"user?.user\"\n    @close=\"editingModal = false\"\n  />\n\n  <UserPasswordChangeModal\n    v-if=\"passwordChangeModal\"\n    :user=\"user?.user\"\n    @close=\"onPasswordChangeModalClose\"\n  />\n\n  <GrantPermissionModal\n    v-if=\"grantPermissionModal\"\n    :user=\"user?.user\"\n    @close=\"onGrantPermissionModalClose\"\n  />\n\n  <header class=\"bg-white\">\n    <div class=\"p-4\">\n      <div class=\"flex items-center justify-between\">\n        <div class=\"flex flex-row items-center gap-5\">\n          <div class=\"group relative h-20 w-20\">\n            <UserAvatar\n              :name=\"user?.user.metadata.name\"\n              :is-current-user=\"\n                user?.user.metadata.name === currentUser?.user.metadata.name\n              \"\n            />\n          </div>\n          <div class=\"block\">\n            <div class=\"flex items-center gap-2\">\n              <h1 class=\"truncate text-lg font-bold text-gray-900\">\n                {{ user?.user.spec.displayName }}\n              </h1>\n              <VTag v-if=\"user?.user.spec.disabled\">\n                {{ $t(\"core.user.fields.disabled\") }}\n              </VTag>\n            </div>\n            <span v-if=\"!isLoading\" class=\"text-sm text-gray-600\">\n              @{{ user?.user.metadata.name }}\n            </span>\n          </div>\n        </div>\n        <div class=\"inline-flex items-center gap-2\">\n          <VButton\n            v-if=\"currentUser?.user.metadata.name === user?.user.metadata.name\"\n            type=\"primary\"\n            @click=\"handleRouteToUC\"\n          >\n            {{ $t(\"core.user.detail.actions.profile.title\") }}\n          </VButton>\n          <VDropdown v-if=\"utils.permission.has(['system:users:manage'])\">\n            <VButton type=\"default\">\n              {{ $t(\"core.common.buttons.edit\") }}\n            </VButton>\n            <template #popper>\n              <VDropdownItem @click=\"editingModal = true\">\n                {{ $t(\"core.user.detail.actions.update_profile.title\") }}\n              </VDropdownItem>\n              <VDropdownItem @click=\"passwordChangeModal = true\">\n                {{ $t(\"core.user.detail.actions.change_password.title\") }}\n              </VDropdownItem>\n              <VDropdownItem\n                v-if=\"\n                  currentUser?.user.metadata.name !== user?.user.metadata.name\n                \"\n                @click=\"grantPermissionModal = true\"\n              >\n                {{ $t(\"core.user.detail.actions.grant_permission.title\") }}\n              </VDropdownItem>\n              <VDropdownDivider\n                v-if=\"\n                  currentUser?.user.metadata.name !== user?.user.metadata.name\n                \"\n              />\n              <VDropdownItem\n                v-if=\"\n                  !!user &&\n                  currentUser?.user.metadata.name !== user?.user.metadata.name\n                \"\n                type=\"danger\"\n                @click=\"\n                  handleEnableOrDisableUser({\n                    name: user.user.metadata.name,\n                    operation: user.user.spec.disabled ? 'enable' : 'disable',\n                    onSuccess: () => {\n                      queryClient.invalidateQueries({\n                        queryKey: ['user-detail'],\n                      });\n                    },\n                  })\n                \"\n              >\n                {{\n                  user.user.spec.disabled\n                    ? $t(\"core.user.operations.enable.title\")\n                    : $t(\"core.user.operations.disable.title\")\n                }}\n              </VDropdownItem>\n              <VDropdownItem\n                v-if=\"\n                  user &&\n                  currentUser?.user.metadata.name !== user?.user.metadata.name\n                \"\n                type=\"danger\"\n                @click=\"handleDelete(user.user)\"\n              >\n                {{ $t(\"core.common.buttons.delete\") }}\n              </VDropdownItem>\n            </template>\n          </VDropdown>\n        </div>\n      </div>\n    </div>\n  </header>\n  <section class=\"bg-white p-4\">\n    <VTabbar\n      v-model:active-id=\"activeTab\"\n      :items=\"tabbarItems\"\n      class=\"w-full\"\n      type=\"outline\"\n    ></VTabbar>\n    <div class=\"mt-2\">\n      <template v-for=\"tab in tabs\" :key=\"tab.id\">\n        <component\n          :is=\"tab.component\"\n          v-if=\"activeTab === tab.id\"\n          :user=\"user\"\n        />\n      </template>\n    </div>\n  </section>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/UserList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedUser, User } from \"@halo-dev/api-client\";\nimport { consoleApiClient, coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconLockPasswordLine,\n  IconRefreshLine,\n  IconShieldUser,\n  IconUserSettings,\n  Toast,\n  VButton,\n  VCard,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { chunk } from \"es-toolkit\";\nimport { computed, onMounted, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport UserCreationModal from \"./components/UserCreationModal.vue\";\nimport UserListItem from \"./components/UserListItem.vue\";\nimport { useFetchRoles } from \"./composables/use-role\";\n\nconst { t } = useI18n();\n\nconst checkedAll = ref(false);\nconst creationModal = ref<boolean>(false);\n\nconst selectedUserNames = ref<string[]>([]);\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\n\nconst { currentUser } = stores.currentUser();\n\nconst ANONYMOUSUSER_NAME = \"anonymousUser\";\nconst DELETEDUSER_NAME = \"ghost\";\n\n// Filters\nconst { data: roles } = useFetchRoles();\nconst page = useRouteQuery<number>(\"page\", 1, {\n  transform: Number,\n});\nconst size = useRouteQuery<number>(\"size\", 20, {\n  transform: Number,\n});\nconst selectedRoleValue = useRouteQuery<string | undefined>(\"role\");\nconst selectedSortValue = useRouteQuery<string | undefined>(\"sort\");\n\nfunction handleClearFilters() {\n  selectedRoleValue.value = undefined;\n  selectedSortValue.value = undefined;\n}\n\nconst hasFilters = computed(() => {\n  return selectedRoleValue.value || selectedSortValue.value;\n});\n\nwatch(\n  () => [selectedRoleValue.value, selectedSortValue.value, keyword.value],\n  () => {\n    page.value = 1;\n  }\n);\n\nconst total = ref(0);\n\nconst {\n  data: users,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery<ListedUser[]>({\n  queryKey: [\n    \"users\",\n    page,\n    size,\n    keyword,\n    selectedSortValue,\n    selectedRoleValue,\n  ],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.user.listUsers({\n      page: page.value,\n      size: size.value,\n      keyword: keyword.value,\n      fieldSelector: [\n        `name!=${ANONYMOUSUSER_NAME}`,\n        `name!=${DELETEDUSER_NAME}`,\n      ],\n      sort: [selectedSortValue.value].filter(Boolean) as string[],\n      role: selectedRoleValue.value,\n    });\n\n    total.value = data.total;\n\n    return data.items;\n  },\n  refetchInterval(data) {\n    const hasDeletingData = data?.some(\n      (user) => !!user.user.metadata.deletionTimestamp\n    );\n\n    return hasDeletingData ? 1000 : false;\n  },\n});\n\nconst handleDeleteInBatch = async () => {\n  Dialog.warning({\n    title: t(\"core.user.operations.delete_in_batch.title\"),\n    description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const userNamesToDelete = selectedUserNames.value.filter(\n        (name) => name != currentUser?.user.metadata.name\n      );\n      await Promise.all(\n        userNamesToDelete.map((name) => {\n          return coreApiClient.user.deleteUser({\n            name,\n          });\n        })\n      );\n      await refetch();\n      selectedUserNames.value.length = 0;\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n};\n\nfunction handleEnableOrDisableInBatch(operation: \"enable\" | \"disable\") {\n  const operations = {\n    enable: {\n      title: t(\"core.user.operations.enable_in_batch.title\"),\n      description: t(\"core.user.operations.enable_in_batch.description\"),\n      request: (name: string) =>\n        consoleApiClient.user.enableUser({ username: name }),\n      condition: (user: User) => user.spec.disabled,\n      message: t(\"core.common.toast.enable_success\"),\n    },\n    disable: {\n      title: t(\"core.user.operations.disable_in_batch.title\"),\n      description: t(\"core.user.operations.disable_in_batch.description\"),\n      request: (name: string) =>\n        consoleApiClient.user.disableUser({ username: name }),\n      condition: (user: User) => !user.spec.disabled,\n      message: t(\"core.common.toast.disable_success\"),\n    },\n  };\n\n  const operationConfig = operations[operation];\n\n  Dialog.warning({\n    title: operationConfig.title,\n    description: operationConfig.description,\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      const filteredUserNames = selectedUserNames.value.filter((name) => {\n        if (name === currentUser?.user.metadata.name) return false;\n        const user = users.value?.find((u) => u.user.metadata.name === name);\n        return user && operationConfig.condition(user.user);\n      });\n\n      const chunks = chunk(filteredUserNames, 5);\n\n      for (const chunk of chunks) {\n        await Promise.all(chunk.map((name) => operationConfig.request(name)));\n      }\n\n      await refetch();\n\n      selectedUserNames.value.length = 0;\n      checkedAll.value = false;\n\n      Toast.success(operationConfig.message);\n    },\n  });\n}\n\nwatch(selectedUserNames, (newValue) => {\n  checkedAll.value =\n    newValue.length ===\n    users.value?.filter(\n      (user) => user.user.metadata.name !== currentUser?.user.metadata.name\n    ).length;\n});\n\nconst checkSelection = (user: User) => {\n  return selectedUserNames.value.includes(user.metadata.name);\n};\n\nconst handleCheckAllChange = (e: Event) => {\n  const { checked } = e.target as HTMLInputElement;\n\n  if (checked) {\n    selectedUserNames.value =\n      users.value\n        ?.filter((user) => {\n          return user.user.metadata.name !== currentUser?.user.metadata.name;\n        })\n        .map((user) => {\n          return user.user.metadata.name;\n        }) || [];\n  } else {\n    selectedUserNames.value.length = 0;\n  }\n};\n\n// Route query action\nconst routeQueryAction = useRouteQuery<string | undefined>(\"action\");\n\nonMounted(() => {\n  if (routeQueryAction.value === \"create\") {\n    creationModal.value = true;\n  }\n});\n\nfunction onCreationModalClose() {\n  creationModal.value = false;\n  routeQueryAction.value = undefined;\n}\n</script>\n<template>\n  <UserCreationModal v-if=\"creationModal\" @close=\"onCreationModalClose\" />\n\n  <VPageHeader :title=\"$t('core.user.title')\">\n    <template #icon>\n      <IconUserSettings />\n    </template>\n    <template #actions>\n      <VButton\n        v-permission=\"['system:roles:view']\"\n        :route=\"{ name: 'Roles' }\"\n        size=\"sm\"\n        type=\"default\"\n      >\n        <template #icon>\n          <IconShieldUser />\n        </template>\n        {{ $t(\"core.user.actions.roles\") }}\n      </VButton>\n      <HasPermission :permissions=\"['*']\">\n        <VButton :route=\"{ name: 'AuthProviders' }\" size=\"sm\" type=\"default\">\n          <template #icon>\n            <IconLockPasswordLine />\n          </template>\n          {{ $t(\"core.user.actions.identity_authentication\") }}\n        </VButton>\n      </HasPermission>\n      <VButton\n        v-permission=\"['system:users:manage']\"\n        type=\"secondary\"\n        @click=\"creationModal = true\"\n      >\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start gap-4 sm:flex-row sm:items-center\"\n          >\n            <div\n              v-permission=\"['system:users:manage']\"\n              class=\"hidden items-center sm:flex\"\n            >\n              <input\n                v-model=\"checkedAll\"\n                type=\"checkbox\"\n                @change=\"handleCheckAllChange\"\n              />\n            </div>\n            <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n              <SearchInput v-if=\"!selectedUserNames.length\" v-model=\"keyword\" />\n              <VSpace v-else>\n                <VButton @click=\"handleEnableOrDisableInBatch('disable')\">\n                  {{ $t(\"core.common.buttons.disable\") }}\n                </VButton>\n                <VButton @click=\"handleEnableOrDisableInBatch('enable')\">\n                  {{ $t(\"core.common.buttons.enable\") }}\n                </VButton>\n                <VButton type=\"danger\" @click=\"handleDeleteInBatch\">\n                  {{ $t(\"core.common.buttons.delete\") }}\n                </VButton>\n              </VSpace>\n            </div>\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <FilterCleanButton\n                v-if=\"hasFilters\"\n                @click=\"handleClearFilters\"\n              />\n              <FilterDropdown\n                v-model=\"selectedRoleValue\"\n                :label=\"$t('core.user.filters.role.label')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.all'),\n                  },\n                  ...(roles?.map((role) => {\n                    return {\n                      label:\n                        role.metadata.annotations?.[\n                          rbacAnnotations.DISPLAY_NAME\n                        ] || role.metadata.name,\n                      value: role.metadata.name,\n                    };\n                  }) || []),\n                ]\"\n              />\n              <FilterDropdown\n                v-model=\"selectedSortValue\"\n                :label=\"$t('core.common.filters.labels.sort')\"\n                :items=\"[\n                  {\n                    label: t('core.common.filters.item_labels.default'),\n                  },\n                  {\n                    label: t('core.user.filters.sort.items.create_time_desc'),\n                    value: 'metadata.creationTimestamp,desc',\n                  },\n                  {\n                    label: t('core.user.filters.sort.items.create_time_asc'),\n                    value: 'metadata.creationTimestamp,asc',\n                  },\n                ]\"\n              />\n              <div class=\"flex flex-row gap-2\">\n                <div\n                  class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                  @click=\"refetch()\"\n                >\n                  <IconRefreshLine\n                    v-tooltip=\"$t('core.common.buttons.refresh')\"\n                    :class=\"{\n                      'animate-spin text-gray-900': isFetching,\n                    }\"\n                    class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                  />\n                </div>\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n\n      <VLoading v-if=\"isLoading\" />\n\n      <Transition v-else-if=\"!users?.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.user.empty.message')\"\n          :title=\"$t('core.user.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"refetch()\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton\n                v-permission=\"['system:users:manage']\"\n                type=\"secondary\"\n                @click=\"creationModal = true\"\n              >\n                <template #icon>\n                  <IconAddCircle />\n                </template>\n                {{ $t(\"core.common.buttons.new\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <UserListItem\n            v-for=\"user in users\"\n            :key=\"user.user.metadata.name\"\n            :user=\"user\"\n            :is-selected=\"checkSelection(user.user)\"\n          >\n            <template\n              v-if=\"utils.permission.has(['system:users:manage'])\"\n              #checkbox\n            >\n              <input\n                v-model=\"selectedUserNames\"\n                :value=\"user.user.metadata.name\"\n                name=\"user-checkbox\"\n                type=\"checkbox\"\n                :disabled=\"\n                  user.user.metadata.name === currentUser?.user.metadata.name\n                \"\n              />\n            </template>\n          </UserListItem>\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :total=\"total\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', { total: total })\n          \"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/components/GrantPermissionModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { User } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation } from \"@tanstack/vue-query\";\nimport { computed, onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { SUPER_ROLE_NAME } from \"@/constants/constants\";\nimport { useFetchRoles, useFetchRoleTemplates } from \"../composables/use-role\";\nimport RolesView from \"./RolesView.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    user?: User;\n  }>(),\n  {\n    user: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst selectedRoleNames = ref<string[]>([]);\n\nonMounted(() => {\n  if (!props.user) {\n    return;\n  }\n  selectedRoleNames.value = JSON.parse(\n    props.user.metadata.annotations?.[rbacAnnotations.ROLE_NAMES] || \"[]\"\n  );\n});\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"core:user:grant-permissions\"],\n  mutationFn: async ({ roles }: { roles: string[] }) => {\n    return await consoleApiClient.user.grantPermission({\n      name: props.user?.metadata.name as string,\n      grantRequest: {\n        roles: roles,\n      },\n    });\n  },\n  onSuccess() {\n    Toast.success(t(\"core.common.toast.operation_success\"));\n    modal.value?.close();\n  },\n});\n\nfunction onSubmit(data: { roles: string[] }) {\n  mutate({ roles: data.roles });\n}\n\nconst { data: allRoles } = useFetchRoles();\nconst { data: allRoleTemplates } = useFetchRoleTemplates();\n\nconst currentRoleTemplates = computed(() => {\n  if (!selectedRoleNames.value.length) {\n    return [];\n  }\n\n  const selectedRoles = allRoles.value?.filter((role) =>\n    selectedRoleNames.value.includes(role.metadata.name)\n  );\n\n  let allDependsRoleTemplates: string[] = [];\n\n  selectedRoles?.forEach((role) => {\n    allDependsRoleTemplates = allDependsRoleTemplates.concat(\n      JSON.parse(\n        role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] || \"[]\"\n      )\n    );\n  });\n\n  return allRoleTemplates.value?.filter((item) => {\n    return allDependsRoleTemplates.includes(item.metadata.name);\n  });\n});\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"$t('core.user.grant_permission_modal.title')\"\n    :width=\"600\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <FormKit\n        id=\"grant-permission-form\"\n        name=\"grant-permission-form\"\n        :config=\"{ validationVisibility: 'submit' }\"\n        type=\"form\"\n        @submit=\"onSubmit\"\n      >\n        <!-- @vue-ignore -->\n        <FormKit\n          v-model=\"selectedRoleNames\"\n          multiple\n          name=\"roles\"\n          :label=\"$t('core.user.grant_permission_modal.fields.role.label')\"\n          type=\"roleSelect\"\n          :placeholder=\"\n            $t('core.user.grant_permission_modal.fields.role.placeholder')\n          \"\n        ></FormKit>\n      </FormKit>\n\n      <div v-if=\"selectedRoleNames.length\">\n        <div\n          v-if=\"selectedRoleNames.includes(SUPER_ROLE_NAME)\"\n          class=\"mt-4 text-sm text-gray-600\"\n        >\n          {{ $t(\"core.user.grant_permission_modal.roles_preview.all\") }}\n        </div>\n\n        <div v-else-if=\"currentRoleTemplates?.length\" class=\"mt-4 space-y-3\">\n          <span class=\"text-sm text-gray-600\">\n            {{ $t(\"core.user.grant_permission_modal.roles_preview.includes\") }}\n          </span>\n          <RolesView :role-templates=\"currentRoleTemplates\" />\n        </div>\n      </div>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('grant-permission-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/components/RolesView.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Role } from \"@halo-dev/api-client\";\nimport { toRefs } from \"vue\";\nimport { useRoleTemplateSelection } from \"@/composables/use-role\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { pluginLabels } from \"@/constants/labels\";\n\nconst props = withDefaults(\n  defineProps<{\n    roleTemplates?: Role[];\n  }>(),\n  {\n    roleTemplates: () => [],\n  }\n);\n\nconst { roleTemplates } = toRefs(props);\n\nconst { roleTemplateGroups } = useRoleTemplateSelection(roleTemplates);\n</script>\n<template>\n  <dl\n    class=\"divide-y divide-gray-100 overflow-hidden rounded-base border border-gray-100\"\n  >\n    <div\n      v-for=\"(group, index) in roleTemplateGroups\"\n      :key=\"index\"\n      class=\"bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6\"\n    >\n      <dt class=\"text-sm font-medium text-gray-900\">\n        <div>\n          {{ $t(`core.rbac.${group.module}`, group.module as string) }}\n        </div>\n        <div\n          v-if=\"\n            group.roles.length &&\n            group.roles[0].metadata.labels?.[pluginLabels.NAME]\n          \"\n          class=\"mt-3 text-xs text-gray-500\"\n        >\n          <i18n-t keypath=\"core.role.common.text.provided_by_plugin\" tag=\"div\">\n            <template #plugin>\n              <RouterLink\n                :to=\"{\n                  name: 'PluginDetail',\n                  params: {\n                    name: group.roles[0].metadata.labels?.[pluginLabels.NAME],\n                  },\n                }\"\n                class=\"hover:text-blue-600\"\n              >\n                {{ group.roles[0].metadata.labels?.[pluginLabels.NAME] }}\n              </RouterLink>\n            </template>\n          </i18n-t>\n        </div>\n      </dt>\n      <dd class=\"mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0\">\n        <ul class=\"space-y-2\">\n          <li v-for=\"role in group.roles\" :key=\"role.metadata.name\">\n            <label\n              class=\"inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary\"\n            >\n              <input type=\"checkbox\" disabled checked />\n              <div class=\"flex flex-1 flex-col gap-y-3\">\n                <span class=\"font-medium text-gray-900\">\n                  {{\n                    $t(\n                      `core.rbac.${\n                        role.metadata.annotations?.[\n                          rbacAnnotations.DISPLAY_NAME\n                        ]\n                      }`,\n                      role.metadata.annotations?.[\n                        rbacAnnotations.DISPLAY_NAME\n                      ] as string\n                    )\n                  }}\n                </span>\n                <span\n                  v-if=\"\n                    role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES]\n                  \"\n                  class=\"text-xs text-gray-400\"\n                >\n                  {{\n                    $t(\"core.role.common.text.dependent_on\", {\n                      roles: JSON.parse(\n                        role.metadata.annotations?.[\n                          rbacAnnotations.DEPENDENCIES\n                        ]\n                      )\n                        .map((item: string) =>\n                          $t(`core.rbac.${item}`, item as string)\n                        )\n                        .join(\"，\"),\n                    })\n                  }}\n                </span>\n              </div>\n            </label>\n          </li>\n        </ul>\n      </dd>\n    </div>\n  </dl>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/components/UserCreationModal.vue",
    "content": "<script lang=\"ts\" setup>\n// core libs\nimport type { CreateUserRequest } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n// components\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\n// hooks\nimport { PASSWORD_REGEX } from \"@/constants/regex\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst formState = ref<CreateUserRequest>({\n  avatar: \"\",\n  bio: \"\",\n  displayName: \"\",\n  email: \"\",\n  name: \"\",\n  password: \"\",\n  roles: [],\n});\nconst isSubmitting = ref(false);\n\nonMounted(() => {\n  setFocus(\"creationUserNameInput\");\n});\n\nconst handleCreateUser = async () => {\n  try {\n    isSubmitting.value = true;\n\n    if (formState.value.name) {\n      formState.value.name = formState.value.name.trim();\n    }\n    if (formState.value.displayName) {\n      formState.value.displayName = formState.value.displayName.trim();\n    }\n\n    await consoleApiClient.user.createUser({\n      createUserRequest: formState.value,\n    });\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n\n    queryClient.invalidateQueries({ queryKey: [\"users\"] });\n  } catch (e) {\n    console.error(\"Failed to create or update user\", e);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"$t('core.user.editing_modal.titles.create')\"\n    :width=\"650\"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      id=\"user-creation-form\"\n      name=\"user-creation-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      type=\"form\"\n      @submit=\"handleCreateUser\"\n    >\n      <FormKit\n        id=\"creationUserNameInput\"\n        v-model=\"formState.name\"\n        :label=\"$t('core.user.editing_modal.fields.username.label')\"\n        type=\"text\"\n        name=\"name\"\n        :validation=\"[\n          ['required'],\n          ['length', 0, 63],\n          [\n            'matches',\n            /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/,\n          ],\n        ]\"\n        :validation-messages=\"{\n          matches: $t('core.user.editing_modal.fields.username.validation'),\n        }\"\n      ></FormKit>\n      <FormKit\n        v-model=\"formState.displayName\"\n        :label=\"$t('core.user.editing_modal.fields.display_name.label')\"\n        type=\"text\"\n        name=\"displayName\"\n        validation=\"required|length:0,50\"\n      ></FormKit>\n      <FormKit\n        v-model=\"formState.email\"\n        :label=\"$t('core.user.editing_modal.fields.email.label')\"\n        type=\"email\"\n        name=\"email\"\n        validation=\"required|email|length:0,100\"\n      ></FormKit>\n      <FormKit\n        v-model=\"formState.password\"\n        :label=\"$t('core.user.change_password_modal.fields.new_password.label')\"\n        type=\"password\"\n        name=\"password\"\n        :validation=\"[\n          ['required'],\n          ['length', 5, 257],\n          ['matches', PASSWORD_REGEX],\n        ]\"\n        :validation-messages=\"{\n          matches: $t('core.formkit.validation.password'),\n        }\"\n      ></FormKit>\n      <!-- @vue-ignore -->\n      <FormKit\n        v-model=\"formState.roles\"\n        :label=\"$t('core.user.grant_permission_modal.fields.role.label')\"\n        type=\"roleSelect\"\n        multiple\n        validation=\"required\"\n      ></FormKit>\n      <FormKit\n        v-model=\"formState.bio\"\n        :label=\"$t('core.user.editing_modal.fields.bio.label')\"\n        type=\"textarea\"\n        name=\"bio\"\n        auto-height\n        :max-auto-height=\"200\"\n        validation=\"length:0,2048\"\n      ></FormKit>\n    </FormKit>\n\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('user-creation-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/components/UserEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { User } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { nextTick, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    user: User;\n  }>(),\n  {}\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst formState = ref<User>(cloneDeep(props.user));\nconst isSubmitting = ref(false);\n\nconst annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();\n\nconst handleUpdateUser = async () => {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  formState.value.metadata.annotations = {\n    ...annotations,\n    ...customAnnotations,\n  };\n\n  try {\n    isSubmitting.value = true;\n\n    if (formState.value.spec.displayName) {\n      formState.value.spec.displayName =\n        formState.value.spec.displayName.trim();\n    }\n\n    await coreApiClient.user.updateUser({\n      name: formState.value.metadata.name,\n      user: formState.value,\n    });\n\n    modal.value?.close();\n\n    queryClient.invalidateQueries({ queryKey: [\"users\"] });\n    queryClient.invalidateQueries({ queryKey: [\"user-detail\"] });\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (e) {\n    console.error(\"Failed to create or update user\", e);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"$t('core.user.editing_modal.titles.update')\"\n    :width=\"700\"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      id=\"user-form\"\n      name=\"user-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      type=\"form\"\n      @submit=\"handleUpdateUser\"\n    >\n      <div>\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.user.editing_modal.groups.general\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              id=\"userNameInput\"\n              v-model=\"formState.metadata.name\"\n              :disabled=\"true\"\n              :label=\"$t('core.user.editing_modal.fields.username.label')\"\n              type=\"text\"\n              name=\"name\"\n            ></FormKit>\n            <FormKit\n              id=\"displayNameInput\"\n              v-model=\"formState.spec.displayName\"\n              :label=\"$t('core.user.editing_modal.fields.display_name.label')\"\n              type=\"text\"\n              name=\"displayName\"\n              validation=\"required|length:0,50\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.email\"\n              :label=\"$t('core.user.editing_modal.fields.email.label')\"\n              type=\"email\"\n              name=\"email\"\n              validation=\"required|email|length:0,100\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.bio\"\n              :label=\"$t('core.user.editing_modal.fields.bio.label')\"\n              type=\"textarea\"\n              name=\"bio\"\n              auto-height\n              :max-auto-height=\"200\"\n              validation=\"length:0,2048\"\n            ></FormKit>\n          </div>\n        </div>\n      </div>\n    </FormKit>\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.user.editing_modal.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"formState.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :value=\"formState.metadata.annotations\"\n          :form-data=\"formState\"\n          kind=\"User\"\n          group=\"\"\n        />\n      </div>\n    </div>\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('user-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/components/UserListItem.vue",
    "content": "<script setup lang=\"ts\">\nimport { coreApiClient, type ListedUser } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconShieldUser,\n  Toast,\n  VAvatar,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { storeToRefs } from \"pinia\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { useUserEnableDisable } from \"../composables/use-user\";\nimport GrantPermissionModal from \"./GrantPermissionModal.vue\";\nimport UserEditingModal from \"./UserEditingModal.vue\";\nimport UserPasswordChangeModal from \"./UserPasswordChangeModal.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    user: ListedUser;\n    isSelected?: boolean;\n  }>(),\n  { isSelected: false }\n);\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\nconst { currentUser } = storeToRefs(stores.currentUser());\n\nconst handleDelete = async () => {\n  Dialog.warning({\n    title: t(\"core.user.operations.delete.title\"),\n    description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      try {\n        await coreApiClient.user.deleteUser({\n          name: props.user.user.metadata.name,\n        });\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n      } catch (e) {\n        console.error(\"Failed to delete user\", e);\n      } finally {\n        queryClient.invalidateQueries({\n          queryKey: [\"users\"],\n        });\n      }\n    },\n  });\n};\n\nconst grantPermissionModal = ref(false);\n\nfunction onGrantPermissionModalClose() {\n  grantPermissionModal.value = false;\n  queryClient.invalidateQueries({\n    queryKey: [\"users\"],\n  });\n}\n\nconst passwordChangeModal = ref<boolean>(false);\n\nfunction onPasswordChangeModalClose() {\n  passwordChangeModal.value = false;\n  queryClient.invalidateQueries({\n    queryKey: [\"users\"],\n  });\n}\n\nconst editingModal = ref<boolean>(false);\n\nfunction onEditingModalClose() {\n  editingModal.value = false;\n  queryClient.invalidateQueries({\n    queryKey: [\"users\"],\n  });\n}\n\nconst { handleEnableOrDisableUser } = useUserEnableDisable();\n</script>\n\n<template>\n  <UserEditingModal\n    v-if=\"editingModal\"\n    :user=\"user.user\"\n    @close=\"onEditingModalClose\"\n  />\n  <GrantPermissionModal\n    v-if=\"grantPermissionModal\"\n    :user=\"user.user\"\n    @close=\"onGrantPermissionModalClose\"\n  />\n  <UserPasswordChangeModal\n    v-if=\"passwordChangeModal\"\n    :user=\"user.user\"\n    @close=\"onPasswordChangeModalClose\"\n  />\n  <VEntity :is-selected=\"isSelected\">\n    <template #checkbox>\n      <slot name=\"checkbox\" />\n    </template>\n    <template #start>\n      <VEntityField>\n        <template #description>\n          <VAvatar\n            :alt=\"user.user.spec.displayName\"\n            :src=\"user.user.spec.avatar\"\n            size=\"md\"\n          ></VAvatar>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"user.user.spec.displayName\"\n        :description=\"user.user.metadata.name\"\n        :route=\"{\n          name: 'UserDetail',\n          params: { name: user.user.metadata.name },\n        }\"\n      >\n        <template v-if=\"user.user.spec.disabled\" #extra>\n          <VTag>\n            {{ $t(\"core.user.fields.disabled\") }}\n          </VTag>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField>\n        <template #description>\n          <div class=\"flex flex-wrap gap-2\">\n            <VTag v-for=\"role in user.roles\" :key=\"role.metadata.name\">\n              <template #leftIcon>\n                <IconShieldUser />\n              </template>\n              {{\n                role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||\n                role.metadata.name\n              }}\n            </VTag>\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"user.user.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{ utils.date.format(user.user.metadata.creationTimestamp) }}\n          </span>\n        </template>\n      </VEntityField>\n    </template>\n    <template\n      v-if=\"utils.permission.has(['system:users:manage'])\"\n      #dropdownItems\n    >\n      <VDropdownItem @click=\"editingModal = true\">\n        {{ $t(\"core.user.operations.update_profile.title\") }}\n      </VDropdownItem>\n      <VDropdownItem @click=\"passwordChangeModal = true\">\n        {{ $t(\"core.user.operations.change_password.title\") }}\n      </VDropdownItem>\n      <VDropdownItem\n        v-if=\"currentUser?.user.metadata.name !== user.user.metadata.name\"\n        @click=\"grantPermissionModal = true\"\n      >\n        {{ $t(\"core.user.operations.grant_permission.title\") }}\n      </VDropdownItem>\n      <VDropdownDivider\n        v-if=\"currentUser?.user.metadata.name !== user.user.metadata.name\"\n      />\n      <VDropdownItem\n        v-if=\"currentUser?.user.metadata.name !== user.user.metadata.name\"\n        type=\"danger\"\n        @click=\"\n          handleEnableOrDisableUser({\n            name: user.user.metadata.name,\n            operation: user.user.spec.disabled ? 'enable' : 'disable',\n            onSuccess: () => {\n              queryClient.invalidateQueries({\n                queryKey: ['users'],\n              });\n            },\n          })\n        \"\n      >\n        {{\n          user.user.spec.disabled\n            ? $t(\"core.user.operations.enable.title\")\n            : $t(\"core.user.operations.disable.title\")\n        }}\n      </VDropdownItem>\n      <VDropdownItem\n        v-if=\"currentUser?.user.metadata.name !== user.user.metadata.name\"\n        type=\"danger\"\n        @click=\"handleDelete\"\n      >\n        {{ $t(\"core.common.buttons.delete\") }}\n      </VDropdownItem>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/components/UserPasswordChangeModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { User } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { onMounted, ref } from \"vue\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { PASSWORD_REGEX } from \"@/constants/regex\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    user?: User;\n  }>(),\n  {\n    user: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\ninterface PasswordChangeFormState {\n  password: string;\n  password_confirm?: string;\n}\n\nconst initialFormState: PasswordChangeFormState = {\n  password: \"\",\n  password_confirm: \"\",\n};\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst formState = ref<PasswordChangeFormState>(cloneDeep(initialFormState));\nconst isSubmitting = ref(false);\n\nonMounted(() => {\n  setFocus(\"passwordInput\");\n});\n\nconst handleChangePassword = async () => {\n  try {\n    isSubmitting.value = true;\n\n    const changePasswordRequest = cloneDeep(formState.value);\n    delete changePasswordRequest.password_confirm;\n\n    await consoleApiClient.user.changeAnyonePassword({\n      name: props.user?.metadata.name || \"\",\n      changePasswordRequest,\n    });\n\n    modal.value?.close();\n  } catch (e) {\n    console.error(e);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"500\"\n    :title=\"$t('core.user.change_password_modal.title')\"\n    @close=\"emit('close')\"\n  >\n    <!-- @vue-ignore -->\n    <FormKit\n      id=\"password-form\"\n      v-model=\"formState\"\n      name=\"password-form\"\n      :actions=\"false\"\n      type=\"form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleChangePassword\"\n    >\n      <FormKit\n        id=\"passwordInput\"\n        :label=\"$t('core.user.change_password_modal.fields.new_password.label')\"\n        name=\"password\"\n        type=\"password\"\n        :validation=\"[\n          ['required'],\n          ['length', 5, 257],\n          ['matches', PASSWORD_REGEX],\n        ]\"\n        :validation-messages=\"{\n          matches: $t('core.formkit.validation.password'),\n        }\"\n      ></FormKit>\n      <FormKit\n        :label=\"\n          $t('core.user.change_password_modal.fields.confirm_password.label')\n        \"\n        name=\"password_confirm\"\n        type=\"password\"\n        validation=\"confirm|required\"\n      ></FormKit>\n    </FormKit>\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('password-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/console-src/modules/system/users/composables/use-role.ts",
    "content": "import {\n  coreApiClient,\n  paginate,\n  type Role,\n  type RoleV1alpha1ApiListRoleRequest,\n} from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { roleLabels } from \"@/constants/labels\";\n\nexport function useFetchRoles() {\n  return useQuery({\n    queryKey: [\"core:roles\"],\n    queryFn: async () => {\n      return await paginate<RoleV1alpha1ApiListRoleRequest, Role>(\n        (params) => coreApiClient.role.listRole(params),\n        {\n          size: 1000,\n          labelSelector: [`!${roleLabels.TEMPLATE}`],\n        }\n      );\n    },\n    refetchInterval(data) {\n      const hasDeletingRole = data?.some(\n        (item) => !!item.metadata.deletionTimestamp\n      );\n      return hasDeletingRole ? 1000 : false;\n    },\n  });\n}\n\nexport function useFetchRoleTemplates() {\n  return useQuery({\n    queryKey: [\"core:role-templates\"],\n    queryFn: async () => {\n      return await paginate<RoleV1alpha1ApiListRoleRequest, Role>(\n        (params) => coreApiClient.role.listRole(params),\n        {\n          size: 1000,\n          labelSelector: [`${roleLabels.TEMPLATE}=true`, \"!halo.run/hidden\"],\n        }\n      );\n    },\n  });\n}\n"
  },
  {
    "path": "ui/console-src/modules/system/users/composables/use-user.ts",
    "content": "import { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { useI18n } from \"vue-i18n\";\n\nexport function useUserEnableDisable() {\n  const { t } = useI18n();\n\n  const handleEnableOrDisableUser = ({\n    name,\n    operation,\n    onSuccess,\n  }: {\n    name: string;\n    operation: \"enable\" | \"disable\";\n    onSuccess?: () => void;\n  }) => {\n    const operations = {\n      enable: {\n        title: t(\"core.user.operations.enable.title\"),\n        description: t(\"core.user.operations.enable.description\"),\n        request: (name: string) =>\n          consoleApiClient.user.enableUser({ username: name }),\n        message: t(\"core.common.toast.enable_success\"),\n      },\n      disable: {\n        title: t(\"core.user.operations.disable.title\"),\n        description: t(\"core.user.operations.disable.description\"),\n        request: (name: string) =>\n          consoleApiClient.user.disableUser({ username: name }),\n        message: t(\"core.common.toast.disable_success\"),\n      },\n    };\n\n    const operationConfig = operations[operation];\n\n    Dialog.warning({\n      title: operationConfig.title,\n      description: operationConfig.description,\n      confirmType: \"danger\",\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      onConfirm: async () => {\n        try {\n          await operationConfig.request(name);\n\n          Toast.success(operationConfig.message);\n          onSuccess?.();\n        } catch (e) {\n          console.error(\"Failed to enable or disable user\", e);\n        }\n      },\n    });\n  };\n\n  return {\n    handleEnableOrDisableUser,\n  };\n}\n"
  },
  {
    "path": "ui/console-src/modules/system/users/module.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport { IconUserSettings } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  routes: [\n    {\n      path: \"/users\",\n      name: \"UsersRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.user.title\",\n        searchable: true,\n        permissions: [\"system:users:view\"],\n        menu: {\n          name: \"core.sidebar.menu.items.users\",\n          group: \"system\",\n          icon: markRaw(IconUserSettings),\n          priority: 1,\n          mobile: true,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Users\",\n          component: () => import(\"./UserList.vue\"),\n        },\n        {\n          path: \":name\",\n          name: \"UserDetail\",\n          component: () => import(\"./UserDetail.vue\"),\n          meta: {\n            title: \"core.user.detail.title\",\n          },\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/console-src/modules/system/users/tabs/Detail.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { DetailedUser } from \"@halo-dev/api-client\";\nimport {\n  IconInformation,\n  VDescription,\n  VDescriptionItem,\n  VSpace,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport RiVerifiedBadgeLine from \"~icons/ri/verified-badge-line\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\n\nwithDefaults(defineProps<{ user?: DetailedUser }>(), {\n  user: undefined,\n});\n</script>\n<template>\n  <div class=\"border-t border-gray-100\">\n    <VDescription>\n      <VDescriptionItem\n        :label=\"$t('core.user.detail.fields.display_name')\"\n        :content=\"user?.user.spec.displayName\"\n        class=\"!px-2\"\n      />\n      <VDescriptionItem\n        :label=\"$t('core.user.detail.fields.username')\"\n        :content=\"user?.user.metadata.name\"\n        class=\"!px-2\"\n      />\n      <VDescriptionItem\n        :label=\"$t('core.user.detail.fields.email')\"\n        class=\"!px-2\"\n      >\n        <div v-if=\"user?.user.spec.email\" class=\"flex items-center space-x-2\">\n          <span>\n            {{ user?.user.spec.email }}\n          </span>\n          <RiVerifiedBadgeLine\n            v-if=\"user?.user.spec.emailVerified\"\n            v-tooltip=\"$t('core.user.detail.fields.email_verified.tooltip')\"\n            class=\"text-xs text-blue-600\"\n          />\n          <IconInformation\n            v-else\n            v-tooltip=\"$t('core.user.detail.fields.email_not_verified.tooltip')\"\n            class=\"text-xs text-red-500\"\n          />\n        </div>\n        <span v-else>\n          {{ $t(\"core.common.text.none\") }}\n        </span>\n      </VDescriptionItem>\n      <VDescriptionItem\n        :label=\"$t('core.user.detail.fields.roles')\"\n        class=\"!px-2\"\n      >\n        <VSpace>\n          <VTag\n            v-for=\"role in user?.roles\"\n            :key=\"role.metadata.name\"\n            @click=\"\n              $router.push({\n                name: 'RoleDetail',\n                params: { name: role.metadata.name },\n              })\n            \"\n          >\n            {{\n              role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||\n              role.metadata.name\n            }}\n          </VTag>\n        </VSpace>\n      </VDescriptionItem>\n      <VDescriptionItem\n        :label=\"$t('core.user.detail.fields.bio')\"\n        :content=\"user?.user.spec?.bio || $t('core.common.text.none')\"\n        class=\"!px-2\"\n      />\n      <VDescriptionItem\n        :label=\"$t('core.user.detail.fields.creation_time')\"\n        :content=\"utils.date.format(user?.user.metadata?.creationTimestamp)\"\n        class=\"!px-2\"\n      />\n    </VDescription>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/console-src/router/constant.ts",
    "content": "import type { MenuGroupType } from \"@halo-dev/ui-shared\";\n\nexport const coreMenuGroups: MenuGroupType[] = [\n  {\n    id: \"dashboard\",\n    name: undefined,\n    priority: 0,\n  },\n  {\n    id: \"content\",\n    name: \"core.sidebar.menu.groups.content\",\n    priority: 1,\n  },\n  {\n    id: \"interface\",\n    name: \"core.sidebar.menu.groups.interface\",\n    priority: 2,\n  },\n  {\n    id: \"system\",\n    name: \"core.sidebar.menu.groups.system\",\n    priority: 3,\n  },\n  {\n    id: \"tool\",\n    name: \"core.sidebar.menu.groups.tool\",\n    priority: 4,\n  },\n];\n"
  },
  {
    "path": "ui/console-src/router/guards/auth-check.ts",
    "content": "import { stores } from \"@halo-dev/ui-shared\";\nimport type { Router } from \"vue-router\";\n\nexport function setupAuthCheckGuard(router: Router) {\n  router.beforeEach((_to, _, next) => {\n    const currentUserStore = stores.currentUser();\n\n    if (currentUserStore.isAnonymous) {\n      window.location.href = `/login?redirect_uri=${encodeURIComponent(\n        window.location.href\n      )}`;\n      return;\n    }\n\n    next();\n  });\n}\n"
  },
  {
    "path": "ui/console-src/router/guards/permission.ts",
    "content": "import type { Role } from \"@halo-dev/api-client\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport type { RouteLocationNormalized, Router } from \"vue-router\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { SUPER_ROLE_NAME } from \"@/constants/constants\";\n\nexport function setupPermissionGuard(router: Router) {\n  router.beforeEach(async (to, _, next) => {\n    const currentUserStore = stores.currentUser();\n\n    if (isConsoleAccessDisallowed(currentUserStore.currentUser?.roles)) {\n      window.location.href = \"/uc\";\n      return;\n    }\n\n    if (\n      await checkRoutePermissions(\n        to,\n        utils.permission.getUserPermissions() || []\n      )\n    ) {\n      next();\n    } else {\n      next({ name: \"Forbidden\" });\n    }\n  });\n}\n\nfunction isConsoleAccessDisallowed(currentRoles?: Role[]): boolean {\n  if (currentRoles?.some((role) => role.metadata.name === SUPER_ROLE_NAME)) {\n    return false;\n  }\n\n  return (\n    currentRoles?.some(\n      (role) =>\n        role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===\n        \"true\"\n    ) || false\n  );\n}\n\nasync function checkRoutePermissions(\n  to: RouteLocationNormalized,\n  uiPermissions: string[]\n): Promise<boolean> {\n  const { meta } = to;\n\n  if (!meta?.permissions) {\n    return true;\n  }\n\n  if (typeof meta.permissions === \"function\") {\n    try {\n      return await meta.permissions(uiPermissions);\n    } catch (e) {\n      console.error(\n        `Error checking permissions for route ${String(to.name)}:`,\n        e\n      );\n      return false;\n    }\n  }\n\n  return utils.permission.has(meta.permissions as string[]);\n}\n"
  },
  {
    "path": "ui/console-src/router/index.ts",
    "content": "import routesConfig from \"@console/router/routes.config\";\nimport {\n  createRouter,\n  createWebHistory,\n  type RouteLocationNormalized,\n  type RouteLocationNormalizedLoaded,\n} from \"vue-router\";\nimport { setupStopImplicitSubmission } from \"@/formkit/plugins/stop-implicit-submission\";\nimport { setupProcessBarGuard } from \"@/router/process-bar\";\nimport { setupAuthCheckGuard } from \"./guards/auth-check\";\nimport { setupPermissionGuard } from \"./guards/permission\";\n\nconst router = createRouter({\n  history: createWebHistory(\"/console/\"),\n  routes: routesConfig,\n  scrollBehavior: (\n    to: RouteLocationNormalized,\n    from: RouteLocationNormalizedLoaded\n  ) => {\n    if (to.name !== from.name) {\n      return { left: 0, top: 0 };\n    }\n  },\n});\n\nsetupAuthCheckGuard(router);\nsetupPermissionGuard(router);\nsetupProcessBarGuard(router);\nsetupStopImplicitSubmission(router);\n\nexport default router;\n"
  },
  {
    "path": "ui/console-src/router/routes.config.ts",
    "content": "import BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport type { RouteRecordRaw } from \"vue-router\";\n\nexport const routes: Array<RouteRecordRaw> = [\n  {\n    path: \"/:pathMatch(.*)*\",\n    component: BasicLayout,\n    children: [\n      {\n        path: \"\",\n        name: \"NotFound\",\n        component: () => import(\"@/views/exceptions/NotFound.vue\"),\n      },\n    ],\n  },\n  {\n    path: \"/403\",\n    component: BasicLayout,\n    children: [\n      {\n        path: \"\",\n        name: \"Forbidden\",\n        component: () => import(\"@/views/exceptions/Forbidden.vue\"),\n      },\n    ],\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "ui/console-src/stores/theme.ts",
    "content": "import type { Theme } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { defineStore } from \"pinia\";\nimport { ref } from \"vue\";\n\nexport const useThemeStore = defineStore(\"theme\", () => {\n  const activatedTheme = ref<Theme>();\n\n  async function fetchActivatedTheme() {\n    if (!utils.permission.has([\"system:themes:view\"])) {\n      return;\n    }\n\n    try {\n      const { data } = await consoleApiClient.theme.theme.fetchActivatedTheme({\n        mute: true,\n      });\n\n      if (data) {\n        activatedTheme.value = data;\n      }\n    } catch (e) {\n      console.error(\"Failed to fetch active theme\", e);\n    }\n  }\n\n  return { activatedTheme, fetchActivatedTheme };\n});\n"
  },
  {
    "path": "ui/console.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta content=\"IE=edge\" http-equiv=\"X-UA-Compatible\" />\n    <meta content=\"webkit\" name=\"renderer\" />\n    <meta\n      content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\"\n      name=\"viewport\"\n    />\n    <meta content=\"noindex,nofollow\" name=\"robots\" />\n    <title></title>\n    <link href=\"/ui-assets/favicon.ico\" rel=\"shortcut icon\" type=\"image/x-icon\" />\n    <link href=\"/ui-assets/favicon.ico\" rel=\"icon\" type=\"image/x-icon\" />\n    <meta name=\"msapplication-TileColor\" content=\"#da532c\" />\n    <meta name=\"theme-color\" content=\"#ffffff\" />\n    <style>\n      body {\n        height: 100%;\n        background-color: #f5f5f5;\n      }\n\n      #loader {\n        position: absolute;\n        top: 0;\n        right: 0;\n        bottom: 0;\n        left: 0;\n        margin: auto;\n        border: solid 3px #e5e5e5;\n        border-top-color: #333;\n        border-radius: 50%;\n        width: 30px;\n        height: 30px;\n        animation: spin 0.6s linear infinite;\n      }\n\n      @keyframes spin {\n        to {\n          transform: rotate(360deg);\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <noscript>\n      <strong>\n        We're sorry but halo console client doesn't work properly without JavaScript enabled. Please enable it to\n        continue.\n      </strong>\n    </noscript>\n    <div id=\"app\">\n      <div id=\"loader\"></div>\n    </div>\n    <script src=\"/console-src/main.ts\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "ui/docs/components/README.md",
    "content": "# Console 组件介绍\n\n目前 Console 的组件包含基础组件（`@halo-dev/components`）和 Console 端的业务组件，这两种组件都可以在插件中使用。\n\n## 业务组件\n\n### AnnotationsForm\n\n此组件用于为自定义模型设置 annotations 数据，同时支持自定义 key / value 和自定义表单，表单定义方式可以参考：<https://docs.halo.run/developer-guide/annotations-form>\n\n使用方式：\n\n```vue\n<script setup lang=\"ts\">\nconst formState = ref({\n  metadata: {\n    annotations: {}\n  }\n})\n\nconst annotationsFormRef = ref();\n\nasync function handleSubmit () {\n  annotationsFormRef.value?.handleSubmit();\n\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n\n  // AnnotationsForm 中的表单校验失败时，不提交数据\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  // 合并数据，此对象即可最终设置给模型的 metadata.annotations\n  const annotations = {\n    ...annotations,\n    ...customAnnotations,\n  }\n}\n</script>\n\n<template>\n  <AnnotationsForm\n    ref=\"annotationsFormRef\"\n    :value=\"formState.metadata.annotations\"\n    kind=\"Post\"\n    group=\"content.halo.run\"\n  />\n</template>\n```\n\n其中，kind 和 group 为必填项，分别表示模型的 kind 和 group。\n"
  },
  {
    "path": "ui/docs/custom-formkit-input/README.md",
    "content": "# 自定义 FormKit 输入组件\n\n## 原由\n\n目前在 Console 端的所有表单都使用了 FormKit，但 FormKit 内置的 Input 组件并不满足所有的需求，因此需要自定义一些 Input 组件。此外，为了插件和主题能够更加方便的使用系统内的一些数据，所以同样需要自定义一些带数据的选择组件。\n\n## 使用方式\n\n目前已提供以下类型：\n\n- `code`: 代码编辑器\n  - 参数\n    1. language: 目前支持 `yaml`, `html`, `css`, `javascript`, `json`\n    2. height: 编辑器高度，如：`100px`\n- `attachment`: 附件选择\n  - 参数\n    1. accepts：允许上传的文件类型，如：`image/*`\n- `repeater`: 定义一个对象集合，可以让使用者可视化的操作集合。\n  - 参数\n    1. min: 最小数量，默认为 `0`\n    2. max: 最大数量，默认为 `Infinity`，即无限制。\n    3. addLabel: 添加按钮的文本，默认为 `添加`\n    4. addButton: 是否显示添加按钮，默认为 `true`\n    5. upControl: 是否显示上移按钮，默认为 `true`\n    6. downControl: 是否显示下移按钮，默认为 `true`\n    7. insertControl: 是否显示插入按钮，默认为 `true`\n    8. removeControl: 是否显示删除按钮，默认为 `true`\n- `list`: 动态列表，定义一个数组列表。\n  - 参数\n    1. itemType: 列表项的数据类型，用于初始化数据类型，可选参数 `string`, `number`, `boolean`, `object`，默认为 `string`\n    2. min: 最小数量，默认为 `0`\n    3. max: 最大数量，默认为 `Infinity`，即无限制。\n    4. addLabel: 添加按钮的文本，默认为 `添加`\n    5. addButton: 是否显示添加按钮，默认为 `true`\n    6. upControl: 是否显示上移按钮，默认为 `true`\n    7. downControl: 是否显示下移按钮，默认为 `true`\n    8. insertControl: 是否显示插入按钮，默认为 `true`\n    9. removeControl: 是否显示删除按钮，默认为 `true`\n- `menuCheckbox`：选择一组菜单\n- `menuRadio`：选择一个菜单\n- `menuSelect`: 通用菜单选择组件，支持单选、多选、排序\n- `menuItemSelect`：选择菜单项\n- `postSelect`：选择文章\n- `singlePageSelect`：选择自定义页面\n- `categorySelect`：选择分类\n  - 参数\n    1. multiple: 是否多选，默认为 `false`\n- `categoryCheckbox`：选择多个分类\n- `tagSelect`：选择标签\n  - 参数\n    1. multiple: 是否多选，默认为 `false`\n- `tagCheckbox`：选择多个标签\n- `verificationForm`: 远程验证一组数据是否符合某个规则\n  - 参数\n    1. action: 对目标数据进行验证的接口地址\n    2. label: 验证按钮文本\n    3. buttonAttrs: 验证按钮的额外属性\n- `secret`: 用于选择或者管理密钥（Secret）\n  - 参数\n    1. requiredKeys：用于确认所需密钥的字段名称，数组类型，每个元素包含 `key` 和 `help` 两个属性。\n- `select`: 自定义的选择器组件，用于在备选项中选择一个或多个选项\n  - 参数\n    1. `options`：静态数据源。当 `action` 或 `remote` 存在时，此参数无效。\n    2. `action`：远程动态数据源的接口地址。\n    3. `requestOption`： 动态数据源的请求参数，可以通过此参数来指定如何获取数据，适配不同的接口。当 `action` 存在时，此参数有效。\n    4. `remote`：标识当前是否由用户自定义的远程数据源。\n    5. `remoteOption`：当 `remote` 为 `true` 时，此配置项必须存在，用于为 Select 组件提供处理搜索及查询键值对的方法。\n    6. `remoteOptimize`：是否开启远程数据源优化，默认为 `true`。开启后，将会对远程数据源进行优化，减少请求次数。仅在动态数据源下有效。\n    7. `allowCreate`：是否允许创建新选项，默认为 `false`。仅在静态数据源下有效，需要同时开启 `searchable`。\n    8. `clearable`：是否允许清空选项，默认为 `false`。\n    9. `multiple`：是否多选，默认为 `false`。\n    10. `maxCount`：多选时最大可选数量，默认为 `Infinity`。仅在多选时有效。\n    11. `sortable`：是否支持拖动排序，默认为 `false`。仅在多选时有效。\n    12. `searchable`：是否支持搜索内容，默认为 `false`。\n    13. `autoSelect`：当 value 不存在时，是否自动选择第一个选项，默认为 `true`。仅在单选时有效。\n\n在 Vue 单组件中使用：\n\n```vue\n<script lang=\"ts\" setup>\nconst postName = ref(\"\");\n</script>\n\n<template>\n  <FormKit\n    v-model=\"postName\"\n    placeholder=\"请选择文章\"\n    label=\"文章\"\n    type=\"postSelect\"\n    validation=\"required\"\n  />\n</template>\n```\n\n在 FormKit Schema 中使用（插件 / 主题设置表单定义）：\n\n```yaml\n- $formkit: menuRadio\n  name: menus\n  label: 底部菜单组\n```\n\n### select\n\nselect 是一个选择器类型的输入组件，使用者可以从一批待选数据中选择一个或多个选项。它支持单选、多选操作，并且支持静态数据及远程动态数据加载等多种方式。\n\n#### 在 Vue SFC 中以组件形式使用\n\n静态数据源：\n\n```vue\n<script lang=\"ts\" setup></script>\n<template>\n  <FormKit\n    type=\"select\"\n    label=\"What country makes the best food?\"\n    name=\"countries\"\n    placeholder=\"Select a country\"\n    allow-create\n    clearable\n    sortable\n    multiple\n    searchable\n    :options=\"[\n      { label: 'China', value: 'China' },\n      { label: 'USA', value: 'USA' },\n      { label: 'Japan', value: 'Japan' },\n      { label: 'Korea', value: 'Korea' },\n      { label: 'France', value: 'France' },\n      { label: 'Italy', value: 'Italy' },\n      { label: 'Germany', value: 'Germany' },\n      { label: 'UK', value: 'UK' },\n      { label: 'Canada', value: 'Canada' },\n      { label: 'Australia', value: 'Australia' },\n    ]\"\n    help=\"Don’t worry, you can’t get this one wrong.\"\n  />\n</template>\n```\n\n动态数据源：\n\n```vue\n<script lang=\"ts\" setup>\nconst ANONYMOUSUSER_NAME = \"anonymousUser\";\nconst DELETEDUSER_NAME = \"ghost\";\n\nconst handleSelectPostAuthorRemote = {\n  search: async ({ keyword, page, size }) => {\n    const { data } = await consoleApiClient.user.listUsers({\n      page,\n      size,\n      keyword,\n      fieldSelector: [\n        `name!=${ANONYMOUSUSER_NAME}`,\n        `name!=${DELETEDUSER_NAME}`,\n      ],\n    });\n    return {\n      options: data.items.map((item) => ({\n        label: item.user.spec.displayName,\n        value: item.user.metadata.name,\n      })),\n      total: data.total,\n      page: data.page,\n      size: data.size,\n    };\n  },\n\n  findOptionsByValues: () => {\n    return [];\n  },\n};\n</script>\n<template>\n  <FormKit\n    type=\"select\"\n    label=\"The author of the post is?\"\n    name=\"post_author\"\n    placeholder=\"Select a user\"\n    searchable\n    remote\n    :remote-option=\"handleSelectPostAuthorRemote\"\n  />\n</template>\n```\n\n#### 在 FormKit Schema 中使用\n\n静态数据源：\n\n```yaml\n- $formkit: select\n  name: countries\n  label: What country makes the best food?\n  sortable: true\n  multiple: true\n  clearable: true\n  placeholder: Select a country\n  options:\n    - label: China\n      value: cn\n    - label: France\n      value: fr\n    - label: Germany\n      value: de\n    - label: Spain\n      value: es\n    - label: Italy\n      value: ie\n    - label: Greece\n      value: gr\n```\n\n远程动态数据源：\n\n支持远程动态数据源，通过 `action` 和 `requestOption` 参数来指定如何获取数据。\n\n请求的接口将会自动拼接 `page`、`size` 与 `keyword` 参数，其中 `keyword` 为搜索关键词。\n\n```yaml\n- $formkit: select\n  name: postName\n  label: Choose an post\n  clearable: true\n  action: /apis/api.console.halo.run/v1alpha1/posts\n  requestOption:\n    method: GET\n    pageField: page\n    sizeField: size\n    totalField: total\n    itemsField: items\n    labelField: post.spec.title\n    valueField: post.metadata.name\n    fieldSelectorKey: metadata.name\n```\n\n> [!NOTE]\n> 当远程数据具有分页时，可能会出现默认选项不在第一页的情况，此时 Select 组件将会发送另一个查询请求，以获取默认选项的数据。此接口会携带如下参数 `fieldSelector: ${requestOption.fieldSelectorKey}=(value1,value2,value3)`。\n\n> 其中，value1, value2, value3 为默认选项的值。返回值与查询一致，通过 `requestOption` 解析。\n\n### list\n\nlist 是一个数组类型的输入组件，可以让使用者可视化的操作数组。它支持动态添加、删除、上移、下移、插入数组项等操作。\n\n在 Vue SFC 中以组件形式使用：\n\n```vue\n<script lang=\"ts\" setup>\nconst users = ref([]);\n</script>\n\n<template>\n  <FormKit\n    :min=\"1\"\n    :max=\"3\"\n    type=\"list\"\n    label=\"Users\"\n    add-label=\"Add User\"\n    item-type=\"string\"\n  >\n    <template #default=\"{ index }\">\n      <FormKit\n        type=\"text\"\n        :index=\"index\"\n        validation=\"required\"\n      />\n    </template>\n  </FormKit>\n</template>\n```\n\n在 FormKit Schema 中使用：\n\n```yaml\n- $formkit: list\n  name: users\n  label: Users\n  addLabel: Add User\n  min: 1\n  max: 3\n  itemType: string\n  children:\n    - $formkit: text\n      index: \"$index\"\n      validation: required\n```\n\n> [!NOTE]\n> `list` 组件有且只有一个子节点，并且必须为子节点传递 `index` 属性。若想提供多个字段，则建议使用 `group` 组件包裹。\n\n最终得到的数据类似于：\n\n```json\n{\n  \"users\": [\n    \"Jack\",\n    \"John\"\n  ]\n}\n```\n\n### Repeater\n\nRepeater 是一个集合类型的输入组件，可以让使用者可视化的操作集合。\n\n在 Vue SFC 中以组件形式使用：\n\n```vue\n<script lang=\"ts\" setup>\nconst users = ref([]);\n</script>\n\n<template>\n  <FormKit\n    v-model=\"users\"\n    :min=\"1\"\n    :max=\"3\"\n    addLabel=\"Add User\"\n    type=\"repeater\"\n    label=\"Users\"\n  >\n    <FormKit\n      type=\"text\"\n      label=\"Full Name\"\n      name=\"full_name\"\n      validation=\"required\"\n    />\n    <FormKit\n      type=\"email\"\n      label=\"Email\"\n      name=\"email\"\n      validation=\"required|email\"\n    />\n  </FormKit>\n</template>\n```\n\n在 FormKit Schema 中使用：\n\n```yaml\n- $formkit: repeater\n  name: users\n  label: Users\n  addLabel: Add User\n  min: 1\n  max: 3\n  items:\n    - $formkit: text\n      name: full_name\n      label: Full Name\n      validation: required\n    - $formkit: email\n      name: email\n      label: Email\n      validation: required|email\n```\n\n最终得到的数据类似于：\n\n```json\n[\n  {\n    \"full_name\": \"Jack\",\n    \"email\": \"jack@example.com\"\n  },\n  {\n    \"full_name\": \"John\",\n    \"email\": \"john@example.com\"\n  }\n]\n```\n"
  },
  {
    "path": "ui/docs/extension-points/backup.md",
    "content": "# 备份页面选项卡扩展点\n\n## 原由\n\n在 Halo 2.8 中提供了基础备份和恢复的功能，此扩展点是为了提供给插件开发者针对备份扩展更多功能，比如定时备份设置、备份到第三方云存储等。\n\n## 定义方式\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport BackupStorage from \"@/views/BackupStorage.vue\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  components: {},\n  routes: [],\n  extensionPoints: {\n    \"backup:tabs:create\": () => {\n      return [\n        {\n          id: \"storage\",\n          label: \"备份位置\",\n          component: markRaw(BackupStorage),\n        },\n      ];\n    },\n  },\n});\n```\n\nBackupTab 类型：\n\n```ts\nimport type { Component, Raw } from \"vue\";\n\nexport interface BackupTab {\n  id: string;\n  label: string;\n  component: Raw<Component>;\n  permissions?: string[];\n}\n\n```\n"
  },
  {
    "path": "ui/docs/extension-points/comment-content.md",
    "content": "# 评论列表内容显示扩展点\n\n用于替换 Halo 在 Console 的默认评论列表内容显示组件。\n\n> 注意：\n> 此扩展点并非通用扩展点，由于 Halo 早期设定，Halo 在前台的评论组件 UI 部分由 [评论组件插件](http://github.com/halo-dev/plugin-comment-widget) 提供，而在此插件的后续版本中提供了富文本渲染的功能，所以为了保持 Console 的评论列表内容显示与前台一致，所以专为此插件提供了替换输入框的扩展点。\n\n## 定义方式\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport CommentContent from \"./components/CommentContent.vue\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"comment:list-item:content:replace\": () => {\n      return {\n        component: markRaw(CommentContent),\n      };\n    },\n  },\n});\n```\n\n其中，组件需要包含的 props 如下：\n\n1. `content`：评论内容，`html` 格式。\n\n"
  },
  {
    "path": "ui/docs/extension-points/comment-editor.md",
    "content": "# 评论编辑器扩展点\n\n用于替换 Halo 在 Console 的默认评论输入框。\n\n> 注意：\n> 此扩展点并非通用扩展点，由于 Halo 早期设定，Halo 在前台的评论组件 UI 部分由 [评论组件插件](http://github.com/halo-dev/plugin-comment-widget) 提供，而在此插件的后续版本中提供了富文本编辑器的功能，所以为了保持 Console 的评论输入框与前台一致，所以专为此插件提供了替换输入框的扩展点。\n\n## 定义方式\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport CommentEditor from \"./components/CommentEditor.vue\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"comment:editor:replace\": () => {\n      return {\n        component: markRaw(CommentEditor),\n      };\n    },\n  },\n});\n```\n\n其中，组件需要包含的 props 如下：\n\n1. `autoFocus`：是否自动聚焦，需要在组件中判断是否为 `true`，然后聚焦输入框。\n\n需要定义的 emit 如下：\n\n1. `(event: \"update\", value: { content: string; characterCount: number })`：向调用方传递内容和字符数更新的事件。\n"
  },
  {
    "path": "ui/docs/extension-points/comment-subject-ref.md",
    "content": "# 评论来源显示拓展点\n\n在 Console 中，评论管理列表的评论来源默认仅支持显示来自文章和页面的评论，如果其他插件中的业务模块也使用了评论，那么就可以通过该拓展点来扩展评论来源的显示。\n\n## 定义方式\n\n假设以文章为例：\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport type { CommentSubjectRefResult } from \"@halo-dev/ui-shared\";\nimport type { Extension } from \"@halo-dev/api-client\";\nimport type { Post } from \"./types\";\n\nexport default definePlugin({\n  components: {},\n  extensionPoints: {\n    \"comment:subject-ref:create\": () => {\n      return [\n        {\n          kind: \"Post\",\n          group: \"post.halo.run\",\n          resolve: (subject: Extension): CommentSubjectRefResult => {\n            const post = subject as Post;\n            return {\n              label: \"文章\",\n              title: post.spec.title,\n              externalUrl: post.status.permalink,\n              route: {\n                name: \"PostEditor\",\n                params: {\n                  name: post.metadata.name\n                }\n              },\n            };\n          },\n        },\n      ];\n    },\n  },\n});\n```\n\n类型定义如下：\n\n```ts\ntype CommentSubjectRefProvider = {\n  kind: string;               // 自定义模型的类型\n  group: string;              // 自定义模型的分组\n  resolve: (subject: Extension) => CommentSubjectRefResult;\n}\n\ninterface CommentSubjectRefResult {\n  label: string;              // 来源名称（类型）\n  title: string;              // 来源标题\n  route?: RouteLocationRaw;   // Console 的路由，可以设置为来源的详情或者编辑页面\n  externalUrl?: string;       // 访问地址，可以设置为前台资源的访问地址\n}\n```\n"
  },
  {
    "path": "ui/docs/extension-points/dashboard.md",
    "content": "# 仪表盘扩展点\n\n## 概述\n\n仪表盘扩展点允许插件为 Halo 的控制台仪表盘添加自定义小部件和快速操作项。通过这些扩展点，插件可以：\n\n- 创建自定义的仪表盘小部件来展示特定数据或功能\n- 为快速操作小部件添加自定义操作项\n\n## console:dashboard:widgets:create\n\n此扩展点用于创建自定义的仪表盘小部件。\n\n### 定义方式\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport MyCustomWidget from \"./components/MyCustomWidget.vue\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"console:dashboard:widgets:create\": () => {\n      return [\n        {\n          id: \"my-custom-widget\",\n          component: markRaw(MyCustomWidget),\n          group: \"my-plugin\",\n          configFormKitSchema: [\n            {\n              $formkit: \"text\",\n              name: \"title\",\n              label: \"标题\",\n              value: \"默认标题\",\n            },\n            {\n              $formkit: \"number\",\n              name: \"refresh_interval\",\n              label: \"刷新间隔（秒）\",\n              value: 30,\n              min: 10,\n            },\n          ],\n          defaultConfig: {\n            title: \"我的自定义小部件\",\n            refresh_interval: 30,\n          },\n          defaultSize: {\n            w: 6,\n            h: 8,\n            minW: 3,\n            minH: 4,\n            maxW: 12,\n            maxH: 16,\n          },\n          permissions: [\"plugin:my-plugin:view\"],\n        },\n      ];\n    },\n  },\n});\n```\n\n### DashboardWidgetDefinition 类型\n\n```ts\nexport interface DashboardWidgetDefinition {\n  id: string;                                    // 小部件唯一标识符\n  component: Raw<Component>;                     // 小部件 Vue 组件\n  group: string;                                 // 小部件分组，用于在小部件库中分类显示\n  configFormKitSchema?:\n    | Record<string, unknown>[]\n    | (() => Promise<Record<string, unknown>[]>)\n    | (() => Record<string, unknown>[]);         // 配置表单 FormKit 定义，支持异步函数\n  defaultConfig?: Record<string, unknown>;       // 默认配置\n  defaultSize: {                                 // 默认尺寸\n    w: number;                                   // 宽度（网格单位），根据不同屏幕尺寸，网格单位不同，可参考：{ lg: 12, md: 12, sm: 6, xs: 4 }\n    h: number;                                   // 高度（网格单位）\n    minW?: number;                               // 最小宽度\n    minH?: number;                               // 最小高度\n    maxW?: number;                               // 最大宽度\n    maxH?: number;                               // 最大高度\n  };\n  permissions?: string[];                        // 访问权限\n}\n```\n\n### 小部件组件开发\n\n```vue\n<template>\n  <WidgetCard v-bind=\"$attrs\" :body-class=\"['!p-0']\">\n    <template #title>\n      <div class=\"inline-flex items-center gap-2\">\n        <div class=\"text-base font-medium flex-1\">\n          {{ config?.title || \"默认标题\" }}\n        </div>\n        <IconSettings\n          v-if=\"editMode\"\n          class=\"hover:text-gray-600 cursor-pointer\"\n          @click=\"showConfigModal = true\"\n        />\n      </div>\n    </template>\n    \n    <!-- 小部件内容 -->\n    <div class=\"p-4\">\n      <div v-if=\"previewMode\" class=\"text-center text-gray-500\">\n        预览模式\n      </div>\n      <div v-else>\n        <!-- 实际小部件内容 -->\n        <p>刷新间隔：{{ config?.refresh_interval || 30 }}秒</p>\n      </div>\n    </div>\n  </WidgetCard>\n</template>\n\n<script lang=\"ts\" setup>\nimport { IconSettings } from \"@halo-dev/components\";\nimport { ref } from \"vue\";\n\nconst props = defineProps<{\n  editMode?: boolean;        // 是否为编辑模式\n  previewMode?: boolean;     // 是否为预览模式\n  config?: Record<string, unknown>; // 小部件配置\n}>();\n\nconst emit = defineEmits<{\n  // \n  (e: \"update:config\", config: Record<string, unknown>): void;\n}>();\n\n</script>\n```\n\n**小部件组件的属性与事件：**\n\n| 属性          | 类型                    | 说明           |\n|---------------|-------------------------|--------------|\n| `editMode`    | boolean                 | 是否为编辑模式 |\n| `previewMode` | boolean                 | 是否为预览模式 |\n| `config`      | Record<string, unknown> | 小部件配置     |\n\n| 事件            | 说明               |\n|-----------------|------------------|\n| `update:config` | 小部件配置更新事件 |\n\n**WidgetCard 组件的属性与插槽：**\n\n| 属性          | 类型                    | 说明           |\n|---------------|-------------------------|--------------|\n| `title`    | string                 | 小部件标题 |\n| `bodyClass` | string[]                 | 小部件内容区域样式     |\n\n| 插槽      | 说明         |\n|-----------|------------|\n| `title`   | 小部件标题   |\n| `default` | 小部件内容   |\n| `actions` | 小部件操作项 |\n\n**重要说明：**\n\n- 小部件组件必须使用 `WidgetCard` 作为根组件，此组件已经在全局注册，不需要导入\n- 支持 `editMode` 和 `previewMode` 两种模式，当仪表盘处于编辑页面时，`editMode` 为 `true`，在小组件选择列表时，`previewMode` 为 `true`。可以根据这两个属性来控制小部件的显示内容\n- `update:config` 事件通常不需要实现，已经在内部实现了打开配置表单的功能，此事件用于自行实现配置表单\n- 使用 `markRaw()` 包装组件以避免响应式转换\n\n## console:dashboard:widgets:internal:quick-action:item:create\n\n此扩展点用于为快速操作小部件添加自定义操作项。\n\n### 定义方式\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport { IconPlug } from \"@halo-dev/components\";\nimport { useRouter } from \"vue-router\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"console:dashboard:widgets:internal:quick-action:item:create\": () => {\n      return [\n        {\n          id: \"my-plugin-action\",\n          icon: markRaw(IconPlug),\n          title: \"我的插件操作\",\n          action: () => {\n            // do something\n          },\n          permissions: [\"plugin:my-plugin:manage\"],\n        },\n      ];\n    },\n  },\n});\n```\n\n### 自定义组件操作项\n\n你也可以提供自定义组件而不是标准的操作项：\n\n```ts\nimport CustomActionItem from \"./components/CustomActionItem.vue\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"console:dashboard:widgets:internal:quick-action:item:create\": () => {\n      return [\n        {\n          id: \"custom-action\",\n          component: markRaw(CustomActionItem),\n          permissions: [\"plugin:my-plugin:view\"],\n        },\n      ];\n    },\n  },\n});\n```\n\n自定义组件：\n\n```vue\n<template>\n  <div class=\"group relative cursor-pointer rounded-lg bg-blue-50 p-4 transition-all hover:bg-blue-100\">\n    <div class=\"flex items-center gap-3\">\n      <component :is=\"item.icon\" class=\"text-blue-600\" />\n      <div>\n        <h3 class=\"text-sm font-semibold text-blue-900\">\n          {{ item.title }}\n        </h3>\n        <p class=\"text-xs text-blue-700\">自定义操作描述</p>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport type { DashboardWidgetQuickActionItem } from \"@halo-dev/ui-shared\";\n\ndefineProps<{\n  item: DashboardWidgetQuickActionItem;\n}>();\n</script>\n```\n\n### DashboardWidgetQuickActionItem 类型\n\n```ts\ninterface DashboardWidgetQuickActionBaseItem {\n  id: string;                    // 操作项唯一标识符\n  permissions?: string[];        // 访问权限\n}\n\ninterface DashboardWidgetQuickActionComponentItem\n  extends DashboardWidgetQuickActionBaseItem {\n  component: Raw<Component>;     // 自定义组件\n  icon?: Raw<Component>;         // 图标（可选）\n  title?: string;                // 标题（可选）\n  action?: () => void;           // 点击操作（可选）\n}\n\ninterface DashboardWidgetQuickActionStandardItem\n  extends DashboardWidgetQuickActionBaseItem {\n  component?: never;             // 不使用自定义组件\n  icon: Raw<Component>;          // 图标（必需）\n  title: string;                 // 标题（必需）\n  action: () => void;            // 点击操作（必需）\n}\n\ninterface DashboardWidgetQuickActionRouteItem\n  extends DashboardWidgetQuickActionBaseItem {\n  component?: never;             // 不使用自定义组件\n  action?: never;                // 不使用 action 回调\n  icon: Raw<Component>;          // 图标组件（必需）\n  title: string;                 // 标题文本（必需）\n  route: RouteLocationRaw;       // 导航目标路由（必需），可以是路由名称、路径或完整路由配置\n}\n\n\nexport type DashboardWidgetQuickActionItem =\n  | DashboardWidgetQuickActionComponentItem\n  | DashboardWidgetQuickActionStandardItem\n  | DashboardWidgetQuickActionRouteItem;\n```\n\n## 权限控制\n\n两个扩展点都支持权限控制：\n\n- **小部件权限**：通过 `permissions` 字段控制小部件的显示\n- **操作项权限**：通过 `permissions` 字段控制快速操作项的显示\n\n权限检查会自动进行，用户只能看到有权限访问的小部件和操作项。\n"
  },
  {
    "path": "ui/docs/extension-points/default-editor–extension.md",
    "content": "# 默认编辑器扩展点\n\n该扩展点用于扩展默认编辑器的功能，包括 Tiptap Extension，以及工具栏、悬浮工具栏、Slash Command。\n\n## 定义方式\n\n```ts\nimport ExtensionFoo from \"./tiptap/extension-foo.ts\"\n\nexport default definePlugin({\n  extensionPoints: {\n    \"default:editor:extension:create\": () => {\n      return [ExtensionFoo];\n    },\n  },\n});\n```\n\n其中，`ExtensionFoo` 是一个 Tiptap Extension，可以参考 [Tiptap 文档](https://tiptap.dev/) 和 [https://github.com/halo-sigs/richtext-editor/blob/main/docs/extension.md](https://github.com/halo-sigs/richtext-editor/blob/main/docs/extension.md)。\n"
  },
  {
    "path": "ui/docs/extension-points/editor.md",
    "content": "# 编辑器集成扩展点\n\n## 定义方式\n\n```ts\nimport MarkdownEditor from \"./components/MarkdownEditor.vue\"\n\nexport default definePlugin({\n  extensionPoints: {\n    \"editor:create\": () => {\n      return [\n        {\n          name: \"markdown-editor\",\n          displayName: \"Markdown\",\n          logo: \"logo.png\"\n          component: markRaw(MarkdownEditor),\n          rawType: \"markdown\",\n        },\n      ];\n    },\n  },\n});\n```\n\n- name: 编辑器名称，用于标识编辑器\n- displayName: 编辑器显示名称\n- component: 编辑器组件\n- rawType: 编辑器支持的原始类型，可以完全由插件定义。但必须保证最终能够将渲染后的 html 设置到 content 中。\n\n## 组件\n\n组件必须设置两个 `v-model` 绑定。即 `v-model:raw` 和 `v-model:content`，以下是示例：\n\n```vue\n<template>\n  <div>\n    <textarea :value=\"raw\" @input=\"onRawUpdate\" />\n    <div v-html=\"content\" />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { watch } from \"vue\";\nimport marked from \"marked\";\n\nconst props = withDefaults(\n  defineProps<{\n    raw?: string;\n    content: string;\n  }>(),\n  {\n    raw: \"\",\n    content: \"\",\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:raw\", value: string): void;\n  (event: \"update:content\", value: string): void;\n}>();\n\nfunction onRawUpdate(e: Event) {\n  const raw = (e.target as HTMLTextAreaElement).value;\n  emit(\"update:raw\", raw);\n}\n\nwatch(\n  () => props.raw,\n  () => {\n    emit(\"update:content\", marked(props.raw));\n  }\n);\n</script>\n```\n"
  },
  {
    "path": "ui/docs/extension-points/entity-listitem-field.md",
    "content": "# Entity 数据列表显示字段扩展点\n\n## 原由\n\n目前 Halo 2 的 Console 中，展示数据列表是统一使用 Entity 组件，此扩展点用于支持通过插件扩展部分数据列表的显示字段。\n\n## 定义方式\n\n目前支持扩展的数据列表：\n\n- 插件：`\"plugin:list-item:field:create\"?: (plugin: Ref<Plugin>) => | EntityFieldItem[] | Promise<EntityFieldItem[]>`\n- 文章：`\"post:list-item:field:create\"?: (post: Ref<ListedPost>) => | EntityFieldItem[] | Promise<EntityFieldItem[]>`\n\n示例：\n\n> 此示例是在插件列表项中添加一个显示插件启动时间的字段。\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw, type Ref } from \"vue\";\nimport type { Plugin } from \"@halo-dev/api-client\";\nimport { VEntityField } from \"@halo-dev/components\"\n\nexport default definePlugin({\n  extensionPoints: {\n    \"plugin:list-item:field:create\": (plugin: Ref<Plugin>) => {\n      return [\n        {\n          priority: 40,\n          position: \"end\",\n          component: markRaw(VEntityField),\n          props: {\n            title: \"启动时间\"\n            description: plugin.value.status.lastStartTime\n          },\n        },\n      ];\n    },\n  },\n});\n```\n\n`EntityFieldItem` 类型：\n\n```ts\nexport interface EntityFieldItem {\n  priority: number;                     // 优先级，越小越靠前\n  position: \"start\" | \"end\";            // 显示字段的位置\n  component: Raw<Component>;            // 字段组件，可以使用 `@halo-dev/components` 中提供的 `VEntityField`，也可以自定义\n  props?: Record<string, unknown>;      // 组件的 props\n  permissions?: string[];               // 权限设置\n  hidden?: boolean;                     // 是否隐藏\n}\n```\n"
  },
  {
    "path": "ui/docs/extension-points/entity-listitem-operation.md",
    "content": "# Entity 数据列表操作菜单扩展点\n\n## 原由\n\n目前 Halo 2 的 Console 中，展示数据列表是统一使用 Entity 组件，Entity 组件中提供了用于放置操作按钮的插槽，此扩展点用于支持通过插件扩展部分数据列表的操作菜单项。\n\n## 定义方式\n\n目前支持扩展的数据列表：\n\n- 文章：`\"post:list-item:operation:create\"?: (post: Ref<ListedPost>) => | OperationItem<ListedPost>[] | Promise<OperationItem<ListedPost>[]>`\n- 评论：`\"comment:list-item:operation:create\"?: (comment: Ref<ListedComment>) => | OperationItem<ListedComment>[] | Promise<OperationItem<ListedComment>[]>`\n- 回复：`\"reply:list-item:operation:create\"?: (reply: Ref<ListedReply>) => | OperationItem<ListedReply>[] | Promise<OperationItem<ListedReply>[]>`\n- 插件：`\"plugin:list-item:operation:create\"?: (plugin: Ref<Plugin>) => | OperationItem<Plugin>[] | Promise<OperationItem<Plugin>[]>`\n- 备份：`\"backup:list-item:operation:create\"?: (backup: Ref<Backup>) => | OperationItem<Backup>[] | Promise<OperationItem<Backup>[]>`\n- 主题：`\"theme:list-item:operation:create\"?: (theme: Ref<Theme>) => | OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>`\n- 附件：`\"attachment:list-item:operation:create\"?: (attachment: Ref<Attachment>) => | OperationItem<Attachment>[] | Promise<OperationItem<Attachment>[]>`\n\n示例：\n\n> 此示例是在文章列表中添加一个`导出为 Markdown 文档`的操作菜单项。\n\n```ts\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport { VDropdownItem } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport axios from \"axios\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"post:list-item:operation:create\": () => {\n      return [\n        {\n          priority: 21,\n          component: markRaw(VDropdownItem),\n          label: \"导出为 Markdown 文档\",\n          permissions: [],\n          action: async (post: ListedPost) => {\n            const { data } = await axios.get(\n              `/apis/api.console.halo.run/v1alpha1/posts/${post.post.metadata.name}/head-content`\n            );\n            const blob = new Blob([data.raw], {\n              type: \"text/plain;charset=utf-8\",\n            });\n            const url = window.URL.createObjectURL(blob);\n            const link = document.createElement(\"a\");\n            link.href = url;\n            link.download = `${post.post.spec.title}.md`;\n            link.click();\n          },\n        },\n      ];\n    },\n  },\n});\n```\n\n`OperationItem` 类型：\n\n```ts\nexport interface OperationItem<T> {\n  priority: number;                     // 优先级，越小越靠前\n  component: Raw<Component>;            // 菜单项组件，可以使用 `@halo-dev/components` 中提供的 `VDropdownItem`，也可以自定义\n  props?: Record<string, unknown>;      // 组件的 props\n  action?: (item?: T) => void;          // 点击事件\n  label?: string;                       // 菜单项名称\n  hidden?: boolean;                     // 是否隐藏\n  permissions?: string[];               // 权限\n  children?: OperationItem<T>[];        // 子菜单\n}\n```\n"
  },
  {
    "path": "ui/docs/extension-points/plugin-installation-tabs.md",
    "content": "# 插件安装界面选项卡扩展点\n\n## 原由\n\n目前 Halo 原生支持本地上传和远程下载的方式安装插件，此扩展点用于扩展插件安装界面的选项卡，以支持更多的安装方式。\n\n## 定义方式\n\n> 此示例为添加一个安装选项卡用于从 GitHub 上下载插件。\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport GitHubDownloadTab from \"./components/GitHubDownloadTab.vue\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"plugin:installation:tabs:create\": () => {\n      return [\n        {\n          id: \"github\",\n          label: \"GitHub\",\n          component: markRaw(GitHubDownload),\n          props: {\n            foo: \"bar\",\n          },\n          priority: 30,\n        },\n      ];\n    },\n  },\n});\n```\n\n扩展点类型：\n\n```ts\n\"plugin:installation:tabs:create\"?: () =>\n  | PluginInstallationTab[]\n  | Promise<PluginInstallationTab[]>;\n```\n\n`PluginInstallationTab`:\n\n```ts\nexport interface PluginInstallationTab {\n  id: string;                         // 选项卡的唯一标识\n  label: string;                      // 选项卡的名称\n  component: Raw<Component>;          // 选项卡面板的组件\n  props?: Record<string, unknown>;    // 选项卡组件的 props\n  permissions?: string[];             // 权限\n  priority: number;                   // 优先级\n}\n```\n"
  },
  {
    "path": "ui/docs/extension-points/plugin-self-tabs.md",
    "content": "# 插件详情选项卡扩展点\n\n## 原由\n\n部分插件可能需要在 Console 端自行实现 UI 以完成一些自定义的需求，但可能并不希望在菜单中添加一个菜单项，所以希望可以在插件详情页面添加一个自定义 UI 的选项卡。\n\n## 定义方式\n\n```ts\nimport { definePlugin, PluginTab } from \"@halo-dev/ui-shared\";\nimport MyComponent from \"@/views/my-component.vue\";\nimport { markRaw } from \"vue\";\nexport default definePlugin({\n  components: {},\n  routes: [],\n  extensionPoints: {\n    \"plugin:self:tabs:create\": () : PluginTab[] => {\n      return [\n        {\n          id: \"my-tab-panel\",\n          label: \"Custom Panel\",\n          component: markRaw(MyComponent),\n          permissions: []\n        },\n      ];\n    },\n  },\n});\n```\n\nPluginTab 类型：\n\n```ts\nexport interface PluginTab {\n  id: string;\n  label: string;\n  component: Raw<Component>;\n  permissions?: string[];\n}\n```\n"
  },
  {
    "path": "ui/docs/extension-points/theme-list-tabs.md",
    "content": "# 主题管理界面选项卡扩展点\n\n## 原由\n\n目前在 Halo 的主题管理中原生支持本地上传和远程下载的方式安装主题，此扩展点用于扩展主题管理界面的选项卡，以支持更多的安装方式。\n\n## 定义方式\n\n> 此示例为添加一个安装选项卡用于从 GitHub 上下载主题。\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { markRaw } from \"vue\";\nimport GitHubDownloadTab from \"./components/GitHubDownloadTab.vue\";\n\nexport default definePlugin({\n  extensionPoints: {\n    \"theme:list:tabs:create\": () => {\n      return [\n        {\n          id: \"github\",\n          label: \"GitHub\",\n          component: markRaw(GitHubDownload),\n          props: {\n            foo: \"bar\",\n          },\n          priority: 11,\n        },\n      ];\n    },\n  },\n});\n```\n\n扩展点类型：\n\n```ts\n\"theme:list:tabs:create\"?: () =>\n  | ThemeListTab[]\n  | Promise<ThemeListTab[]>;\n```\n\n`ThemeListTab`:\n\n```ts\nexport interface ThemeListTab {\n  id: string;                         // 选项卡的唯一标识\n  label: string;                      // 选项卡的名称\n  component: Raw<Component>;          // 选项卡面板的组件\n  props?: Record<string, unknown>;    // 选项卡组件的 props\n  permissions?: string[];             // 权限\n  priority: number;                   // 优先级\n}\n```\n"
  },
  {
    "path": "ui/docs/project-structure/README.md",
    "content": "# 项目结构\n\n此目录为 Halo 前端项目的根目录，包含 Console 和 User Center 两部分。\n\n## 名词解释\n\n- Console：管理控制台，主要包含内容管理、系统管理、插件、主题等面向管理员的功能。\n- User Center：用户中心，主要面向单个用户。\n\n## 目录结构\n\n```bash\n├── console-src                         # Console 部分的源码\n│   ├── composables\n│   ├── layouts\n│   ├── modules\n│   ├── router\n│   ├── stores\n│   ├── styles\n│   ├── views\n│   ├── App.vue\n│   └── main.ts\n├── packages                            # 公共库，会在 Halo 发布版本的时候发布到 npmjs.com\n│   ├── api-client                      # 根据 OpenAPI 生成的 API 客户端\n│   ├── components                      # 基础组件库\n│   └── shared                          # 共享库，主要提供给插件使用\n├── src                                 # Console 和 User Center 共享的源码\n│   ├── assets\n│   ├── components\n│   ├── constants\n│   ├── formkit\n│   ├── locales\n│   ├── setup\n│   ├── stores\n│   ├── types\n│   ├── utils\n│   └── vite\n├── uc-src                              # User Center 部分的源码\n│   ├── router\n│   ├── App.vue\n│   └── main.ts\n├── env.d.ts\n├── console.html\n├── package.json\n├── pnpm-lock.yaml\n├── pnpm-workspace.yaml\n├── postcss.config.js\n├── prettier.config.js\n├── tailwind.config.js\n├── tsconfig.app.json\n├── tsconfig.json\n├── tsconfig.node.json\n├── tsconfig.vitest.json\n├── uc.html\n└── vite.config.ts                      # Console 和 User Center 共用的 Vite 配置\n```\n\n可以注意到 Console 和 User Center 仅仅只是使用源码目录和多页面入口进行区分，本质上还是同一个项目。\n\n## 开发环境访问方式\n\n开发环境下只启动一个 Vite Dev Server，默认端口为 `3000`。\n\n开发时应通过后端访问：\n\n- `http://localhost:8090/console`\n- `http://localhost:8090/uc`\n\n这是因为后端在开发环境中会根据 `application/src/main/resources/application-dev.yaml` 中的 `halo.ui.proxy.*` 配置，将 `/console/**` 和 `/uc/**` 的 HTML 页面请求代理到 `http://localhost:3000/`。\n\n不能直接使用 `http://localhost:3000/console` 或 `http://localhost:3000/uc` 访问页面，因为前端运行后发起的后端 API 请求会产生跨域问题。\n\n需要注意的是，开发环境下后端只代理页面入口，不代理静态资源路径。页面中的脚本和样式资源仍然由 Vite Dev Server 直接提供，但页面入口本身应始终从 Halo 后端地址进入。\n\n## 构建产物\n\n构建时，Console 和 User Center 会通过多页面模式生成到同一个产物目录：\n\n```bash\nbuild/dist/ui\n├── console.html\n├── uc.html\n└── ui-assets/\n```\n\n随后后端构建过程会将这些文件复制到应用资源目录，生产环境中的访问方式为：\n\n- `/console/**` 返回 `ui/console.html`\n- `/uc/**` 返回 `ui/uc.html`\n- `/ui-assets/**` 提供前端静态资源\n"
  },
  {
    "path": "ui/docs/routes-generation/README.md",
    "content": "# 路由和 Console 端菜单的生成\n\n## 简述\n\n目前的路由以及菜单都是动态生成的，由 `基础路由`、`核心模块路由`、`插件模块路由` 三部分组成。\n\n定义文件位置：\n\n- 基础路由：`src/router/routes.config.ts`,\n- 核心模块路由：`src/modules/**/module.ts`,\n\n## 定义方式\n\n统一由 `@halo-dev/ui-shared` 包中的 `definePlugin` 方法配置。如：\n\n```ts\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport BasicLayout from \"@console/layouts/BasicLayout.vue\";\nimport AttachmentList from \"./AttachmentList.vue\";\nimport AttachmentSelectorModal from \"./components/AttachmentSelectorModal.vue\";\nimport { IconFolder } from \"@halo-dev/components\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  name: \"attachmentModule\",\n  components: [AttachmentSelectorModal],\n  routes: [\n    {\n      path: \"/attachments\",\n      component: BasicLayout,\n      children: [\n        {\n          path: \"\",\n          name: \"Attachments\",\n          component: AttachmentList,\n          meta: {\n            title: \"附件\",\n            permissions: [\"system:attachments:view\"],\n            menu: {\n              name: \"附件\",\n              group: \"content\",\n              icon: markRaw(IconFolder),\n              priority: 3,\n              mobile: true,\n            },\n          },\n        },\n      ],\n    },\n  ],\n});\n```\n\n其中，如果要将路由添加到侧边的菜单，那么需要在 `meta` 中定义好 `menu` 对象，menu 对象类型详解如下：\n\n```ts\ninterface RouteMeta {\n  title?: string;\n  searchable?: boolean;\n  permissions?: string[];\n  core?: boolean;\n  menu?: {\n    name: string;               // 菜单名称\n    group?: CoreMenuGroupId;    // 菜单分组 ID，详见下方 CoreMenuGroupId 定义\n    icon?: Component;           // 菜单图标，类型为 Vue 组件，可以使用 `@halo-dev/components` 包中的图标组件，或者自行接入 https://github.com/antfu/unplugin-icons\n    priority: number;           // 排序字段，相对于 group，插件中提供的菜单将始终放在最后\n    mobile?: boolean;           // 是否添加到移动端底部的菜单\n  };\n}\n```\n\nCoreMenuGroupId：\n\n```ts\ndeclare type CoreMenuGroupId = \"dashboard\" | \"content\" | \"interface\" | \"system\" | \"tool\";\n```\n\n这是核心内置的菜单分组，但如果插件需要自定义分组，可以直接填写分组名，如：\n\n```ts\n{\n  name: \"帖子\",\n  group: \"社区\",\n  icon: markRaw(IconCummunity),\n  priority: 1,\n  mobile: false,\n}\n```\n\n## 插件接入\n\n定义方式与系统核心模块的定义方式一致，在 `definePlugin` 方法配置即可。主要额外注意的是，如果插件的路由需要基础布局（继承 BasicLayout），需要配置 `parentName`，如：\n\n```ts\nexport default definePlugin({\n  routes: [\n    {\n      parentName: \"Root\",\n      route: {\n        path: \"/migrate\",\n        children: [\n          {\n            path: \"\",\n            name: \"Migrate\",\n            component: MigrateView,\n            meta: {\n              title: \"迁移\",\n              searchable: true,\n              menu: {\n                name: \"迁移\",\n                group: \"tool\",\n                icon: markRaw(IconGrid),\n                priority: 0,\n              },\n            },\n          },\n        ],\n      },\n    },\n  ]\n})\n```\n\n## 权限\n\n在 `meta` 中配置 `permissions` 即可。类型为 UI 权限标识的数组，如 `[\"system:attachments:view\"]`。如果当前用户没有对应权限，那么将不会注册路由和菜单。\n"
  },
  {
    "path": "ui/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"unplugin-icons/types/vue\" />\n\nexport {};\n\nimport type { CoreMenuGroupId } from \"@halo-dev/ui-shared\";\nimport \"vue-router\";\nimport \"axios\";\n\ndeclare module \"*.vue\" {\n  import type { DefineComponent } from \"vue\";\n  // eslint-disable-next-line\n  const component: DefineComponent<{}, {}, any>;\n  export default component;\n}\n\ndeclare module \"vue-router\" {\n  import type { Component } from \"vue\";\n\n  interface RouteMeta {\n    title?: string;\n    description?: string;\n    searchable?: boolean;\n    permissions?:\n      | string[]\n      | ((uiPermissions: string[]) => boolean | Promise<boolean>);\n    core?: boolean;\n    hideFooter?: boolean;\n    menu?: {\n      name: string;\n      group?: CoreMenuGroupId;\n      icon?: Component;\n      priority: number;\n      mobile?: boolean;\n    };\n  }\n}\n\ndeclare module \"axios\" {\n  export interface AxiosRequestConfig {\n    mute?: boolean;\n  }\n}\n"
  },
  {
    "path": "ui/eslint.config.ts",
    "content": "import pluginVitest from \"@vitest/eslint-plugin\";\nimport skipFormatting from \"@vue/eslint-config-prettier/skip-formatting\";\nimport {\n  defineConfigWithVueTs,\n  vueTsConfigs,\n} from \"@vue/eslint-config-typescript\";\nimport { Linter } from \"eslint\";\nimport eslintPluginUnicorn from \"eslint-plugin-unicorn\";\nimport pluginVue from \"eslint-plugin-vue\";\n\nexport default defineConfigWithVueTs(\n  {\n    name: \"app/global-ignores\",\n    ignores: [\n      \"**/dist/**\",\n      \"**/node_modules/**\",\n      \"packages/api-client/src/\",\n      // TODO: Remove this in the future, only for compatibility.\n      \"packages/console-shared/**\",\n      \"packages/components/storybook-static/**\",\n      \"**/build/**\",\n    ],\n  },\n\n  pluginVue.configs[\"flat/recommended\"],\n  vueTsConfigs.recommended,\n\n  {\n    name: \"app/base\",\n    files: [\"**/*.{ts,mts,tsx,vue}\"],\n    plugins: {\n      unicorn: eslintPluginUnicorn,\n    },\n    rules: {\n      \"vue/multi-word-component-names\": 0,\n      \"@typescript-eslint/ban-ts-comment\": 0,\n      \"vue/no-v-html\": 0,\n      \"@typescript-eslint/no-unused-vars\": [\n        \"error\",\n        {\n          args: \"all\",\n          argsIgnorePattern: \"^_\",\n          caughtErrors: \"all\",\n          caughtErrorsIgnorePattern: \"^_\",\n          destructuredArrayIgnorePattern: \"^_\",\n          varsIgnorePattern: \"^_\",\n          ignoreRestSiblings: true,\n        },\n      ],\n      \"unicorn/prefer-node-protocol\": \"warn\",\n    },\n  },\n\n  {\n    name: \"app/config-files\",\n    files: [\"**/*.config.{js,cjs}\"],\n    rules: {\n      \"@typescript-eslint/no-require-imports\": \"off\",\n    },\n  },\n\n  {\n    ...pluginVitest.configs.recommended,\n    files: [\"**/__tests__/*\"],\n  },\n\n  skipFormatting\n) as Linter.Config;\n"
  },
  {
    "path": "ui/package.json",
    "content": "{\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"type\": \"module\",\n  \"scripts\": {\n    \"prepare\": \"cd .. && husky ui/.husky\",\n    \"dev\": \"vite --host\",\n    \"build\": \"vue-tsc --noEmit -p tsconfig.app.json --composite false && vite build\",\n    \"build:packages\": \"pnpm -r run build\",\n    \"api-client:gen\": \"pnpm -C packages/api-client gen\",\n    \"test:unit\": \"vitest --run && pnpm run test:unit:packages\",\n    \"test:unit:watch\": \"vitest --watch\",\n    \"test:unit:ui\": \"vitest --watch --ui\",\n    \"typecheck\": \"vue-tsc --noEmit -p tsconfig.app.json --composite false && pnpm run typecheck:packages\",\n    \"lint\": \"eslint . --max-warnings=0 -f html -o build/lint-result/index.html\",\n    \"format\": \"oxfmt\",\n    \"format:check\": \"oxfmt --check\",\n    \"typecheck:packages\": \"pnpm --parallel -r run typecheck\",\n    \"test:unit:packages\": \"pnpm --parallel -r run test:unit\",\n    \"publish:packages\": \"pnpm -r publish --access public --no-git-checks\"\n  },\n  \"dependencies\": {\n    \"@ckpack/vue-color\": \"^1.6.0\",\n    \"@codemirror/commands\": \"^6.10.0\",\n    \"@codemirror/lang-css\": \"^6.3.1\",\n    \"@codemirror/lang-html\": \"^6.4.11\",\n    \"@codemirror/lang-javascript\": \"^6.2.4\",\n    \"@codemirror/lang-json\": \"^6.0.2\",\n    \"@codemirror/lang-markdown\": \"^6.5.0\",\n    \"@codemirror/lang-yaml\": \"^6.1.2\",\n    \"@codemirror/language\": \"^6.11.3\",\n    \"@codemirror/state\": \"^6.5.2\",\n    \"@codemirror/view\": \"^6.38.8\",\n    \"@ctrl/tinycolor\": \"^4.2.0\",\n    \"@emoji-mart/data\": \"^1.2.1\",\n    \"@formkit/addons\": \"^1.7.2\",\n    \"@formkit/core\": \"^1.7.2\",\n    \"@formkit/drag-and-drop\": \"^0.1.6\",\n    \"@formkit/i18n\": \"^1.7.2\",\n    \"@formkit/inputs\": \"^1.7.2\",\n    \"@formkit/themes\": \"^1.7.2\",\n    \"@formkit/utils\": \"^1.7.2\",\n    \"@formkit/validation\": \"^1.7.2\",\n    \"@formkit/vue\": \"^1.7.2\",\n    \"@halo-dev/api-client\": \"workspace:*\",\n    \"@halo-dev/components\": \"workspace:*\",\n    \"@halo-dev/console-shared\": \"workspace:*\",\n    \"@halo-dev/richtext-editor\": \"workspace:*\",\n    \"@halo-dev/ui-shared\": \"workspace:*\",\n    \"@he-tree/tree-utils\": \"0.1.0-alpha.6\",\n    \"@he-tree/vue\": \"^2.9.4\",\n    \"@iconify/vue\": \"^5.0.0\",\n    \"@number-flow/vue\": \"^0.4.8\",\n    \"@tanstack/vue-query\": \"^4.43.0\",\n    \"@tanstack/vue-virtual\": \"^3.13.13\",\n    \"@uppy/core\": \"^3.11.3\",\n    \"@uppy/dashboard\": \"^3.8.3\",\n    \"@uppy/drag-drop\": \"^3.1.0\",\n    \"@uppy/file-input\": \"^3.1.2\",\n    \"@uppy/image-editor\": \"^2.4.6\",\n    \"@uppy/locales\": \"^3.5.3\",\n    \"@uppy/progress-bar\": \"^3.1.1\",\n    \"@uppy/status-bar\": \"^3.3.3\",\n    \"@uppy/vue\": \"^1.1.2\",\n    \"@uppy/xhr-upload\": \"3.6.0\",\n    \"@vueuse/components\": \"^12.8.2\",\n    \"@vueuse/core\": \"^12.8.2\",\n    \"@vueuse/integrations\": \"^12.8.2\",\n    \"@vueuse/router\": \"^12.8.2\",\n    \"@vueuse/shared\": \"^12.8.2\",\n    \"axios\": \"^1.12.2\",\n    \"codemirror\": \"^6.0.2\",\n    \"colorjs.io\": \"^0.4.3\",\n    \"core-js\": \"^3.43.0\",\n    \"cropperjs\": \"^1.5.13\",\n    \"crypto-js\": \"^4.2.0\",\n    \"dompurify\": \"^3.2.2\",\n    \"emoji-mart\": \"^5.6.0\",\n    \"es-toolkit\": \"^1.41.0\",\n    \"fuse.js\": \"^7.1.0\",\n    \"lodash-es\": \"^4.17.21\",\n    \"mime\": \"^4.1.0\",\n    \"nprogress\": \"^0.2.0\",\n    \"object-hash\": \"^3.0.0\",\n    \"overlayscrollbars\": \"^2.5.0\",\n    \"overlayscrollbars-vue\": \"^0.5.7\",\n    \"path-browserify\": \"^1.0.1\",\n    \"pinia\": \"^3.0.4\",\n    \"pretty-bytes\": \"^6.0.0\",\n    \"qrcode\": \"^1.5.3\",\n    \"sanitize-html\": \"^2.17.0\",\n    \"short-unique-id\": \"^5.0.2\",\n    \"transliteration\": \"^2.3.5\",\n    \"ua-parser-js\": \"^1.0.38\",\n    \"visual-dom-diff\": \"^0.7.3\",\n    \"vue\": \"^3.5.27\",\n    \"vue-demi\": \"^0.14.10\",\n    \"vue-draggable-plus\": \"^0.4.1\",\n    \"vue-grid-layout\": \"3.0.0-beta1\",\n    \"vue-i18n\": \"^11.2.8\",\n    \"vue-router\": \"^5.0.2\"\n  },\n  \"devDependencies\": {\n    \"@iconify/json\": \"^2.2.409\",\n    \"@iconify/types\": \"^2.0.0\",\n    \"@intlify/unplugin-vue-i18n\": \"^11.0.7\",\n    \"@tailwindcss/aspect-ratio\": \"^0.4.2\",\n    \"@tailwindcss/container-queries\": \"^0.1.1\",\n    \"@tailwindcss/forms\": \"^0.5.10\",\n    \"@tsconfig/node22\": \"^22.0.5\",\n    \"@types/crypto-js\": \"^4.2.2\",\n    \"@types/dompurify\": \"^3.0.5\",\n    \"@types/jsdom\": \"^27.0.0\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^24.11.0\",\n    \"@types/nprogress\": \"^0.2.3\",\n    \"@types/object-hash\": \"^3.0.6\",\n    \"@types/sanitize-html\": \"^2.16.0\",\n    \"@types/ua-parser-js\": \"^0.7.39\",\n    \"@typescript/native-preview\": \"7.0.0-dev.20250619.1\",\n    \"@vitejs/plugin-vue\": \"^6.0.5\",\n    \"@vitejs/plugin-vue-jsx\": \"^5.1.5\",\n    \"@vitest/eslint-plugin\": \"^1.4.3\",\n    \"@vitest/ui\": \"^4.0.13\",\n    \"@vue/compiler-sfc\": \"^3.5.27\",\n    \"@vue/eslint-config-prettier\": \"^10.2.0\",\n    \"@vue/eslint-config-typescript\": \"^14.6.0\",\n    \"@vue/test-utils\": \"^2.4.6\",\n    \"@vue/tsconfig\": \"^0.5.1\",\n    \"autoprefixer\": \"^10.4.22\",\n    \"baseline-browser-mapping\": \"^2.9.19\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-plugin-unicorn\": \"^62.0.0\",\n    \"eslint-plugin-vue\": \"^10.6.0\",\n    \"husky\": \"^9.1.7\",\n    \"jsdom\": \"^27.2.0\",\n    \"lint-staged\": \"^16.2.7\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"oxfmt\": \"^0.36.0\",\n    \"postcss\": \"^8.5.6\",\n    \"postcss-viewport-height-correction\": \"^1.1.1\",\n    \"rollup-plugin-gzip\": \"^4.1.1\",\n    \"sass-embedded\": \"^1.93.3\",\n    \"tailwindcss\": \"^3.4.17\",\n    \"tailwindcss-themer\": \"^4.1.1\",\n    \"tsdown\": \"^0.21.2\",\n    \"typescript\": \"~5.9.3\",\n    \"unplugin-icons\": \"^23.0.1\",\n    \"vite\": \"^8.0.0\",\n    \"vite-plugin-dts\": \"^4.5.4\",\n    \"vite-plugin-externals\": \"^0.6.2\",\n    \"vite-plugin-html\": \"^3.2.2\",\n    \"vite-plugin-static-copy\": \"^3.2.0\",\n    \"vitest\": \"^4.1.0\",\n    \"vue-tsc\": \"^3.2.4\"\n  },\n  \"resolutions\": {\n    \"axios\": \"^1.12.2\"\n  },\n  \"lint-staged\": {\n    \"**/*\": \"oxfmt --no-error-on-unmatched-pattern\",\n    \"*.{ts,mts,tsx,vue}\": [\n      \"eslint --fix --max-warnings=0 --no-warn-ignored\"\n    ]\n  },\n  \"browserslist\": [\n    \"defaults\"\n  ],\n  \"packageManager\": \"pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017\",\n  \"pnpm\": {\n    \"overrides\": {\n      \"@lezer/javascript\": \"1.5.4\",\n      \"prosemirror-model\": \"1.25.1\",\n      \"prosemirror-transform\": \"1.10.4\",\n      \"prosemirror-view\": \"1.40.0\"\n    },\n    \"onlyBuiltDependencies\": [\n      \"@nestjs/core\",\n      \"@openapitools/openapi-generator-cli\",\n      \"@parcel/watcher\",\n      \"@tailwindcss/oxide\",\n      \"core-js\",\n      \"esbuild\",\n      \"vue-demi\"\n    ],\n    \"patchedDependencies\": {\n      \"@tiptap/extension-drag-handle@3.17.1\": \"patches/@tiptap__extension-drag-handle@3.17.1.patch\"\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/api-client/.openapi_config.yaml",
    "content": "supportsES6: true\nuseSingleRequestParameter: true\nwithSeparateModelsAndApi: true\napiPackage: api\nmodelPackage: models\n"
  },
  {
    "path": "ui/packages/api-client/README.md",
    "content": "# @halo-dev/api-client\n\nHalo 2.0 的 JavaScript API 客户端请求库。使用 [OpenAPI Generator](https://openapi-generator.tech/) 生成。\n\n## 使用\n\n```javascript\nimport {\n  coreApiClient,\n  consoleApiClient,\n  ucApiClient,\n  publicApiClient,\n  createCoreApiClient,\n  createConsoleApiClient,\n  createUcApiClient,\n  createPublicApiClient,\n  axiosInstance,\n} from \"@halo-dev/api-client\";\n```\n\n- **coreApiClient**: 为 Halo 所有自定义模型的 CRUD 接口封装的 api client。\n- **consoleApiClient**: 为 Halo 针对 Console 提供的接口封装的 api client。\n- **ucApiClient**: 为 Halo 针对 UC 提供的接口封装的 api client。\n- **publicApiClient**: 为 Halo 所有公开访问的接口封装的 api client。\n- **createCoreApiClient**: 用于创建自定义模型的 CRUD 接口封装的 api client，需要传入 axios 实例。\n- **createConsoleApiClient**: 用于创建 Console 接口封装的 api client，需要传入 axios 实例。\n- **createUcApiClient**: 用于创建 UC 接口封装的 api client，需要传入 axios 实例。\n- **createPublicApiClient**: 用于创建公开访问接口封装的 api client，需要传入 axios 实例。\n- **axiosInstance**: 内部默认创建的 axios 实例。\n\n### 在插件中使用\n\n```shell\npnpm install @halo-dev/api-client axios\n```\n\n由于已经在 Console 和 UC 项目中引入并设置好了 Axios 拦截器，所以直接使用即可：\n\n```javascript\nimport { coreApiClient } from \"@halo-dev/api-client\";\n\ncoreApiClient.content.post.listPost().then((response) => {\n  // handle response\n});\n```\n\n此外，在最新的 `@halo-dev/ui-plugin-bundler-kit@2.17.0` 中，已经排除了 `@halo-dev/api-client`、`axios` 依赖，所以最终产物中的相关依赖会自动使用 Halo 本身提供的依赖，无需关心最终产物大小。\n\n详细文档可查阅：[插件开发 / API 请求](https://docs.halo.run/developer-guide/plugin/api-reference/ui/api-request)\n\n### 在外部项目中使用\n\n```shell\npnpm install @halo-dev/api-client axios\n```\n\n```javascript\nimport axios from \"axios\";\n\nconst axiosInstance = axios.create({\n  baseURL: \"http://localhost:8090\",\n});\n\nconst coreApiClient = createCoreApiClient(axiosInstance);\n\ncoreApiClient.content.post.listPost().then((response) => {\n  // handle response\n});\n```\n"
  },
  {
    "path": "ui/packages/api-client/entry/api-client.ts",
    "content": "import axios, { AxiosInstance } from \"axios\";\nimport QueryString from \"qs\";\nimport {\n  AnnotationSettingV1alpha1Api,\n  AnnotationSettingV1AlphaUcApi,\n  AttachmentV1alpha1Api,\n  AttachmentV1alpha1ConsoleApi,\n  AttachmentV1alpha1UcApi,\n  AuthProviderV1alpha1Api,\n  AuthProviderV1alpha1ConsoleApi,\n  BackupV1alpha1Api,\n  CategoryV1alpha1Api,\n  CategoryV1alpha1PublicApi,\n  CommentV1alpha1Api,\n  CommentV1alpha1ConsoleApi,\n  CommentV1alpha1PublicApi,\n  ConfigMapV1alpha1Api,\n  CounterV1alpha1Api,\n  DeviceV1alpha1UcApi,\n  ExtensionDefinitionV1alpha1Api,\n  ExtensionPointDefinitionV1alpha1Api,\n  GroupV1alpha1Api,\n  IndexV1alpha1PublicApi,\n  IndicesV1alpha1ConsoleApi,\n  LocalThumbnailV1alpha1Api,\n  MenuItemV1alpha1Api,\n  MenuV1alpha1Api,\n  MenuV1alpha1PublicApi,\n  MetricsV1alpha1PublicApi,\n  MigrationV1alpha1ConsoleApi,\n  NotificationTemplateV1alpha1Api,\n  NotificationV1alpha1Api,\n  NotificationV1alpha1PublicApi,\n  NotificationV1alpha1UcApi,\n  NotifierDescriptorV1alpha1Api,\n  NotifierV1alpha1ConsoleApi,\n  PersonalAccessTokenV1alpha1Api,\n  PersonalAccessTokenV1alpha1UcApi,\n  PluginV1alpha1Api,\n  PluginV1alpha1ConsoleApi,\n  PolicyAlpha1ConsoleApi,\n  PolicyTemplateV1alpha1Api,\n  PolicyV1alpha1Api,\n  PostV1alpha1Api,\n  PostV1alpha1ConsoleApi,\n  PostV1alpha1PublicApi,\n  PostV1alpha1UcApi,\n  ReasonTypeV1alpha1Api,\n  ReasonV1alpha1Api,\n  ReplyV1alpha1Api,\n  ReplyV1alpha1ConsoleApi,\n  ReverseProxyV1alpha1Api,\n  RoleBindingV1alpha1Api,\n  RoleV1alpha1Api,\n  SecretV1alpha1Api,\n  SettingV1alpha1Api,\n  SinglePageV1alpha1Api,\n  SinglePageV1alpha1ConsoleApi,\n  SinglePageV1alpha1PublicApi,\n  SnapshotV1alpha1Api,\n  SnapshotV1alpha1UcApi,\n  SubscriptionV1alpha1Api,\n  SystemConfigV1alpha1ConsoleApi,\n  SystemV1alpha1ConsoleApi,\n  SystemV1alpha1PublicApi,\n  TagV1alpha1Api,\n  TagV1alpha1ConsoleApi,\n  TagV1alpha1PublicApi,\n  ThemeV1alpha1Api,\n  ThemeV1alpha1ConsoleApi,\n  ThumbnailV1alpha1Api,\n  TwoFactorAuthV1alpha1UcApi,\n  UserConnectionV1alpha1Api,\n  UserPreferenceV1alpha1UcApi,\n  UserV1alpha1Api,\n  UserV1alpha1ConsoleApi,\n} from \"../src\";\n\nconst defaultAxiosInstance = axios.create({\n  baseURL: \"\",\n  withCredentials: true,\n  paramsSerializer: (params) => {\n    return QueryString.stringify(params, { arrayFormat: \"repeat\" });\n  },\n});\n\ndefaultAxiosInstance.defaults.headers.common[\"X-Requested-With\"] =\n  \"XMLHttpRequest\";\n\n/**\n * Create a core api client\n *\n * This is the API client for the CRUD interface that is automatically generated for Halo's core extensions.\n *\n * @see <https://github.com/halo-dev/rfcs/tree/main/extension>\n * @param baseURL Halo backend base URL\n * @param axiosInstance Axios instance\n * @returns Core api client\n * @example\n * const axiosInstance = axios.create({\n *  baseURL: \"https://demo.halo.run\",\n *  withCredentials: true,\n * })\n *\n * axiosInstance.interceptors.request.use((config) => {\n *   // do something before request\n *   return config;\n * }, (error) => {\n *   // do something with request error\n *   return Promise.reject(error);\n * });\n *\n * const coreApiClient = createCoreApiClient(axiosInstance);\n *\n * coreApiClient.content.post.listContentHaloRunV1alpha1Post().then(response => {\n *    // handle response\n * })\n */\nfunction createCoreApiClient(axiosInstance: AxiosInstance) {\n  const baseURL = axiosInstance.defaults.baseURL;\n\n  return {\n    // core\n    annotationSetting: new AnnotationSettingV1alpha1Api(\n      undefined,\n      baseURL,\n      axiosInstance\n    ),\n    menu: new MenuV1alpha1Api(undefined, baseURL, axiosInstance),\n    menuItem: new MenuItemV1alpha1Api(undefined, baseURL, axiosInstance),\n    setting: new SettingV1alpha1Api(undefined, baseURL, axiosInstance),\n    configMap: new ConfigMapV1alpha1Api(undefined, baseURL, axiosInstance),\n    secret: new SecretV1alpha1Api(undefined, baseURL, axiosInstance),\n    user: new UserV1alpha1Api(undefined, baseURL, axiosInstance),\n    role: new RoleV1alpha1Api(undefined, baseURL, axiosInstance),\n    roleBinding: new RoleBindingV1alpha1Api(undefined, baseURL, axiosInstance),\n\n    // content.halo.run\n    content: {\n      category: new CategoryV1alpha1Api(undefined, baseURL, axiosInstance),\n      comment: new CommentV1alpha1Api(undefined, baseURL, axiosInstance),\n      post: new PostV1alpha1Api(undefined, baseURL, axiosInstance),\n      reply: new ReplyV1alpha1Api(undefined, baseURL, axiosInstance),\n      singlePage: new SinglePageV1alpha1Api(undefined, baseURL, axiosInstance),\n      snapshot: new SnapshotV1alpha1Api(undefined, baseURL, axiosInstance),\n      tag: new TagV1alpha1Api(undefined, baseURL, axiosInstance),\n    },\n\n    // auth.halo.run\n    auth: {\n      authProvider: new AuthProviderV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      userConnection: new UserConnectionV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n\n    // storage.halo.run\n    storage: {\n      attachment: new AttachmentV1alpha1Api(undefined, baseURL, axiosInstance),\n      group: new GroupV1alpha1Api(undefined, baseURL, axiosInstance),\n      policy: new PolicyV1alpha1Api(undefined, baseURL, axiosInstance),\n      policyTemplate: new PolicyTemplateV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      localThumbnail: new LocalThumbnailV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      thumbnail: new ThumbnailV1alpha1Api(undefined, baseURL, axiosInstance),\n    },\n\n    // plugin.halo.run\n    plugin: {\n      extensionDefinition: new ExtensionDefinitionV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      extensionPointDefinition: new ExtensionPointDefinitionV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      plugin: new PluginV1alpha1Api(undefined, baseURL, axiosInstance),\n      reverseProxy: new ReverseProxyV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n\n    // metrics.halo.run\n    metrics: {\n      counter: new CounterV1alpha1Api(undefined, baseURL, axiosInstance),\n    },\n\n    // theme.halo.run\n    theme: {\n      theme: new ThemeV1alpha1Api(undefined, baseURL, axiosInstance),\n    },\n\n    // notification.halo.run\n    notification: {\n      notification: new NotificationV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      notificationTemplate: new NotificationTemplateV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      notifierDescriptor: new NotifierDescriptorV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      reason: new ReasonV1alpha1Api(undefined, baseURL, axiosInstance),\n      reasonType: new ReasonTypeV1alpha1Api(undefined, baseURL, axiosInstance),\n      subscription: new SubscriptionV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n\n    // migration.halo.run\n    migration: {\n      backup: new BackupV1alpha1Api(undefined, baseURL, axiosInstance),\n    },\n\n    // security.halo.run\n    security: {\n      personalAccessToken: new PersonalAccessTokenV1alpha1Api(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n  };\n}\n\n/**\n * Create a console api client\n *\n * Console api client is used for console related operations\n *\n * @param baseURL Halo backend base URL\n * @param axiosInstance Axios instance\n * @returns Console api client\n * @example\n * const axiosInstance = axios.create({\n *  baseURL: \"https://demo.halo.run\",\n *  withCredentials: true,\n * })\n *\n * axiosInstance.interceptors.request.use((config) => {\n *   // do something before request\n *   return config;\n * }, (error) => {\n *   // do something with request error\n *   return Promise.reject(error);\n * });\n *\n * const consoleApiClient = createConsoleApiClient(axiosInstance);\n *\n * consoleApiClient.content.post.listPosts().then(response => {\n *    // handle response\n * })\n */\nfunction createConsoleApiClient(axiosInstance: AxiosInstance) {\n  const baseURL = axiosInstance.defaults.baseURL;\n\n  return {\n    user: new UserV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n    system: new SystemV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n    migration: new MigrationV1alpha1ConsoleApi(\n      undefined,\n      baseURL,\n      axiosInstance\n    ),\n    storage: {\n      attachment: new AttachmentV1alpha1ConsoleApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      policy: new PolicyAlpha1ConsoleApi(undefined, baseURL, axiosInstance),\n    },\n    auth: {\n      authProvider: new AuthProviderV1alpha1ConsoleApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n    content: {\n      comment: new CommentV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n      reply: new ReplyV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n      indices: new IndicesV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n      post: new PostV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n      singlePage: new SinglePageV1alpha1ConsoleApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      tag: new TagV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n    },\n    notification: {\n      notifier: new NotifierV1alpha1ConsoleApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n    plugin: {\n      plugin: new PluginV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n    },\n    theme: {\n      theme: new ThemeV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),\n    },\n    configMap: {\n      system: new SystemConfigV1alpha1ConsoleApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n  };\n}\n\n/**\n * Create a uc api client\n *\n * Uc api client is used for user content related operations\n *\n * @param baseURL Halo backend base URL\n * @param axiosInstance Axios instance\n * @returns Uc api client\n * @example\n * const axiosInstance = axios.create({\n *  baseURL: \"https://demo.halo.run\",\n *  withCredentials: true,\n * })\n *\n * axiosInstance.interceptors.request.use((config) => {\n *   // do something before request\n *   return config;\n * }, (error) => {\n *   // do something with request error\n *   return Promise.reject(error);\n * });\n *\n * const ucApiClient = createUcApiClient(axiosInstance);\n *\n * ucApiClient.content.post.listMyPosts().then(response => {\n *    // handle response\n * })\n */\nfunction createUcApiClient(axiosInstance: AxiosInstance) {\n  const baseURL = axiosInstance.defaults.baseURL;\n\n  return {\n    storage: {\n      attachment: new AttachmentV1alpha1UcApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n    content: {\n      post: new PostV1alpha1UcApi(undefined, baseURL, axiosInstance),\n      snapshot: new SnapshotV1alpha1UcApi(undefined, baseURL, axiosInstance),\n    },\n    security: {\n      twoFactor: new TwoFactorAuthV1alpha1UcApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      personalAccessToken: new PersonalAccessTokenV1alpha1UcApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      device: new DeviceV1alpha1UcApi(undefined, baseURL, axiosInstance),\n    },\n    notification: {\n      notification: new NotificationV1alpha1UcApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n    user: {\n      preference: new UserPreferenceV1alpha1UcApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n    core: {\n      annotationSetting: new AnnotationSettingV1AlphaUcApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n    },\n  };\n}\n\n/**\n * Create a public api client\n *\n * Public api client is used for public content related operations, no need authentication\n *\n * @param baseURL Halo backend base URL\n * @param axiosInstance Axios instance\n * @returns Public api client\n * @example\n * const axiosInstance = axios.create({\n *   baseURL: \"https://demo.halo.run\",\n *   withCredentials: true,\n * })\n *\n * axiosInstance.interceptors.request.use((config) => {\n *   // do something before request\n *   return config;\n * }, (error) => {\n *   // do something with request error\n *   return Promise.reject(error);\n * });\n *\n * const publicApiClient = createPublicApiClient(axiosInstance);\n *\n * publicApiClient.content.post.searchPost({ keyword: \"foo\" }).then(response => {\n *    // handle response\n * })\n */\nfunction createPublicApiClient(axiosInstance: AxiosInstance) {\n  const baseURL = axiosInstance.defaults.baseURL;\n\n  return {\n    menu: new MenuV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n    stats: new SystemV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n    content: {\n      category: new CategoryV1alpha1PublicApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      tag: new TagV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n      singlePage: new SinglePageV1alpha1PublicApi(\n        undefined,\n        baseURL,\n        axiosInstance\n      ),\n      post: new PostV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n      /**\n       * @deprecated Use comment instead\n       */\n      comment: new CommentV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n    },\n    comment: new CommentV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n    metrics: {\n      metrics: new MetricsV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n    },\n    notification: new NotificationV1alpha1PublicApi(\n      undefined,\n      baseURL,\n      axiosInstance\n    ),\n    index: new IndexV1alpha1PublicApi(undefined, baseURL, axiosInstance),\n  };\n}\n\nconst defaultCoreApiClient =\n  /*#__PURE__*/ createCoreApiClient(defaultAxiosInstance);\nconst defaultConsoleApiClient =\n  /*#__PURE__*/ createConsoleApiClient(defaultAxiosInstance);\nconst defaultUcApiClient =\n  /*#__PURE__*/ createUcApiClient(defaultAxiosInstance);\nconst defaultPublicApiClient =\n  /*#__PURE__*/ createPublicApiClient(defaultAxiosInstance);\n\nexport {\n  defaultAxiosInstance as axiosInstance,\n  defaultConsoleApiClient as consoleApiClient,\n  defaultCoreApiClient as coreApiClient,\n  createConsoleApiClient,\n  createCoreApiClient,\n  createPublicApiClient,\n  createUcApiClient,\n  defaultPublicApiClient as publicApiClient,\n  defaultUcApiClient as ucApiClient,\n};\n"
  },
  {
    "path": "ui/packages/api-client/entry/index.ts",
    "content": "export * from \"../src\";\nexport * from \"./api-client\";\nexport * from \"./utils\";\n"
  },
  {
    "path": "ui/packages/api-client/entry/utils/index.ts",
    "content": "export * from \"./paginate\";\n"
  },
  {
    "path": "ui/packages/api-client/entry/utils/paginate.ts",
    "content": "import type { AxiosResponse } from \"axios\";\n\ntype ListResponse<TItem> = {\n  items: TItem[];\n  hasNext: boolean;\n};\n\n/**\n * Query all items from the list function.\n *\n * @param listFn - The function to list the items.\n * @param params - The parameters to list the items.\n * @returns The items.\n */\nexport async function paginate<TParams extends { page?: number }, TItem>(\n  listFn: (params: TParams) => Promise<AxiosResponse<ListResponse<TItem>>>,\n  params?: Omit<TParams, \"page\">\n): Promise<TItem[]> {\n  const result: TItem[] = [];\n  let page = 1;\n  let hasNext = true;\n\n  while (hasNext) {\n    const { data } = await listFn({\n      ...params,\n      page,\n    } as TParams);\n    result.push(...data.items);\n    page += 1;\n    hasNext = data.hasNext;\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "ui/packages/api-client/openapitools.json",
    "content": "{\n  \"$schema\": \"./node_modules/@openapitools/openapi-generator-cli/config.schema.json\",\n  \"spaces\": 2,\n  \"generator-cli\": {\n    \"version\": \"7.17.0\"\n  }\n}\n"
  },
  {
    "path": "ui/packages/api-client/package.json",
    "content": "{\n  \"name\": \"@halo-dev/api-client\",\n  \"version\": \"2.23.0\",\n  \"description\": \"API Client for Halo 2\",\n  \"homepage\": \"https://github.com/halo-dev/halo/tree/main/ui/packages/api-client#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/halo-dev/halo/issues\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"@halo-dev\",\n  \"contributors\": [\n    {\n      \"name\": \"Ryan Wang\",\n      \"url\": \"https://github.com/ruibaby\"\n    }\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/halo-dev/halo.git\",\n    \"directory\": \"ui/packages/api-client\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"./dist/*\",\n        \"./dist/entry/index.d.ts\"\n      ]\n    }\n  },\n  \"exports\": {\n    \".\": \"./dist/index.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"gen\": \"rimraf --glob './src/**' && openapi-generator-cli generate -i ../../../api-docs/openapi/v3_0/aggregated.json -g typescript-axios -c ./.openapi_config.yaml -o ./src --type-mappings='set=Array' --global-property apiDocs=false,modelDocs=false\",\n    \"prepublishOnly\": \"pnpm run build\"\n  },\n  \"dependencies\": {\n    \"qs\": \"^6.14.0\"\n  },\n  \"devDependencies\": {\n    \"@openapitools/openapi-generator-cli\": \"^2.28.1\",\n    \"@types/qs\": \"^6.14.0\",\n    \"rimraf\": \"^5.0.10\"\n  },\n  \"peerDependencies\": {\n    \"axios\": \"^1.12.*\"\n  },\n  \"inlinedDependencies\": {\n    \"es-errors\": \"1.3.0\",\n    \"object-inspect\": \"1.13.4\",\n    \"side-channel-list\": \"1.0.0\",\n    \"es-object-atoms\": \"1.1.1\",\n    \"math-intrinsics\": \"1.1.0\",\n    \"gopd\": \"1.2.0\",\n    \"es-define-property\": \"1.0.1\",\n    \"has-symbols\": \"1.1.0\",\n    \"get-proto\": \"1.0.1\",\n    \"function-bind\": \"1.1.2\",\n    \"call-bind-apply-helpers\": \"1.0.2\",\n    \"dunder-proto\": \"1.0.1\",\n    \"hasown\": \"2.0.2\",\n    \"get-intrinsic\": \"1.3.0\",\n    \"call-bound\": \"1.0.4\",\n    \"side-channel-map\": \"1.0.1\",\n    \"side-channel-weakmap\": \"1.0.2\",\n    \"side-channel\": \"1.1.0\",\n    \"qs\": \"6.14.0\"\n  }\n}\n"
  },
  {
    "path": "ui/packages/api-client/src/.gitignore",
    "content": "wwwroot/*.js\nnode_modules\ntypings\ndist\n"
  },
  {
    "path": "ui/packages/api-client/src/.npmignore",
    "content": "# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm"
  },
  {
    "path": "ui/packages/api-client/src/.openapi-generator/FILES",
    "content": ".gitignore\n.npmignore\n.openapi-generator-ignore\napi.ts\napi/annotation-setting-v1-alpha-uc-api.ts\napi/annotation-setting-v1alpha1-api.ts\napi/attachment-v1alpha1-api.ts\napi/attachment-v1alpha1-console-api.ts\napi/attachment-v1alpha1-uc-api.ts\napi/auth-provider-v1alpha1-api.ts\napi/auth-provider-v1alpha1-console-api.ts\napi/backup-v1alpha1-api.ts\napi/category-v1alpha1-api.ts\napi/category-v1alpha1-public-api.ts\napi/comment-v1alpha1-api.ts\napi/comment-v1alpha1-console-api.ts\napi/comment-v1alpha1-public-api.ts\napi/config-map-v1alpha1-api.ts\napi/counter-v1alpha1-api.ts\napi/device-v1alpha1-api.ts\napi/device-v1alpha1-uc-api.ts\napi/extension-definition-v1alpha1-api.ts\napi/extension-point-definition-v1alpha1-api.ts\napi/group-v1alpha1-api.ts\napi/index-v1alpha1-public-api.ts\napi/indices-v1alpha1-console-api.ts\napi/local-thumbnail-v1alpha1-api.ts\napi/menu-item-v1alpha1-api.ts\napi/menu-v1alpha1-api.ts\napi/menu-v1alpha1-public-api.ts\napi/metrics-v1alpha1-public-api.ts\napi/migration-v1alpha1-console-api.ts\napi/notification-template-v1alpha1-api.ts\napi/notification-v1alpha1-api.ts\napi/notification-v1alpha1-public-api.ts\napi/notification-v1alpha1-uc-api.ts\napi/notifier-descriptor-v1alpha1-api.ts\napi/notifier-v1alpha1-console-api.ts\napi/notifier-v1alpha1-uc-api.ts\napi/personal-access-token-v1alpha1-api.ts\napi/personal-access-token-v1alpha1-uc-api.ts\napi/plugin-v1alpha1-api.ts\napi/plugin-v1alpha1-console-api.ts\napi/plugin-v1alpha1-public-api.ts\napi/policy-alpha1-console-api.ts\napi/policy-template-v1alpha1-api.ts\napi/policy-v1alpha1-api.ts\napi/post-v1alpha1-api.ts\napi/post-v1alpha1-console-api.ts\napi/post-v1alpha1-public-api.ts\napi/post-v1alpha1-uc-api.ts\napi/reason-type-v1alpha1-api.ts\napi/reason-v1alpha1-api.ts\napi/remember-me-token-v1alpha1-api.ts\napi/reply-v1alpha1-api.ts\napi/reply-v1alpha1-console-api.ts\napi/reverse-proxy-v1alpha1-api.ts\napi/role-binding-v1alpha1-api.ts\napi/role-v1alpha1-api.ts\napi/secret-v1alpha1-api.ts\napi/setting-v1alpha1-api.ts\napi/single-page-v1alpha1-api.ts\napi/single-page-v1alpha1-console-api.ts\napi/single-page-v1alpha1-public-api.ts\napi/snapshot-v1alpha1-api.ts\napi/snapshot-v1alpha1-uc-api.ts\napi/subscription-v1alpha1-api.ts\napi/system-config-v1alpha1-console-api.ts\napi/system-v1alpha1-console-api.ts\napi/system-v1alpha1-public-api.ts\napi/tag-v1alpha1-api.ts\napi/tag-v1alpha1-console-api.ts\napi/tag-v1alpha1-public-api.ts\napi/theme-v1alpha1-api.ts\napi/theme-v1alpha1-console-api.ts\napi/thumbnail-v1alpha1-api.ts\napi/thumbnail-v1alpha1-public-api.ts\napi/two-factor-auth-v1alpha1-uc-api.ts\napi/user-connection-v1alpha1-api.ts\napi/user-connection-v1alpha1-uc-api.ts\napi/user-preference-v1alpha1-uc-api.ts\napi/user-v1alpha1-api.ts\napi/user-v1alpha1-console-api.ts\nbase.ts\ncommon.ts\nconfiguration.ts\ngit_push.sh\nindex.ts\nmodels/add-operation.ts\nmodels/annotation-setting-list.ts\nmodels/annotation-setting-spec.ts\nmodels/annotation-setting.ts\nmodels/attachment-list.ts\nmodels/attachment-spec.ts\nmodels/attachment-status.ts\nmodels/attachment.ts\nmodels/auth-provider-list.ts\nmodels/auth-provider-spec.ts\nmodels/auth-provider.ts\nmodels/author.ts\nmodels/backup-file.ts\nmodels/backup-list.ts\nmodels/backup-spec.ts\nmodels/backup-status.ts\nmodels/backup.ts\nmodels/category-list.ts\nmodels/category-spec.ts\nmodels/category-status.ts\nmodels/category-vo-list.ts\nmodels/category-vo.ts\nmodels/category.ts\nmodels/change-own-password-request.ts\nmodels/change-password-request.ts\nmodels/comment-email-owner.ts\nmodels/comment-list.ts\nmodels/comment-owner.ts\nmodels/comment-request.ts\nmodels/comment-spec.ts\nmodels/comment-stats-vo.ts\nmodels/comment-stats.ts\nmodels/comment-status.ts\nmodels/comment-vo-list.ts\nmodels/comment-vo.ts\nmodels/comment-with-reply-vo-list.ts\nmodels/comment-with-reply-vo.ts\nmodels/comment.ts\nmodels/condition.ts\nmodels/config-map-list.ts\nmodels/config-map-ref.ts\nmodels/config-map.ts\nmodels/content-update-param.ts\nmodels/content-vo.ts\nmodels/content-wrapper.ts\nmodels/content.ts\nmodels/contributor-vo.ts\nmodels/contributor.ts\nmodels/copy-operation.ts\nmodels/counter-list.ts\nmodels/counter-request.ts\nmodels/counter.ts\nmodels/create-user-request.ts\nmodels/custom-templates.ts\nmodels/dashboard-stats.ts\nmodels/detailed-user.ts\nmodels/device-list.ts\nmodels/device-spec.ts\nmodels/device-status.ts\nmodels/device.ts\nmodels/email-config-validation-request.ts\nmodels/email-verify-request.ts\nmodels/excerpt.ts\nmodels/extension-definition-list.ts\nmodels/extension-definition.ts\nmodels/extension-point-definition-list.ts\nmodels/extension-point-definition.ts\nmodels/extension-point-spec.ts\nmodels/extension-spec.ts\nmodels/extension.ts\nmodels/file-reverse-proxy-provider.ts\nmodels/grant-request.ts\nmodels/group-kind.ts\nmodels/group-list.ts\nmodels/group-spec.ts\nmodels/group-status.ts\nmodels/group.ts\nmodels/halo-document.ts\nmodels/index.ts\nmodels/install-from-uri-request.ts\nmodels/interest-reason-subject.ts\nmodels/interest-reason.ts\nmodels/json-patch-inner.ts\nmodels/license.ts\nmodels/list-result-reply-vo.ts\nmodels/listed-auth-provider.ts\nmodels/listed-comment-list.ts\nmodels/listed-comment.ts\nmodels/listed-post-list.ts\nmodels/listed-post-vo-list.ts\nmodels/listed-post-vo.ts\nmodels/listed-post.ts\nmodels/listed-reply-list.ts\nmodels/listed-reply.ts\nmodels/listed-single-page-list.ts\nmodels/listed-single-page-vo-list.ts\nmodels/listed-single-page-vo.ts\nmodels/listed-single-page.ts\nmodels/listed-snapshot-dto.ts\nmodels/listed-snapshot-spec.ts\nmodels/listed-user.ts\nmodels/local-thumbnail-list.ts\nmodels/local-thumbnail-spec.ts\nmodels/local-thumbnail-status.ts\nmodels/local-thumbnail.ts\nmodels/mark-specified-request.ts\nmodels/menu-item-list.ts\nmodels/menu-item-spec.ts\nmodels/menu-item-status.ts\nmodels/menu-item-vo.ts\nmodels/menu-item.ts\nmodels/menu-list.ts\nmodels/menu-spec.ts\nmodels/menu-vo.ts\nmodels/menu.ts\nmodels/metadata.ts\nmodels/move-operation.ts\nmodels/navigation-post-vo.ts\nmodels/notification-list.ts\nmodels/notification-spec.ts\nmodels/notification-template-list.ts\nmodels/notification-template-spec.ts\nmodels/notification-template.ts\nmodels/notification.ts\nmodels/notifier-descriptor-list.ts\nmodels/notifier-descriptor-spec.ts\nmodels/notifier-descriptor.ts\nmodels/notifier-info.ts\nmodels/notifier-setting-ref.ts\nmodels/owner-info.ts\nmodels/password-request.ts\nmodels/pat-spec.ts\nmodels/personal-access-token-list.ts\nmodels/personal-access-token.ts\nmodels/plugin-author.ts\nmodels/plugin-list.ts\nmodels/plugin-running-state-request.ts\nmodels/plugin-spec.ts\nmodels/plugin-status.ts\nmodels/plugin.ts\nmodels/policy-list.ts\nmodels/policy-rule.ts\nmodels/policy-spec.ts\nmodels/policy-template-list.ts\nmodels/policy-template-spec.ts\nmodels/policy-template.ts\nmodels/policy.ts\nmodels/post-list.ts\nmodels/post-request.ts\nmodels/post-spec.ts\nmodels/post-status.ts\nmodels/post-vo.ts\nmodels/post.ts\nmodels/reason-attributes.ts\nmodels/reason-list.ts\nmodels/reason-property.ts\nmodels/reason-selector.ts\nmodels/reason-spec.ts\nmodels/reason-subject.ts\nmodels/reason-type-info.ts\nmodels/reason-type-list.ts\nmodels/reason-type-notifier-collection-request.ts\nmodels/reason-type-notifier-matrix.ts\nmodels/reason-type-notifier-request.ts\nmodels/reason-type-spec.ts\nmodels/reason-type.ts\nmodels/reason.ts\nmodels/ref.ts\nmodels/remember-me-token-list.ts\nmodels/remember-me-token-spec.ts\nmodels/remember-me-token.ts\nmodels/remove-operation.ts\nmodels/replace-operation.ts\nmodels/reply-list.ts\nmodels/reply-request.ts\nmodels/reply-spec.ts\nmodels/reply-status.ts\nmodels/reply-vo-list.ts\nmodels/reply-vo.ts\nmodels/reply.ts\nmodels/reverse-proxy-list.ts\nmodels/reverse-proxy-rule.ts\nmodels/reverse-proxy.ts\nmodels/revert-snapshot-for-post-param.ts\nmodels/revert-snapshot-for-single-param.ts\nmodels/role-binding-list.ts\nmodels/role-binding.ts\nmodels/role-list.ts\nmodels/role-ref.ts\nmodels/role.ts\nmodels/search-option.ts\nmodels/search-result.ts\nmodels/secret-list.ts\nmodels/secret.ts\nmodels/setting-form.ts\nmodels/setting-list.ts\nmodels/setting-ref.ts\nmodels/setting-spec.ts\nmodels/setting.ts\nmodels/setup-request.ts\nmodels/single-page-list.ts\nmodels/single-page-request.ts\nmodels/single-page-spec.ts\nmodels/single-page-status.ts\nmodels/single-page-vo.ts\nmodels/single-page.ts\nmodels/site-stats-vo.ts\nmodels/snap-shot-spec.ts\nmodels/snapshot-list.ts\nmodels/snapshot.ts\nmodels/stats-vo.ts\nmodels/stats.ts\nmodels/subject.ts\nmodels/subscription-list.ts\nmodels/subscription-spec.ts\nmodels/subscription-subscriber.ts\nmodels/subscription.ts\nmodels/tag-list.ts\nmodels/tag-spec.ts\nmodels/tag-status.ts\nmodels/tag-vo-list.ts\nmodels/tag-vo.ts\nmodels/tag.ts\nmodels/template-content.ts\nmodels/template-descriptor.ts\nmodels/test-operation.ts\nmodels/theme-list.ts\nmodels/theme-spec.ts\nmodels/theme-status.ts\nmodels/theme.ts\nmodels/thumbnail-list.ts\nmodels/thumbnail-spec.ts\nmodels/thumbnail.ts\nmodels/totp-auth-link-response.ts\nmodels/totp-request.ts\nmodels/two-factor-auth-settings.ts\nmodels/uc-upload-from-url-request.ts\nmodels/upgrade-from-uri-request.ts\nmodels/upload-from-url-request.ts\nmodels/user-connection-list.ts\nmodels/user-connection-spec.ts\nmodels/user-connection.ts\nmodels/user-device.ts\nmodels/user-endpoint-listed-user-list.ts\nmodels/user-list.ts\nmodels/user-permission.ts\nmodels/user-spec.ts\nmodels/user-status.ts\nmodels/user.ts\nmodels/verify-code-request.ts\nmodels/vote-request.ts\n"
  },
  {
    "path": "ui/packages/api-client/src/.openapi-generator/VERSION",
    "content": "7.17.0\n"
  },
  {
    "path": "ui/packages/api-client/src/.openapi-generator-ignore",
    "content": "# OpenAPI Generator Ignore\n# Generated by openapi-generator https://github.com/openapitools/openapi-generator\n\n# Use this file to prevent files from being overwritten by the generator.\n# The patterns follow closely to .gitignore or .dockerignore.\n\n# As an example, the C# client generator defines ApiClient.cs.\n# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:\n#ApiClient.cs\n\n# You can match any string of characters against a directory, file or extension with a single asterisk (*):\n#foo/*/qux\n# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux\n\n# You can recursively match patterns against a directory, file or extension with a double asterisk (**):\n#foo/**/qux\n# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux\n\n# You can also negate patterns with an exclamation (!).\n# For example, you can ignore all files in a docs folder with the file extension .md:\n#docs/*.md\n# Then explicitly reverse the ignore rule for a single file:\n#!docs/README.md\n"
  },
  {
    "path": "ui/packages/api-client/src/api/annotation-setting-v1-alpha-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { AnnotationSetting } from '../models';\n/**\n * AnnotationSettingV1AlphaUcApi - axios parameter creator\n */\nexport const AnnotationSettingV1AlphaUcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * List available AnnotationSettings for the given targetRef. The available AnnotationSettings are determined by the currently activated theme and started plugins.\n         * @param {string} targetRef The targetRef of the AnnotationSetting. e.g.: \\&#39;content.halo.run/Post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAvailableAnnotationSettings: async (targetRef: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'targetRef' is not null or undefined\n            assertParamExists('listAvailableAnnotationSettings', 'targetRef', targetRef)\n            const localVarPath = `/apis/uc.api.halo.run/v1alpha1/annotationsettings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (targetRef !== undefined) {\n                localVarQueryParameter['targetRef'] = targetRef;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AnnotationSettingV1AlphaUcApi - functional programming interface\n */\nexport const AnnotationSettingV1AlphaUcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AnnotationSettingV1AlphaUcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * List available AnnotationSettings for the given targetRef. The available AnnotationSettings are determined by the currently activated theme and started plugins.\n         * @param {string} targetRef The targetRef of the AnnotationSetting. e.g.: \\&#39;content.halo.run/Post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listAvailableAnnotationSettings(targetRef: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AnnotationSetting>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listAvailableAnnotationSettings(targetRef, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AnnotationSettingV1AlphaUcApi.listAvailableAnnotationSettings']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * AnnotationSettingV1AlphaUcApi - factory interface\n */\nexport const AnnotationSettingV1AlphaUcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AnnotationSettingV1AlphaUcApiFp(configuration)\n    return {\n        /**\n         * List available AnnotationSettings for the given targetRef. The available AnnotationSettings are determined by the currently activated theme and started plugins.\n         * @param {AnnotationSettingV1AlphaUcApiListAvailableAnnotationSettingsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAvailableAnnotationSettings(requestParameters: AnnotationSettingV1AlphaUcApiListAvailableAnnotationSettingsRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<AnnotationSetting>> {\n            return localVarFp.listAvailableAnnotationSettings(requestParameters.targetRef, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for listAvailableAnnotationSettings operation in AnnotationSettingV1AlphaUcApi.\n */\nexport interface AnnotationSettingV1AlphaUcApiListAvailableAnnotationSettingsRequest {\n    /**\n     * The targetRef of the AnnotationSetting. e.g.: \\&#39;content.halo.run/Post\n     */\n    readonly targetRef: string\n}\n\n/**\n * AnnotationSettingV1AlphaUcApi - object-oriented interface\n */\nexport class AnnotationSettingV1AlphaUcApi extends BaseAPI {\n    /**\n     * List available AnnotationSettings for the given targetRef. The available AnnotationSettings are determined by the currently activated theme and started plugins.\n     * @param {AnnotationSettingV1AlphaUcApiListAvailableAnnotationSettingsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listAvailableAnnotationSettings(requestParameters: AnnotationSettingV1AlphaUcApiListAvailableAnnotationSettingsRequest, options?: RawAxiosRequestConfig) {\n        return AnnotationSettingV1AlphaUcApiFp(this.configuration).listAvailableAnnotationSettings(requestParameters.targetRef, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/annotation-setting-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { AnnotationSetting } from '../models';\n// @ts-ignore\nimport type { AnnotationSettingList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * AnnotationSettingV1alpha1Api - axios parameter creator\n */\nexport const AnnotationSettingV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create AnnotationSetting\n         * @param {AnnotationSetting} [annotationSetting] Fresh annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAnnotationSetting: async (annotationSetting?: AnnotationSetting, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/annotationsettings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(annotationSetting, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAnnotationSetting: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteAnnotationSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/annotationsettings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAnnotationSetting: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getAnnotationSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/annotationsettings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List AnnotationSetting\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAnnotationSetting: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/annotationsettings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchAnnotationSetting: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchAnnotationSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/annotationsettings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {AnnotationSetting} [annotationSetting] Updated annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAnnotationSetting: async (name: string, annotationSetting?: AnnotationSetting, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateAnnotationSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/annotationsettings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(annotationSetting, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AnnotationSettingV1alpha1Api - functional programming interface\n */\nexport const AnnotationSettingV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AnnotationSettingV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create AnnotationSetting\n         * @param {AnnotationSetting} [annotationSetting] Fresh annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createAnnotationSetting(annotationSetting?: AnnotationSetting, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AnnotationSetting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createAnnotationSetting(annotationSetting, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AnnotationSettingV1alpha1Api.createAnnotationSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteAnnotationSetting(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAnnotationSetting(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AnnotationSettingV1alpha1Api.deleteAnnotationSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAnnotationSetting(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AnnotationSetting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAnnotationSetting(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AnnotationSettingV1alpha1Api.getAnnotationSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List AnnotationSetting\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listAnnotationSetting(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AnnotationSettingList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listAnnotationSetting(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AnnotationSettingV1alpha1Api.listAnnotationSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchAnnotationSetting(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AnnotationSetting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchAnnotationSetting(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AnnotationSettingV1alpha1Api.patchAnnotationSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update AnnotationSetting\n         * @param {string} name Name of annotationsetting\n         * @param {AnnotationSetting} [annotationSetting] Updated annotationsetting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateAnnotationSetting(name: string, annotationSetting?: AnnotationSetting, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AnnotationSetting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateAnnotationSetting(name, annotationSetting, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AnnotationSettingV1alpha1Api.updateAnnotationSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * AnnotationSettingV1alpha1Api - factory interface\n */\nexport const AnnotationSettingV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AnnotationSettingV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create AnnotationSetting\n         * @param {AnnotationSettingV1alpha1ApiCreateAnnotationSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiCreateAnnotationSettingRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AnnotationSetting> {\n            return localVarFp.createAnnotationSetting(requestParameters.annotationSetting, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete AnnotationSetting\n         * @param {AnnotationSettingV1alpha1ApiDeleteAnnotationSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiDeleteAnnotationSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteAnnotationSetting(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get AnnotationSetting\n         * @param {AnnotationSettingV1alpha1ApiGetAnnotationSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiGetAnnotationSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<AnnotationSetting> {\n            return localVarFp.getAnnotationSetting(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List AnnotationSetting\n         * @param {AnnotationSettingV1alpha1ApiListAnnotationSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiListAnnotationSettingRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AnnotationSettingList> {\n            return localVarFp.listAnnotationSetting(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch AnnotationSetting\n         * @param {AnnotationSettingV1alpha1ApiPatchAnnotationSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiPatchAnnotationSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<AnnotationSetting> {\n            return localVarFp.patchAnnotationSetting(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update AnnotationSetting\n         * @param {AnnotationSettingV1alpha1ApiUpdateAnnotationSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiUpdateAnnotationSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<AnnotationSetting> {\n            return localVarFp.updateAnnotationSetting(requestParameters.name, requestParameters.annotationSetting, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createAnnotationSetting operation in AnnotationSettingV1alpha1Api.\n */\nexport interface AnnotationSettingV1alpha1ApiCreateAnnotationSettingRequest {\n    /**\n     * Fresh annotationsetting\n     */\n    readonly annotationSetting?: AnnotationSetting\n}\n\n/**\n * Request parameters for deleteAnnotationSetting operation in AnnotationSettingV1alpha1Api.\n */\nexport interface AnnotationSettingV1alpha1ApiDeleteAnnotationSettingRequest {\n    /**\n     * Name of annotationsetting\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getAnnotationSetting operation in AnnotationSettingV1alpha1Api.\n */\nexport interface AnnotationSettingV1alpha1ApiGetAnnotationSettingRequest {\n    /**\n     * Name of annotationsetting\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listAnnotationSetting operation in AnnotationSettingV1alpha1Api.\n */\nexport interface AnnotationSettingV1alpha1ApiListAnnotationSettingRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchAnnotationSetting operation in AnnotationSettingV1alpha1Api.\n */\nexport interface AnnotationSettingV1alpha1ApiPatchAnnotationSettingRequest {\n    /**\n     * Name of annotationsetting\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateAnnotationSetting operation in AnnotationSettingV1alpha1Api.\n */\nexport interface AnnotationSettingV1alpha1ApiUpdateAnnotationSettingRequest {\n    /**\n     * Name of annotationsetting\n     */\n    readonly name: string\n\n    /**\n     * Updated annotationsetting\n     */\n    readonly annotationSetting?: AnnotationSetting\n}\n\n/**\n * AnnotationSettingV1alpha1Api - object-oriented interface\n */\nexport class AnnotationSettingV1alpha1Api extends BaseAPI {\n    /**\n     * Create AnnotationSetting\n     * @param {AnnotationSettingV1alpha1ApiCreateAnnotationSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiCreateAnnotationSettingRequest = {}, options?: RawAxiosRequestConfig) {\n        return AnnotationSettingV1alpha1ApiFp(this.configuration).createAnnotationSetting(requestParameters.annotationSetting, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete AnnotationSetting\n     * @param {AnnotationSettingV1alpha1ApiDeleteAnnotationSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiDeleteAnnotationSettingRequest, options?: RawAxiosRequestConfig) {\n        return AnnotationSettingV1alpha1ApiFp(this.configuration).deleteAnnotationSetting(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get AnnotationSetting\n     * @param {AnnotationSettingV1alpha1ApiGetAnnotationSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiGetAnnotationSettingRequest, options?: RawAxiosRequestConfig) {\n        return AnnotationSettingV1alpha1ApiFp(this.configuration).getAnnotationSetting(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List AnnotationSetting\n     * @param {AnnotationSettingV1alpha1ApiListAnnotationSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiListAnnotationSettingRequest = {}, options?: RawAxiosRequestConfig) {\n        return AnnotationSettingV1alpha1ApiFp(this.configuration).listAnnotationSetting(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch AnnotationSetting\n     * @param {AnnotationSettingV1alpha1ApiPatchAnnotationSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiPatchAnnotationSettingRequest, options?: RawAxiosRequestConfig) {\n        return AnnotationSettingV1alpha1ApiFp(this.configuration).patchAnnotationSetting(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update AnnotationSetting\n     * @param {AnnotationSettingV1alpha1ApiUpdateAnnotationSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateAnnotationSetting(requestParameters: AnnotationSettingV1alpha1ApiUpdateAnnotationSettingRequest, options?: RawAxiosRequestConfig) {\n        return AnnotationSettingV1alpha1ApiFp(this.configuration).updateAnnotationSetting(requestParameters.name, requestParameters.annotationSetting, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/attachment-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Attachment } from '../models';\n// @ts-ignore\nimport type { AttachmentList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * AttachmentV1alpha1Api - axios parameter creator\n */\nexport const AttachmentV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Attachment\n         * @param {Attachment} [attachment] Fresh attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAttachment: async (attachment?: Attachment, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/attachments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(attachment, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Attachment\n         * @param {string} name Name of attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAttachment: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteAttachment', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/attachments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Attachment\n         * @param {string} name Name of attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAttachment: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getAttachment', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/attachments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Attachment\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAttachment: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/attachments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Attachment\n         * @param {string} name Name of attachment\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchAttachment: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchAttachment', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/attachments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Attachment\n         * @param {string} name Name of attachment\n         * @param {Attachment} [attachment] Updated attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAttachment: async (name: string, attachment?: Attachment, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateAttachment', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/attachments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(attachment, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AttachmentV1alpha1Api - functional programming interface\n */\nexport const AttachmentV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AttachmentV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Attachment\n         * @param {Attachment} [attachment] Fresh attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createAttachment(attachment?: Attachment, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createAttachment(attachment, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1Api.createAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Attachment\n         * @param {string} name Name of attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteAttachment(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAttachment(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1Api.deleteAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Attachment\n         * @param {string} name Name of attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAttachment(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAttachment(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1Api.getAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Attachment\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listAttachment(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AttachmentList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listAttachment(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1Api.listAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Attachment\n         * @param {string} name Name of attachment\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchAttachment(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchAttachment(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1Api.patchAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Attachment\n         * @param {string} name Name of attachment\n         * @param {Attachment} [attachment] Updated attachment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateAttachment(name: string, attachment?: Attachment, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateAttachment(name, attachment, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1Api.updateAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * AttachmentV1alpha1Api - factory interface\n */\nexport const AttachmentV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AttachmentV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Attachment\n         * @param {AttachmentV1alpha1ApiCreateAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAttachment(requestParameters: AttachmentV1alpha1ApiCreateAttachmentRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.createAttachment(requestParameters.attachment, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Attachment\n         * @param {AttachmentV1alpha1ApiDeleteAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAttachment(requestParameters: AttachmentV1alpha1ApiDeleteAttachmentRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteAttachment(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Attachment\n         * @param {AttachmentV1alpha1ApiGetAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAttachment(requestParameters: AttachmentV1alpha1ApiGetAttachmentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.getAttachment(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Attachment\n         * @param {AttachmentV1alpha1ApiListAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAttachment(requestParameters: AttachmentV1alpha1ApiListAttachmentRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AttachmentList> {\n            return localVarFp.listAttachment(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Attachment\n         * @param {AttachmentV1alpha1ApiPatchAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchAttachment(requestParameters: AttachmentV1alpha1ApiPatchAttachmentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.patchAttachment(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Attachment\n         * @param {AttachmentV1alpha1ApiUpdateAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAttachment(requestParameters: AttachmentV1alpha1ApiUpdateAttachmentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.updateAttachment(requestParameters.name, requestParameters.attachment, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createAttachment operation in AttachmentV1alpha1Api.\n */\nexport interface AttachmentV1alpha1ApiCreateAttachmentRequest {\n    /**\n     * Fresh attachment\n     */\n    readonly attachment?: Attachment\n}\n\n/**\n * Request parameters for deleteAttachment operation in AttachmentV1alpha1Api.\n */\nexport interface AttachmentV1alpha1ApiDeleteAttachmentRequest {\n    /**\n     * Name of attachment\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getAttachment operation in AttachmentV1alpha1Api.\n */\nexport interface AttachmentV1alpha1ApiGetAttachmentRequest {\n    /**\n     * Name of attachment\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listAttachment operation in AttachmentV1alpha1Api.\n */\nexport interface AttachmentV1alpha1ApiListAttachmentRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchAttachment operation in AttachmentV1alpha1Api.\n */\nexport interface AttachmentV1alpha1ApiPatchAttachmentRequest {\n    /**\n     * Name of attachment\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateAttachment operation in AttachmentV1alpha1Api.\n */\nexport interface AttachmentV1alpha1ApiUpdateAttachmentRequest {\n    /**\n     * Name of attachment\n     */\n    readonly name: string\n\n    /**\n     * Updated attachment\n     */\n    readonly attachment?: Attachment\n}\n\n/**\n * AttachmentV1alpha1Api - object-oriented interface\n */\nexport class AttachmentV1alpha1Api extends BaseAPI {\n    /**\n     * Create Attachment\n     * @param {AttachmentV1alpha1ApiCreateAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createAttachment(requestParameters: AttachmentV1alpha1ApiCreateAttachmentRequest = {}, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ApiFp(this.configuration).createAttachment(requestParameters.attachment, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Attachment\n     * @param {AttachmentV1alpha1ApiDeleteAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteAttachment(requestParameters: AttachmentV1alpha1ApiDeleteAttachmentRequest, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ApiFp(this.configuration).deleteAttachment(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Attachment\n     * @param {AttachmentV1alpha1ApiGetAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getAttachment(requestParameters: AttachmentV1alpha1ApiGetAttachmentRequest, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ApiFp(this.configuration).getAttachment(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Attachment\n     * @param {AttachmentV1alpha1ApiListAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listAttachment(requestParameters: AttachmentV1alpha1ApiListAttachmentRequest = {}, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ApiFp(this.configuration).listAttachment(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Attachment\n     * @param {AttachmentV1alpha1ApiPatchAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchAttachment(requestParameters: AttachmentV1alpha1ApiPatchAttachmentRequest, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ApiFp(this.configuration).patchAttachment(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Attachment\n     * @param {AttachmentV1alpha1ApiUpdateAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateAttachment(requestParameters: AttachmentV1alpha1ApiUpdateAttachmentRequest, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ApiFp(this.configuration).updateAttachment(requestParameters.name, requestParameters.attachment, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/attachment-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Attachment } from '../models';\n// @ts-ignore\nimport type { AttachmentList } from '../models';\n// @ts-ignore\nimport type { UploadFromUrlRequest } from '../models';\n/**\n * AttachmentV1alpha1ConsoleApi - axios parameter creator\n */\nexport const AttachmentV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {UploadFromUrlRequest} uploadFromUrlRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        externalTransferAttachment: async (uploadFromUrlRequest: UploadFromUrlRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'uploadFromUrlRequest' is not null or undefined\n            assertParamExists('externalTransferAttachment', 'uploadFromUrlRequest', uploadFromUrlRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/attachments/-/upload-from-url`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(uploadFromUrlRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {boolean} [ungrouped] Filter attachments without group. This parameter will ignore group parameter.\n         * @param {string} [keyword] Keyword for searching.\n         * @param {Array<string>} [accepts] Acceptable media types.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        searchAttachments: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, accepts?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/attachments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (ungrouped !== undefined) {\n                localVarQueryParameter['ungrouped'] = ungrouped;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n            if (accepts) {\n                localVarQueryParameter['accepts'] = accepts;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {File} file \n         * @param {string} policyName Storage policy name\n         * @param {string} [groupName] The name of the group to which the attachment belongs\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadAttachment: async (file: File, policyName: string, groupName?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'file' is not null or undefined\n            assertParamExists('uploadAttachment', 'file', file)\n            // verify required parameter 'policyName' is not null or undefined\n            assertParamExists('uploadAttachment', 'policyName', policyName)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/attachments/upload`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n            if (groupName !== undefined) { \n                localVarFormParams.append('groupName', groupName as any);\n            }\n    \n            if (policyName !== undefined) { \n                localVarFormParams.append('policyName', policyName as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upload attachment endpoint for console.\n         * @param {File} [file] The file to upload. If not provided, the url will be used.\n         * @param {string} [filename] The filename to use when uploading from url. If not provided, the filename will be  extracted from the url.\n         * @param {string} [url] The url to upload from. If not provided, the file will be used.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadAttachmentForConsole: async (file?: File, filename?: string, url?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/console.api.storage.halo.run/v1alpha1/attachments/-/upload`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n            if (filename !== undefined) { \n                localVarFormParams.append('filename', filename as any);\n            }\n    \n            if (url !== undefined) { \n                localVarFormParams.append('url', url as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AttachmentV1alpha1ConsoleApi - functional programming interface\n */\nexport const AttachmentV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AttachmentV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {UploadFromUrlRequest} uploadFromUrlRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async externalTransferAttachment(uploadFromUrlRequest: UploadFromUrlRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.externalTransferAttachment(uploadFromUrlRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1ConsoleApi.externalTransferAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * \n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {boolean} [ungrouped] Filter attachments without group. This parameter will ignore group parameter.\n         * @param {string} [keyword] Keyword for searching.\n         * @param {Array<string>} [accepts] Acceptable media types.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async searchAttachments(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, accepts?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AttachmentList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.searchAttachments(page, size, labelSelector, fieldSelector, sort, ungrouped, keyword, accepts, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1ConsoleApi.searchAttachments']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * \n         * @param {File} file \n         * @param {string} policyName Storage policy name\n         * @param {string} [groupName] The name of the group to which the attachment belongs\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async uploadAttachment(file: File, policyName: string, groupName?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadAttachment(file, policyName, groupName, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1ConsoleApi.uploadAttachment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upload attachment endpoint for console.\n         * @param {File} [file] The file to upload. If not provided, the url will be used.\n         * @param {string} [filename] The filename to use when uploading from url. If not provided, the filename will be  extracted from the url.\n         * @param {string} [url] The url to upload from. If not provided, the file will be used.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async uploadAttachmentForConsole(file?: File, filename?: string, url?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadAttachmentForConsole(file, filename, url, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1ConsoleApi.uploadAttachmentForConsole']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * AttachmentV1alpha1ConsoleApi - factory interface\n */\nexport const AttachmentV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AttachmentV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        externalTransferAttachment(requestParameters: AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.externalTransferAttachment(requestParameters.uploadFromUrlRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        searchAttachments(requestParameters: AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AttachmentList> {\n            return localVarFp.searchAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, requestParameters.accepts, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {AttachmentV1alpha1ConsoleApiUploadAttachmentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadAttachment(requestParameters: AttachmentV1alpha1ConsoleApiUploadAttachmentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.uploadAttachment(requestParameters.file, requestParameters.policyName, requestParameters.groupName, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upload attachment endpoint for console.\n         * @param {AttachmentV1alpha1ConsoleApiUploadAttachmentForConsoleRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadAttachmentForConsole(requestParameters: AttachmentV1alpha1ConsoleApiUploadAttachmentForConsoleRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.uploadAttachmentForConsole(requestParameters.file, requestParameters.filename, requestParameters.url, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for externalTransferAttachment operation in AttachmentV1alpha1ConsoleApi.\n */\nexport interface AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest {\n    readonly uploadFromUrlRequest: UploadFromUrlRequest\n}\n\n/**\n * Request parameters for searchAttachments operation in AttachmentV1alpha1ConsoleApi.\n */\nexport interface AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Filter attachments without group. This parameter will ignore group parameter.\n     */\n    readonly ungrouped?: boolean\n\n    /**\n     * Keyword for searching.\n     */\n    readonly keyword?: string\n\n    /**\n     * Acceptable media types.\n     */\n    readonly accepts?: Array<string>\n}\n\n/**\n * Request parameters for uploadAttachment operation in AttachmentV1alpha1ConsoleApi.\n */\nexport interface AttachmentV1alpha1ConsoleApiUploadAttachmentRequest {\n    readonly file: File\n\n    /**\n     * Storage policy name\n     */\n    readonly policyName: string\n\n    /**\n     * The name of the group to which the attachment belongs\n     */\n    readonly groupName?: string\n}\n\n/**\n * Request parameters for uploadAttachmentForConsole operation in AttachmentV1alpha1ConsoleApi.\n */\nexport interface AttachmentV1alpha1ConsoleApiUploadAttachmentForConsoleRequest {\n    /**\n     * The file to upload. If not provided, the url will be used.\n     */\n    readonly file?: File\n\n    /**\n     * The filename to use when uploading from url. If not provided, the filename will be  extracted from the url.\n     */\n    readonly filename?: string\n\n    /**\n     * The url to upload from. If not provided, the file will be used.\n     */\n    readonly url?: string\n}\n\n/**\n * AttachmentV1alpha1ConsoleApi - object-oriented interface\n */\nexport class AttachmentV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * \n     * @param {AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public externalTransferAttachment(requestParameters: AttachmentV1alpha1ConsoleApiExternalTransferAttachmentRequest, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ConsoleApiFp(this.configuration).externalTransferAttachment(requestParameters.uploadFromUrlRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public searchAttachments(requestParameters: AttachmentV1alpha1ConsoleApiSearchAttachmentsRequest = {}, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ConsoleApiFp(this.configuration).searchAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, requestParameters.accepts, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {AttachmentV1alpha1ConsoleApiUploadAttachmentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public uploadAttachment(requestParameters: AttachmentV1alpha1ConsoleApiUploadAttachmentRequest, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ConsoleApiFp(this.configuration).uploadAttachment(requestParameters.file, requestParameters.policyName, requestParameters.groupName, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upload attachment endpoint for console.\n     * @param {AttachmentV1alpha1ConsoleApiUploadAttachmentForConsoleRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public uploadAttachmentForConsole(requestParameters: AttachmentV1alpha1ConsoleApiUploadAttachmentForConsoleRequest = {}, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1ConsoleApiFp(this.configuration).uploadAttachmentForConsole(requestParameters.file, requestParameters.filename, requestParameters.url, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/attachment-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Attachment } from '../models';\n// @ts-ignore\nimport type { AttachmentList } from '../models';\n// @ts-ignore\nimport type { UcUploadFromUrlRequest } from '../models';\n/**\n * AttachmentV1alpha1UcApi - axios parameter creator\n */\nexport const AttachmentV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create attachment for the given post. Deprecated in favor of /attachments/-/upload.\n         * @param {File} file \n         * @param {boolean} [waitForPermalink] Wait for permalink.\n         * @param {string} [postName] Post name.\n         * @param {string} [singlePageName] Single page name.\n         * @param {*} [options] Override http request option.\n         * @deprecated\n         * @throws {RequiredError}\n         */\n        createAttachmentForPost: async (file: File, waitForPermalink?: boolean, postName?: string, singlePageName?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'file' is not null or undefined\n            assertParamExists('createAttachmentForPost', 'file', file)\n            const localVarPath = `/apis/uc.api.storage.halo.run/v1alpha1/attachments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (waitForPermalink !== undefined) {\n                localVarQueryParameter['waitForPermalink'] = waitForPermalink;\n            }\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n            if (postName !== undefined) { \n                localVarFormParams.append('postName', postName as any);\n            }\n    \n            if (singlePageName !== undefined) { \n                localVarFormParams.append('singlePageName', singlePageName as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upload attachment from the given URL. Deprecated in favor of /attachments/-/upload.\n         * @param {UcUploadFromUrlRequest} ucUploadFromUrlRequest \n         * @param {boolean} [waitForPermalink] Wait for permalink.\n         * @param {*} [options] Override http request option.\n         * @deprecated\n         * @throws {RequiredError}\n         */\n        externalTransferAttachment1: async (ucUploadFromUrlRequest: UcUploadFromUrlRequest, waitForPermalink?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'ucUploadFromUrlRequest' is not null or undefined\n            assertParamExists('externalTransferAttachment1', 'ucUploadFromUrlRequest', ucUploadFromUrlRequest)\n            const localVarPath = `/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload-from-url`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (waitForPermalink !== undefined) {\n                localVarQueryParameter['waitForPermalink'] = waitForPermalink;\n            }\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(ucUploadFromUrlRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List attachments of the current user uploaded.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {boolean} [ungrouped] Filter attachments without group. This parameter will ignore group parameter.\n         * @param {string} [keyword] Keyword for searching.\n         * @param {Array<string>} [accepts] Acceptable media types.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMyAttachments: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, accepts?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.storage.halo.run/v1alpha1/attachments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (ungrouped !== undefined) {\n                localVarQueryParameter['ungrouped'] = ungrouped;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n            if (accepts) {\n                localVarQueryParameter['accepts'] = accepts;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upload attachment to user center storage.\n         * @param {File} [file] The file to upload. If not provided, the url will be used.\n         * @param {string} [filename] The filename to use when uploading from url. If not provided, the filename will be  extracted from the url.\n         * @param {string} [url] The url to upload from. If not provided, the file will be used.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadAttachmentForUc: async (file?: File, filename?: string, url?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n            if (filename !== undefined) { \n                localVarFormParams.append('filename', filename as any);\n            }\n    \n            if (url !== undefined) { \n                localVarFormParams.append('url', url as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AttachmentV1alpha1UcApi - functional programming interface\n */\nexport const AttachmentV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AttachmentV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create attachment for the given post. Deprecated in favor of /attachments/-/upload.\n         * @param {File} file \n         * @param {boolean} [waitForPermalink] Wait for permalink.\n         * @param {string} [postName] Post name.\n         * @param {string} [singlePageName] Single page name.\n         * @param {*} [options] Override http request option.\n         * @deprecated\n         * @throws {RequiredError}\n         */\n        async createAttachmentForPost(file: File, waitForPermalink?: boolean, postName?: string, singlePageName?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createAttachmentForPost(file, waitForPermalink, postName, singlePageName, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.createAttachmentForPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upload attachment from the given URL. Deprecated in favor of /attachments/-/upload.\n         * @param {UcUploadFromUrlRequest} ucUploadFromUrlRequest \n         * @param {boolean} [waitForPermalink] Wait for permalink.\n         * @param {*} [options] Override http request option.\n         * @deprecated\n         * @throws {RequiredError}\n         */\n        async externalTransferAttachment1(ucUploadFromUrlRequest: UcUploadFromUrlRequest, waitForPermalink?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.externalTransferAttachment1(ucUploadFromUrlRequest, waitForPermalink, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.externalTransferAttachment1']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List attachments of the current user uploaded.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {boolean} [ungrouped] Filter attachments without group. This parameter will ignore group parameter.\n         * @param {string} [keyword] Keyword for searching.\n         * @param {Array<string>} [accepts] Acceptable media types.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listMyAttachments(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, ungrouped?: boolean, keyword?: string, accepts?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AttachmentList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listMyAttachments(page, size, labelSelector, fieldSelector, sort, ungrouped, keyword, accepts, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.listMyAttachments']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upload attachment to user center storage.\n         * @param {File} [file] The file to upload. If not provided, the url will be used.\n         * @param {string} [filename] The filename to use when uploading from url. If not provided, the filename will be  extracted from the url.\n         * @param {string} [url] The url to upload from. If not provided, the file will be used.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async uploadAttachmentForUc(file?: File, filename?: string, url?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadAttachmentForUc(file, filename, url, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AttachmentV1alpha1UcApi.uploadAttachmentForUc']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * AttachmentV1alpha1UcApi - factory interface\n */\nexport const AttachmentV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AttachmentV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Create attachment for the given post. Deprecated in favor of /attachments/-/upload.\n         * @param {AttachmentV1alpha1UcApiCreateAttachmentForPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @deprecated\n         * @throws {RequiredError}\n         */\n        createAttachmentForPost(requestParameters: AttachmentV1alpha1UcApiCreateAttachmentForPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.createAttachmentForPost(requestParameters.file, requestParameters.waitForPermalink, requestParameters.postName, requestParameters.singlePageName, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upload attachment from the given URL. Deprecated in favor of /attachments/-/upload.\n         * @param {AttachmentV1alpha1UcApiExternalTransferAttachment1Request} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @deprecated\n         * @throws {RequiredError}\n         */\n        externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.externalTransferAttachment1(requestParameters.ucUploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List attachments of the current user uploaded.\n         * @param {AttachmentV1alpha1UcApiListMyAttachmentsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMyAttachments(requestParameters: AttachmentV1alpha1UcApiListMyAttachmentsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AttachmentList> {\n            return localVarFp.listMyAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, requestParameters.accepts, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upload attachment to user center storage.\n         * @param {AttachmentV1alpha1UcApiUploadAttachmentForUcRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadAttachmentForUc(requestParameters: AttachmentV1alpha1UcApiUploadAttachmentForUcRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Attachment> {\n            return localVarFp.uploadAttachmentForUc(requestParameters.file, requestParameters.filename, requestParameters.url, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createAttachmentForPost operation in AttachmentV1alpha1UcApi.\n */\nexport interface AttachmentV1alpha1UcApiCreateAttachmentForPostRequest {\n    readonly file: File\n\n    /**\n     * Wait for permalink.\n     */\n    readonly waitForPermalink?: boolean\n\n    /**\n     * Post name.\n     */\n    readonly postName?: string\n\n    /**\n     * Single page name.\n     */\n    readonly singlePageName?: string\n}\n\n/**\n * Request parameters for externalTransferAttachment1 operation in AttachmentV1alpha1UcApi.\n */\nexport interface AttachmentV1alpha1UcApiExternalTransferAttachment1Request {\n    readonly ucUploadFromUrlRequest: UcUploadFromUrlRequest\n\n    /**\n     * Wait for permalink.\n     */\n    readonly waitForPermalink?: boolean\n}\n\n/**\n * Request parameters for listMyAttachments operation in AttachmentV1alpha1UcApi.\n */\nexport interface AttachmentV1alpha1UcApiListMyAttachmentsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Filter attachments without group. This parameter will ignore group parameter.\n     */\n    readonly ungrouped?: boolean\n\n    /**\n     * Keyword for searching.\n     */\n    readonly keyword?: string\n\n    /**\n     * Acceptable media types.\n     */\n    readonly accepts?: Array<string>\n}\n\n/**\n * Request parameters for uploadAttachmentForUc operation in AttachmentV1alpha1UcApi.\n */\nexport interface AttachmentV1alpha1UcApiUploadAttachmentForUcRequest {\n    /**\n     * The file to upload. If not provided, the url will be used.\n     */\n    readonly file?: File\n\n    /**\n     * The filename to use when uploading from url. If not provided, the filename will be  extracted from the url.\n     */\n    readonly filename?: string\n\n    /**\n     * The url to upload from. If not provided, the file will be used.\n     */\n    readonly url?: string\n}\n\n/**\n * AttachmentV1alpha1UcApi - object-oriented interface\n */\nexport class AttachmentV1alpha1UcApi extends BaseAPI {\n    /**\n     * Create attachment for the given post. Deprecated in favor of /attachments/-/upload.\n     * @param {AttachmentV1alpha1UcApiCreateAttachmentForPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @deprecated\n     * @throws {RequiredError}\n     */\n    public createAttachmentForPost(requestParameters: AttachmentV1alpha1UcApiCreateAttachmentForPostRequest, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1UcApiFp(this.configuration).createAttachmentForPost(requestParameters.file, requestParameters.waitForPermalink, requestParameters.postName, requestParameters.singlePageName, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upload attachment from the given URL. Deprecated in favor of /attachments/-/upload.\n     * @param {AttachmentV1alpha1UcApiExternalTransferAttachment1Request} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @deprecated\n     * @throws {RequiredError}\n     */\n    public externalTransferAttachment1(requestParameters: AttachmentV1alpha1UcApiExternalTransferAttachment1Request, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1UcApiFp(this.configuration).externalTransferAttachment1(requestParameters.ucUploadFromUrlRequest, requestParameters.waitForPermalink, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List attachments of the current user uploaded.\n     * @param {AttachmentV1alpha1UcApiListMyAttachmentsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listMyAttachments(requestParameters: AttachmentV1alpha1UcApiListMyAttachmentsRequest = {}, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1UcApiFp(this.configuration).listMyAttachments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.ungrouped, requestParameters.keyword, requestParameters.accepts, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upload attachment to user center storage.\n     * @param {AttachmentV1alpha1UcApiUploadAttachmentForUcRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public uploadAttachmentForUc(requestParameters: AttachmentV1alpha1UcApiUploadAttachmentForUcRequest = {}, options?: RawAxiosRequestConfig) {\n        return AttachmentV1alpha1UcApiFp(this.configuration).uploadAttachmentForUc(requestParameters.file, requestParameters.filename, requestParameters.url, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/auth-provider-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { AuthProvider } from '../models';\n// @ts-ignore\nimport type { AuthProviderList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * AuthProviderV1alpha1Api - axios parameter creator\n */\nexport const AuthProviderV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create AuthProvider\n         * @param {AuthProvider} [authProvider] Fresh authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAuthProvider: async (authProvider?: AuthProvider, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/authproviders`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(authProvider, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAuthProvider: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteAuthProvider', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/authproviders/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAuthProvider: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getAuthProvider', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/authproviders/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List AuthProvider\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAuthProvider: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/authproviders`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchAuthProvider: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchAuthProvider', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/authproviders/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {AuthProvider} [authProvider] Updated authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAuthProvider: async (name: string, authProvider?: AuthProvider, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateAuthProvider', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/authproviders/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(authProvider, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AuthProviderV1alpha1Api - functional programming interface\n */\nexport const AuthProviderV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AuthProviderV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create AuthProvider\n         * @param {AuthProvider} [authProvider] Fresh authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createAuthProvider(authProvider?: AuthProvider, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthProvider>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createAuthProvider(authProvider, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1Api.createAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteAuthProvider(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAuthProvider(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1Api.deleteAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAuthProvider(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthProvider>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAuthProvider(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1Api.getAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List AuthProvider\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listAuthProvider(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthProviderList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listAuthProvider(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1Api.listAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchAuthProvider(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthProvider>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchAuthProvider(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1Api.patchAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update AuthProvider\n         * @param {string} name Name of authprovider\n         * @param {AuthProvider} [authProvider] Updated authprovider\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateAuthProvider(name: string, authProvider?: AuthProvider, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthProvider>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateAuthProvider(name, authProvider, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1Api.updateAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * AuthProviderV1alpha1Api - factory interface\n */\nexport const AuthProviderV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AuthProviderV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create AuthProvider\n         * @param {AuthProviderV1alpha1ApiCreateAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAuthProvider(requestParameters: AuthProviderV1alpha1ApiCreateAuthProviderRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AuthProvider> {\n            return localVarFp.createAuthProvider(requestParameters.authProvider, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete AuthProvider\n         * @param {AuthProviderV1alpha1ApiDeleteAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAuthProvider(requestParameters: AuthProviderV1alpha1ApiDeleteAuthProviderRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteAuthProvider(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get AuthProvider\n         * @param {AuthProviderV1alpha1ApiGetAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAuthProvider(requestParameters: AuthProviderV1alpha1ApiGetAuthProviderRequest, options?: RawAxiosRequestConfig): AxiosPromise<AuthProvider> {\n            return localVarFp.getAuthProvider(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List AuthProvider\n         * @param {AuthProviderV1alpha1ApiListAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAuthProvider(requestParameters: AuthProviderV1alpha1ApiListAuthProviderRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<AuthProviderList> {\n            return localVarFp.listAuthProvider(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch AuthProvider\n         * @param {AuthProviderV1alpha1ApiPatchAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchAuthProvider(requestParameters: AuthProviderV1alpha1ApiPatchAuthProviderRequest, options?: RawAxiosRequestConfig): AxiosPromise<AuthProvider> {\n            return localVarFp.patchAuthProvider(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update AuthProvider\n         * @param {AuthProviderV1alpha1ApiUpdateAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAuthProvider(requestParameters: AuthProviderV1alpha1ApiUpdateAuthProviderRequest, options?: RawAxiosRequestConfig): AxiosPromise<AuthProvider> {\n            return localVarFp.updateAuthProvider(requestParameters.name, requestParameters.authProvider, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createAuthProvider operation in AuthProviderV1alpha1Api.\n */\nexport interface AuthProviderV1alpha1ApiCreateAuthProviderRequest {\n    /**\n     * Fresh authprovider\n     */\n    readonly authProvider?: AuthProvider\n}\n\n/**\n * Request parameters for deleteAuthProvider operation in AuthProviderV1alpha1Api.\n */\nexport interface AuthProviderV1alpha1ApiDeleteAuthProviderRequest {\n    /**\n     * Name of authprovider\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getAuthProvider operation in AuthProviderV1alpha1Api.\n */\nexport interface AuthProviderV1alpha1ApiGetAuthProviderRequest {\n    /**\n     * Name of authprovider\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listAuthProvider operation in AuthProviderV1alpha1Api.\n */\nexport interface AuthProviderV1alpha1ApiListAuthProviderRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchAuthProvider operation in AuthProviderV1alpha1Api.\n */\nexport interface AuthProviderV1alpha1ApiPatchAuthProviderRequest {\n    /**\n     * Name of authprovider\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateAuthProvider operation in AuthProviderV1alpha1Api.\n */\nexport interface AuthProviderV1alpha1ApiUpdateAuthProviderRequest {\n    /**\n     * Name of authprovider\n     */\n    readonly name: string\n\n    /**\n     * Updated authprovider\n     */\n    readonly authProvider?: AuthProvider\n}\n\n/**\n * AuthProviderV1alpha1Api - object-oriented interface\n */\nexport class AuthProviderV1alpha1Api extends BaseAPI {\n    /**\n     * Create AuthProvider\n     * @param {AuthProviderV1alpha1ApiCreateAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createAuthProvider(requestParameters: AuthProviderV1alpha1ApiCreateAuthProviderRequest = {}, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ApiFp(this.configuration).createAuthProvider(requestParameters.authProvider, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete AuthProvider\n     * @param {AuthProviderV1alpha1ApiDeleteAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteAuthProvider(requestParameters: AuthProviderV1alpha1ApiDeleteAuthProviderRequest, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ApiFp(this.configuration).deleteAuthProvider(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get AuthProvider\n     * @param {AuthProviderV1alpha1ApiGetAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getAuthProvider(requestParameters: AuthProviderV1alpha1ApiGetAuthProviderRequest, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ApiFp(this.configuration).getAuthProvider(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List AuthProvider\n     * @param {AuthProviderV1alpha1ApiListAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listAuthProvider(requestParameters: AuthProviderV1alpha1ApiListAuthProviderRequest = {}, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ApiFp(this.configuration).listAuthProvider(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch AuthProvider\n     * @param {AuthProviderV1alpha1ApiPatchAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchAuthProvider(requestParameters: AuthProviderV1alpha1ApiPatchAuthProviderRequest, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ApiFp(this.configuration).patchAuthProvider(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update AuthProvider\n     * @param {AuthProviderV1alpha1ApiUpdateAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateAuthProvider(requestParameters: AuthProviderV1alpha1ApiUpdateAuthProviderRequest, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ApiFp(this.configuration).updateAuthProvider(requestParameters.name, requestParameters.authProvider, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/auth-provider-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { AuthProvider } from '../models';\n// @ts-ignore\nimport type { ListedAuthProvider } from '../models';\n/**\n * AuthProviderV1alpha1ConsoleApi - axios parameter creator\n */\nexport const AuthProviderV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Disables an auth provider\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disableAuthProvider: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('disableAuthProvider', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/auth-providers/{name}/disable`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Enables an auth provider\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        enableAuthProvider: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('enableAuthProvider', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/auth-providers/{name}/enable`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Lists all auth providers\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAuthProviders: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/auth-providers`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AuthProviderV1alpha1ConsoleApi - functional programming interface\n */\nexport const AuthProviderV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AuthProviderV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Disables an auth provider\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async disableAuthProvider(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthProvider>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.disableAuthProvider(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1ConsoleApi.disableAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Enables an auth provider\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async enableAuthProvider(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthProvider>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.enableAuthProvider(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1ConsoleApi.enableAuthProvider']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Lists all auth providers\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listAuthProviders(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ListedAuthProvider>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listAuthProviders(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['AuthProviderV1alpha1ConsoleApi.listAuthProviders']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * AuthProviderV1alpha1ConsoleApi - factory interface\n */\nexport const AuthProviderV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AuthProviderV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Disables an auth provider\n         * @param {AuthProviderV1alpha1ConsoleApiDisableAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disableAuthProvider(requestParameters: AuthProviderV1alpha1ConsoleApiDisableAuthProviderRequest, options?: RawAxiosRequestConfig): AxiosPromise<AuthProvider> {\n            return localVarFp.disableAuthProvider(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Enables an auth provider\n         * @param {AuthProviderV1alpha1ConsoleApiEnableAuthProviderRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        enableAuthProvider(requestParameters: AuthProviderV1alpha1ConsoleApiEnableAuthProviderRequest, options?: RawAxiosRequestConfig): AxiosPromise<AuthProvider> {\n            return localVarFp.enableAuthProvider(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Lists all auth providers\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listAuthProviders(options?: RawAxiosRequestConfig): AxiosPromise<Array<ListedAuthProvider>> {\n            return localVarFp.listAuthProviders(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for disableAuthProvider operation in AuthProviderV1alpha1ConsoleApi.\n */\nexport interface AuthProviderV1alpha1ConsoleApiDisableAuthProviderRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for enableAuthProvider operation in AuthProviderV1alpha1ConsoleApi.\n */\nexport interface AuthProviderV1alpha1ConsoleApiEnableAuthProviderRequest {\n    readonly name: string\n}\n\n/**\n * AuthProviderV1alpha1ConsoleApi - object-oriented interface\n */\nexport class AuthProviderV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Disables an auth provider\n     * @param {AuthProviderV1alpha1ConsoleApiDisableAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public disableAuthProvider(requestParameters: AuthProviderV1alpha1ConsoleApiDisableAuthProviderRequest, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ConsoleApiFp(this.configuration).disableAuthProvider(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Enables an auth provider\n     * @param {AuthProviderV1alpha1ConsoleApiEnableAuthProviderRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public enableAuthProvider(requestParameters: AuthProviderV1alpha1ConsoleApiEnableAuthProviderRequest, options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ConsoleApiFp(this.configuration).enableAuthProvider(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Lists all auth providers\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listAuthProviders(options?: RawAxiosRequestConfig) {\n        return AuthProviderV1alpha1ConsoleApiFp(this.configuration).listAuthProviders(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/backup-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Backup } from '../models';\n// @ts-ignore\nimport type { BackupList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * BackupV1alpha1Api - axios parameter creator\n */\nexport const BackupV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Backup\n         * @param {Backup} [backup] Fresh backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createBackup: async (backup?: Backup, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/migration.halo.run/v1alpha1/backups`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(backup, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Backup\n         * @param {string} name Name of backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteBackup: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteBackup', 'name', name)\n            const localVarPath = `/apis/migration.halo.run/v1alpha1/backups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Backup\n         * @param {string} name Name of backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBackup: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getBackup', 'name', name)\n            const localVarPath = `/apis/migration.halo.run/v1alpha1/backups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Backup\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listBackup: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/migration.halo.run/v1alpha1/backups`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Backup\n         * @param {string} name Name of backup\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchBackup: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchBackup', 'name', name)\n            const localVarPath = `/apis/migration.halo.run/v1alpha1/backups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Backup\n         * @param {string} name Name of backup\n         * @param {Backup} [backup] Updated backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateBackup: async (name: string, backup?: Backup, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateBackup', 'name', name)\n            const localVarPath = `/apis/migration.halo.run/v1alpha1/backups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(backup, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * BackupV1alpha1Api - functional programming interface\n */\nexport const BackupV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = BackupV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Backup\n         * @param {Backup} [backup] Fresh backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createBackup(backup?: Backup, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Backup>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createBackup(backup, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['BackupV1alpha1Api.createBackup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Backup\n         * @param {string} name Name of backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteBackup(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteBackup(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['BackupV1alpha1Api.deleteBackup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Backup\n         * @param {string} name Name of backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getBackup(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Backup>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getBackup(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['BackupV1alpha1Api.getBackup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Backup\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listBackup(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<BackupList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listBackup(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['BackupV1alpha1Api.listBackup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Backup\n         * @param {string} name Name of backup\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchBackup(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Backup>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchBackup(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['BackupV1alpha1Api.patchBackup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Backup\n         * @param {string} name Name of backup\n         * @param {Backup} [backup] Updated backup\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateBackup(name: string, backup?: Backup, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Backup>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateBackup(name, backup, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['BackupV1alpha1Api.updateBackup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * BackupV1alpha1Api - factory interface\n */\nexport const BackupV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = BackupV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Backup\n         * @param {BackupV1alpha1ApiCreateBackupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createBackup(requestParameters: BackupV1alpha1ApiCreateBackupRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Backup> {\n            return localVarFp.createBackup(requestParameters.backup, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Backup\n         * @param {BackupV1alpha1ApiDeleteBackupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteBackup(requestParameters: BackupV1alpha1ApiDeleteBackupRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteBackup(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Backup\n         * @param {BackupV1alpha1ApiGetBackupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBackup(requestParameters: BackupV1alpha1ApiGetBackupRequest, options?: RawAxiosRequestConfig): AxiosPromise<Backup> {\n            return localVarFp.getBackup(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Backup\n         * @param {BackupV1alpha1ApiListBackupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listBackup(requestParameters: BackupV1alpha1ApiListBackupRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<BackupList> {\n            return localVarFp.listBackup(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Backup\n         * @param {BackupV1alpha1ApiPatchBackupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchBackup(requestParameters: BackupV1alpha1ApiPatchBackupRequest, options?: RawAxiosRequestConfig): AxiosPromise<Backup> {\n            return localVarFp.patchBackup(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Backup\n         * @param {BackupV1alpha1ApiUpdateBackupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateBackup(requestParameters: BackupV1alpha1ApiUpdateBackupRequest, options?: RawAxiosRequestConfig): AxiosPromise<Backup> {\n            return localVarFp.updateBackup(requestParameters.name, requestParameters.backup, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createBackup operation in BackupV1alpha1Api.\n */\nexport interface BackupV1alpha1ApiCreateBackupRequest {\n    /**\n     * Fresh backup\n     */\n    readonly backup?: Backup\n}\n\n/**\n * Request parameters for deleteBackup operation in BackupV1alpha1Api.\n */\nexport interface BackupV1alpha1ApiDeleteBackupRequest {\n    /**\n     * Name of backup\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getBackup operation in BackupV1alpha1Api.\n */\nexport interface BackupV1alpha1ApiGetBackupRequest {\n    /**\n     * Name of backup\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listBackup operation in BackupV1alpha1Api.\n */\nexport interface BackupV1alpha1ApiListBackupRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchBackup operation in BackupV1alpha1Api.\n */\nexport interface BackupV1alpha1ApiPatchBackupRequest {\n    /**\n     * Name of backup\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateBackup operation in BackupV1alpha1Api.\n */\nexport interface BackupV1alpha1ApiUpdateBackupRequest {\n    /**\n     * Name of backup\n     */\n    readonly name: string\n\n    /**\n     * Updated backup\n     */\n    readonly backup?: Backup\n}\n\n/**\n * BackupV1alpha1Api - object-oriented interface\n */\nexport class BackupV1alpha1Api extends BaseAPI {\n    /**\n     * Create Backup\n     * @param {BackupV1alpha1ApiCreateBackupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createBackup(requestParameters: BackupV1alpha1ApiCreateBackupRequest = {}, options?: RawAxiosRequestConfig) {\n        return BackupV1alpha1ApiFp(this.configuration).createBackup(requestParameters.backup, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Backup\n     * @param {BackupV1alpha1ApiDeleteBackupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteBackup(requestParameters: BackupV1alpha1ApiDeleteBackupRequest, options?: RawAxiosRequestConfig) {\n        return BackupV1alpha1ApiFp(this.configuration).deleteBackup(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Backup\n     * @param {BackupV1alpha1ApiGetBackupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getBackup(requestParameters: BackupV1alpha1ApiGetBackupRequest, options?: RawAxiosRequestConfig) {\n        return BackupV1alpha1ApiFp(this.configuration).getBackup(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Backup\n     * @param {BackupV1alpha1ApiListBackupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listBackup(requestParameters: BackupV1alpha1ApiListBackupRequest = {}, options?: RawAxiosRequestConfig) {\n        return BackupV1alpha1ApiFp(this.configuration).listBackup(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Backup\n     * @param {BackupV1alpha1ApiPatchBackupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchBackup(requestParameters: BackupV1alpha1ApiPatchBackupRequest, options?: RawAxiosRequestConfig) {\n        return BackupV1alpha1ApiFp(this.configuration).patchBackup(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Backup\n     * @param {BackupV1alpha1ApiUpdateBackupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateBackup(requestParameters: BackupV1alpha1ApiUpdateBackupRequest, options?: RawAxiosRequestConfig) {\n        return BackupV1alpha1ApiFp(this.configuration).updateBackup(requestParameters.name, requestParameters.backup, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/category-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Category } from '../models';\n// @ts-ignore\nimport type { CategoryList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * CategoryV1alpha1Api - axios parameter creator\n */\nexport const CategoryV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Category\n         * @param {Category} [category] Fresh category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCategory: async (category?: Category, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/categories`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(category, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Category\n         * @param {string} name Name of category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCategory: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteCategory', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/categories/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Category\n         * @param {string} name Name of category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCategory: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getCategory', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/categories/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Category\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listCategory: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/categories`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Category\n         * @param {string} name Name of category\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchCategory: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchCategory', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/categories/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Category\n         * @param {string} name Name of category\n         * @param {Category} [category] Updated category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCategory: async (name: string, category?: Category, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateCategory', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/categories/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(category, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CategoryV1alpha1Api - functional programming interface\n */\nexport const CategoryV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CategoryV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Category\n         * @param {Category} [category] Fresh category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createCategory(category?: Category, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Category>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createCategory(category, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1Api.createCategory']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Category\n         * @param {string} name Name of category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteCategory(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteCategory(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1Api.deleteCategory']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Category\n         * @param {string} name Name of category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCategory(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Category>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCategory(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1Api.getCategory']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Category\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listCategory(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CategoryList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listCategory(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1Api.listCategory']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Category\n         * @param {string} name Name of category\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchCategory(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Category>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchCategory(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1Api.patchCategory']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Category\n         * @param {string} name Name of category\n         * @param {Category} [category] Updated category\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateCategory(name: string, category?: Category, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Category>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateCategory(name, category, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1Api.updateCategory']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * CategoryV1alpha1Api - factory interface\n */\nexport const CategoryV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CategoryV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Category\n         * @param {CategoryV1alpha1ApiCreateCategoryRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCategory(requestParameters: CategoryV1alpha1ApiCreateCategoryRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Category> {\n            return localVarFp.createCategory(requestParameters.category, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Category\n         * @param {CategoryV1alpha1ApiDeleteCategoryRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCategory(requestParameters: CategoryV1alpha1ApiDeleteCategoryRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteCategory(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Category\n         * @param {CategoryV1alpha1ApiGetCategoryRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCategory(requestParameters: CategoryV1alpha1ApiGetCategoryRequest, options?: RawAxiosRequestConfig): AxiosPromise<Category> {\n            return localVarFp.getCategory(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Category\n         * @param {CategoryV1alpha1ApiListCategoryRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listCategory(requestParameters: CategoryV1alpha1ApiListCategoryRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<CategoryList> {\n            return localVarFp.listCategory(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Category\n         * @param {CategoryV1alpha1ApiPatchCategoryRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchCategory(requestParameters: CategoryV1alpha1ApiPatchCategoryRequest, options?: RawAxiosRequestConfig): AxiosPromise<Category> {\n            return localVarFp.patchCategory(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Category\n         * @param {CategoryV1alpha1ApiUpdateCategoryRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCategory(requestParameters: CategoryV1alpha1ApiUpdateCategoryRequest, options?: RawAxiosRequestConfig): AxiosPromise<Category> {\n            return localVarFp.updateCategory(requestParameters.name, requestParameters.category, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createCategory operation in CategoryV1alpha1Api.\n */\nexport interface CategoryV1alpha1ApiCreateCategoryRequest {\n    /**\n     * Fresh category\n     */\n    readonly category?: Category\n}\n\n/**\n * Request parameters for deleteCategory operation in CategoryV1alpha1Api.\n */\nexport interface CategoryV1alpha1ApiDeleteCategoryRequest {\n    /**\n     * Name of category\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getCategory operation in CategoryV1alpha1Api.\n */\nexport interface CategoryV1alpha1ApiGetCategoryRequest {\n    /**\n     * Name of category\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listCategory operation in CategoryV1alpha1Api.\n */\nexport interface CategoryV1alpha1ApiListCategoryRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchCategory operation in CategoryV1alpha1Api.\n */\nexport interface CategoryV1alpha1ApiPatchCategoryRequest {\n    /**\n     * Name of category\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateCategory operation in CategoryV1alpha1Api.\n */\nexport interface CategoryV1alpha1ApiUpdateCategoryRequest {\n    /**\n     * Name of category\n     */\n    readonly name: string\n\n    /**\n     * Updated category\n     */\n    readonly category?: Category\n}\n\n/**\n * CategoryV1alpha1Api - object-oriented interface\n */\nexport class CategoryV1alpha1Api extends BaseAPI {\n    /**\n     * Create Category\n     * @param {CategoryV1alpha1ApiCreateCategoryRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createCategory(requestParameters: CategoryV1alpha1ApiCreateCategoryRequest = {}, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1ApiFp(this.configuration).createCategory(requestParameters.category, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Category\n     * @param {CategoryV1alpha1ApiDeleteCategoryRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteCategory(requestParameters: CategoryV1alpha1ApiDeleteCategoryRequest, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1ApiFp(this.configuration).deleteCategory(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Category\n     * @param {CategoryV1alpha1ApiGetCategoryRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getCategory(requestParameters: CategoryV1alpha1ApiGetCategoryRequest, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1ApiFp(this.configuration).getCategory(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Category\n     * @param {CategoryV1alpha1ApiListCategoryRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listCategory(requestParameters: CategoryV1alpha1ApiListCategoryRequest = {}, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1ApiFp(this.configuration).listCategory(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Category\n     * @param {CategoryV1alpha1ApiPatchCategoryRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchCategory(requestParameters: CategoryV1alpha1ApiPatchCategoryRequest, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1ApiFp(this.configuration).patchCategory(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Category\n     * @param {CategoryV1alpha1ApiUpdateCategoryRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateCategory(requestParameters: CategoryV1alpha1ApiUpdateCategoryRequest, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1ApiFp(this.configuration).updateCategory(requestParameters.name, requestParameters.category, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/category-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { CategoryVo } from '../models';\n// @ts-ignore\nimport type { CategoryVoList } from '../models';\n// @ts-ignore\nimport type { ListedPostVoList } from '../models';\n/**\n * CategoryV1alpha1PublicApi - axios parameter creator\n */\nexport const CategoryV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Lists categories.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryCategories: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/categories`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Gets category by name.\n         * @param {string} name Category name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryCategoryByName: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryCategoryByName', 'name', name)\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/categories/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Lists posts by category name.\n         * @param {string} name Category name\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostsByCategoryName: async (name: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryPostsByCategoryName', 'name', name)\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/categories/{name}/posts`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CategoryV1alpha1PublicApi - functional programming interface\n */\nexport const CategoryV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CategoryV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Lists categories.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryCategories(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CategoryVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryCategories(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1PublicApi.queryCategories']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Gets category by name.\n         * @param {string} name Category name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryCategoryByName(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CategoryVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryCategoryByName(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1PublicApi.queryCategoryByName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Lists posts by category name.\n         * @param {string} name Category name\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryPostsByCategoryName(name: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryPostsByCategoryName(name, page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CategoryV1alpha1PublicApi.queryPostsByCategoryName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * CategoryV1alpha1PublicApi - factory interface\n */\nexport const CategoryV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CategoryV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Lists categories.\n         * @param {CategoryV1alpha1PublicApiQueryCategoriesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryCategories(requestParameters: CategoryV1alpha1PublicApiQueryCategoriesRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<CategoryVoList> {\n            return localVarFp.queryCategories(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Gets category by name.\n         * @param {CategoryV1alpha1PublicApiQueryCategoryByNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryCategoryByName(requestParameters: CategoryV1alpha1PublicApiQueryCategoryByNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<CategoryVo> {\n            return localVarFp.queryCategoryByName(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Lists posts by category name.\n         * @param {CategoryV1alpha1PublicApiQueryPostsByCategoryNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostsByCategoryName(requestParameters: CategoryV1alpha1PublicApiQueryPostsByCategoryNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<ListedPostVoList> {\n            return localVarFp.queryPostsByCategoryName(requestParameters.name, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for queryCategories operation in CategoryV1alpha1PublicApi.\n */\nexport interface CategoryV1alpha1PublicApiQueryCategoriesRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for queryCategoryByName operation in CategoryV1alpha1PublicApi.\n */\nexport interface CategoryV1alpha1PublicApiQueryCategoryByNameRequest {\n    /**\n     * Category name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for queryPostsByCategoryName operation in CategoryV1alpha1PublicApi.\n */\nexport interface CategoryV1alpha1PublicApiQueryPostsByCategoryNameRequest {\n    /**\n     * Category name\n     */\n    readonly name: string\n\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * CategoryV1alpha1PublicApi - object-oriented interface\n */\nexport class CategoryV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Lists categories.\n     * @param {CategoryV1alpha1PublicApiQueryCategoriesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryCategories(requestParameters: CategoryV1alpha1PublicApiQueryCategoriesRequest = {}, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1PublicApiFp(this.configuration).queryCategories(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Gets category by name.\n     * @param {CategoryV1alpha1PublicApiQueryCategoryByNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryCategoryByName(requestParameters: CategoryV1alpha1PublicApiQueryCategoryByNameRequest, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1PublicApiFp(this.configuration).queryCategoryByName(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Lists posts by category name.\n     * @param {CategoryV1alpha1PublicApiQueryPostsByCategoryNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryPostsByCategoryName(requestParameters: CategoryV1alpha1PublicApiQueryPostsByCategoryNameRequest, options?: RawAxiosRequestConfig) {\n        return CategoryV1alpha1PublicApiFp(this.configuration).queryPostsByCategoryName(requestParameters.name, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/comment-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Comment } from '../models';\n// @ts-ignore\nimport type { CommentList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * CommentV1alpha1Api - axios parameter creator\n */\nexport const CommentV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Comment\n         * @param {Comment} [comment] Fresh comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createComment: async (comment?: Comment, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/comments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(comment, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Comment\n         * @param {string} name Name of comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteComment: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteComment', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/comments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Comment\n         * @param {string} name Name of comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getComment: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getComment', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/comments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Comment\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listComment: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/comments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Comment\n         * @param {string} name Name of comment\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchComment: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchComment', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/comments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Comment\n         * @param {string} name Name of comment\n         * @param {Comment} [comment] Updated comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateComment: async (name: string, comment?: Comment, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateComment', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/comments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(comment, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CommentV1alpha1Api - functional programming interface\n */\nexport const CommentV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CommentV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Comment\n         * @param {Comment} [comment] Fresh comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createComment(comment?: Comment, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Comment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createComment(comment, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1Api.createComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Comment\n         * @param {string} name Name of comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteComment(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteComment(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1Api.deleteComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Comment\n         * @param {string} name Name of comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getComment(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Comment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getComment(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1Api.getComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Comment\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listComment(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CommentList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listComment(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1Api.listComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Comment\n         * @param {string} name Name of comment\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchComment(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Comment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchComment(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1Api.patchComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Comment\n         * @param {string} name Name of comment\n         * @param {Comment} [comment] Updated comment\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateComment(name: string, comment?: Comment, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Comment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateComment(name, comment, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1Api.updateComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * CommentV1alpha1Api - factory interface\n */\nexport const CommentV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CommentV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Comment\n         * @param {CommentV1alpha1ApiCreateCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createComment(requestParameters: CommentV1alpha1ApiCreateCommentRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Comment> {\n            return localVarFp.createComment(requestParameters.comment, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Comment\n         * @param {CommentV1alpha1ApiDeleteCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteComment(requestParameters: CommentV1alpha1ApiDeleteCommentRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteComment(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Comment\n         * @param {CommentV1alpha1ApiGetCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getComment(requestParameters: CommentV1alpha1ApiGetCommentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Comment> {\n            return localVarFp.getComment(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Comment\n         * @param {CommentV1alpha1ApiListCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listComment(requestParameters: CommentV1alpha1ApiListCommentRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<CommentList> {\n            return localVarFp.listComment(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Comment\n         * @param {CommentV1alpha1ApiPatchCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchComment(requestParameters: CommentV1alpha1ApiPatchCommentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Comment> {\n            return localVarFp.patchComment(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Comment\n         * @param {CommentV1alpha1ApiUpdateCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateComment(requestParameters: CommentV1alpha1ApiUpdateCommentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Comment> {\n            return localVarFp.updateComment(requestParameters.name, requestParameters.comment, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createComment operation in CommentV1alpha1Api.\n */\nexport interface CommentV1alpha1ApiCreateCommentRequest {\n    /**\n     * Fresh comment\n     */\n    readonly comment?: Comment\n}\n\n/**\n * Request parameters for deleteComment operation in CommentV1alpha1Api.\n */\nexport interface CommentV1alpha1ApiDeleteCommentRequest {\n    /**\n     * Name of comment\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getComment operation in CommentV1alpha1Api.\n */\nexport interface CommentV1alpha1ApiGetCommentRequest {\n    /**\n     * Name of comment\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listComment operation in CommentV1alpha1Api.\n */\nexport interface CommentV1alpha1ApiListCommentRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchComment operation in CommentV1alpha1Api.\n */\nexport interface CommentV1alpha1ApiPatchCommentRequest {\n    /**\n     * Name of comment\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateComment operation in CommentV1alpha1Api.\n */\nexport interface CommentV1alpha1ApiUpdateCommentRequest {\n    /**\n     * Name of comment\n     */\n    readonly name: string\n\n    /**\n     * Updated comment\n     */\n    readonly comment?: Comment\n}\n\n/**\n * CommentV1alpha1Api - object-oriented interface\n */\nexport class CommentV1alpha1Api extends BaseAPI {\n    /**\n     * Create Comment\n     * @param {CommentV1alpha1ApiCreateCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createComment(requestParameters: CommentV1alpha1ApiCreateCommentRequest = {}, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ApiFp(this.configuration).createComment(requestParameters.comment, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Comment\n     * @param {CommentV1alpha1ApiDeleteCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteComment(requestParameters: CommentV1alpha1ApiDeleteCommentRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ApiFp(this.configuration).deleteComment(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Comment\n     * @param {CommentV1alpha1ApiGetCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getComment(requestParameters: CommentV1alpha1ApiGetCommentRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ApiFp(this.configuration).getComment(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Comment\n     * @param {CommentV1alpha1ApiListCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listComment(requestParameters: CommentV1alpha1ApiListCommentRequest = {}, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ApiFp(this.configuration).listComment(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Comment\n     * @param {CommentV1alpha1ApiPatchCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchComment(requestParameters: CommentV1alpha1ApiPatchCommentRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ApiFp(this.configuration).patchComment(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Comment\n     * @param {CommentV1alpha1ApiUpdateCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateComment(requestParameters: CommentV1alpha1ApiUpdateCommentRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ApiFp(this.configuration).updateComment(requestParameters.name, requestParameters.comment, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/comment-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Comment } from '../models';\n// @ts-ignore\nimport type { CommentRequest } from '../models';\n// @ts-ignore\nimport type { ListedCommentList } from '../models';\n// @ts-ignore\nimport type { Reply } from '../models';\n// @ts-ignore\nimport type { ReplyRequest } from '../models';\n/**\n * CommentV1alpha1ConsoleApi - axios parameter creator\n */\nexport const CommentV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create a comment.\n         * @param {CommentRequest} commentRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createComment: async (commentRequest: CommentRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'commentRequest' is not null or undefined\n            assertParamExists('createComment', 'commentRequest', commentRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/comments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(commentRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Create a reply.\n         * @param {string} name \n         * @param {ReplyRequest} replyRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReply: async (name: string, replyRequest: ReplyRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('createReply', 'name', name)\n            // verify required parameter 'replyRequest' is not null or undefined\n            assertParamExists('createReply', 'replyRequest', replyRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/comments/{name}/reply`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(replyRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List comments.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Comments filtered by keyword.\n         * @param {string} [ownerKind] Commenter kind.\n         * @param {string} [ownerName] Commenter name.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listComments: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, ownerKind?: string, ownerName?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/comments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n            if (ownerKind !== undefined) {\n                localVarQueryParameter['ownerKind'] = ownerKind;\n            }\n\n            if (ownerName !== undefined) {\n                localVarQueryParameter['ownerName'] = ownerName;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CommentV1alpha1ConsoleApi - functional programming interface\n */\nexport const CommentV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CommentV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create a comment.\n         * @param {CommentRequest} commentRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createComment(commentRequest: CommentRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Comment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createComment(commentRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1ConsoleApi.createComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Create a reply.\n         * @param {string} name \n         * @param {ReplyRequest} replyRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createReply(name: string, replyRequest: ReplyRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reply>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createReply(name, replyRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1ConsoleApi.createReply']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List comments.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Comments filtered by keyword.\n         * @param {string} [ownerKind] Commenter kind.\n         * @param {string} [ownerName] Commenter name.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listComments(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, ownerKind?: string, ownerName?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedCommentList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listComments(page, size, labelSelector, fieldSelector, sort, keyword, ownerKind, ownerName, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1ConsoleApi.listComments']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * CommentV1alpha1ConsoleApi - factory interface\n */\nexport const CommentV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CommentV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Create a comment.\n         * @param {CommentV1alpha1ConsoleApiCreateCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createComment(requestParameters: CommentV1alpha1ConsoleApiCreateCommentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Comment> {\n            return localVarFp.createComment(requestParameters.commentRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Create a reply.\n         * @param {CommentV1alpha1ConsoleApiCreateReplyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReply(requestParameters: CommentV1alpha1ConsoleApiCreateReplyRequest, options?: RawAxiosRequestConfig): AxiosPromise<Reply> {\n            return localVarFp.createReply(requestParameters.name, requestParameters.replyRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List comments.\n         * @param {CommentV1alpha1ConsoleApiListCommentsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listComments(requestParameters: CommentV1alpha1ConsoleApiListCommentsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ListedCommentList> {\n            return localVarFp.listComments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, requestParameters.ownerKind, requestParameters.ownerName, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createComment operation in CommentV1alpha1ConsoleApi.\n */\nexport interface CommentV1alpha1ConsoleApiCreateCommentRequest {\n    readonly commentRequest: CommentRequest\n}\n\n/**\n * Request parameters for createReply operation in CommentV1alpha1ConsoleApi.\n */\nexport interface CommentV1alpha1ConsoleApiCreateReplyRequest {\n    readonly name: string\n\n    readonly replyRequest: ReplyRequest\n}\n\n/**\n * Request parameters for listComments operation in CommentV1alpha1ConsoleApi.\n */\nexport interface CommentV1alpha1ConsoleApiListCommentsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Comments filtered by keyword.\n     */\n    readonly keyword?: string\n\n    /**\n     * Commenter kind.\n     */\n    readonly ownerKind?: string\n\n    /**\n     * Commenter name.\n     */\n    readonly ownerName?: string\n}\n\n/**\n * CommentV1alpha1ConsoleApi - object-oriented interface\n */\nexport class CommentV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Create a comment.\n     * @param {CommentV1alpha1ConsoleApiCreateCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createComment(requestParameters: CommentV1alpha1ConsoleApiCreateCommentRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ConsoleApiFp(this.configuration).createComment(requestParameters.commentRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Create a reply.\n     * @param {CommentV1alpha1ConsoleApiCreateReplyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createReply(requestParameters: CommentV1alpha1ConsoleApiCreateReplyRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ConsoleApiFp(this.configuration).createReply(requestParameters.name, requestParameters.replyRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List comments.\n     * @param {CommentV1alpha1ConsoleApiListCommentsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listComments(requestParameters: CommentV1alpha1ConsoleApiListCommentsRequest = {}, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1ConsoleApiFp(this.configuration).listComments(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, requestParameters.ownerKind, requestParameters.ownerName, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/comment-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Comment } from '../models';\n// @ts-ignore\nimport type { CommentRequest } from '../models';\n// @ts-ignore\nimport type { CommentVoList } from '../models';\n// @ts-ignore\nimport type { CommentWithReplyVoList } from '../models';\n// @ts-ignore\nimport type { Reply } from '../models';\n// @ts-ignore\nimport type { ReplyRequest } from '../models';\n// @ts-ignore\nimport type { ReplyVoList } from '../models';\n/**\n * CommentV1alpha1PublicApi - axios parameter creator\n */\nexport const CommentV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create a comment.\n         * @param {CommentRequest} commentRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createComment1: async (commentRequest: CommentRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'commentRequest' is not null or undefined\n            assertParamExists('createComment1', 'commentRequest', commentRequest)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/comments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(commentRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Create a reply.\n         * @param {string} name \n         * @param {ReplyRequest} replyRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReply1: async (name: string, replyRequest: ReplyRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('createReply1', 'name', name)\n            // verify required parameter 'replyRequest' is not null or undefined\n            assertParamExists('createReply1', 'replyRequest', replyRequest)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/comments/{name}/reply`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(replyRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get a comment.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getComment: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getComment', 'name', name)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/comments/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List comment replies.\n         * @param {string} name \n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listCommentReplies: async (name: string, page?: number, size?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('listCommentReplies', 'name', name)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/comments/{name}/reply`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List comments.\n         * @param {string} version The comment subject version.\n         * @param {string} kind The comment subject kind.\n         * @param {string} name The comment subject name.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [group] The comment subject group.\n         * @param {boolean} [withReplies] Whether to include replies. Default is false.\n         * @param {number} [replySize] Reply size of the comment, default is 10, only works when withReplies is true.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listComments1: async (version: string, kind: string, name: string, page?: number, size?: number, sort?: Array<string>, group?: string, withReplies?: boolean, replySize?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'version' is not null or undefined\n            assertParamExists('listComments1', 'version', version)\n            // verify required parameter 'kind' is not null or undefined\n            assertParamExists('listComments1', 'kind', kind)\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('listComments1', 'name', name)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/comments`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (group !== undefined) {\n                localVarQueryParameter['group'] = group;\n            }\n\n            if (version !== undefined) {\n                localVarQueryParameter['version'] = version;\n            }\n\n            if (kind !== undefined) {\n                localVarQueryParameter['kind'] = kind;\n            }\n\n            if (name !== undefined) {\n                localVarQueryParameter['name'] = name;\n            }\n\n            if (withReplies !== undefined) {\n                localVarQueryParameter['withReplies'] = withReplies;\n            }\n\n            if (replySize !== undefined) {\n                localVarQueryParameter['replySize'] = replySize;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CommentV1alpha1PublicApi - functional programming interface\n */\nexport const CommentV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CommentV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create a comment.\n         * @param {CommentRequest} commentRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createComment1(commentRequest: CommentRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Comment>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createComment1(commentRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1PublicApi.createComment1']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Create a reply.\n         * @param {string} name \n         * @param {ReplyRequest} replyRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createReply1(name: string, replyRequest: ReplyRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reply>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createReply1(name, replyRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1PublicApi.createReply1']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get a comment.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getComment(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CommentVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getComment(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1PublicApi.getComment']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List comment replies.\n         * @param {string} name \n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listCommentReplies(name: string, page?: number, size?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReplyVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listCommentReplies(name, page, size, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1PublicApi.listCommentReplies']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List comments.\n         * @param {string} version The comment subject version.\n         * @param {string} kind The comment subject kind.\n         * @param {string} name The comment subject name.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [group] The comment subject group.\n         * @param {boolean} [withReplies] Whether to include replies. Default is false.\n         * @param {number} [replySize] Reply size of the comment, default is 10, only works when withReplies is true.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listComments1(version: string, kind: string, name: string, page?: number, size?: number, sort?: Array<string>, group?: string, withReplies?: boolean, replySize?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CommentWithReplyVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listComments1(version, kind, name, page, size, sort, group, withReplies, replySize, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CommentV1alpha1PublicApi.listComments1']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * CommentV1alpha1PublicApi - factory interface\n */\nexport const CommentV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CommentV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Create a comment.\n         * @param {CommentV1alpha1PublicApiCreateComment1Request} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createComment1(requestParameters: CommentV1alpha1PublicApiCreateComment1Request, options?: RawAxiosRequestConfig): AxiosPromise<Comment> {\n            return localVarFp.createComment1(requestParameters.commentRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Create a reply.\n         * @param {CommentV1alpha1PublicApiCreateReply1Request} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReply1(requestParameters: CommentV1alpha1PublicApiCreateReply1Request, options?: RawAxiosRequestConfig): AxiosPromise<Reply> {\n            return localVarFp.createReply1(requestParameters.name, requestParameters.replyRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get a comment.\n         * @param {CommentV1alpha1PublicApiGetCommentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getComment(requestParameters: CommentV1alpha1PublicApiGetCommentRequest, options?: RawAxiosRequestConfig): AxiosPromise<CommentVoList> {\n            return localVarFp.getComment(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List comment replies.\n         * @param {CommentV1alpha1PublicApiListCommentRepliesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listCommentReplies(requestParameters: CommentV1alpha1PublicApiListCommentRepliesRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReplyVoList> {\n            return localVarFp.listCommentReplies(requestParameters.name, requestParameters.page, requestParameters.size, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List comments.\n         * @param {CommentV1alpha1PublicApiListComments1Request} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listComments1(requestParameters: CommentV1alpha1PublicApiListComments1Request, options?: RawAxiosRequestConfig): AxiosPromise<CommentWithReplyVoList> {\n            return localVarFp.listComments1(requestParameters.version, requestParameters.kind, requestParameters.name, requestParameters.page, requestParameters.size, requestParameters.sort, requestParameters.group, requestParameters.withReplies, requestParameters.replySize, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createComment1 operation in CommentV1alpha1PublicApi.\n */\nexport interface CommentV1alpha1PublicApiCreateComment1Request {\n    readonly commentRequest: CommentRequest\n}\n\n/**\n * Request parameters for createReply1 operation in CommentV1alpha1PublicApi.\n */\nexport interface CommentV1alpha1PublicApiCreateReply1Request {\n    readonly name: string\n\n    readonly replyRequest: ReplyRequest\n}\n\n/**\n * Request parameters for getComment operation in CommentV1alpha1PublicApi.\n */\nexport interface CommentV1alpha1PublicApiGetCommentRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for listCommentReplies operation in CommentV1alpha1PublicApi.\n */\nexport interface CommentV1alpha1PublicApiListCommentRepliesRequest {\n    readonly name: string\n\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n}\n\n/**\n * Request parameters for listComments1 operation in CommentV1alpha1PublicApi.\n */\nexport interface CommentV1alpha1PublicApiListComments1Request {\n    /**\n     * The comment subject version.\n     */\n    readonly version: string\n\n    /**\n     * The comment subject kind.\n     */\n    readonly kind: string\n\n    /**\n     * The comment subject name.\n     */\n    readonly name: string\n\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * The comment subject group.\n     */\n    readonly group?: string\n\n    /**\n     * Whether to include replies. Default is false.\n     */\n    readonly withReplies?: boolean\n\n    /**\n     * Reply size of the comment, default is 10, only works when withReplies is true.\n     */\n    readonly replySize?: number\n}\n\n/**\n * CommentV1alpha1PublicApi - object-oriented interface\n */\nexport class CommentV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Create a comment.\n     * @param {CommentV1alpha1PublicApiCreateComment1Request} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createComment1(requestParameters: CommentV1alpha1PublicApiCreateComment1Request, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1PublicApiFp(this.configuration).createComment1(requestParameters.commentRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Create a reply.\n     * @param {CommentV1alpha1PublicApiCreateReply1Request} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createReply1(requestParameters: CommentV1alpha1PublicApiCreateReply1Request, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1PublicApiFp(this.configuration).createReply1(requestParameters.name, requestParameters.replyRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get a comment.\n     * @param {CommentV1alpha1PublicApiGetCommentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getComment(requestParameters: CommentV1alpha1PublicApiGetCommentRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1PublicApiFp(this.configuration).getComment(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List comment replies.\n     * @param {CommentV1alpha1PublicApiListCommentRepliesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listCommentReplies(requestParameters: CommentV1alpha1PublicApiListCommentRepliesRequest, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1PublicApiFp(this.configuration).listCommentReplies(requestParameters.name, requestParameters.page, requestParameters.size, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List comments.\n     * @param {CommentV1alpha1PublicApiListComments1Request} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listComments1(requestParameters: CommentV1alpha1PublicApiListComments1Request, options?: RawAxiosRequestConfig) {\n        return CommentV1alpha1PublicApiFp(this.configuration).listComments1(requestParameters.version, requestParameters.kind, requestParameters.name, requestParameters.page, requestParameters.size, requestParameters.sort, requestParameters.group, requestParameters.withReplies, requestParameters.replySize, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/config-map-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ConfigMap } from '../models';\n// @ts-ignore\nimport type { ConfigMapList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * ConfigMapV1alpha1Api - axios parameter creator\n */\nexport const ConfigMapV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create ConfigMap\n         * @param {ConfigMap} [configMap] Fresh configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createConfigMap: async (configMap?: ConfigMap, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/configmaps`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(configMap, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete ConfigMap\n         * @param {string} name Name of configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteConfigMap: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteConfigMap', 'name', name)\n            const localVarPath = `/api/v1alpha1/configmaps/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get ConfigMap\n         * @param {string} name Name of configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getConfigMap: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getConfigMap', 'name', name)\n            const localVarPath = `/api/v1alpha1/configmaps/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List ConfigMap\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listConfigMap: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/configmaps`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch ConfigMap\n         * @param {string} name Name of configmap\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchConfigMap: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchConfigMap', 'name', name)\n            const localVarPath = `/api/v1alpha1/configmaps/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update ConfigMap\n         * @param {string} name Name of configmap\n         * @param {ConfigMap} [configMap] Updated configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateConfigMap: async (name: string, configMap?: ConfigMap, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateConfigMap', 'name', name)\n            const localVarPath = `/api/v1alpha1/configmaps/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(configMap, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ConfigMapV1alpha1Api - functional programming interface\n */\nexport const ConfigMapV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ConfigMapV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create ConfigMap\n         * @param {ConfigMap} [configMap] Fresh configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createConfigMap(configMap?: ConfigMap, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConfigMap>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createConfigMap(configMap, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ConfigMapV1alpha1Api.createConfigMap']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete ConfigMap\n         * @param {string} name Name of configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteConfigMap(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteConfigMap(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ConfigMapV1alpha1Api.deleteConfigMap']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get ConfigMap\n         * @param {string} name Name of configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getConfigMap(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConfigMap>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getConfigMap(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ConfigMapV1alpha1Api.getConfigMap']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List ConfigMap\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listConfigMap(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConfigMapList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listConfigMap(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ConfigMapV1alpha1Api.listConfigMap']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch ConfigMap\n         * @param {string} name Name of configmap\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchConfigMap(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConfigMap>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchConfigMap(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ConfigMapV1alpha1Api.patchConfigMap']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update ConfigMap\n         * @param {string} name Name of configmap\n         * @param {ConfigMap} [configMap] Updated configmap\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateConfigMap(name: string, configMap?: ConfigMap, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConfigMap>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateConfigMap(name, configMap, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ConfigMapV1alpha1Api.updateConfigMap']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ConfigMapV1alpha1Api - factory interface\n */\nexport const ConfigMapV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ConfigMapV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create ConfigMap\n         * @param {ConfigMapV1alpha1ApiCreateConfigMapRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createConfigMap(requestParameters: ConfigMapV1alpha1ApiCreateConfigMapRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ConfigMap> {\n            return localVarFp.createConfigMap(requestParameters.configMap, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete ConfigMap\n         * @param {ConfigMapV1alpha1ApiDeleteConfigMapRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteConfigMap(requestParameters: ConfigMapV1alpha1ApiDeleteConfigMapRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteConfigMap(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get ConfigMap\n         * @param {ConfigMapV1alpha1ApiGetConfigMapRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getConfigMap(requestParameters: ConfigMapV1alpha1ApiGetConfigMapRequest, options?: RawAxiosRequestConfig): AxiosPromise<ConfigMap> {\n            return localVarFp.getConfigMap(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List ConfigMap\n         * @param {ConfigMapV1alpha1ApiListConfigMapRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listConfigMap(requestParameters: ConfigMapV1alpha1ApiListConfigMapRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ConfigMapList> {\n            return localVarFp.listConfigMap(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch ConfigMap\n         * @param {ConfigMapV1alpha1ApiPatchConfigMapRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchConfigMap(requestParameters: ConfigMapV1alpha1ApiPatchConfigMapRequest, options?: RawAxiosRequestConfig): AxiosPromise<ConfigMap> {\n            return localVarFp.patchConfigMap(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update ConfigMap\n         * @param {ConfigMapV1alpha1ApiUpdateConfigMapRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateConfigMap(requestParameters: ConfigMapV1alpha1ApiUpdateConfigMapRequest, options?: RawAxiosRequestConfig): AxiosPromise<ConfigMap> {\n            return localVarFp.updateConfigMap(requestParameters.name, requestParameters.configMap, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createConfigMap operation in ConfigMapV1alpha1Api.\n */\nexport interface ConfigMapV1alpha1ApiCreateConfigMapRequest {\n    /**\n     * Fresh configmap\n     */\n    readonly configMap?: ConfigMap\n}\n\n/**\n * Request parameters for deleteConfigMap operation in ConfigMapV1alpha1Api.\n */\nexport interface ConfigMapV1alpha1ApiDeleteConfigMapRequest {\n    /**\n     * Name of configmap\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getConfigMap operation in ConfigMapV1alpha1Api.\n */\nexport interface ConfigMapV1alpha1ApiGetConfigMapRequest {\n    /**\n     * Name of configmap\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listConfigMap operation in ConfigMapV1alpha1Api.\n */\nexport interface ConfigMapV1alpha1ApiListConfigMapRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchConfigMap operation in ConfigMapV1alpha1Api.\n */\nexport interface ConfigMapV1alpha1ApiPatchConfigMapRequest {\n    /**\n     * Name of configmap\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateConfigMap operation in ConfigMapV1alpha1Api.\n */\nexport interface ConfigMapV1alpha1ApiUpdateConfigMapRequest {\n    /**\n     * Name of configmap\n     */\n    readonly name: string\n\n    /**\n     * Updated configmap\n     */\n    readonly configMap?: ConfigMap\n}\n\n/**\n * ConfigMapV1alpha1Api - object-oriented interface\n */\nexport class ConfigMapV1alpha1Api extends BaseAPI {\n    /**\n     * Create ConfigMap\n     * @param {ConfigMapV1alpha1ApiCreateConfigMapRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createConfigMap(requestParameters: ConfigMapV1alpha1ApiCreateConfigMapRequest = {}, options?: RawAxiosRequestConfig) {\n        return ConfigMapV1alpha1ApiFp(this.configuration).createConfigMap(requestParameters.configMap, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete ConfigMap\n     * @param {ConfigMapV1alpha1ApiDeleteConfigMapRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteConfigMap(requestParameters: ConfigMapV1alpha1ApiDeleteConfigMapRequest, options?: RawAxiosRequestConfig) {\n        return ConfigMapV1alpha1ApiFp(this.configuration).deleteConfigMap(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get ConfigMap\n     * @param {ConfigMapV1alpha1ApiGetConfigMapRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getConfigMap(requestParameters: ConfigMapV1alpha1ApiGetConfigMapRequest, options?: RawAxiosRequestConfig) {\n        return ConfigMapV1alpha1ApiFp(this.configuration).getConfigMap(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List ConfigMap\n     * @param {ConfigMapV1alpha1ApiListConfigMapRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listConfigMap(requestParameters: ConfigMapV1alpha1ApiListConfigMapRequest = {}, options?: RawAxiosRequestConfig) {\n        return ConfigMapV1alpha1ApiFp(this.configuration).listConfigMap(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch ConfigMap\n     * @param {ConfigMapV1alpha1ApiPatchConfigMapRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchConfigMap(requestParameters: ConfigMapV1alpha1ApiPatchConfigMapRequest, options?: RawAxiosRequestConfig) {\n        return ConfigMapV1alpha1ApiFp(this.configuration).patchConfigMap(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update ConfigMap\n     * @param {ConfigMapV1alpha1ApiUpdateConfigMapRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateConfigMap(requestParameters: ConfigMapV1alpha1ApiUpdateConfigMapRequest, options?: RawAxiosRequestConfig) {\n        return ConfigMapV1alpha1ApiFp(this.configuration).updateConfigMap(requestParameters.name, requestParameters.configMap, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/counter-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Counter } from '../models';\n// @ts-ignore\nimport type { CounterList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * CounterV1alpha1Api - axios parameter creator\n */\nexport const CounterV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Counter\n         * @param {Counter} [counter] Fresh counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCounter: async (counter?: Counter, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/metrics.halo.run/v1alpha1/counters`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(counter, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Counter\n         * @param {string} name Name of counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCounter: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteCounter', 'name', name)\n            const localVarPath = `/apis/metrics.halo.run/v1alpha1/counters/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Counter\n         * @param {string} name Name of counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCounter: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getCounter', 'name', name)\n            const localVarPath = `/apis/metrics.halo.run/v1alpha1/counters/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Counter\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listCounter: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/metrics.halo.run/v1alpha1/counters`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Counter\n         * @param {string} name Name of counter\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchCounter: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchCounter', 'name', name)\n            const localVarPath = `/apis/metrics.halo.run/v1alpha1/counters/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Counter\n         * @param {string} name Name of counter\n         * @param {Counter} [counter] Updated counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCounter: async (name: string, counter?: Counter, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateCounter', 'name', name)\n            const localVarPath = `/apis/metrics.halo.run/v1alpha1/counters/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(counter, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CounterV1alpha1Api - functional programming interface\n */\nexport const CounterV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CounterV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Counter\n         * @param {Counter} [counter] Fresh counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createCounter(counter?: Counter, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Counter>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createCounter(counter, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CounterV1alpha1Api.createCounter']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Counter\n         * @param {string} name Name of counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteCounter(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteCounter(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CounterV1alpha1Api.deleteCounter']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Counter\n         * @param {string} name Name of counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCounter(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Counter>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCounter(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CounterV1alpha1Api.getCounter']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Counter\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listCounter(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CounterList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listCounter(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CounterV1alpha1Api.listCounter']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Counter\n         * @param {string} name Name of counter\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchCounter(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Counter>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchCounter(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CounterV1alpha1Api.patchCounter']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Counter\n         * @param {string} name Name of counter\n         * @param {Counter} [counter] Updated counter\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateCounter(name: string, counter?: Counter, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Counter>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateCounter(name, counter, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['CounterV1alpha1Api.updateCounter']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * CounterV1alpha1Api - factory interface\n */\nexport const CounterV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CounterV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Counter\n         * @param {CounterV1alpha1ApiCreateCounterRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCounter(requestParameters: CounterV1alpha1ApiCreateCounterRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Counter> {\n            return localVarFp.createCounter(requestParameters.counter, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Counter\n         * @param {CounterV1alpha1ApiDeleteCounterRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCounter(requestParameters: CounterV1alpha1ApiDeleteCounterRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteCounter(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Counter\n         * @param {CounterV1alpha1ApiGetCounterRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCounter(requestParameters: CounterV1alpha1ApiGetCounterRequest, options?: RawAxiosRequestConfig): AxiosPromise<Counter> {\n            return localVarFp.getCounter(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Counter\n         * @param {CounterV1alpha1ApiListCounterRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listCounter(requestParameters: CounterV1alpha1ApiListCounterRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<CounterList> {\n            return localVarFp.listCounter(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Counter\n         * @param {CounterV1alpha1ApiPatchCounterRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchCounter(requestParameters: CounterV1alpha1ApiPatchCounterRequest, options?: RawAxiosRequestConfig): AxiosPromise<Counter> {\n            return localVarFp.patchCounter(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Counter\n         * @param {CounterV1alpha1ApiUpdateCounterRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCounter(requestParameters: CounterV1alpha1ApiUpdateCounterRequest, options?: RawAxiosRequestConfig): AxiosPromise<Counter> {\n            return localVarFp.updateCounter(requestParameters.name, requestParameters.counter, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createCounter operation in CounterV1alpha1Api.\n */\nexport interface CounterV1alpha1ApiCreateCounterRequest {\n    /**\n     * Fresh counter\n     */\n    readonly counter?: Counter\n}\n\n/**\n * Request parameters for deleteCounter operation in CounterV1alpha1Api.\n */\nexport interface CounterV1alpha1ApiDeleteCounterRequest {\n    /**\n     * Name of counter\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getCounter operation in CounterV1alpha1Api.\n */\nexport interface CounterV1alpha1ApiGetCounterRequest {\n    /**\n     * Name of counter\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listCounter operation in CounterV1alpha1Api.\n */\nexport interface CounterV1alpha1ApiListCounterRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchCounter operation in CounterV1alpha1Api.\n */\nexport interface CounterV1alpha1ApiPatchCounterRequest {\n    /**\n     * Name of counter\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateCounter operation in CounterV1alpha1Api.\n */\nexport interface CounterV1alpha1ApiUpdateCounterRequest {\n    /**\n     * Name of counter\n     */\n    readonly name: string\n\n    /**\n     * Updated counter\n     */\n    readonly counter?: Counter\n}\n\n/**\n * CounterV1alpha1Api - object-oriented interface\n */\nexport class CounterV1alpha1Api extends BaseAPI {\n    /**\n     * Create Counter\n     * @param {CounterV1alpha1ApiCreateCounterRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createCounter(requestParameters: CounterV1alpha1ApiCreateCounterRequest = {}, options?: RawAxiosRequestConfig) {\n        return CounterV1alpha1ApiFp(this.configuration).createCounter(requestParameters.counter, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Counter\n     * @param {CounterV1alpha1ApiDeleteCounterRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteCounter(requestParameters: CounterV1alpha1ApiDeleteCounterRequest, options?: RawAxiosRequestConfig) {\n        return CounterV1alpha1ApiFp(this.configuration).deleteCounter(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Counter\n     * @param {CounterV1alpha1ApiGetCounterRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getCounter(requestParameters: CounterV1alpha1ApiGetCounterRequest, options?: RawAxiosRequestConfig) {\n        return CounterV1alpha1ApiFp(this.configuration).getCounter(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Counter\n     * @param {CounterV1alpha1ApiListCounterRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listCounter(requestParameters: CounterV1alpha1ApiListCounterRequest = {}, options?: RawAxiosRequestConfig) {\n        return CounterV1alpha1ApiFp(this.configuration).listCounter(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Counter\n     * @param {CounterV1alpha1ApiPatchCounterRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchCounter(requestParameters: CounterV1alpha1ApiPatchCounterRequest, options?: RawAxiosRequestConfig) {\n        return CounterV1alpha1ApiFp(this.configuration).patchCounter(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Counter\n     * @param {CounterV1alpha1ApiUpdateCounterRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateCounter(requestParameters: CounterV1alpha1ApiUpdateCounterRequest, options?: RawAxiosRequestConfig) {\n        return CounterV1alpha1ApiFp(this.configuration).updateCounter(requestParameters.name, requestParameters.counter, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/device-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Device } from '../models';\n// @ts-ignore\nimport type { DeviceList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * DeviceV1alpha1Api - axios parameter creator\n */\nexport const DeviceV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Device\n         * @param {Device} [device] Fresh device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createDevice: async (device?: Device, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/security.halo.run/v1alpha1/devices`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(device, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Device\n         * @param {string} name Name of device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteDevice: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteDevice', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/devices/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Device\n         * @param {string} name Name of device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDevice: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getDevice', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/devices/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Device\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listDevice: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/security.halo.run/v1alpha1/devices`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Device\n         * @param {string} name Name of device\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchDevice: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchDevice', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/devices/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Device\n         * @param {string} name Name of device\n         * @param {Device} [device] Updated device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDevice: async (name: string, device?: Device, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateDevice', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/devices/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(device, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * DeviceV1alpha1Api - functional programming interface\n */\nexport const DeviceV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = DeviceV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Device\n         * @param {Device} [device] Fresh device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createDevice(device?: Device, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Device>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createDevice(device, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1Api.createDevice']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Device\n         * @param {string} name Name of device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteDevice(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteDevice(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1Api.deleteDevice']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Device\n         * @param {string} name Name of device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getDevice(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Device>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getDevice(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1Api.getDevice']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Device\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listDevice(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listDevice(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1Api.listDevice']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Device\n         * @param {string} name Name of device\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchDevice(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Device>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchDevice(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1Api.patchDevice']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Device\n         * @param {string} name Name of device\n         * @param {Device} [device] Updated device\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateDevice(name: string, device?: Device, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Device>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateDevice(name, device, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1Api.updateDevice']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * DeviceV1alpha1Api - factory interface\n */\nexport const DeviceV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = DeviceV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Device\n         * @param {DeviceV1alpha1ApiCreateDeviceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createDevice(requestParameters: DeviceV1alpha1ApiCreateDeviceRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Device> {\n            return localVarFp.createDevice(requestParameters.device, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Device\n         * @param {DeviceV1alpha1ApiDeleteDeviceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteDevice(requestParameters: DeviceV1alpha1ApiDeleteDeviceRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteDevice(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Device\n         * @param {DeviceV1alpha1ApiGetDeviceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDevice(requestParameters: DeviceV1alpha1ApiGetDeviceRequest, options?: RawAxiosRequestConfig): AxiosPromise<Device> {\n            return localVarFp.getDevice(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Device\n         * @param {DeviceV1alpha1ApiListDeviceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listDevice(requestParameters: DeviceV1alpha1ApiListDeviceRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<DeviceList> {\n            return localVarFp.listDevice(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Device\n         * @param {DeviceV1alpha1ApiPatchDeviceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchDevice(requestParameters: DeviceV1alpha1ApiPatchDeviceRequest, options?: RawAxiosRequestConfig): AxiosPromise<Device> {\n            return localVarFp.patchDevice(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Device\n         * @param {DeviceV1alpha1ApiUpdateDeviceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDevice(requestParameters: DeviceV1alpha1ApiUpdateDeviceRequest, options?: RawAxiosRequestConfig): AxiosPromise<Device> {\n            return localVarFp.updateDevice(requestParameters.name, requestParameters.device, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createDevice operation in DeviceV1alpha1Api.\n */\nexport interface DeviceV1alpha1ApiCreateDeviceRequest {\n    /**\n     * Fresh device\n     */\n    readonly device?: Device\n}\n\n/**\n * Request parameters for deleteDevice operation in DeviceV1alpha1Api.\n */\nexport interface DeviceV1alpha1ApiDeleteDeviceRequest {\n    /**\n     * Name of device\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getDevice operation in DeviceV1alpha1Api.\n */\nexport interface DeviceV1alpha1ApiGetDeviceRequest {\n    /**\n     * Name of device\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listDevice operation in DeviceV1alpha1Api.\n */\nexport interface DeviceV1alpha1ApiListDeviceRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchDevice operation in DeviceV1alpha1Api.\n */\nexport interface DeviceV1alpha1ApiPatchDeviceRequest {\n    /**\n     * Name of device\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateDevice operation in DeviceV1alpha1Api.\n */\nexport interface DeviceV1alpha1ApiUpdateDeviceRequest {\n    /**\n     * Name of device\n     */\n    readonly name: string\n\n    /**\n     * Updated device\n     */\n    readonly device?: Device\n}\n\n/**\n * DeviceV1alpha1Api - object-oriented interface\n */\nexport class DeviceV1alpha1Api extends BaseAPI {\n    /**\n     * Create Device\n     * @param {DeviceV1alpha1ApiCreateDeviceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createDevice(requestParameters: DeviceV1alpha1ApiCreateDeviceRequest = {}, options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1ApiFp(this.configuration).createDevice(requestParameters.device, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Device\n     * @param {DeviceV1alpha1ApiDeleteDeviceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteDevice(requestParameters: DeviceV1alpha1ApiDeleteDeviceRequest, options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1ApiFp(this.configuration).deleteDevice(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Device\n     * @param {DeviceV1alpha1ApiGetDeviceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getDevice(requestParameters: DeviceV1alpha1ApiGetDeviceRequest, options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1ApiFp(this.configuration).getDevice(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Device\n     * @param {DeviceV1alpha1ApiListDeviceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listDevice(requestParameters: DeviceV1alpha1ApiListDeviceRequest = {}, options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1ApiFp(this.configuration).listDevice(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Device\n     * @param {DeviceV1alpha1ApiPatchDeviceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchDevice(requestParameters: DeviceV1alpha1ApiPatchDeviceRequest, options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1ApiFp(this.configuration).patchDevice(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Device\n     * @param {DeviceV1alpha1ApiUpdateDeviceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateDevice(requestParameters: DeviceV1alpha1ApiUpdateDeviceRequest, options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1ApiFp(this.configuration).updateDevice(requestParameters.name, requestParameters.device, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/device-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { UserDevice } from '../models';\n/**\n * DeviceV1alpha1UcApi - axios parameter creator\n */\nexport const DeviceV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * List all user devices\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listDevices: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/devices`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Revoke a own device\n         * @param {string} deviceId Device ID\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revokeDevice: async (deviceId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'deviceId' is not null or undefined\n            assertParamExists('revokeDevice', 'deviceId', deviceId)\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/devices/{deviceId}`\n                .replace(`{${\"deviceId\"}}`, encodeURIComponent(String(deviceId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * DeviceV1alpha1UcApi - functional programming interface\n */\nexport const DeviceV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = DeviceV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * List all user devices\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listDevices(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserDevice>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listDevices(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1UcApi.listDevices']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Revoke a own device\n         * @param {string} deviceId Device ID\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async revokeDevice(deviceId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.revokeDevice(deviceId, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['DeviceV1alpha1UcApi.revokeDevice']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * DeviceV1alpha1UcApi - factory interface\n */\nexport const DeviceV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = DeviceV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * List all user devices\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listDevices(options?: RawAxiosRequestConfig): AxiosPromise<Array<UserDevice>> {\n            return localVarFp.listDevices(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Revoke a own device\n         * @param {DeviceV1alpha1UcApiRevokeDeviceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revokeDevice(requestParameters: DeviceV1alpha1UcApiRevokeDeviceRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.revokeDevice(requestParameters.deviceId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for revokeDevice operation in DeviceV1alpha1UcApi.\n */\nexport interface DeviceV1alpha1UcApiRevokeDeviceRequest {\n    /**\n     * Device ID\n     */\n    readonly deviceId: string\n}\n\n/**\n * DeviceV1alpha1UcApi - object-oriented interface\n */\nexport class DeviceV1alpha1UcApi extends BaseAPI {\n    /**\n     * List all user devices\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listDevices(options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1UcApiFp(this.configuration).listDevices(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Revoke a own device\n     * @param {DeviceV1alpha1UcApiRevokeDeviceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public revokeDevice(requestParameters: DeviceV1alpha1UcApiRevokeDeviceRequest, options?: RawAxiosRequestConfig) {\n        return DeviceV1alpha1UcApiFp(this.configuration).revokeDevice(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/extension-definition-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ExtensionDefinition } from '../models';\n// @ts-ignore\nimport type { ExtensionDefinitionList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * ExtensionDefinitionV1alpha1Api - axios parameter creator\n */\nexport const ExtensionDefinitionV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create ExtensionDefinition\n         * @param {ExtensionDefinition} [extensionDefinition] Fresh extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createExtensionDefinition: async (extensionDefinition?: ExtensionDefinition, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensiondefinitions`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(extensionDefinition, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteExtensionDefinition: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteExtensionDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensiondefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getExtensionDefinition: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getExtensionDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensiondefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List ExtensionDefinition\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listExtensionDefinition: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensiondefinitions`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchExtensionDefinition: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchExtensionDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensiondefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {ExtensionDefinition} [extensionDefinition] Updated extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateExtensionDefinition: async (name: string, extensionDefinition?: ExtensionDefinition, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateExtensionDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensiondefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(extensionDefinition, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ExtensionDefinitionV1alpha1Api - functional programming interface\n */\nexport const ExtensionDefinitionV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ExtensionDefinitionV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create ExtensionDefinition\n         * @param {ExtensionDefinition} [extensionDefinition] Fresh extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createExtensionDefinition(extensionDefinition?: ExtensionDefinition, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createExtensionDefinition(extensionDefinition, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionDefinitionV1alpha1Api.createExtensionDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteExtensionDefinition(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteExtensionDefinition(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionDefinitionV1alpha1Api.deleteExtensionDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getExtensionDefinition(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getExtensionDefinition(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionDefinitionV1alpha1Api.getExtensionDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List ExtensionDefinition\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listExtensionDefinition(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionDefinitionList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listExtensionDefinition(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionDefinitionV1alpha1Api.listExtensionDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchExtensionDefinition(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchExtensionDefinition(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionDefinitionV1alpha1Api.patchExtensionDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update ExtensionDefinition\n         * @param {string} name Name of extensiondefinition\n         * @param {ExtensionDefinition} [extensionDefinition] Updated extensiondefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateExtensionDefinition(name: string, extensionDefinition?: ExtensionDefinition, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateExtensionDefinition(name, extensionDefinition, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionDefinitionV1alpha1Api.updateExtensionDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ExtensionDefinitionV1alpha1Api - factory interface\n */\nexport const ExtensionDefinitionV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ExtensionDefinitionV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create ExtensionDefinition\n         * @param {ExtensionDefinitionV1alpha1ApiCreateExtensionDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiCreateExtensionDefinitionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionDefinition> {\n            return localVarFp.createExtensionDefinition(requestParameters.extensionDefinition, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete ExtensionDefinition\n         * @param {ExtensionDefinitionV1alpha1ApiDeleteExtensionDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiDeleteExtensionDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteExtensionDefinition(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get ExtensionDefinition\n         * @param {ExtensionDefinitionV1alpha1ApiGetExtensionDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiGetExtensionDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionDefinition> {\n            return localVarFp.getExtensionDefinition(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List ExtensionDefinition\n         * @param {ExtensionDefinitionV1alpha1ApiListExtensionDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiListExtensionDefinitionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionDefinitionList> {\n            return localVarFp.listExtensionDefinition(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch ExtensionDefinition\n         * @param {ExtensionDefinitionV1alpha1ApiPatchExtensionDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiPatchExtensionDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionDefinition> {\n            return localVarFp.patchExtensionDefinition(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update ExtensionDefinition\n         * @param {ExtensionDefinitionV1alpha1ApiUpdateExtensionDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiUpdateExtensionDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionDefinition> {\n            return localVarFp.updateExtensionDefinition(requestParameters.name, requestParameters.extensionDefinition, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createExtensionDefinition operation in ExtensionDefinitionV1alpha1Api.\n */\nexport interface ExtensionDefinitionV1alpha1ApiCreateExtensionDefinitionRequest {\n    /**\n     * Fresh extensiondefinition\n     */\n    readonly extensionDefinition?: ExtensionDefinition\n}\n\n/**\n * Request parameters for deleteExtensionDefinition operation in ExtensionDefinitionV1alpha1Api.\n */\nexport interface ExtensionDefinitionV1alpha1ApiDeleteExtensionDefinitionRequest {\n    /**\n     * Name of extensiondefinition\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getExtensionDefinition operation in ExtensionDefinitionV1alpha1Api.\n */\nexport interface ExtensionDefinitionV1alpha1ApiGetExtensionDefinitionRequest {\n    /**\n     * Name of extensiondefinition\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listExtensionDefinition operation in ExtensionDefinitionV1alpha1Api.\n */\nexport interface ExtensionDefinitionV1alpha1ApiListExtensionDefinitionRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchExtensionDefinition operation in ExtensionDefinitionV1alpha1Api.\n */\nexport interface ExtensionDefinitionV1alpha1ApiPatchExtensionDefinitionRequest {\n    /**\n     * Name of extensiondefinition\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateExtensionDefinition operation in ExtensionDefinitionV1alpha1Api.\n */\nexport interface ExtensionDefinitionV1alpha1ApiUpdateExtensionDefinitionRequest {\n    /**\n     * Name of extensiondefinition\n     */\n    readonly name: string\n\n    /**\n     * Updated extensiondefinition\n     */\n    readonly extensionDefinition?: ExtensionDefinition\n}\n\n/**\n * ExtensionDefinitionV1alpha1Api - object-oriented interface\n */\nexport class ExtensionDefinitionV1alpha1Api extends BaseAPI {\n    /**\n     * Create ExtensionDefinition\n     * @param {ExtensionDefinitionV1alpha1ApiCreateExtensionDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiCreateExtensionDefinitionRequest = {}, options?: RawAxiosRequestConfig) {\n        return ExtensionDefinitionV1alpha1ApiFp(this.configuration).createExtensionDefinition(requestParameters.extensionDefinition, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete ExtensionDefinition\n     * @param {ExtensionDefinitionV1alpha1ApiDeleteExtensionDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiDeleteExtensionDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionDefinitionV1alpha1ApiFp(this.configuration).deleteExtensionDefinition(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get ExtensionDefinition\n     * @param {ExtensionDefinitionV1alpha1ApiGetExtensionDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiGetExtensionDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionDefinitionV1alpha1ApiFp(this.configuration).getExtensionDefinition(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List ExtensionDefinition\n     * @param {ExtensionDefinitionV1alpha1ApiListExtensionDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiListExtensionDefinitionRequest = {}, options?: RawAxiosRequestConfig) {\n        return ExtensionDefinitionV1alpha1ApiFp(this.configuration).listExtensionDefinition(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch ExtensionDefinition\n     * @param {ExtensionDefinitionV1alpha1ApiPatchExtensionDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiPatchExtensionDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionDefinitionV1alpha1ApiFp(this.configuration).patchExtensionDefinition(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update ExtensionDefinition\n     * @param {ExtensionDefinitionV1alpha1ApiUpdateExtensionDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateExtensionDefinition(requestParameters: ExtensionDefinitionV1alpha1ApiUpdateExtensionDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionDefinitionV1alpha1ApiFp(this.configuration).updateExtensionDefinition(requestParameters.name, requestParameters.extensionDefinition, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/extension-point-definition-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ExtensionPointDefinition } from '../models';\n// @ts-ignore\nimport type { ExtensionPointDefinitionList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * ExtensionPointDefinitionV1alpha1Api - axios parameter creator\n */\nexport const ExtensionPointDefinitionV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create ExtensionPointDefinition\n         * @param {ExtensionPointDefinition} [extensionPointDefinition] Fresh extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createExtensionPointDefinition: async (extensionPointDefinition?: ExtensionPointDefinition, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(extensionPointDefinition, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteExtensionPointDefinition: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteExtensionPointDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getExtensionPointDefinition: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getExtensionPointDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List ExtensionPointDefinition\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listExtensionPointDefinition: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchExtensionPointDefinition: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchExtensionPointDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {ExtensionPointDefinition} [extensionPointDefinition] Updated extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateExtensionPointDefinition: async (name: string, extensionPointDefinition?: ExtensionPointDefinition, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateExtensionPointDefinition', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/extensionpointdefinitions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(extensionPointDefinition, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ExtensionPointDefinitionV1alpha1Api - functional programming interface\n */\nexport const ExtensionPointDefinitionV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ExtensionPointDefinitionV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create ExtensionPointDefinition\n         * @param {ExtensionPointDefinition} [extensionPointDefinition] Fresh extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createExtensionPointDefinition(extensionPointDefinition?: ExtensionPointDefinition, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionPointDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createExtensionPointDefinition(extensionPointDefinition, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionPointDefinitionV1alpha1Api.createExtensionPointDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteExtensionPointDefinition(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteExtensionPointDefinition(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionPointDefinitionV1alpha1Api.deleteExtensionPointDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getExtensionPointDefinition(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionPointDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getExtensionPointDefinition(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionPointDefinitionV1alpha1Api.getExtensionPointDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List ExtensionPointDefinition\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listExtensionPointDefinition(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionPointDefinitionList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listExtensionPointDefinition(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionPointDefinitionV1alpha1Api.listExtensionPointDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchExtensionPointDefinition(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionPointDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchExtensionPointDefinition(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionPointDefinitionV1alpha1Api.patchExtensionPointDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update ExtensionPointDefinition\n         * @param {string} name Name of extensionpointdefinition\n         * @param {ExtensionPointDefinition} [extensionPointDefinition] Updated extensionpointdefinition\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateExtensionPointDefinition(name: string, extensionPointDefinition?: ExtensionPointDefinition, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExtensionPointDefinition>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateExtensionPointDefinition(name, extensionPointDefinition, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ExtensionPointDefinitionV1alpha1Api.updateExtensionPointDefinition']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ExtensionPointDefinitionV1alpha1Api - factory interface\n */\nexport const ExtensionPointDefinitionV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ExtensionPointDefinitionV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create ExtensionPointDefinition\n         * @param {ExtensionPointDefinitionV1alpha1ApiCreateExtensionPointDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiCreateExtensionPointDefinitionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionPointDefinition> {\n            return localVarFp.createExtensionPointDefinition(requestParameters.extensionPointDefinition, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete ExtensionPointDefinition\n         * @param {ExtensionPointDefinitionV1alpha1ApiDeleteExtensionPointDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiDeleteExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteExtensionPointDefinition(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get ExtensionPointDefinition\n         * @param {ExtensionPointDefinitionV1alpha1ApiGetExtensionPointDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiGetExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionPointDefinition> {\n            return localVarFp.getExtensionPointDefinition(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List ExtensionPointDefinition\n         * @param {ExtensionPointDefinitionV1alpha1ApiListExtensionPointDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiListExtensionPointDefinitionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionPointDefinitionList> {\n            return localVarFp.listExtensionPointDefinition(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch ExtensionPointDefinition\n         * @param {ExtensionPointDefinitionV1alpha1ApiPatchExtensionPointDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiPatchExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionPointDefinition> {\n            return localVarFp.patchExtensionPointDefinition(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update ExtensionPointDefinition\n         * @param {ExtensionPointDefinitionV1alpha1ApiUpdateExtensionPointDefinitionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiUpdateExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig): AxiosPromise<ExtensionPointDefinition> {\n            return localVarFp.updateExtensionPointDefinition(requestParameters.name, requestParameters.extensionPointDefinition, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createExtensionPointDefinition operation in ExtensionPointDefinitionV1alpha1Api.\n */\nexport interface ExtensionPointDefinitionV1alpha1ApiCreateExtensionPointDefinitionRequest {\n    /**\n     * Fresh extensionpointdefinition\n     */\n    readonly extensionPointDefinition?: ExtensionPointDefinition\n}\n\n/**\n * Request parameters for deleteExtensionPointDefinition operation in ExtensionPointDefinitionV1alpha1Api.\n */\nexport interface ExtensionPointDefinitionV1alpha1ApiDeleteExtensionPointDefinitionRequest {\n    /**\n     * Name of extensionpointdefinition\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getExtensionPointDefinition operation in ExtensionPointDefinitionV1alpha1Api.\n */\nexport interface ExtensionPointDefinitionV1alpha1ApiGetExtensionPointDefinitionRequest {\n    /**\n     * Name of extensionpointdefinition\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listExtensionPointDefinition operation in ExtensionPointDefinitionV1alpha1Api.\n */\nexport interface ExtensionPointDefinitionV1alpha1ApiListExtensionPointDefinitionRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchExtensionPointDefinition operation in ExtensionPointDefinitionV1alpha1Api.\n */\nexport interface ExtensionPointDefinitionV1alpha1ApiPatchExtensionPointDefinitionRequest {\n    /**\n     * Name of extensionpointdefinition\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateExtensionPointDefinition operation in ExtensionPointDefinitionV1alpha1Api.\n */\nexport interface ExtensionPointDefinitionV1alpha1ApiUpdateExtensionPointDefinitionRequest {\n    /**\n     * Name of extensionpointdefinition\n     */\n    readonly name: string\n\n    /**\n     * Updated extensionpointdefinition\n     */\n    readonly extensionPointDefinition?: ExtensionPointDefinition\n}\n\n/**\n * ExtensionPointDefinitionV1alpha1Api - object-oriented interface\n */\nexport class ExtensionPointDefinitionV1alpha1Api extends BaseAPI {\n    /**\n     * Create ExtensionPointDefinition\n     * @param {ExtensionPointDefinitionV1alpha1ApiCreateExtensionPointDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiCreateExtensionPointDefinitionRequest = {}, options?: RawAxiosRequestConfig) {\n        return ExtensionPointDefinitionV1alpha1ApiFp(this.configuration).createExtensionPointDefinition(requestParameters.extensionPointDefinition, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete ExtensionPointDefinition\n     * @param {ExtensionPointDefinitionV1alpha1ApiDeleteExtensionPointDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiDeleteExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionPointDefinitionV1alpha1ApiFp(this.configuration).deleteExtensionPointDefinition(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get ExtensionPointDefinition\n     * @param {ExtensionPointDefinitionV1alpha1ApiGetExtensionPointDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiGetExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionPointDefinitionV1alpha1ApiFp(this.configuration).getExtensionPointDefinition(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List ExtensionPointDefinition\n     * @param {ExtensionPointDefinitionV1alpha1ApiListExtensionPointDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiListExtensionPointDefinitionRequest = {}, options?: RawAxiosRequestConfig) {\n        return ExtensionPointDefinitionV1alpha1ApiFp(this.configuration).listExtensionPointDefinition(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch ExtensionPointDefinition\n     * @param {ExtensionPointDefinitionV1alpha1ApiPatchExtensionPointDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiPatchExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionPointDefinitionV1alpha1ApiFp(this.configuration).patchExtensionPointDefinition(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update ExtensionPointDefinition\n     * @param {ExtensionPointDefinitionV1alpha1ApiUpdateExtensionPointDefinitionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateExtensionPointDefinition(requestParameters: ExtensionPointDefinitionV1alpha1ApiUpdateExtensionPointDefinitionRequest, options?: RawAxiosRequestConfig) {\n        return ExtensionPointDefinitionV1alpha1ApiFp(this.configuration).updateExtensionPointDefinition(requestParameters.name, requestParameters.extensionPointDefinition, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/group-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Group } from '../models';\n// @ts-ignore\nimport type { GroupList } from '../models';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n/**\n * GroupV1alpha1Api - axios parameter creator\n */\nexport const GroupV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Group\n         * @param {Group} [group] Fresh group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createGroup: async (group?: Group, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/groups`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(group, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Group\n         * @param {string} name Name of group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteGroup: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteGroup', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/groups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Group\n         * @param {string} name Name of group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getGroup: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getGroup', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/groups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Group\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listGroup: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/groups`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Group\n         * @param {string} name Name of group\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchGroup: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchGroup', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/groups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Group\n         * @param {string} name Name of group\n         * @param {Group} [group] Updated group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateGroup: async (name: string, group?: Group, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateGroup', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/groups/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(group, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * GroupV1alpha1Api - functional programming interface\n */\nexport const GroupV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = GroupV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Group\n         * @param {Group} [group] Fresh group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createGroup(group?: Group, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Group>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createGroup(group, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['GroupV1alpha1Api.createGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Group\n         * @param {string} name Name of group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteGroup(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteGroup(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['GroupV1alpha1Api.deleteGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Group\n         * @param {string} name Name of group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getGroup(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Group>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getGroup(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['GroupV1alpha1Api.getGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Group\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listGroup(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GroupList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listGroup(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['GroupV1alpha1Api.listGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Group\n         * @param {string} name Name of group\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchGroup(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Group>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchGroup(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['GroupV1alpha1Api.patchGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Group\n         * @param {string} name Name of group\n         * @param {Group} [group] Updated group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateGroup(name: string, group?: Group, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Group>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateGroup(name, group, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['GroupV1alpha1Api.updateGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * GroupV1alpha1Api - factory interface\n */\nexport const GroupV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = GroupV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Group\n         * @param {GroupV1alpha1ApiCreateGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createGroup(requestParameters: GroupV1alpha1ApiCreateGroupRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Group> {\n            return localVarFp.createGroup(requestParameters.group, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Group\n         * @param {GroupV1alpha1ApiDeleteGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteGroup(requestParameters: GroupV1alpha1ApiDeleteGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteGroup(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Group\n         * @param {GroupV1alpha1ApiGetGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getGroup(requestParameters: GroupV1alpha1ApiGetGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<Group> {\n            return localVarFp.getGroup(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Group\n         * @param {GroupV1alpha1ApiListGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listGroup(requestParameters: GroupV1alpha1ApiListGroupRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<GroupList> {\n            return localVarFp.listGroup(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Group\n         * @param {GroupV1alpha1ApiPatchGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchGroup(requestParameters: GroupV1alpha1ApiPatchGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<Group> {\n            return localVarFp.patchGroup(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Group\n         * @param {GroupV1alpha1ApiUpdateGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateGroup(requestParameters: GroupV1alpha1ApiUpdateGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<Group> {\n            return localVarFp.updateGroup(requestParameters.name, requestParameters.group, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createGroup operation in GroupV1alpha1Api.\n */\nexport interface GroupV1alpha1ApiCreateGroupRequest {\n    /**\n     * Fresh group\n     */\n    readonly group?: Group\n}\n\n/**\n * Request parameters for deleteGroup operation in GroupV1alpha1Api.\n */\nexport interface GroupV1alpha1ApiDeleteGroupRequest {\n    /**\n     * Name of group\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getGroup operation in GroupV1alpha1Api.\n */\nexport interface GroupV1alpha1ApiGetGroupRequest {\n    /**\n     * Name of group\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listGroup operation in GroupV1alpha1Api.\n */\nexport interface GroupV1alpha1ApiListGroupRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchGroup operation in GroupV1alpha1Api.\n */\nexport interface GroupV1alpha1ApiPatchGroupRequest {\n    /**\n     * Name of group\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateGroup operation in GroupV1alpha1Api.\n */\nexport interface GroupV1alpha1ApiUpdateGroupRequest {\n    /**\n     * Name of group\n     */\n    readonly name: string\n\n    /**\n     * Updated group\n     */\n    readonly group?: Group\n}\n\n/**\n * GroupV1alpha1Api - object-oriented interface\n */\nexport class GroupV1alpha1Api extends BaseAPI {\n    /**\n     * Create Group\n     * @param {GroupV1alpha1ApiCreateGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createGroup(requestParameters: GroupV1alpha1ApiCreateGroupRequest = {}, options?: RawAxiosRequestConfig) {\n        return GroupV1alpha1ApiFp(this.configuration).createGroup(requestParameters.group, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Group\n     * @param {GroupV1alpha1ApiDeleteGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteGroup(requestParameters: GroupV1alpha1ApiDeleteGroupRequest, options?: RawAxiosRequestConfig) {\n        return GroupV1alpha1ApiFp(this.configuration).deleteGroup(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Group\n     * @param {GroupV1alpha1ApiGetGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getGroup(requestParameters: GroupV1alpha1ApiGetGroupRequest, options?: RawAxiosRequestConfig) {\n        return GroupV1alpha1ApiFp(this.configuration).getGroup(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Group\n     * @param {GroupV1alpha1ApiListGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listGroup(requestParameters: GroupV1alpha1ApiListGroupRequest = {}, options?: RawAxiosRequestConfig) {\n        return GroupV1alpha1ApiFp(this.configuration).listGroup(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Group\n     * @param {GroupV1alpha1ApiPatchGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchGroup(requestParameters: GroupV1alpha1ApiPatchGroupRequest, options?: RawAxiosRequestConfig) {\n        return GroupV1alpha1ApiFp(this.configuration).patchGroup(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Group\n     * @param {GroupV1alpha1ApiUpdateGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateGroup(requestParameters: GroupV1alpha1ApiUpdateGroupRequest, options?: RawAxiosRequestConfig) {\n        return GroupV1alpha1ApiFp(this.configuration).updateGroup(requestParameters.name, requestParameters.group, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/index-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { SearchOption } from '../models';\n// @ts-ignore\nimport type { SearchResult } from '../models';\n/**\n * IndexV1alpha1PublicApi - axios parameter creator\n */\nexport const IndexV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Search indices.\n         * @param {SearchOption} [searchOption] Please note that the \\&quot;filterPublished\\&quot;, \\&quot;filterExposed\\&quot; and \\&quot;filterRecycled\\&quot; fields are ignored in this endpoint.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        indicesSearch: async (searchOption?: SearchOption, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.halo.run/v1alpha1/indices/-/search`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(searchOption, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * IndexV1alpha1PublicApi - functional programming interface\n */\nexport const IndexV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = IndexV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Search indices.\n         * @param {SearchOption} [searchOption] Please note that the \\&quot;filterPublished\\&quot;, \\&quot;filterExposed\\&quot; and \\&quot;filterRecycled\\&quot; fields are ignored in this endpoint.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async indicesSearch(searchOption?: SearchOption, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResult>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.indicesSearch(searchOption, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['IndexV1alpha1PublicApi.indicesSearch']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * IndexV1alpha1PublicApi - factory interface\n */\nexport const IndexV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = IndexV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Search indices.\n         * @param {IndexV1alpha1PublicApiIndicesSearchRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        indicesSearch(requestParameters: IndexV1alpha1PublicApiIndicesSearchRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SearchResult> {\n            return localVarFp.indicesSearch(requestParameters.searchOption, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for indicesSearch operation in IndexV1alpha1PublicApi.\n */\nexport interface IndexV1alpha1PublicApiIndicesSearchRequest {\n    /**\n     * Please note that the \\&quot;filterPublished\\&quot;, \\&quot;filterExposed\\&quot; and \\&quot;filterRecycled\\&quot; fields are ignored in this endpoint.\n     */\n    readonly searchOption?: SearchOption\n}\n\n/**\n * IndexV1alpha1PublicApi - object-oriented interface\n */\nexport class IndexV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Search indices.\n     * @param {IndexV1alpha1PublicApiIndicesSearchRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public indicesSearch(requestParameters: IndexV1alpha1PublicApiIndicesSearchRequest = {}, options?: RawAxiosRequestConfig) {\n        return IndexV1alpha1PublicApiFp(this.configuration).indicesSearch(requestParameters.searchOption, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/indices-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * IndicesV1alpha1ConsoleApi - axios parameter creator\n */\nexport const IndicesV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Rebuild all indices\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        rebuildAllIndices: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/indices/-/rebuild`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * IndicesV1alpha1ConsoleApi - functional programming interface\n */\nexport const IndicesV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = IndicesV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Rebuild all indices\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async rebuildAllIndices(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.rebuildAllIndices(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['IndicesV1alpha1ConsoleApi.rebuildAllIndices']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * IndicesV1alpha1ConsoleApi - factory interface\n */\nexport const IndicesV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = IndicesV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Rebuild all indices\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        rebuildAllIndices(options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.rebuildAllIndices(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * IndicesV1alpha1ConsoleApi - object-oriented interface\n */\nexport class IndicesV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Rebuild all indices\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public rebuildAllIndices(options?: RawAxiosRequestConfig) {\n        return IndicesV1alpha1ConsoleApiFp(this.configuration).rebuildAllIndices(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/local-thumbnail-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { LocalThumbnail } from '../models';\n// @ts-ignore\nimport type { LocalThumbnailList } from '../models';\n/**\n * LocalThumbnailV1alpha1Api - axios parameter creator\n */\nexport const LocalThumbnailV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create LocalThumbnail\n         * @param {LocalThumbnail} [localThumbnail] Fresh localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createLocalThumbnail: async (localThumbnail?: LocalThumbnail, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/localthumbnails`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(localThumbnail, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteLocalThumbnail: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteLocalThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/localthumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getLocalThumbnail: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getLocalThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/localthumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List LocalThumbnail\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listLocalThumbnail: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/localthumbnails`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchLocalThumbnail: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchLocalThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/localthumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {LocalThumbnail} [localThumbnail] Updated localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateLocalThumbnail: async (name: string, localThumbnail?: LocalThumbnail, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateLocalThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/localthumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(localThumbnail, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * LocalThumbnailV1alpha1Api - functional programming interface\n */\nexport const LocalThumbnailV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = LocalThumbnailV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create LocalThumbnail\n         * @param {LocalThumbnail} [localThumbnail] Fresh localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createLocalThumbnail(localThumbnail?: LocalThumbnail, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LocalThumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createLocalThumbnail(localThumbnail, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['LocalThumbnailV1alpha1Api.createLocalThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteLocalThumbnail(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteLocalThumbnail(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['LocalThumbnailV1alpha1Api.deleteLocalThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getLocalThumbnail(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LocalThumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getLocalThumbnail(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['LocalThumbnailV1alpha1Api.getLocalThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List LocalThumbnail\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listLocalThumbnail(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LocalThumbnailList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listLocalThumbnail(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['LocalThumbnailV1alpha1Api.listLocalThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchLocalThumbnail(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LocalThumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchLocalThumbnail(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['LocalThumbnailV1alpha1Api.patchLocalThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update LocalThumbnail\n         * @param {string} name Name of localthumbnail\n         * @param {LocalThumbnail} [localThumbnail] Updated localthumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateLocalThumbnail(name: string, localThumbnail?: LocalThumbnail, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LocalThumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateLocalThumbnail(name, localThumbnail, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['LocalThumbnailV1alpha1Api.updateLocalThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * LocalThumbnailV1alpha1Api - factory interface\n */\nexport const LocalThumbnailV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = LocalThumbnailV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create LocalThumbnail\n         * @param {LocalThumbnailV1alpha1ApiCreateLocalThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiCreateLocalThumbnailRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<LocalThumbnail> {\n            return localVarFp.createLocalThumbnail(requestParameters.localThumbnail, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete LocalThumbnail\n         * @param {LocalThumbnailV1alpha1ApiDeleteLocalThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiDeleteLocalThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteLocalThumbnail(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get LocalThumbnail\n         * @param {LocalThumbnailV1alpha1ApiGetLocalThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiGetLocalThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<LocalThumbnail> {\n            return localVarFp.getLocalThumbnail(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List LocalThumbnail\n         * @param {LocalThumbnailV1alpha1ApiListLocalThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiListLocalThumbnailRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<LocalThumbnailList> {\n            return localVarFp.listLocalThumbnail(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch LocalThumbnail\n         * @param {LocalThumbnailV1alpha1ApiPatchLocalThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiPatchLocalThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<LocalThumbnail> {\n            return localVarFp.patchLocalThumbnail(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update LocalThumbnail\n         * @param {LocalThumbnailV1alpha1ApiUpdateLocalThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiUpdateLocalThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<LocalThumbnail> {\n            return localVarFp.updateLocalThumbnail(requestParameters.name, requestParameters.localThumbnail, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createLocalThumbnail operation in LocalThumbnailV1alpha1Api.\n */\nexport interface LocalThumbnailV1alpha1ApiCreateLocalThumbnailRequest {\n    /**\n     * Fresh localthumbnail\n     */\n    readonly localThumbnail?: LocalThumbnail\n}\n\n/**\n * Request parameters for deleteLocalThumbnail operation in LocalThumbnailV1alpha1Api.\n */\nexport interface LocalThumbnailV1alpha1ApiDeleteLocalThumbnailRequest {\n    /**\n     * Name of localthumbnail\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getLocalThumbnail operation in LocalThumbnailV1alpha1Api.\n */\nexport interface LocalThumbnailV1alpha1ApiGetLocalThumbnailRequest {\n    /**\n     * Name of localthumbnail\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listLocalThumbnail operation in LocalThumbnailV1alpha1Api.\n */\nexport interface LocalThumbnailV1alpha1ApiListLocalThumbnailRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchLocalThumbnail operation in LocalThumbnailV1alpha1Api.\n */\nexport interface LocalThumbnailV1alpha1ApiPatchLocalThumbnailRequest {\n    /**\n     * Name of localthumbnail\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateLocalThumbnail operation in LocalThumbnailV1alpha1Api.\n */\nexport interface LocalThumbnailV1alpha1ApiUpdateLocalThumbnailRequest {\n    /**\n     * Name of localthumbnail\n     */\n    readonly name: string\n\n    /**\n     * Updated localthumbnail\n     */\n    readonly localThumbnail?: LocalThumbnail\n}\n\n/**\n * LocalThumbnailV1alpha1Api - object-oriented interface\n */\nexport class LocalThumbnailV1alpha1Api extends BaseAPI {\n    /**\n     * Create LocalThumbnail\n     * @param {LocalThumbnailV1alpha1ApiCreateLocalThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiCreateLocalThumbnailRequest = {}, options?: RawAxiosRequestConfig) {\n        return LocalThumbnailV1alpha1ApiFp(this.configuration).createLocalThumbnail(requestParameters.localThumbnail, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete LocalThumbnail\n     * @param {LocalThumbnailV1alpha1ApiDeleteLocalThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiDeleteLocalThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return LocalThumbnailV1alpha1ApiFp(this.configuration).deleteLocalThumbnail(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get LocalThumbnail\n     * @param {LocalThumbnailV1alpha1ApiGetLocalThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiGetLocalThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return LocalThumbnailV1alpha1ApiFp(this.configuration).getLocalThumbnail(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List LocalThumbnail\n     * @param {LocalThumbnailV1alpha1ApiListLocalThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiListLocalThumbnailRequest = {}, options?: RawAxiosRequestConfig) {\n        return LocalThumbnailV1alpha1ApiFp(this.configuration).listLocalThumbnail(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch LocalThumbnail\n     * @param {LocalThumbnailV1alpha1ApiPatchLocalThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiPatchLocalThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return LocalThumbnailV1alpha1ApiFp(this.configuration).patchLocalThumbnail(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update LocalThumbnail\n     * @param {LocalThumbnailV1alpha1ApiUpdateLocalThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateLocalThumbnail(requestParameters: LocalThumbnailV1alpha1ApiUpdateLocalThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return LocalThumbnailV1alpha1ApiFp(this.configuration).updateLocalThumbnail(requestParameters.name, requestParameters.localThumbnail, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/menu-item-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { MenuItem } from '../models';\n// @ts-ignore\nimport type { MenuItemList } from '../models';\n/**\n * MenuItemV1alpha1Api - axios parameter creator\n */\nexport const MenuItemV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create MenuItem\n         * @param {MenuItem} [menuItem] Fresh menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createMenuItem: async (menuItem?: MenuItem, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/menuitems`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(menuItem, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete MenuItem\n         * @param {string} name Name of menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteMenuItem: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteMenuItem', 'name', name)\n            const localVarPath = `/api/v1alpha1/menuitems/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get MenuItem\n         * @param {string} name Name of menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMenuItem: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getMenuItem', 'name', name)\n            const localVarPath = `/api/v1alpha1/menuitems/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List MenuItem\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMenuItem: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/menuitems`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch MenuItem\n         * @param {string} name Name of menuitem\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchMenuItem: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchMenuItem', 'name', name)\n            const localVarPath = `/api/v1alpha1/menuitems/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update MenuItem\n         * @param {string} name Name of menuitem\n         * @param {MenuItem} [menuItem] Updated menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMenuItem: async (name: string, menuItem?: MenuItem, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateMenuItem', 'name', name)\n            const localVarPath = `/api/v1alpha1/menuitems/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(menuItem, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MenuItemV1alpha1Api - functional programming interface\n */\nexport const MenuItemV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MenuItemV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create MenuItem\n         * @param {MenuItem} [menuItem] Fresh menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createMenuItem(menuItem?: MenuItem, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuItem>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createMenuItem(menuItem, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuItemV1alpha1Api.createMenuItem']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete MenuItem\n         * @param {string} name Name of menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteMenuItem(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteMenuItem(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuItemV1alpha1Api.deleteMenuItem']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get MenuItem\n         * @param {string} name Name of menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMenuItem(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuItem>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMenuItem(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuItemV1alpha1Api.getMenuItem']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List MenuItem\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listMenuItem(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuItemList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listMenuItem(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuItemV1alpha1Api.listMenuItem']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch MenuItem\n         * @param {string} name Name of menuitem\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchMenuItem(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuItem>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchMenuItem(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuItemV1alpha1Api.patchMenuItem']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update MenuItem\n         * @param {string} name Name of menuitem\n         * @param {MenuItem} [menuItem] Updated menuitem\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateMenuItem(name: string, menuItem?: MenuItem, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuItem>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateMenuItem(name, menuItem, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuItemV1alpha1Api.updateMenuItem']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * MenuItemV1alpha1Api - factory interface\n */\nexport const MenuItemV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MenuItemV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create MenuItem\n         * @param {MenuItemV1alpha1ApiCreateMenuItemRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createMenuItem(requestParameters: MenuItemV1alpha1ApiCreateMenuItemRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<MenuItem> {\n            return localVarFp.createMenuItem(requestParameters.menuItem, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete MenuItem\n         * @param {MenuItemV1alpha1ApiDeleteMenuItemRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteMenuItem(requestParameters: MenuItemV1alpha1ApiDeleteMenuItemRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteMenuItem(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get MenuItem\n         * @param {MenuItemV1alpha1ApiGetMenuItemRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMenuItem(requestParameters: MenuItemV1alpha1ApiGetMenuItemRequest, options?: RawAxiosRequestConfig): AxiosPromise<MenuItem> {\n            return localVarFp.getMenuItem(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List MenuItem\n         * @param {MenuItemV1alpha1ApiListMenuItemRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMenuItem(requestParameters: MenuItemV1alpha1ApiListMenuItemRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<MenuItemList> {\n            return localVarFp.listMenuItem(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch MenuItem\n         * @param {MenuItemV1alpha1ApiPatchMenuItemRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchMenuItem(requestParameters: MenuItemV1alpha1ApiPatchMenuItemRequest, options?: RawAxiosRequestConfig): AxiosPromise<MenuItem> {\n            return localVarFp.patchMenuItem(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update MenuItem\n         * @param {MenuItemV1alpha1ApiUpdateMenuItemRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMenuItem(requestParameters: MenuItemV1alpha1ApiUpdateMenuItemRequest, options?: RawAxiosRequestConfig): AxiosPromise<MenuItem> {\n            return localVarFp.updateMenuItem(requestParameters.name, requestParameters.menuItem, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createMenuItem operation in MenuItemV1alpha1Api.\n */\nexport interface MenuItemV1alpha1ApiCreateMenuItemRequest {\n    /**\n     * Fresh menuitem\n     */\n    readonly menuItem?: MenuItem\n}\n\n/**\n * Request parameters for deleteMenuItem operation in MenuItemV1alpha1Api.\n */\nexport interface MenuItemV1alpha1ApiDeleteMenuItemRequest {\n    /**\n     * Name of menuitem\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getMenuItem operation in MenuItemV1alpha1Api.\n */\nexport interface MenuItemV1alpha1ApiGetMenuItemRequest {\n    /**\n     * Name of menuitem\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listMenuItem operation in MenuItemV1alpha1Api.\n */\nexport interface MenuItemV1alpha1ApiListMenuItemRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchMenuItem operation in MenuItemV1alpha1Api.\n */\nexport interface MenuItemV1alpha1ApiPatchMenuItemRequest {\n    /**\n     * Name of menuitem\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateMenuItem operation in MenuItemV1alpha1Api.\n */\nexport interface MenuItemV1alpha1ApiUpdateMenuItemRequest {\n    /**\n     * Name of menuitem\n     */\n    readonly name: string\n\n    /**\n     * Updated menuitem\n     */\n    readonly menuItem?: MenuItem\n}\n\n/**\n * MenuItemV1alpha1Api - object-oriented interface\n */\nexport class MenuItemV1alpha1Api extends BaseAPI {\n    /**\n     * Create MenuItem\n     * @param {MenuItemV1alpha1ApiCreateMenuItemRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createMenuItem(requestParameters: MenuItemV1alpha1ApiCreateMenuItemRequest = {}, options?: RawAxiosRequestConfig) {\n        return MenuItemV1alpha1ApiFp(this.configuration).createMenuItem(requestParameters.menuItem, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete MenuItem\n     * @param {MenuItemV1alpha1ApiDeleteMenuItemRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteMenuItem(requestParameters: MenuItemV1alpha1ApiDeleteMenuItemRequest, options?: RawAxiosRequestConfig) {\n        return MenuItemV1alpha1ApiFp(this.configuration).deleteMenuItem(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get MenuItem\n     * @param {MenuItemV1alpha1ApiGetMenuItemRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getMenuItem(requestParameters: MenuItemV1alpha1ApiGetMenuItemRequest, options?: RawAxiosRequestConfig) {\n        return MenuItemV1alpha1ApiFp(this.configuration).getMenuItem(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List MenuItem\n     * @param {MenuItemV1alpha1ApiListMenuItemRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listMenuItem(requestParameters: MenuItemV1alpha1ApiListMenuItemRequest = {}, options?: RawAxiosRequestConfig) {\n        return MenuItemV1alpha1ApiFp(this.configuration).listMenuItem(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch MenuItem\n     * @param {MenuItemV1alpha1ApiPatchMenuItemRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchMenuItem(requestParameters: MenuItemV1alpha1ApiPatchMenuItemRequest, options?: RawAxiosRequestConfig) {\n        return MenuItemV1alpha1ApiFp(this.configuration).patchMenuItem(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update MenuItem\n     * @param {MenuItemV1alpha1ApiUpdateMenuItemRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateMenuItem(requestParameters: MenuItemV1alpha1ApiUpdateMenuItemRequest, options?: RawAxiosRequestConfig) {\n        return MenuItemV1alpha1ApiFp(this.configuration).updateMenuItem(requestParameters.name, requestParameters.menuItem, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/menu-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Menu } from '../models';\n// @ts-ignore\nimport type { MenuList } from '../models';\n/**\n * MenuV1alpha1Api - axios parameter creator\n */\nexport const MenuV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Menu\n         * @param {Menu} [menu] Fresh menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createMenu: async (menu?: Menu, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/menus`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(menu, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Menu\n         * @param {string} name Name of menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteMenu: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteMenu', 'name', name)\n            const localVarPath = `/api/v1alpha1/menus/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Menu\n         * @param {string} name Name of menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMenu: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getMenu', 'name', name)\n            const localVarPath = `/api/v1alpha1/menus/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Menu\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMenu: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/menus`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Menu\n         * @param {string} name Name of menu\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchMenu: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchMenu', 'name', name)\n            const localVarPath = `/api/v1alpha1/menus/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Menu\n         * @param {string} name Name of menu\n         * @param {Menu} [menu] Updated menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMenu: async (name: string, menu?: Menu, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateMenu', 'name', name)\n            const localVarPath = `/api/v1alpha1/menus/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(menu, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MenuV1alpha1Api - functional programming interface\n */\nexport const MenuV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MenuV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Menu\n         * @param {Menu} [menu] Fresh menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createMenu(menu?: Menu, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Menu>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createMenu(menu, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1Api.createMenu']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Menu\n         * @param {string} name Name of menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteMenu(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteMenu(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1Api.deleteMenu']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Menu\n         * @param {string} name Name of menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMenu(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Menu>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMenu(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1Api.getMenu']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Menu\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listMenu(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listMenu(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1Api.listMenu']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Menu\n         * @param {string} name Name of menu\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchMenu(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Menu>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchMenu(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1Api.patchMenu']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Menu\n         * @param {string} name Name of menu\n         * @param {Menu} [menu] Updated menu\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateMenu(name: string, menu?: Menu, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Menu>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateMenu(name, menu, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1Api.updateMenu']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * MenuV1alpha1Api - factory interface\n */\nexport const MenuV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MenuV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Menu\n         * @param {MenuV1alpha1ApiCreateMenuRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createMenu(requestParameters: MenuV1alpha1ApiCreateMenuRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Menu> {\n            return localVarFp.createMenu(requestParameters.menu, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Menu\n         * @param {MenuV1alpha1ApiDeleteMenuRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteMenu(requestParameters: MenuV1alpha1ApiDeleteMenuRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteMenu(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Menu\n         * @param {MenuV1alpha1ApiGetMenuRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMenu(requestParameters: MenuV1alpha1ApiGetMenuRequest, options?: RawAxiosRequestConfig): AxiosPromise<Menu> {\n            return localVarFp.getMenu(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Menu\n         * @param {MenuV1alpha1ApiListMenuRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMenu(requestParameters: MenuV1alpha1ApiListMenuRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<MenuList> {\n            return localVarFp.listMenu(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Menu\n         * @param {MenuV1alpha1ApiPatchMenuRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchMenu(requestParameters: MenuV1alpha1ApiPatchMenuRequest, options?: RawAxiosRequestConfig): AxiosPromise<Menu> {\n            return localVarFp.patchMenu(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Menu\n         * @param {MenuV1alpha1ApiUpdateMenuRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMenu(requestParameters: MenuV1alpha1ApiUpdateMenuRequest, options?: RawAxiosRequestConfig): AxiosPromise<Menu> {\n            return localVarFp.updateMenu(requestParameters.name, requestParameters.menu, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createMenu operation in MenuV1alpha1Api.\n */\nexport interface MenuV1alpha1ApiCreateMenuRequest {\n    /**\n     * Fresh menu\n     */\n    readonly menu?: Menu\n}\n\n/**\n * Request parameters for deleteMenu operation in MenuV1alpha1Api.\n */\nexport interface MenuV1alpha1ApiDeleteMenuRequest {\n    /**\n     * Name of menu\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getMenu operation in MenuV1alpha1Api.\n */\nexport interface MenuV1alpha1ApiGetMenuRequest {\n    /**\n     * Name of menu\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listMenu operation in MenuV1alpha1Api.\n */\nexport interface MenuV1alpha1ApiListMenuRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchMenu operation in MenuV1alpha1Api.\n */\nexport interface MenuV1alpha1ApiPatchMenuRequest {\n    /**\n     * Name of menu\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateMenu operation in MenuV1alpha1Api.\n */\nexport interface MenuV1alpha1ApiUpdateMenuRequest {\n    /**\n     * Name of menu\n     */\n    readonly name: string\n\n    /**\n     * Updated menu\n     */\n    readonly menu?: Menu\n}\n\n/**\n * MenuV1alpha1Api - object-oriented interface\n */\nexport class MenuV1alpha1Api extends BaseAPI {\n    /**\n     * Create Menu\n     * @param {MenuV1alpha1ApiCreateMenuRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createMenu(requestParameters: MenuV1alpha1ApiCreateMenuRequest = {}, options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1ApiFp(this.configuration).createMenu(requestParameters.menu, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Menu\n     * @param {MenuV1alpha1ApiDeleteMenuRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteMenu(requestParameters: MenuV1alpha1ApiDeleteMenuRequest, options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1ApiFp(this.configuration).deleteMenu(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Menu\n     * @param {MenuV1alpha1ApiGetMenuRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getMenu(requestParameters: MenuV1alpha1ApiGetMenuRequest, options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1ApiFp(this.configuration).getMenu(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Menu\n     * @param {MenuV1alpha1ApiListMenuRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listMenu(requestParameters: MenuV1alpha1ApiListMenuRequest = {}, options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1ApiFp(this.configuration).listMenu(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Menu\n     * @param {MenuV1alpha1ApiPatchMenuRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchMenu(requestParameters: MenuV1alpha1ApiPatchMenuRequest, options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1ApiFp(this.configuration).patchMenu(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Menu\n     * @param {MenuV1alpha1ApiUpdateMenuRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateMenu(requestParameters: MenuV1alpha1ApiUpdateMenuRequest, options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1ApiFp(this.configuration).updateMenu(requestParameters.name, requestParameters.menu, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/menu-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { MenuVo } from '../models';\n/**\n * MenuV1alpha1PublicApi - axios parameter creator\n */\nexport const MenuV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Gets menu by name.\n         * @param {string} name Menu name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryMenuByName: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryMenuByName', 'name', name)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/menus/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Gets primary menu.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPrimaryMenu: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.halo.run/v1alpha1/menus/-`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MenuV1alpha1PublicApi - functional programming interface\n */\nexport const MenuV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MenuV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Gets menu by name.\n         * @param {string} name Menu name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryMenuByName(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryMenuByName(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1PublicApi.queryMenuByName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Gets primary menu.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryPrimaryMenu(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MenuVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryPrimaryMenu(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MenuV1alpha1PublicApi.queryPrimaryMenu']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * MenuV1alpha1PublicApi - factory interface\n */\nexport const MenuV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MenuV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Gets menu by name.\n         * @param {MenuV1alpha1PublicApiQueryMenuByNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryMenuByName(requestParameters: MenuV1alpha1PublicApiQueryMenuByNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<MenuVo> {\n            return localVarFp.queryMenuByName(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Gets primary menu.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPrimaryMenu(options?: RawAxiosRequestConfig): AxiosPromise<MenuVo> {\n            return localVarFp.queryPrimaryMenu(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for queryMenuByName operation in MenuV1alpha1PublicApi.\n */\nexport interface MenuV1alpha1PublicApiQueryMenuByNameRequest {\n    /**\n     * Menu name\n     */\n    readonly name: string\n}\n\n/**\n * MenuV1alpha1PublicApi - object-oriented interface\n */\nexport class MenuV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Gets menu by name.\n     * @param {MenuV1alpha1PublicApiQueryMenuByNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryMenuByName(requestParameters: MenuV1alpha1PublicApiQueryMenuByNameRequest, options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1PublicApiFp(this.configuration).queryMenuByName(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Gets primary menu.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryPrimaryMenu(options?: RawAxiosRequestConfig) {\n        return MenuV1alpha1PublicApiFp(this.configuration).queryPrimaryMenu(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/metrics-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { CounterRequest } from '../models';\n// @ts-ignore\nimport type { VoteRequest } from '../models';\n/**\n * MetricsV1alpha1PublicApi - axios parameter creator\n */\nexport const MetricsV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Count an extension resource visits.\n         * @param {CounterRequest} counterRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        count: async (counterRequest: CounterRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'counterRequest' is not null or undefined\n            assertParamExists('count', 'counterRequest', counterRequest)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/trackers/counter`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(counterRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Downvote an extension resource.\n         * @param {VoteRequest} voteRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        downvote: async (voteRequest: VoteRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'voteRequest' is not null or undefined\n            assertParamExists('downvote', 'voteRequest', voteRequest)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/trackers/downvote`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(voteRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upvote an extension resource.\n         * @param {VoteRequest} voteRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upvote: async (voteRequest: VoteRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'voteRequest' is not null or undefined\n            assertParamExists('upvote', 'voteRequest', voteRequest)\n            const localVarPath = `/apis/api.halo.run/v1alpha1/trackers/upvote`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(voteRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MetricsV1alpha1PublicApi - functional programming interface\n */\nexport const MetricsV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MetricsV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Count an extension resource visits.\n         * @param {CounterRequest} counterRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async count(counterRequest: CounterRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.count(counterRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MetricsV1alpha1PublicApi.count']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Downvote an extension resource.\n         * @param {VoteRequest} voteRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async downvote(voteRequest: VoteRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.downvote(voteRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MetricsV1alpha1PublicApi.downvote']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upvote an extension resource.\n         * @param {VoteRequest} voteRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async upvote(voteRequest: VoteRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.upvote(voteRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MetricsV1alpha1PublicApi.upvote']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * MetricsV1alpha1PublicApi - factory interface\n */\nexport const MetricsV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MetricsV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Count an extension resource visits.\n         * @param {MetricsV1alpha1PublicApiCountRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        count(requestParameters: MetricsV1alpha1PublicApiCountRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.count(requestParameters.counterRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Downvote an extension resource.\n         * @param {MetricsV1alpha1PublicApiDownvoteRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        downvote(requestParameters: MetricsV1alpha1PublicApiDownvoteRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.downvote(requestParameters.voteRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upvote an extension resource.\n         * @param {MetricsV1alpha1PublicApiUpvoteRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upvote(requestParameters: MetricsV1alpha1PublicApiUpvoteRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.upvote(requestParameters.voteRequest, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for count operation in MetricsV1alpha1PublicApi.\n */\nexport interface MetricsV1alpha1PublicApiCountRequest {\n    readonly counterRequest: CounterRequest\n}\n\n/**\n * Request parameters for downvote operation in MetricsV1alpha1PublicApi.\n */\nexport interface MetricsV1alpha1PublicApiDownvoteRequest {\n    readonly voteRequest: VoteRequest\n}\n\n/**\n * Request parameters for upvote operation in MetricsV1alpha1PublicApi.\n */\nexport interface MetricsV1alpha1PublicApiUpvoteRequest {\n    readonly voteRequest: VoteRequest\n}\n\n/**\n * MetricsV1alpha1PublicApi - object-oriented interface\n */\nexport class MetricsV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Count an extension resource visits.\n     * @param {MetricsV1alpha1PublicApiCountRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public count(requestParameters: MetricsV1alpha1PublicApiCountRequest, options?: RawAxiosRequestConfig) {\n        return MetricsV1alpha1PublicApiFp(this.configuration).count(requestParameters.counterRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Downvote an extension resource.\n     * @param {MetricsV1alpha1PublicApiDownvoteRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public downvote(requestParameters: MetricsV1alpha1PublicApiDownvoteRequest, options?: RawAxiosRequestConfig) {\n        return MetricsV1alpha1PublicApiFp(this.configuration).downvote(requestParameters.voteRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upvote an extension resource.\n     * @param {MetricsV1alpha1PublicApiUpvoteRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public upvote(requestParameters: MetricsV1alpha1PublicApiUpvoteRequest, options?: RawAxiosRequestConfig) {\n        return MetricsV1alpha1PublicApiFp(this.configuration).upvote(requestParameters.voteRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/migration-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { BackupFile } from '../models';\n/**\n * MigrationV1alpha1ConsoleApi - axios parameter creator\n */\nexport const MigrationV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} name Backup name.\n         * @param {string} filename Backup filename.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        downloadBackups: async (name: string, filename: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('downloadBackups', 'name', name)\n            // verify required parameter 'filename' is not null or undefined\n            assertParamExists('downloadBackups', 'filename', filename)\n            const localVarPath = `/apis/console.api.migration.halo.run/v1alpha1/backups/{name}/files/{filename}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)))\n                .replace(`{${\"filename\"}}`, encodeURIComponent(String(filename)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get backup files from backup root.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBackupFiles: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/console.api.migration.halo.run/v1alpha1/backup-files`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Restore backup by uploading file or providing download link or backup name.\n         * @param {string} [backupName] Backup metadata name.\n         * @param {string} [downloadUrl] Remote backup HTTP URL.\n         * @param {File} [file] \n         * @param {string} [filename] Filename of backup file in backups root.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        restoreBackup: async (backupName?: string, downloadUrl?: string, file?: File, filename?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/console.api.migration.halo.run/v1alpha1/restorations`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (backupName !== undefined) { \n                localVarFormParams.append('backupName', backupName as any);\n            }\n    \n            if (downloadUrl !== undefined) { \n                localVarFormParams.append('downloadUrl', downloadUrl as any);\n            }\n    \n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n            if (filename !== undefined) { \n                localVarFormParams.append('filename', filename as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MigrationV1alpha1ConsoleApi - functional programming interface\n */\nexport const MigrationV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MigrationV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} name Backup name.\n         * @param {string} filename Backup filename.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async downloadBackups(name: string, filename: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.downloadBackups(name, filename, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MigrationV1alpha1ConsoleApi.downloadBackups']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get backup files from backup root.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getBackupFiles(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BackupFile>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getBackupFiles(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MigrationV1alpha1ConsoleApi.getBackupFiles']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Restore backup by uploading file or providing download link or backup name.\n         * @param {string} [backupName] Backup metadata name.\n         * @param {string} [downloadUrl] Remote backup HTTP URL.\n         * @param {File} [file] \n         * @param {string} [filename] Filename of backup file in backups root.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async restoreBackup(backupName?: string, downloadUrl?: string, file?: File, filename?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.restoreBackup(backupName, downloadUrl, file, filename, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['MigrationV1alpha1ConsoleApi.restoreBackup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * MigrationV1alpha1ConsoleApi - factory interface\n */\nexport const MigrationV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MigrationV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {MigrationV1alpha1ConsoleApiDownloadBackupsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        downloadBackups(requestParameters: MigrationV1alpha1ConsoleApiDownloadBackupsRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.downloadBackups(requestParameters.name, requestParameters.filename, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get backup files from backup root.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBackupFiles(options?: RawAxiosRequestConfig): AxiosPromise<Array<BackupFile>> {\n            return localVarFp.getBackupFiles(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Restore backup by uploading file or providing download link or backup name.\n         * @param {MigrationV1alpha1ConsoleApiRestoreBackupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        restoreBackup(requestParameters: MigrationV1alpha1ConsoleApiRestoreBackupRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.restoreBackup(requestParameters.backupName, requestParameters.downloadUrl, requestParameters.file, requestParameters.filename, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for downloadBackups operation in MigrationV1alpha1ConsoleApi.\n */\nexport interface MigrationV1alpha1ConsoleApiDownloadBackupsRequest {\n    /**\n     * Backup name.\n     */\n    readonly name: string\n\n    /**\n     * Backup filename.\n     */\n    readonly filename: string\n}\n\n/**\n * Request parameters for restoreBackup operation in MigrationV1alpha1ConsoleApi.\n */\nexport interface MigrationV1alpha1ConsoleApiRestoreBackupRequest {\n    /**\n     * Backup metadata name.\n     */\n    readonly backupName?: string\n\n    /**\n     * Remote backup HTTP URL.\n     */\n    readonly downloadUrl?: string\n\n    readonly file?: File\n\n    /**\n     * Filename of backup file in backups root.\n     */\n    readonly filename?: string\n}\n\n/**\n * MigrationV1alpha1ConsoleApi - object-oriented interface\n */\nexport class MigrationV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * \n     * @param {MigrationV1alpha1ConsoleApiDownloadBackupsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public downloadBackups(requestParameters: MigrationV1alpha1ConsoleApiDownloadBackupsRequest, options?: RawAxiosRequestConfig) {\n        return MigrationV1alpha1ConsoleApiFp(this.configuration).downloadBackups(requestParameters.name, requestParameters.filename, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get backup files from backup root.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getBackupFiles(options?: RawAxiosRequestConfig) {\n        return MigrationV1alpha1ConsoleApiFp(this.configuration).getBackupFiles(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Restore backup by uploading file or providing download link or backup name.\n     * @param {MigrationV1alpha1ConsoleApiRestoreBackupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public restoreBackup(requestParameters: MigrationV1alpha1ConsoleApiRestoreBackupRequest = {}, options?: RawAxiosRequestConfig) {\n        return MigrationV1alpha1ConsoleApiFp(this.configuration).restoreBackup(requestParameters.backupName, requestParameters.downloadUrl, requestParameters.file, requestParameters.filename, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/notification-template-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { NotificationTemplate } from '../models';\n// @ts-ignore\nimport type { NotificationTemplateList } from '../models';\n/**\n * NotificationTemplateV1alpha1Api - axios parameter creator\n */\nexport const NotificationTemplateV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create NotificationTemplate\n         * @param {NotificationTemplate} [notificationTemplate] Fresh notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotificationTemplate: async (notificationTemplate?: NotificationTemplate, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notificationtemplates`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(notificationTemplate, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotificationTemplate: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteNotificationTemplate', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notificationtemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotificationTemplate: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getNotificationTemplate', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notificationtemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List NotificationTemplate\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listNotificationTemplate: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notificationtemplates`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchNotificationTemplate: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchNotificationTemplate', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notificationtemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {NotificationTemplate} [notificationTemplate] Updated notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotificationTemplate: async (name: string, notificationTemplate?: NotificationTemplate, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateNotificationTemplate', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notificationtemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(notificationTemplate, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotificationTemplateV1alpha1Api - functional programming interface\n */\nexport const NotificationTemplateV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotificationTemplateV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create NotificationTemplate\n         * @param {NotificationTemplate} [notificationTemplate] Fresh notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createNotificationTemplate(notificationTemplate?: NotificationTemplate, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createNotificationTemplate(notificationTemplate, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationTemplateV1alpha1Api.createNotificationTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteNotificationTemplate(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteNotificationTemplate(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationTemplateV1alpha1Api.deleteNotificationTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getNotificationTemplate(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getNotificationTemplate(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationTemplateV1alpha1Api.getNotificationTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List NotificationTemplate\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listNotificationTemplate(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationTemplateList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listNotificationTemplate(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationTemplateV1alpha1Api.listNotificationTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchNotificationTemplate(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchNotificationTemplate(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationTemplateV1alpha1Api.patchNotificationTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update NotificationTemplate\n         * @param {string} name Name of notificationtemplate\n         * @param {NotificationTemplate} [notificationTemplate] Updated notificationtemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateNotificationTemplate(name: string, notificationTemplate?: NotificationTemplate, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateNotificationTemplate(name, notificationTemplate, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationTemplateV1alpha1Api.updateNotificationTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * NotificationTemplateV1alpha1Api - factory interface\n */\nexport const NotificationTemplateV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotificationTemplateV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create NotificationTemplate\n         * @param {NotificationTemplateV1alpha1ApiCreateNotificationTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiCreateNotificationTemplateRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<NotificationTemplate> {\n            return localVarFp.createNotificationTemplate(requestParameters.notificationTemplate, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete NotificationTemplate\n         * @param {NotificationTemplateV1alpha1ApiDeleteNotificationTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiDeleteNotificationTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteNotificationTemplate(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get NotificationTemplate\n         * @param {NotificationTemplateV1alpha1ApiGetNotificationTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiGetNotificationTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<NotificationTemplate> {\n            return localVarFp.getNotificationTemplate(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List NotificationTemplate\n         * @param {NotificationTemplateV1alpha1ApiListNotificationTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiListNotificationTemplateRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<NotificationTemplateList> {\n            return localVarFp.listNotificationTemplate(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch NotificationTemplate\n         * @param {NotificationTemplateV1alpha1ApiPatchNotificationTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiPatchNotificationTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<NotificationTemplate> {\n            return localVarFp.patchNotificationTemplate(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update NotificationTemplate\n         * @param {NotificationTemplateV1alpha1ApiUpdateNotificationTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiUpdateNotificationTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<NotificationTemplate> {\n            return localVarFp.updateNotificationTemplate(requestParameters.name, requestParameters.notificationTemplate, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createNotificationTemplate operation in NotificationTemplateV1alpha1Api.\n */\nexport interface NotificationTemplateV1alpha1ApiCreateNotificationTemplateRequest {\n    /**\n     * Fresh notificationtemplate\n     */\n    readonly notificationTemplate?: NotificationTemplate\n}\n\n/**\n * Request parameters for deleteNotificationTemplate operation in NotificationTemplateV1alpha1Api.\n */\nexport interface NotificationTemplateV1alpha1ApiDeleteNotificationTemplateRequest {\n    /**\n     * Name of notificationtemplate\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getNotificationTemplate operation in NotificationTemplateV1alpha1Api.\n */\nexport interface NotificationTemplateV1alpha1ApiGetNotificationTemplateRequest {\n    /**\n     * Name of notificationtemplate\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listNotificationTemplate operation in NotificationTemplateV1alpha1Api.\n */\nexport interface NotificationTemplateV1alpha1ApiListNotificationTemplateRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchNotificationTemplate operation in NotificationTemplateV1alpha1Api.\n */\nexport interface NotificationTemplateV1alpha1ApiPatchNotificationTemplateRequest {\n    /**\n     * Name of notificationtemplate\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateNotificationTemplate operation in NotificationTemplateV1alpha1Api.\n */\nexport interface NotificationTemplateV1alpha1ApiUpdateNotificationTemplateRequest {\n    /**\n     * Name of notificationtemplate\n     */\n    readonly name: string\n\n    /**\n     * Updated notificationtemplate\n     */\n    readonly notificationTemplate?: NotificationTemplate\n}\n\n/**\n * NotificationTemplateV1alpha1Api - object-oriented interface\n */\nexport class NotificationTemplateV1alpha1Api extends BaseAPI {\n    /**\n     * Create NotificationTemplate\n     * @param {NotificationTemplateV1alpha1ApiCreateNotificationTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiCreateNotificationTemplateRequest = {}, options?: RawAxiosRequestConfig) {\n        return NotificationTemplateV1alpha1ApiFp(this.configuration).createNotificationTemplate(requestParameters.notificationTemplate, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete NotificationTemplate\n     * @param {NotificationTemplateV1alpha1ApiDeleteNotificationTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiDeleteNotificationTemplateRequest, options?: RawAxiosRequestConfig) {\n        return NotificationTemplateV1alpha1ApiFp(this.configuration).deleteNotificationTemplate(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get NotificationTemplate\n     * @param {NotificationTemplateV1alpha1ApiGetNotificationTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiGetNotificationTemplateRequest, options?: RawAxiosRequestConfig) {\n        return NotificationTemplateV1alpha1ApiFp(this.configuration).getNotificationTemplate(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List NotificationTemplate\n     * @param {NotificationTemplateV1alpha1ApiListNotificationTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiListNotificationTemplateRequest = {}, options?: RawAxiosRequestConfig) {\n        return NotificationTemplateV1alpha1ApiFp(this.configuration).listNotificationTemplate(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch NotificationTemplate\n     * @param {NotificationTemplateV1alpha1ApiPatchNotificationTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiPatchNotificationTemplateRequest, options?: RawAxiosRequestConfig) {\n        return NotificationTemplateV1alpha1ApiFp(this.configuration).patchNotificationTemplate(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update NotificationTemplate\n     * @param {NotificationTemplateV1alpha1ApiUpdateNotificationTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateNotificationTemplate(requestParameters: NotificationTemplateV1alpha1ApiUpdateNotificationTemplateRequest, options?: RawAxiosRequestConfig) {\n        return NotificationTemplateV1alpha1ApiFp(this.configuration).updateNotificationTemplate(requestParameters.name, requestParameters.notificationTemplate, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/notification-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Notification } from '../models';\n// @ts-ignore\nimport type { NotificationList } from '../models';\n/**\n * NotificationV1alpha1Api - axios parameter creator\n */\nexport const NotificationV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Notification\n         * @param {Notification} [notification] Fresh notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotification: async (notification?: Notification, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifications`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(notification, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Notification\n         * @param {string} name Name of notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotification: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteNotification', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifications/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Notification\n         * @param {string} name Name of notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotification: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getNotification', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifications/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Notification\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listNotification: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifications`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Notification\n         * @param {string} name Name of notification\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchNotification: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchNotification', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifications/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Notification\n         * @param {string} name Name of notification\n         * @param {Notification} [notification] Updated notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotification: async (name: string, notification?: Notification, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateNotification', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifications/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(notification, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotificationV1alpha1Api - functional programming interface\n */\nexport const NotificationV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotificationV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Notification\n         * @param {Notification} [notification] Fresh notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createNotification(notification?: Notification, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Notification>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createNotification(notification, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1Api.createNotification']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Notification\n         * @param {string} name Name of notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteNotification(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteNotification(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1Api.deleteNotification']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Notification\n         * @param {string} name Name of notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getNotification(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Notification>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getNotification(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1Api.getNotification']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Notification\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listNotification(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listNotification(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1Api.listNotification']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Notification\n         * @param {string} name Name of notification\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchNotification(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Notification>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchNotification(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1Api.patchNotification']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Notification\n         * @param {string} name Name of notification\n         * @param {Notification} [notification] Updated notification\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateNotification(name: string, notification?: Notification, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Notification>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateNotification(name, notification, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1Api.updateNotification']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * NotificationV1alpha1Api - factory interface\n */\nexport const NotificationV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotificationV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Notification\n         * @param {NotificationV1alpha1ApiCreateNotificationRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotification(requestParameters: NotificationV1alpha1ApiCreateNotificationRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Notification> {\n            return localVarFp.createNotification(requestParameters.notification, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Notification\n         * @param {NotificationV1alpha1ApiDeleteNotificationRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotification(requestParameters: NotificationV1alpha1ApiDeleteNotificationRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteNotification(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Notification\n         * @param {NotificationV1alpha1ApiGetNotificationRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotification(requestParameters: NotificationV1alpha1ApiGetNotificationRequest, options?: RawAxiosRequestConfig): AxiosPromise<Notification> {\n            return localVarFp.getNotification(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Notification\n         * @param {NotificationV1alpha1ApiListNotificationRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listNotification(requestParameters: NotificationV1alpha1ApiListNotificationRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<NotificationList> {\n            return localVarFp.listNotification(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Notification\n         * @param {NotificationV1alpha1ApiPatchNotificationRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchNotification(requestParameters: NotificationV1alpha1ApiPatchNotificationRequest, options?: RawAxiosRequestConfig): AxiosPromise<Notification> {\n            return localVarFp.patchNotification(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Notification\n         * @param {NotificationV1alpha1ApiUpdateNotificationRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotification(requestParameters: NotificationV1alpha1ApiUpdateNotificationRequest, options?: RawAxiosRequestConfig): AxiosPromise<Notification> {\n            return localVarFp.updateNotification(requestParameters.name, requestParameters.notification, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createNotification operation in NotificationV1alpha1Api.\n */\nexport interface NotificationV1alpha1ApiCreateNotificationRequest {\n    /**\n     * Fresh notification\n     */\n    readonly notification?: Notification\n}\n\n/**\n * Request parameters for deleteNotification operation in NotificationV1alpha1Api.\n */\nexport interface NotificationV1alpha1ApiDeleteNotificationRequest {\n    /**\n     * Name of notification\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getNotification operation in NotificationV1alpha1Api.\n */\nexport interface NotificationV1alpha1ApiGetNotificationRequest {\n    /**\n     * Name of notification\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listNotification operation in NotificationV1alpha1Api.\n */\nexport interface NotificationV1alpha1ApiListNotificationRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchNotification operation in NotificationV1alpha1Api.\n */\nexport interface NotificationV1alpha1ApiPatchNotificationRequest {\n    /**\n     * Name of notification\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateNotification operation in NotificationV1alpha1Api.\n */\nexport interface NotificationV1alpha1ApiUpdateNotificationRequest {\n    /**\n     * Name of notification\n     */\n    readonly name: string\n\n    /**\n     * Updated notification\n     */\n    readonly notification?: Notification\n}\n\n/**\n * NotificationV1alpha1Api - object-oriented interface\n */\nexport class NotificationV1alpha1Api extends BaseAPI {\n    /**\n     * Create Notification\n     * @param {NotificationV1alpha1ApiCreateNotificationRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createNotification(requestParameters: NotificationV1alpha1ApiCreateNotificationRequest = {}, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1ApiFp(this.configuration).createNotification(requestParameters.notification, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Notification\n     * @param {NotificationV1alpha1ApiDeleteNotificationRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteNotification(requestParameters: NotificationV1alpha1ApiDeleteNotificationRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1ApiFp(this.configuration).deleteNotification(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Notification\n     * @param {NotificationV1alpha1ApiGetNotificationRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getNotification(requestParameters: NotificationV1alpha1ApiGetNotificationRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1ApiFp(this.configuration).getNotification(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Notification\n     * @param {NotificationV1alpha1ApiListNotificationRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listNotification(requestParameters: NotificationV1alpha1ApiListNotificationRequest = {}, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1ApiFp(this.configuration).listNotification(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Notification\n     * @param {NotificationV1alpha1ApiPatchNotificationRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchNotification(requestParameters: NotificationV1alpha1ApiPatchNotificationRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1ApiFp(this.configuration).patchNotification(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Notification\n     * @param {NotificationV1alpha1ApiUpdateNotificationRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateNotification(requestParameters: NotificationV1alpha1ApiUpdateNotificationRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1ApiFp(this.configuration).updateNotification(requestParameters.name, requestParameters.notification, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/notification-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * NotificationV1alpha1PublicApi - axios parameter creator\n */\nexport const NotificationV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Unsubscribe a subscription\n         * @param {string} name Subscription name\n         * @param {string} token Unsubscribe token\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        unsubscribe: async (name: string, token: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('unsubscribe', 'name', name)\n            // verify required parameter 'token' is not null or undefined\n            assertParamExists('unsubscribe', 'token', token)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/subscriptions/{name}/unsubscribe`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (token !== undefined) {\n                localVarQueryParameter['token'] = token;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotificationV1alpha1PublicApi - functional programming interface\n */\nexport const NotificationV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotificationV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Unsubscribe a subscription\n         * @param {string} name Subscription name\n         * @param {string} token Unsubscribe token\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async unsubscribe(name: string, token: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.unsubscribe(name, token, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1PublicApi.unsubscribe']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * NotificationV1alpha1PublicApi - factory interface\n */\nexport const NotificationV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotificationV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Unsubscribe a subscription\n         * @param {NotificationV1alpha1PublicApiUnsubscribeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        unsubscribe(requestParameters: NotificationV1alpha1PublicApiUnsubscribeRequest, options?: RawAxiosRequestConfig): AxiosPromise<string> {\n            return localVarFp.unsubscribe(requestParameters.name, requestParameters.token, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for unsubscribe operation in NotificationV1alpha1PublicApi.\n */\nexport interface NotificationV1alpha1PublicApiUnsubscribeRequest {\n    /**\n     * Subscription name\n     */\n    readonly name: string\n\n    /**\n     * Unsubscribe token\n     */\n    readonly token: string\n}\n\n/**\n * NotificationV1alpha1PublicApi - object-oriented interface\n */\nexport class NotificationV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Unsubscribe a subscription\n     * @param {NotificationV1alpha1PublicApiUnsubscribeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public unsubscribe(requestParameters: NotificationV1alpha1PublicApiUnsubscribeRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1PublicApiFp(this.configuration).unsubscribe(requestParameters.name, requestParameters.token, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/notification-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { MarkSpecifiedRequest } from '../models';\n// @ts-ignore\nimport type { Notification } from '../models';\n// @ts-ignore\nimport type { NotificationList } from '../models';\n// @ts-ignore\nimport type { ReasonTypeNotifierCollectionRequest } from '../models';\n// @ts-ignore\nimport type { ReasonTypeNotifierMatrix } from '../models';\n/**\n * NotificationV1alpha1UcApi - axios parameter creator\n */\nexport const NotificationV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Delete the specified notification.\n         * @param {string} username Username\n         * @param {string} name Notification name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSpecifiedNotification: async (username: string, name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('deleteSpecifiedNotification', 'username', username)\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteSpecifiedNotification', 'name', name)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)))\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List notification preferences for the authenticated user.\n         * @param {string} username Username\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUserNotificationPreferences: async (username: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('listUserNotificationPreferences', 'username', username)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notification-preferences`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List notifications for the authenticated user.\n         * @param {string} username Username\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUserNotifications: async (username: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('listUserNotifications', 'username', username)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Mark the specified notification as read.\n         * @param {string} username Username\n         * @param {string} name Notification name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        markNotificationAsRead: async (username: string, name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('markNotificationAsRead', 'username', username)\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('markNotificationAsRead', 'name', name)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}/mark-as-read`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)))\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Mark the specified notifications as read.\n         * @param {string} username Username\n         * @param {MarkSpecifiedRequest} markSpecifiedRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        markNotificationsAsRead: async (username: string, markSpecifiedRequest: MarkSpecifiedRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('markNotificationsAsRead', 'username', username)\n            // verify required parameter 'markSpecifiedRequest' is not null or undefined\n            assertParamExists('markNotificationsAsRead', 'markSpecifiedRequest', markSpecifiedRequest)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/-/mark-specified-as-read`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(markSpecifiedRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Save notification preferences for the authenticated user.\n         * @param {string} username Username\n         * @param {ReasonTypeNotifierCollectionRequest} [reasonTypeNotifierCollectionRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveUserNotificationPreferences: async (username: string, reasonTypeNotifierCollectionRequest?: ReasonTypeNotifierCollectionRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('saveUserNotificationPreferences', 'username', username)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notification-preferences`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reasonTypeNotifierCollectionRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotificationV1alpha1UcApi - functional programming interface\n */\nexport const NotificationV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotificationV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Delete the specified notification.\n         * @param {string} username Username\n         * @param {string} name Notification name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteSpecifiedNotification(username: string, name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Notification>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSpecifiedNotification(username, name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1UcApi.deleteSpecifiedNotification']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List notification preferences for the authenticated user.\n         * @param {string} username Username\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listUserNotificationPreferences(username: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonTypeNotifierMatrix>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listUserNotificationPreferences(username, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1UcApi.listUserNotificationPreferences']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List notifications for the authenticated user.\n         * @param {string} username Username\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listUserNotifications(username: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listUserNotifications(username, page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1UcApi.listUserNotifications']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Mark the specified notification as read.\n         * @param {string} username Username\n         * @param {string} name Notification name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async markNotificationAsRead(username: string, name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Notification>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.markNotificationAsRead(username, name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1UcApi.markNotificationAsRead']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Mark the specified notifications as read.\n         * @param {string} username Username\n         * @param {MarkSpecifiedRequest} markSpecifiedRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async markNotificationsAsRead(username: string, markSpecifiedRequest: MarkSpecifiedRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.markNotificationsAsRead(username, markSpecifiedRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1UcApi.markNotificationsAsRead']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Save notification preferences for the authenticated user.\n         * @param {string} username Username\n         * @param {ReasonTypeNotifierCollectionRequest} [reasonTypeNotifierCollectionRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async saveUserNotificationPreferences(username: string, reasonTypeNotifierCollectionRequest?: ReasonTypeNotifierCollectionRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonTypeNotifierMatrix>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.saveUserNotificationPreferences(username, reasonTypeNotifierCollectionRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotificationV1alpha1UcApi.saveUserNotificationPreferences']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * NotificationV1alpha1UcApi - factory interface\n */\nexport const NotificationV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotificationV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Delete the specified notification.\n         * @param {NotificationV1alpha1UcApiDeleteSpecifiedNotificationRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSpecifiedNotification(requestParameters: NotificationV1alpha1UcApiDeleteSpecifiedNotificationRequest, options?: RawAxiosRequestConfig): AxiosPromise<Notification> {\n            return localVarFp.deleteSpecifiedNotification(requestParameters.username, requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List notification preferences for the authenticated user.\n         * @param {NotificationV1alpha1UcApiListUserNotificationPreferencesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUserNotificationPreferences(requestParameters: NotificationV1alpha1UcApiListUserNotificationPreferencesRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReasonTypeNotifierMatrix> {\n            return localVarFp.listUserNotificationPreferences(requestParameters.username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List notifications for the authenticated user.\n         * @param {NotificationV1alpha1UcApiListUserNotificationsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUserNotifications(requestParameters: NotificationV1alpha1UcApiListUserNotificationsRequest, options?: RawAxiosRequestConfig): AxiosPromise<NotificationList> {\n            return localVarFp.listUserNotifications(requestParameters.username, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Mark the specified notification as read.\n         * @param {NotificationV1alpha1UcApiMarkNotificationAsReadRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        markNotificationAsRead(requestParameters: NotificationV1alpha1UcApiMarkNotificationAsReadRequest, options?: RawAxiosRequestConfig): AxiosPromise<Notification> {\n            return localVarFp.markNotificationAsRead(requestParameters.username, requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Mark the specified notifications as read.\n         * @param {NotificationV1alpha1UcApiMarkNotificationsAsReadRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        markNotificationsAsRead(requestParameters: NotificationV1alpha1UcApiMarkNotificationsAsReadRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<string>> {\n            return localVarFp.markNotificationsAsRead(requestParameters.username, requestParameters.markSpecifiedRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Save notification preferences for the authenticated user.\n         * @param {NotificationV1alpha1UcApiSaveUserNotificationPreferencesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveUserNotificationPreferences(requestParameters: NotificationV1alpha1UcApiSaveUserNotificationPreferencesRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReasonTypeNotifierMatrix> {\n            return localVarFp.saveUserNotificationPreferences(requestParameters.username, requestParameters.reasonTypeNotifierCollectionRequest, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for deleteSpecifiedNotification operation in NotificationV1alpha1UcApi.\n */\nexport interface NotificationV1alpha1UcApiDeleteSpecifiedNotificationRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n\n    /**\n     * Notification name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listUserNotificationPreferences operation in NotificationV1alpha1UcApi.\n */\nexport interface NotificationV1alpha1UcApiListUserNotificationPreferencesRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n}\n\n/**\n * Request parameters for listUserNotifications operation in NotificationV1alpha1UcApi.\n */\nexport interface NotificationV1alpha1UcApiListUserNotificationsRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for markNotificationAsRead operation in NotificationV1alpha1UcApi.\n */\nexport interface NotificationV1alpha1UcApiMarkNotificationAsReadRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n\n    /**\n     * Notification name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for markNotificationsAsRead operation in NotificationV1alpha1UcApi.\n */\nexport interface NotificationV1alpha1UcApiMarkNotificationsAsReadRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n\n    readonly markSpecifiedRequest: MarkSpecifiedRequest\n}\n\n/**\n * Request parameters for saveUserNotificationPreferences operation in NotificationV1alpha1UcApi.\n */\nexport interface NotificationV1alpha1UcApiSaveUserNotificationPreferencesRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n\n    readonly reasonTypeNotifierCollectionRequest?: ReasonTypeNotifierCollectionRequest\n}\n\n/**\n * NotificationV1alpha1UcApi - object-oriented interface\n */\nexport class NotificationV1alpha1UcApi extends BaseAPI {\n    /**\n     * Delete the specified notification.\n     * @param {NotificationV1alpha1UcApiDeleteSpecifiedNotificationRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteSpecifiedNotification(requestParameters: NotificationV1alpha1UcApiDeleteSpecifiedNotificationRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1UcApiFp(this.configuration).deleteSpecifiedNotification(requestParameters.username, requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List notification preferences for the authenticated user.\n     * @param {NotificationV1alpha1UcApiListUserNotificationPreferencesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listUserNotificationPreferences(requestParameters: NotificationV1alpha1UcApiListUserNotificationPreferencesRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1UcApiFp(this.configuration).listUserNotificationPreferences(requestParameters.username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List notifications for the authenticated user.\n     * @param {NotificationV1alpha1UcApiListUserNotificationsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listUserNotifications(requestParameters: NotificationV1alpha1UcApiListUserNotificationsRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1UcApiFp(this.configuration).listUserNotifications(requestParameters.username, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Mark the specified notification as read.\n     * @param {NotificationV1alpha1UcApiMarkNotificationAsReadRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public markNotificationAsRead(requestParameters: NotificationV1alpha1UcApiMarkNotificationAsReadRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1UcApiFp(this.configuration).markNotificationAsRead(requestParameters.username, requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Mark the specified notifications as read.\n     * @param {NotificationV1alpha1UcApiMarkNotificationsAsReadRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public markNotificationsAsRead(requestParameters: NotificationV1alpha1UcApiMarkNotificationsAsReadRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1UcApiFp(this.configuration).markNotificationsAsRead(requestParameters.username, requestParameters.markSpecifiedRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Save notification preferences for the authenticated user.\n     * @param {NotificationV1alpha1UcApiSaveUserNotificationPreferencesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public saveUserNotificationPreferences(requestParameters: NotificationV1alpha1UcApiSaveUserNotificationPreferencesRequest, options?: RawAxiosRequestConfig) {\n        return NotificationV1alpha1UcApiFp(this.configuration).saveUserNotificationPreferences(requestParameters.username, requestParameters.reasonTypeNotifierCollectionRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/notifier-descriptor-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { NotifierDescriptor } from '../models';\n// @ts-ignore\nimport type { NotifierDescriptorList } from '../models';\n/**\n * NotifierDescriptorV1alpha1Api - axios parameter creator\n */\nexport const NotifierDescriptorV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create NotifierDescriptor\n         * @param {NotifierDescriptor} [notifierDescriptor] Fresh notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotifierDescriptor: async (notifierDescriptor?: NotifierDescriptor, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifierDescriptors`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(notifierDescriptor, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotifierDescriptor: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteNotifierDescriptor', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifierDescriptors/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotifierDescriptor: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getNotifierDescriptor', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifierDescriptors/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List NotifierDescriptor\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listNotifierDescriptor: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifierDescriptors`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchNotifierDescriptor: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchNotifierDescriptor', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifierDescriptors/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {NotifierDescriptor} [notifierDescriptor] Updated notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotifierDescriptor: async (name: string, notifierDescriptor?: NotifierDescriptor, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateNotifierDescriptor', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/notifierDescriptors/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(notifierDescriptor, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotifierDescriptorV1alpha1Api - functional programming interface\n */\nexport const NotifierDescriptorV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotifierDescriptorV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create NotifierDescriptor\n         * @param {NotifierDescriptor} [notifierDescriptor] Fresh notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createNotifierDescriptor(notifierDescriptor?: NotifierDescriptor, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotifierDescriptor>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createNotifierDescriptor(notifierDescriptor, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierDescriptorV1alpha1Api.createNotifierDescriptor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteNotifierDescriptor(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteNotifierDescriptor(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierDescriptorV1alpha1Api.deleteNotifierDescriptor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getNotifierDescriptor(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotifierDescriptor>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getNotifierDescriptor(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierDescriptorV1alpha1Api.getNotifierDescriptor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List NotifierDescriptor\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listNotifierDescriptor(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotifierDescriptorList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listNotifierDescriptor(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierDescriptorV1alpha1Api.listNotifierDescriptor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchNotifierDescriptor(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotifierDescriptor>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchNotifierDescriptor(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierDescriptorV1alpha1Api.patchNotifierDescriptor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update NotifierDescriptor\n         * @param {string} name Name of notifierDescriptor\n         * @param {NotifierDescriptor} [notifierDescriptor] Updated notifierDescriptor\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateNotifierDescriptor(name: string, notifierDescriptor?: NotifierDescriptor, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotifierDescriptor>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateNotifierDescriptor(name, notifierDescriptor, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierDescriptorV1alpha1Api.updateNotifierDescriptor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * NotifierDescriptorV1alpha1Api - factory interface\n */\nexport const NotifierDescriptorV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotifierDescriptorV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create NotifierDescriptor\n         * @param {NotifierDescriptorV1alpha1ApiCreateNotifierDescriptorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiCreateNotifierDescriptorRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<NotifierDescriptor> {\n            return localVarFp.createNotifierDescriptor(requestParameters.notifierDescriptor, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete NotifierDescriptor\n         * @param {NotifierDescriptorV1alpha1ApiDeleteNotifierDescriptorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiDeleteNotifierDescriptorRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteNotifierDescriptor(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get NotifierDescriptor\n         * @param {NotifierDescriptorV1alpha1ApiGetNotifierDescriptorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiGetNotifierDescriptorRequest, options?: RawAxiosRequestConfig): AxiosPromise<NotifierDescriptor> {\n            return localVarFp.getNotifierDescriptor(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List NotifierDescriptor\n         * @param {NotifierDescriptorV1alpha1ApiListNotifierDescriptorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiListNotifierDescriptorRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<NotifierDescriptorList> {\n            return localVarFp.listNotifierDescriptor(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch NotifierDescriptor\n         * @param {NotifierDescriptorV1alpha1ApiPatchNotifierDescriptorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiPatchNotifierDescriptorRequest, options?: RawAxiosRequestConfig): AxiosPromise<NotifierDescriptor> {\n            return localVarFp.patchNotifierDescriptor(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update NotifierDescriptor\n         * @param {NotifierDescriptorV1alpha1ApiUpdateNotifierDescriptorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiUpdateNotifierDescriptorRequest, options?: RawAxiosRequestConfig): AxiosPromise<NotifierDescriptor> {\n            return localVarFp.updateNotifierDescriptor(requestParameters.name, requestParameters.notifierDescriptor, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createNotifierDescriptor operation in NotifierDescriptorV1alpha1Api.\n */\nexport interface NotifierDescriptorV1alpha1ApiCreateNotifierDescriptorRequest {\n    /**\n     * Fresh notifierDescriptor\n     */\n    readonly notifierDescriptor?: NotifierDescriptor\n}\n\n/**\n * Request parameters for deleteNotifierDescriptor operation in NotifierDescriptorV1alpha1Api.\n */\nexport interface NotifierDescriptorV1alpha1ApiDeleteNotifierDescriptorRequest {\n    /**\n     * Name of notifierDescriptor\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getNotifierDescriptor operation in NotifierDescriptorV1alpha1Api.\n */\nexport interface NotifierDescriptorV1alpha1ApiGetNotifierDescriptorRequest {\n    /**\n     * Name of notifierDescriptor\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listNotifierDescriptor operation in NotifierDescriptorV1alpha1Api.\n */\nexport interface NotifierDescriptorV1alpha1ApiListNotifierDescriptorRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchNotifierDescriptor operation in NotifierDescriptorV1alpha1Api.\n */\nexport interface NotifierDescriptorV1alpha1ApiPatchNotifierDescriptorRequest {\n    /**\n     * Name of notifierDescriptor\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateNotifierDescriptor operation in NotifierDescriptorV1alpha1Api.\n */\nexport interface NotifierDescriptorV1alpha1ApiUpdateNotifierDescriptorRequest {\n    /**\n     * Name of notifierDescriptor\n     */\n    readonly name: string\n\n    /**\n     * Updated notifierDescriptor\n     */\n    readonly notifierDescriptor?: NotifierDescriptor\n}\n\n/**\n * NotifierDescriptorV1alpha1Api - object-oriented interface\n */\nexport class NotifierDescriptorV1alpha1Api extends BaseAPI {\n    /**\n     * Create NotifierDescriptor\n     * @param {NotifierDescriptorV1alpha1ApiCreateNotifierDescriptorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiCreateNotifierDescriptorRequest = {}, options?: RawAxiosRequestConfig) {\n        return NotifierDescriptorV1alpha1ApiFp(this.configuration).createNotifierDescriptor(requestParameters.notifierDescriptor, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete NotifierDescriptor\n     * @param {NotifierDescriptorV1alpha1ApiDeleteNotifierDescriptorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiDeleteNotifierDescriptorRequest, options?: RawAxiosRequestConfig) {\n        return NotifierDescriptorV1alpha1ApiFp(this.configuration).deleteNotifierDescriptor(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get NotifierDescriptor\n     * @param {NotifierDescriptorV1alpha1ApiGetNotifierDescriptorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiGetNotifierDescriptorRequest, options?: RawAxiosRequestConfig) {\n        return NotifierDescriptorV1alpha1ApiFp(this.configuration).getNotifierDescriptor(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List NotifierDescriptor\n     * @param {NotifierDescriptorV1alpha1ApiListNotifierDescriptorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiListNotifierDescriptorRequest = {}, options?: RawAxiosRequestConfig) {\n        return NotifierDescriptorV1alpha1ApiFp(this.configuration).listNotifierDescriptor(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch NotifierDescriptor\n     * @param {NotifierDescriptorV1alpha1ApiPatchNotifierDescriptorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiPatchNotifierDescriptorRequest, options?: RawAxiosRequestConfig) {\n        return NotifierDescriptorV1alpha1ApiFp(this.configuration).patchNotifierDescriptor(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update NotifierDescriptor\n     * @param {NotifierDescriptorV1alpha1ApiUpdateNotifierDescriptorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateNotifierDescriptor(requestParameters: NotifierDescriptorV1alpha1ApiUpdateNotifierDescriptorRequest, options?: RawAxiosRequestConfig) {\n        return NotifierDescriptorV1alpha1ApiFp(this.configuration).updateNotifierDescriptor(requestParameters.name, requestParameters.notifierDescriptor, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/notifier-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { EmailConfigValidationRequest } from '../models';\n/**\n * NotifierV1alpha1ConsoleApi - axios parameter creator\n */\nexport const NotifierV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Fetch sender config of notifier\n         * @param {string} name Notifier name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSenderConfig: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchSenderConfig', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/notifiers/{name}/sender-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Save sender config of notifier\n         * @param {string} name Notifier name\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveSenderConfig: async (name: string, body: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('saveSenderConfig', 'name', name)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('saveSenderConfig', 'body', body)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/notifiers/{name}/sender-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Verify email sender config.\n         * @param {EmailConfigValidationRequest} emailConfigValidationRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        verifyEmailSenderConfig: async (emailConfigValidationRequest: EmailConfigValidationRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'emailConfigValidationRequest' is not null or undefined\n            assertParamExists('verifyEmailSenderConfig', 'emailConfigValidationRequest', emailConfigValidationRequest)\n            const localVarPath = `/apis/console.api.notification.halo.run/v1alpha1/notifiers/default-email-notifier/verify-connection`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(emailConfigValidationRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotifierV1alpha1ConsoleApi - functional programming interface\n */\nexport const NotifierV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotifierV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Fetch sender config of notifier\n         * @param {string} name Notifier name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchSenderConfig(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchSenderConfig(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierV1alpha1ConsoleApi.fetchSenderConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Save sender config of notifier\n         * @param {string} name Notifier name\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async saveSenderConfig(name: string, body: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.saveSenderConfig(name, body, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierV1alpha1ConsoleApi.saveSenderConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Verify email sender config.\n         * @param {EmailConfigValidationRequest} emailConfigValidationRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async verifyEmailSenderConfig(emailConfigValidationRequest: EmailConfigValidationRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.verifyEmailSenderConfig(emailConfigValidationRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierV1alpha1ConsoleApi.verifyEmailSenderConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * NotifierV1alpha1ConsoleApi - factory interface\n */\nexport const NotifierV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotifierV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Fetch sender config of notifier\n         * @param {NotifierV1alpha1ConsoleApiFetchSenderConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSenderConfig(requestParameters: NotifierV1alpha1ConsoleApiFetchSenderConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {\n            return localVarFp.fetchSenderConfig(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Save sender config of notifier\n         * @param {NotifierV1alpha1ConsoleApiSaveSenderConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveSenderConfig(requestParameters: NotifierV1alpha1ConsoleApiSaveSenderConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.saveSenderConfig(requestParameters.name, requestParameters.body, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Verify email sender config.\n         * @param {NotifierV1alpha1ConsoleApiVerifyEmailSenderConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        verifyEmailSenderConfig(requestParameters: NotifierV1alpha1ConsoleApiVerifyEmailSenderConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.verifyEmailSenderConfig(requestParameters.emailConfigValidationRequest, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for fetchSenderConfig operation in NotifierV1alpha1ConsoleApi.\n */\nexport interface NotifierV1alpha1ConsoleApiFetchSenderConfigRequest {\n    /**\n     * Notifier name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for saveSenderConfig operation in NotifierV1alpha1ConsoleApi.\n */\nexport interface NotifierV1alpha1ConsoleApiSaveSenderConfigRequest {\n    /**\n     * Notifier name\n     */\n    readonly name: string\n\n    readonly body: object\n}\n\n/**\n * Request parameters for verifyEmailSenderConfig operation in NotifierV1alpha1ConsoleApi.\n */\nexport interface NotifierV1alpha1ConsoleApiVerifyEmailSenderConfigRequest {\n    readonly emailConfigValidationRequest: EmailConfigValidationRequest\n}\n\n/**\n * NotifierV1alpha1ConsoleApi - object-oriented interface\n */\nexport class NotifierV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Fetch sender config of notifier\n     * @param {NotifierV1alpha1ConsoleApiFetchSenderConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchSenderConfig(requestParameters: NotifierV1alpha1ConsoleApiFetchSenderConfigRequest, options?: RawAxiosRequestConfig) {\n        return NotifierV1alpha1ConsoleApiFp(this.configuration).fetchSenderConfig(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Save sender config of notifier\n     * @param {NotifierV1alpha1ConsoleApiSaveSenderConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public saveSenderConfig(requestParameters: NotifierV1alpha1ConsoleApiSaveSenderConfigRequest, options?: RawAxiosRequestConfig) {\n        return NotifierV1alpha1ConsoleApiFp(this.configuration).saveSenderConfig(requestParameters.name, requestParameters.body, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Verify email sender config.\n     * @param {NotifierV1alpha1ConsoleApiVerifyEmailSenderConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public verifyEmailSenderConfig(requestParameters: NotifierV1alpha1ConsoleApiVerifyEmailSenderConfigRequest, options?: RawAxiosRequestConfig) {\n        return NotifierV1alpha1ConsoleApiFp(this.configuration).verifyEmailSenderConfig(requestParameters.emailConfigValidationRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/notifier-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * NotifierV1alpha1UcApi - axios parameter creator\n */\nexport const NotifierV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Fetch receiver config of notifier\n         * @param {string} name Notifier name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchReceiverConfig: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchReceiverConfig', 'name', name)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/notifiers/{name}/receiver-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Save receiver config of notifier\n         * @param {string} name Notifier name\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveReceiverConfig: async (name: string, body: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('saveReceiverConfig', 'name', name)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('saveReceiverConfig', 'body', body)\n            const localVarPath = `/apis/api.notification.halo.run/v1alpha1/notifiers/{name}/receiver-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotifierV1alpha1UcApi - functional programming interface\n */\nexport const NotifierV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotifierV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Fetch receiver config of notifier\n         * @param {string} name Notifier name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchReceiverConfig(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchReceiverConfig(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierV1alpha1UcApi.fetchReceiverConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Save receiver config of notifier\n         * @param {string} name Notifier name\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async saveReceiverConfig(name: string, body: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.saveReceiverConfig(name, body, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['NotifierV1alpha1UcApi.saveReceiverConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * NotifierV1alpha1UcApi - factory interface\n */\nexport const NotifierV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotifierV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Fetch receiver config of notifier\n         * @param {NotifierV1alpha1UcApiFetchReceiverConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchReceiverConfig(requestParameters: NotifierV1alpha1UcApiFetchReceiverConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {\n            return localVarFp.fetchReceiverConfig(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Save receiver config of notifier\n         * @param {NotifierV1alpha1UcApiSaveReceiverConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveReceiverConfig(requestParameters: NotifierV1alpha1UcApiSaveReceiverConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.saveReceiverConfig(requestParameters.name, requestParameters.body, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for fetchReceiverConfig operation in NotifierV1alpha1UcApi.\n */\nexport interface NotifierV1alpha1UcApiFetchReceiverConfigRequest {\n    /**\n     * Notifier name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for saveReceiverConfig operation in NotifierV1alpha1UcApi.\n */\nexport interface NotifierV1alpha1UcApiSaveReceiverConfigRequest {\n    /**\n     * Notifier name\n     */\n    readonly name: string\n\n    readonly body: object\n}\n\n/**\n * NotifierV1alpha1UcApi - object-oriented interface\n */\nexport class NotifierV1alpha1UcApi extends BaseAPI {\n    /**\n     * Fetch receiver config of notifier\n     * @param {NotifierV1alpha1UcApiFetchReceiverConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchReceiverConfig(requestParameters: NotifierV1alpha1UcApiFetchReceiverConfigRequest, options?: RawAxiosRequestConfig) {\n        return NotifierV1alpha1UcApiFp(this.configuration).fetchReceiverConfig(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Save receiver config of notifier\n     * @param {NotifierV1alpha1UcApiSaveReceiverConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public saveReceiverConfig(requestParameters: NotifierV1alpha1UcApiSaveReceiverConfigRequest, options?: RawAxiosRequestConfig) {\n        return NotifierV1alpha1UcApiFp(this.configuration).saveReceiverConfig(requestParameters.name, requestParameters.body, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/personal-access-token-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { PersonalAccessToken } from '../models';\n// @ts-ignore\nimport type { PersonalAccessTokenList } from '../models';\n/**\n * PersonalAccessTokenV1alpha1Api - axios parameter creator\n */\nexport const PersonalAccessTokenV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create PersonalAccessToken\n         * @param {PersonalAccessToken} [personalAccessToken] Fresh personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPersonalAccessToken: async (personalAccessToken?: PersonalAccessToken, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/security.halo.run/v1alpha1/personalaccesstokens`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(personalAccessToken, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePersonalAccessToken: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deletePersonalAccessToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/personalaccesstokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPersonalAccessToken: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getPersonalAccessToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/personalaccesstokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List PersonalAccessToken\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPersonalAccessToken: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/security.halo.run/v1alpha1/personalaccesstokens`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPersonalAccessToken: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchPersonalAccessToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/personalaccesstokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {PersonalAccessToken} [personalAccessToken] Updated personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePersonalAccessToken: async (name: string, personalAccessToken?: PersonalAccessToken, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePersonalAccessToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/personalaccesstokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(personalAccessToken, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PersonalAccessTokenV1alpha1Api - functional programming interface\n */\nexport const PersonalAccessTokenV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PersonalAccessTokenV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create PersonalAccessToken\n         * @param {PersonalAccessToken} [personalAccessToken] Fresh personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createPersonalAccessToken(personalAccessToken?: PersonalAccessToken, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonalAccessToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createPersonalAccessToken(personalAccessToken, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1Api.createPersonalAccessToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePersonalAccessToken(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePersonalAccessToken(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1Api.deletePersonalAccessToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPersonalAccessToken(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonalAccessToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPersonalAccessToken(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1Api.getPersonalAccessToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List PersonalAccessToken\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPersonalAccessToken(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonalAccessTokenList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPersonalAccessToken(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1Api.listPersonalAccessToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchPersonalAccessToken(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonalAccessToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchPersonalAccessToken(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1Api.patchPersonalAccessToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update PersonalAccessToken\n         * @param {string} name Name of personalaccesstoken\n         * @param {PersonalAccessToken} [personalAccessToken] Updated personalaccesstoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePersonalAccessToken(name: string, personalAccessToken?: PersonalAccessToken, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonalAccessToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePersonalAccessToken(name, personalAccessToken, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1Api.updatePersonalAccessToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PersonalAccessTokenV1alpha1Api - factory interface\n */\nexport const PersonalAccessTokenV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PersonalAccessTokenV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create PersonalAccessToken\n         * @param {PersonalAccessTokenV1alpha1ApiCreatePersonalAccessTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiCreatePersonalAccessTokenRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PersonalAccessToken> {\n            return localVarFp.createPersonalAccessToken(requestParameters.personalAccessToken, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete PersonalAccessToken\n         * @param {PersonalAccessTokenV1alpha1ApiDeletePersonalAccessTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiDeletePersonalAccessTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deletePersonalAccessToken(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get PersonalAccessToken\n         * @param {PersonalAccessTokenV1alpha1ApiGetPersonalAccessTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiGetPersonalAccessTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<PersonalAccessToken> {\n            return localVarFp.getPersonalAccessToken(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List PersonalAccessToken\n         * @param {PersonalAccessTokenV1alpha1ApiListPersonalAccessTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiListPersonalAccessTokenRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PersonalAccessTokenList> {\n            return localVarFp.listPersonalAccessToken(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch PersonalAccessToken\n         * @param {PersonalAccessTokenV1alpha1ApiPatchPersonalAccessTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiPatchPersonalAccessTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<PersonalAccessToken> {\n            return localVarFp.patchPersonalAccessToken(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update PersonalAccessToken\n         * @param {PersonalAccessTokenV1alpha1ApiUpdatePersonalAccessTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiUpdatePersonalAccessTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<PersonalAccessToken> {\n            return localVarFp.updatePersonalAccessToken(requestParameters.name, requestParameters.personalAccessToken, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createPersonalAccessToken operation in PersonalAccessTokenV1alpha1Api.\n */\nexport interface PersonalAccessTokenV1alpha1ApiCreatePersonalAccessTokenRequest {\n    /**\n     * Fresh personalaccesstoken\n     */\n    readonly personalAccessToken?: PersonalAccessToken\n}\n\n/**\n * Request parameters for deletePersonalAccessToken operation in PersonalAccessTokenV1alpha1Api.\n */\nexport interface PersonalAccessTokenV1alpha1ApiDeletePersonalAccessTokenRequest {\n    /**\n     * Name of personalaccesstoken\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getPersonalAccessToken operation in PersonalAccessTokenV1alpha1Api.\n */\nexport interface PersonalAccessTokenV1alpha1ApiGetPersonalAccessTokenRequest {\n    /**\n     * Name of personalaccesstoken\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listPersonalAccessToken operation in PersonalAccessTokenV1alpha1Api.\n */\nexport interface PersonalAccessTokenV1alpha1ApiListPersonalAccessTokenRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchPersonalAccessToken operation in PersonalAccessTokenV1alpha1Api.\n */\nexport interface PersonalAccessTokenV1alpha1ApiPatchPersonalAccessTokenRequest {\n    /**\n     * Name of personalaccesstoken\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updatePersonalAccessToken operation in PersonalAccessTokenV1alpha1Api.\n */\nexport interface PersonalAccessTokenV1alpha1ApiUpdatePersonalAccessTokenRequest {\n    /**\n     * Name of personalaccesstoken\n     */\n    readonly name: string\n\n    /**\n     * Updated personalaccesstoken\n     */\n    readonly personalAccessToken?: PersonalAccessToken\n}\n\n/**\n * PersonalAccessTokenV1alpha1Api - object-oriented interface\n */\nexport class PersonalAccessTokenV1alpha1Api extends BaseAPI {\n    /**\n     * Create PersonalAccessToken\n     * @param {PersonalAccessTokenV1alpha1ApiCreatePersonalAccessTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiCreatePersonalAccessTokenRequest = {}, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1ApiFp(this.configuration).createPersonalAccessToken(requestParameters.personalAccessToken, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete PersonalAccessToken\n     * @param {PersonalAccessTokenV1alpha1ApiDeletePersonalAccessTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deletePersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiDeletePersonalAccessTokenRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1ApiFp(this.configuration).deletePersonalAccessToken(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get PersonalAccessToken\n     * @param {PersonalAccessTokenV1alpha1ApiGetPersonalAccessTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiGetPersonalAccessTokenRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1ApiFp(this.configuration).getPersonalAccessToken(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List PersonalAccessToken\n     * @param {PersonalAccessTokenV1alpha1ApiListPersonalAccessTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiListPersonalAccessTokenRequest = {}, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1ApiFp(this.configuration).listPersonalAccessToken(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch PersonalAccessToken\n     * @param {PersonalAccessTokenV1alpha1ApiPatchPersonalAccessTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchPersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiPatchPersonalAccessTokenRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1ApiFp(this.configuration).patchPersonalAccessToken(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update PersonalAccessToken\n     * @param {PersonalAccessTokenV1alpha1ApiUpdatePersonalAccessTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePersonalAccessToken(requestParameters: PersonalAccessTokenV1alpha1ApiUpdatePersonalAccessTokenRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1ApiFp(this.configuration).updatePersonalAccessToken(requestParameters.name, requestParameters.personalAccessToken, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/personal-access-token-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { PersonalAccessToken } from '../models';\n/**\n * PersonalAccessTokenV1alpha1UcApi - axios parameter creator\n */\nexport const PersonalAccessTokenV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Delete a PAT\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePat: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deletePat', 'name', name)\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Generate a PAT.\n         * @param {PersonalAccessToken} personalAccessToken \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        generatePat: async (personalAccessToken: PersonalAccessToken, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'personalAccessToken' is not null or undefined\n            assertParamExists('generatePat', 'personalAccessToken', personalAccessToken)\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(personalAccessToken, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Obtain a PAT.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        obtainPat: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('obtainPat', 'name', name)\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Obtain PAT list.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        obtainPats: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Restore a PAT.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        restorePat: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('restorePat', 'name', name)\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}/actions/restoration`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Revoke a PAT\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revokePat: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('revokePat', 'name', name)\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/personalaccesstokens/{name}/actions/revocation`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PersonalAccessTokenV1alpha1UcApi - functional programming interface\n */\nexport const PersonalAccessTokenV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PersonalAccessTokenV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Delete a PAT\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePat(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePat(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1UcApi.deletePat']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Generate a PAT.\n         * @param {PersonalAccessToken} personalAccessToken \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async generatePat(personalAccessToken: PersonalAccessToken, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonalAccessToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.generatePat(personalAccessToken, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1UcApi.generatePat']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Obtain a PAT.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async obtainPat(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.obtainPat(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1UcApi.obtainPat']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Obtain PAT list.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async obtainPats(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonalAccessToken>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.obtainPats(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1UcApi.obtainPats']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Restore a PAT.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async restorePat(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.restorePat(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1UcApi.restorePat']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Revoke a PAT\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async revokePat(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.revokePat(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PersonalAccessTokenV1alpha1UcApi.revokePat']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PersonalAccessTokenV1alpha1UcApi - factory interface\n */\nexport const PersonalAccessTokenV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PersonalAccessTokenV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Delete a PAT\n         * @param {PersonalAccessTokenV1alpha1UcApiDeletePatRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePat(requestParameters: PersonalAccessTokenV1alpha1UcApiDeletePatRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deletePat(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Generate a PAT.\n         * @param {PersonalAccessTokenV1alpha1UcApiGeneratePatRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        generatePat(requestParameters: PersonalAccessTokenV1alpha1UcApiGeneratePatRequest, options?: RawAxiosRequestConfig): AxiosPromise<PersonalAccessToken> {\n            return localVarFp.generatePat(requestParameters.personalAccessToken, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Obtain a PAT.\n         * @param {PersonalAccessTokenV1alpha1UcApiObtainPatRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        obtainPat(requestParameters: PersonalAccessTokenV1alpha1UcApiObtainPatRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.obtainPat(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Obtain PAT list.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        obtainPats(options?: RawAxiosRequestConfig): AxiosPromise<Array<PersonalAccessToken>> {\n            return localVarFp.obtainPats(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Restore a PAT.\n         * @param {PersonalAccessTokenV1alpha1UcApiRestorePatRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        restorePat(requestParameters: PersonalAccessTokenV1alpha1UcApiRestorePatRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.restorePat(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Revoke a PAT\n         * @param {PersonalAccessTokenV1alpha1UcApiRevokePatRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revokePat(requestParameters: PersonalAccessTokenV1alpha1UcApiRevokePatRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.revokePat(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for deletePat operation in PersonalAccessTokenV1alpha1UcApi.\n */\nexport interface PersonalAccessTokenV1alpha1UcApiDeletePatRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for generatePat operation in PersonalAccessTokenV1alpha1UcApi.\n */\nexport interface PersonalAccessTokenV1alpha1UcApiGeneratePatRequest {\n    readonly personalAccessToken: PersonalAccessToken\n}\n\n/**\n * Request parameters for obtainPat operation in PersonalAccessTokenV1alpha1UcApi.\n */\nexport interface PersonalAccessTokenV1alpha1UcApiObtainPatRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for restorePat operation in PersonalAccessTokenV1alpha1UcApi.\n */\nexport interface PersonalAccessTokenV1alpha1UcApiRestorePatRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for revokePat operation in PersonalAccessTokenV1alpha1UcApi.\n */\nexport interface PersonalAccessTokenV1alpha1UcApiRevokePatRequest {\n    readonly name: string\n}\n\n/**\n * PersonalAccessTokenV1alpha1UcApi - object-oriented interface\n */\nexport class PersonalAccessTokenV1alpha1UcApi extends BaseAPI {\n    /**\n     * Delete a PAT\n     * @param {PersonalAccessTokenV1alpha1UcApiDeletePatRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deletePat(requestParameters: PersonalAccessTokenV1alpha1UcApiDeletePatRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1UcApiFp(this.configuration).deletePat(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Generate a PAT.\n     * @param {PersonalAccessTokenV1alpha1UcApiGeneratePatRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public generatePat(requestParameters: PersonalAccessTokenV1alpha1UcApiGeneratePatRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1UcApiFp(this.configuration).generatePat(requestParameters.personalAccessToken, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Obtain a PAT.\n     * @param {PersonalAccessTokenV1alpha1UcApiObtainPatRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public obtainPat(requestParameters: PersonalAccessTokenV1alpha1UcApiObtainPatRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1UcApiFp(this.configuration).obtainPat(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Obtain PAT list.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public obtainPats(options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1UcApiFp(this.configuration).obtainPats(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Restore a PAT.\n     * @param {PersonalAccessTokenV1alpha1UcApiRestorePatRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public restorePat(requestParameters: PersonalAccessTokenV1alpha1UcApiRestorePatRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1UcApiFp(this.configuration).restorePat(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Revoke a PAT\n     * @param {PersonalAccessTokenV1alpha1UcApiRevokePatRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public revokePat(requestParameters: PersonalAccessTokenV1alpha1UcApiRevokePatRequest, options?: RawAxiosRequestConfig) {\n        return PersonalAccessTokenV1alpha1UcApiFp(this.configuration).revokePat(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/plugin-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Plugin } from '../models';\n// @ts-ignore\nimport type { PluginList } from '../models';\n/**\n * PluginV1alpha1Api - axios parameter creator\n */\nexport const PluginV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Plugin\n         * @param {Plugin} [plugin] Fresh plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPlugin: async (plugin?: Plugin, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/plugins`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(plugin, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Plugin\n         * @param {string} name Name of plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePlugin: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deletePlugin', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/plugins/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Plugin\n         * @param {string} name Name of plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPlugin: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getPlugin', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/plugins/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Plugin\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPlugin: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/plugins`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Plugin\n         * @param {string} name Name of plugin\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPlugin: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchPlugin', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/plugins/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Plugin\n         * @param {string} name Name of plugin\n         * @param {Plugin} [plugin] Updated plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePlugin: async (name: string, plugin?: Plugin, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePlugin', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/plugins/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(plugin, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PluginV1alpha1Api - functional programming interface\n */\nexport const PluginV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PluginV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Plugin\n         * @param {Plugin} [plugin] Fresh plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createPlugin(plugin?: Plugin, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createPlugin(plugin, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1Api.createPlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Plugin\n         * @param {string} name Name of plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePlugin(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePlugin(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1Api.deletePlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Plugin\n         * @param {string} name Name of plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPlugin(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPlugin(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1Api.getPlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Plugin\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPlugin(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PluginList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPlugin(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1Api.listPlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Plugin\n         * @param {string} name Name of plugin\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchPlugin(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchPlugin(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1Api.patchPlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Plugin\n         * @param {string} name Name of plugin\n         * @param {Plugin} [plugin] Updated plugin\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePlugin(name: string, plugin?: Plugin, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePlugin(name, plugin, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1Api.updatePlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PluginV1alpha1Api - factory interface\n */\nexport const PluginV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PluginV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Plugin\n         * @param {PluginV1alpha1ApiCreatePluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPlugin(requestParameters: PluginV1alpha1ApiCreatePluginRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.createPlugin(requestParameters.plugin, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Plugin\n         * @param {PluginV1alpha1ApiDeletePluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePlugin(requestParameters: PluginV1alpha1ApiDeletePluginRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deletePlugin(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Plugin\n         * @param {PluginV1alpha1ApiGetPluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPlugin(requestParameters: PluginV1alpha1ApiGetPluginRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.getPlugin(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Plugin\n         * @param {PluginV1alpha1ApiListPluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPlugin(requestParameters: PluginV1alpha1ApiListPluginRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PluginList> {\n            return localVarFp.listPlugin(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Plugin\n         * @param {PluginV1alpha1ApiPatchPluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPlugin(requestParameters: PluginV1alpha1ApiPatchPluginRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.patchPlugin(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Plugin\n         * @param {PluginV1alpha1ApiUpdatePluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePlugin(requestParameters: PluginV1alpha1ApiUpdatePluginRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.updatePlugin(requestParameters.name, requestParameters.plugin, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createPlugin operation in PluginV1alpha1Api.\n */\nexport interface PluginV1alpha1ApiCreatePluginRequest {\n    /**\n     * Fresh plugin\n     */\n    readonly plugin?: Plugin\n}\n\n/**\n * Request parameters for deletePlugin operation in PluginV1alpha1Api.\n */\nexport interface PluginV1alpha1ApiDeletePluginRequest {\n    /**\n     * Name of plugin\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getPlugin operation in PluginV1alpha1Api.\n */\nexport interface PluginV1alpha1ApiGetPluginRequest {\n    /**\n     * Name of plugin\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listPlugin operation in PluginV1alpha1Api.\n */\nexport interface PluginV1alpha1ApiListPluginRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchPlugin operation in PluginV1alpha1Api.\n */\nexport interface PluginV1alpha1ApiPatchPluginRequest {\n    /**\n     * Name of plugin\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updatePlugin operation in PluginV1alpha1Api.\n */\nexport interface PluginV1alpha1ApiUpdatePluginRequest {\n    /**\n     * Name of plugin\n     */\n    readonly name: string\n\n    /**\n     * Updated plugin\n     */\n    readonly plugin?: Plugin\n}\n\n/**\n * PluginV1alpha1Api - object-oriented interface\n */\nexport class PluginV1alpha1Api extends BaseAPI {\n    /**\n     * Create Plugin\n     * @param {PluginV1alpha1ApiCreatePluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createPlugin(requestParameters: PluginV1alpha1ApiCreatePluginRequest = {}, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ApiFp(this.configuration).createPlugin(requestParameters.plugin, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Plugin\n     * @param {PluginV1alpha1ApiDeletePluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deletePlugin(requestParameters: PluginV1alpha1ApiDeletePluginRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ApiFp(this.configuration).deletePlugin(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Plugin\n     * @param {PluginV1alpha1ApiGetPluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getPlugin(requestParameters: PluginV1alpha1ApiGetPluginRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ApiFp(this.configuration).getPlugin(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Plugin\n     * @param {PluginV1alpha1ApiListPluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPlugin(requestParameters: PluginV1alpha1ApiListPluginRequest = {}, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ApiFp(this.configuration).listPlugin(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Plugin\n     * @param {PluginV1alpha1ApiPatchPluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchPlugin(requestParameters: PluginV1alpha1ApiPatchPluginRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ApiFp(this.configuration).patchPlugin(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Plugin\n     * @param {PluginV1alpha1ApiUpdatePluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePlugin(requestParameters: PluginV1alpha1ApiUpdatePluginRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ApiFp(this.configuration).updatePlugin(requestParameters.name, requestParameters.plugin, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/plugin-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ConfigMap } from '../models';\n// @ts-ignore\nimport type { InstallFromUriRequest } from '../models';\n// @ts-ignore\nimport type { Plugin } from '../models';\n// @ts-ignore\nimport type { PluginList } from '../models';\n// @ts-ignore\nimport type { PluginRunningStateRequest } from '../models';\n// @ts-ignore\nimport type { Setting } from '../models';\n// @ts-ignore\nimport type { UpgradeFromUriRequest } from '../models';\n/**\n * PluginV1alpha1ConsoleApi - axios parameter creator\n */\nexport const PluginV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Change the running state of a plugin by name.\n         * @param {string} name \n         * @param {PluginRunningStateRequest} pluginRunningStateRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changePluginRunningState: async (name: string, pluginRunningStateRequest: PluginRunningStateRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('changePluginRunningState', 'name', name)\n            // verify required parameter 'pluginRunningStateRequest' is not null or undefined\n            assertParamExists('changePluginRunningState', 'pluginRunningStateRequest', pluginRunningStateRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/plugin-state`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(pluginRunningStateRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Merge all CSS bundles of enabled plugins into one.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchCssBundle: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Merge all JS bundles of enabled plugins into one.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchJsBundle: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch converted json config of plugin by configured configMapName.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPluginJsonConfig: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchPluginJsonConfig', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/json-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch setting of plugin.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPluginSetting: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchPluginSetting', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/setting`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Install a plugin by uploading a Jar file.\n         * @param {File} [file] \n         * @param {string} [presetName] Plugin preset name. We will find the plugin from plugin presets\n         * @param {InstallPluginSourceEnum} [source] Install source. Default is file.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installPlugin: async (file?: File, presetName?: string, source?: InstallPluginSourceEnum, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/install`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n            if (presetName !== undefined) { \n                localVarFormParams.append('presetName', presetName as any);\n            }\n    \n            if (source !== undefined) { \n                localVarFormParams.append('source', source as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Install a plugin from uri.\n         * @param {InstallFromUriRequest} installFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installPluginFromUri: async (installFromUriRequest: InstallFromUriRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'installFromUriRequest' is not null or undefined\n            assertParamExists('installPluginFromUri', 'installFromUriRequest', installFromUriRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(installFromUriRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List plugins using query criteria and sort params\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Keyword of plugin name or description\n         * @param {boolean} [enabled] Whether the plugin is enabled\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPlugins: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, enabled?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n            if (enabled !== undefined) {\n                localVarQueryParameter['enabled'] = enabled;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Reload a plugin by name.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        reloadPlugin: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('reloadPlugin', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/reload`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Reset the configMap of plugin setting.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        resetPluginConfig: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('resetPluginConfig', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/reset-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update the config of plugin setting.\n         * @param {string} name \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePluginJsonConfig: async (name: string, body: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePluginJsonConfig', 'name', name)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('updatePluginJsonConfig', 'body', body)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/json-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upgrade a plugin by uploading a Jar file\n         * @param {string} name \n         * @param {File} [file] \n         * @param {string} [presetName] Plugin preset name. We will find the plugin from plugin presets\n         * @param {UpgradePluginSourceEnum} [source] Install source. Default is file.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradePlugin: async (name: string, file?: File, presetName?: string, source?: UpgradePluginSourceEnum, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('upgradePlugin', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/upgrade`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n            if (presetName !== undefined) { \n                localVarFormParams.append('presetName', presetName as any);\n            }\n    \n            if (source !== undefined) { \n                localVarFormParams.append('source', source as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upgrade a plugin from uri.\n         * @param {string} name \n         * @param {UpgradeFromUriRequest} upgradeFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradePluginFromUri: async (name: string, upgradeFromUriRequest: UpgradeFromUriRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('upgradePluginFromUri', 'name', name)\n            // verify required parameter 'upgradeFromUriRequest' is not null or undefined\n            assertParamExists('upgradePluginFromUri', 'upgradeFromUriRequest', upgradeFromUriRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugins/{name}/upgrade-from-uri`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(upgradeFromUriRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PluginV1alpha1ConsoleApi - functional programming interface\n */\nexport const PluginV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PluginV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Change the running state of a plugin by name.\n         * @param {string} name \n         * @param {PluginRunningStateRequest} pluginRunningStateRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async changePluginRunningState(name: string, pluginRunningStateRequest: PluginRunningStateRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.changePluginRunningState(name, pluginRunningStateRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.changePluginRunningState']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Merge all CSS bundles of enabled plugins into one.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchCssBundle(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchCssBundle(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.fetchCssBundle']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Merge all JS bundles of enabled plugins into one.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchJsBundle(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchJsBundle(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.fetchJsBundle']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch converted json config of plugin by configured configMapName.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchPluginJsonConfig(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchPluginJsonConfig(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.fetchPluginJsonConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch setting of plugin.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchPluginSetting(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Setting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchPluginSetting(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.fetchPluginSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Install a plugin by uploading a Jar file.\n         * @param {File} [file] \n         * @param {string} [presetName] Plugin preset name. We will find the plugin from plugin presets\n         * @param {InstallPluginSourceEnum} [source] Install source. Default is file.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async installPlugin(file?: File, presetName?: string, source?: InstallPluginSourceEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.installPlugin(file, presetName, source, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.installPlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Install a plugin from uri.\n         * @param {InstallFromUriRequest} installFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async installPluginFromUri(installFromUriRequest: InstallFromUriRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.installPluginFromUri(installFromUriRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.installPluginFromUri']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List plugins using query criteria and sort params\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Keyword of plugin name or description\n         * @param {boolean} [enabled] Whether the plugin is enabled\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPlugins(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, enabled?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PluginList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPlugins(page, size, labelSelector, fieldSelector, sort, keyword, enabled, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.listPlugins']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Reload a plugin by name.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async reloadPlugin(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.reloadPlugin(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.reloadPlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Reset the configMap of plugin setting.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async resetPluginConfig(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConfigMap>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.resetPluginConfig(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.resetPluginConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update the config of plugin setting.\n         * @param {string} name \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePluginJsonConfig(name: string, body: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePluginJsonConfig(name, body, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.updatePluginJsonConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upgrade a plugin by uploading a Jar file\n         * @param {string} name \n         * @param {File} [file] \n         * @param {string} [presetName] Plugin preset name. We will find the plugin from plugin presets\n         * @param {UpgradePluginSourceEnum} [source] Install source. Default is file.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async upgradePlugin(name: string, file?: File, presetName?: string, source?: UpgradePluginSourceEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.upgradePlugin(name, file, presetName, source, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.upgradePlugin']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upgrade a plugin from uri.\n         * @param {string} name \n         * @param {UpgradeFromUriRequest} upgradeFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async upgradePluginFromUri(name: string, upgradeFromUriRequest: UpgradeFromUriRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Plugin>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.upgradePluginFromUri(name, upgradeFromUriRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.upgradePluginFromUri']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PluginV1alpha1ConsoleApi - factory interface\n */\nexport const PluginV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PluginV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Change the running state of a plugin by name.\n         * @param {PluginV1alpha1ConsoleApiChangePluginRunningStateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changePluginRunningState(requestParameters: PluginV1alpha1ConsoleApiChangePluginRunningStateRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.changePluginRunningState(requestParameters.name, requestParameters.pluginRunningStateRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Merge all CSS bundles of enabled plugins into one.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchCssBundle(options?: RawAxiosRequestConfig): AxiosPromise<string> {\n            return localVarFp.fetchCssBundle(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Merge all JS bundles of enabled plugins into one.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchJsBundle(options?: RawAxiosRequestConfig): AxiosPromise<string> {\n            return localVarFp.fetchJsBundle(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch converted json config of plugin by configured configMapName.\n         * @param {PluginV1alpha1ConsoleApiFetchPluginJsonConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPluginJsonConfig(requestParameters: PluginV1alpha1ConsoleApiFetchPluginJsonConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {\n            return localVarFp.fetchPluginJsonConfig(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch setting of plugin.\n         * @param {PluginV1alpha1ConsoleApiFetchPluginSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPluginSetting(requestParameters: PluginV1alpha1ConsoleApiFetchPluginSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<Setting> {\n            return localVarFp.fetchPluginSetting(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Install a plugin by uploading a Jar file.\n         * @param {PluginV1alpha1ConsoleApiInstallPluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installPlugin(requestParameters: PluginV1alpha1ConsoleApiInstallPluginRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.installPlugin(requestParameters.file, requestParameters.presetName, requestParameters.source, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Install a plugin from uri.\n         * @param {PluginV1alpha1ConsoleApiInstallPluginFromUriRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installPluginFromUri(requestParameters: PluginV1alpha1ConsoleApiInstallPluginFromUriRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.installPluginFromUri(requestParameters.installFromUriRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List plugins using query criteria and sort params\n         * @param {PluginV1alpha1ConsoleApiListPluginsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPlugins(requestParameters: PluginV1alpha1ConsoleApiListPluginsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PluginList> {\n            return localVarFp.listPlugins(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, requestParameters.enabled, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Reload a plugin by name.\n         * @param {PluginV1alpha1ConsoleApiReloadPluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        reloadPlugin(requestParameters: PluginV1alpha1ConsoleApiReloadPluginRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.reloadPlugin(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Reset the configMap of plugin setting.\n         * @param {PluginV1alpha1ConsoleApiResetPluginConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        resetPluginConfig(requestParameters: PluginV1alpha1ConsoleApiResetPluginConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<ConfigMap> {\n            return localVarFp.resetPluginConfig(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update the config of plugin setting.\n         * @param {PluginV1alpha1ConsoleApiUpdatePluginJsonConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePluginJsonConfig(requestParameters: PluginV1alpha1ConsoleApiUpdatePluginJsonConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.updatePluginJsonConfig(requestParameters.name, requestParameters.body, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upgrade a plugin by uploading a Jar file\n         * @param {PluginV1alpha1ConsoleApiUpgradePluginRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradePlugin(requestParameters: PluginV1alpha1ConsoleApiUpgradePluginRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.upgradePlugin(requestParameters.name, requestParameters.file, requestParameters.presetName, requestParameters.source, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upgrade a plugin from uri.\n         * @param {PluginV1alpha1ConsoleApiUpgradePluginFromUriRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradePluginFromUri(requestParameters: PluginV1alpha1ConsoleApiUpgradePluginFromUriRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {\n            return localVarFp.upgradePluginFromUri(requestParameters.name, requestParameters.upgradeFromUriRequest, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for changePluginRunningState operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiChangePluginRunningStateRequest {\n    readonly name: string\n\n    readonly pluginRunningStateRequest: PluginRunningStateRequest\n}\n\n/**\n * Request parameters for fetchPluginJsonConfig operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiFetchPluginJsonConfigRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for fetchPluginSetting operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiFetchPluginSettingRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for installPlugin operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiInstallPluginRequest {\n    readonly file?: File\n\n    /**\n     * Plugin preset name. We will find the plugin from plugin presets\n     */\n    readonly presetName?: string\n\n    /**\n     * Install source. Default is file.\n     */\n    readonly source?: InstallPluginSourceEnum\n}\n\n/**\n * Request parameters for installPluginFromUri operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiInstallPluginFromUriRequest {\n    readonly installFromUriRequest: InstallFromUriRequest\n}\n\n/**\n * Request parameters for listPlugins operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiListPluginsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Keyword of plugin name or description\n     */\n    readonly keyword?: string\n\n    /**\n     * Whether the plugin is enabled\n     */\n    readonly enabled?: boolean\n}\n\n/**\n * Request parameters for reloadPlugin operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiReloadPluginRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for resetPluginConfig operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiResetPluginConfigRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for updatePluginJsonConfig operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiUpdatePluginJsonConfigRequest {\n    readonly name: string\n\n    readonly body: object\n}\n\n/**\n * Request parameters for upgradePlugin operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiUpgradePluginRequest {\n    readonly name: string\n\n    readonly file?: File\n\n    /**\n     * Plugin preset name. We will find the plugin from plugin presets\n     */\n    readonly presetName?: string\n\n    /**\n     * Install source. Default is file.\n     */\n    readonly source?: UpgradePluginSourceEnum\n}\n\n/**\n * Request parameters for upgradePluginFromUri operation in PluginV1alpha1ConsoleApi.\n */\nexport interface PluginV1alpha1ConsoleApiUpgradePluginFromUriRequest {\n    readonly name: string\n\n    readonly upgradeFromUriRequest: UpgradeFromUriRequest\n}\n\n/**\n * PluginV1alpha1ConsoleApi - object-oriented interface\n */\nexport class PluginV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Change the running state of a plugin by name.\n     * @param {PluginV1alpha1ConsoleApiChangePluginRunningStateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public changePluginRunningState(requestParameters: PluginV1alpha1ConsoleApiChangePluginRunningStateRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).changePluginRunningState(requestParameters.name, requestParameters.pluginRunningStateRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Merge all CSS bundles of enabled plugins into one.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchCssBundle(options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).fetchCssBundle(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Merge all JS bundles of enabled plugins into one.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchJsBundle(options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).fetchJsBundle(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch converted json config of plugin by configured configMapName.\n     * @param {PluginV1alpha1ConsoleApiFetchPluginJsonConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchPluginJsonConfig(requestParameters: PluginV1alpha1ConsoleApiFetchPluginJsonConfigRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).fetchPluginJsonConfig(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch setting of plugin.\n     * @param {PluginV1alpha1ConsoleApiFetchPluginSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchPluginSetting(requestParameters: PluginV1alpha1ConsoleApiFetchPluginSettingRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).fetchPluginSetting(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Install a plugin by uploading a Jar file.\n     * @param {PluginV1alpha1ConsoleApiInstallPluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public installPlugin(requestParameters: PluginV1alpha1ConsoleApiInstallPluginRequest = {}, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).installPlugin(requestParameters.file, requestParameters.presetName, requestParameters.source, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Install a plugin from uri.\n     * @param {PluginV1alpha1ConsoleApiInstallPluginFromUriRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public installPluginFromUri(requestParameters: PluginV1alpha1ConsoleApiInstallPluginFromUriRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).installPluginFromUri(requestParameters.installFromUriRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List plugins using query criteria and sort params\n     * @param {PluginV1alpha1ConsoleApiListPluginsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPlugins(requestParameters: PluginV1alpha1ConsoleApiListPluginsRequest = {}, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).listPlugins(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, requestParameters.enabled, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Reload a plugin by name.\n     * @param {PluginV1alpha1ConsoleApiReloadPluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public reloadPlugin(requestParameters: PluginV1alpha1ConsoleApiReloadPluginRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).reloadPlugin(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Reset the configMap of plugin setting.\n     * @param {PluginV1alpha1ConsoleApiResetPluginConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public resetPluginConfig(requestParameters: PluginV1alpha1ConsoleApiResetPluginConfigRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).resetPluginConfig(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update the config of plugin setting.\n     * @param {PluginV1alpha1ConsoleApiUpdatePluginJsonConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePluginJsonConfig(requestParameters: PluginV1alpha1ConsoleApiUpdatePluginJsonConfigRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).updatePluginJsonConfig(requestParameters.name, requestParameters.body, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upgrade a plugin by uploading a Jar file\n     * @param {PluginV1alpha1ConsoleApiUpgradePluginRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public upgradePlugin(requestParameters: PluginV1alpha1ConsoleApiUpgradePluginRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).upgradePlugin(requestParameters.name, requestParameters.file, requestParameters.presetName, requestParameters.source, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upgrade a plugin from uri.\n     * @param {PluginV1alpha1ConsoleApiUpgradePluginFromUriRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public upgradePluginFromUri(requestParameters: PluginV1alpha1ConsoleApiUpgradePluginFromUriRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1ConsoleApiFp(this.configuration).upgradePluginFromUri(requestParameters.name, requestParameters.upgradeFromUriRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\nexport const InstallPluginSourceEnum = {\n    File: 'FILE',\n    Preset: 'PRESET',\n    Url: 'URL'\n} as const;\nexport type InstallPluginSourceEnum = typeof InstallPluginSourceEnum[keyof typeof InstallPluginSourceEnum];\nexport const UpgradePluginSourceEnum = {\n    File: 'FILE',\n    Preset: 'PRESET',\n    Url: 'URL'\n} as const;\nexport type UpgradePluginSourceEnum = typeof UpgradePluginSourceEnum[keyof typeof UpgradePluginSourceEnum];\n"
  },
  {
    "path": "ui/packages/api-client/src/api/plugin-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * PluginV1alpha1PublicApi - axios parameter creator\n */\nexport const PluginV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Gets plugin available by name.\n         * @param {string} name Plugin name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPluginAvailableByName: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryPluginAvailableByName', 'name', name)\n            const localVarPath = `/apis/api.plugin.halo.run/v1alpha1/plugins/{name}/available`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PluginV1alpha1PublicApi - functional programming interface\n */\nexport const PluginV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PluginV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Gets plugin available by name.\n         * @param {string} name Plugin name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryPluginAvailableByName(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<boolean>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryPluginAvailableByName(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1PublicApi.queryPluginAvailableByName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PluginV1alpha1PublicApi - factory interface\n */\nexport const PluginV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PluginV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Gets plugin available by name.\n         * @param {PluginV1alpha1PublicApiQueryPluginAvailableByNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPluginAvailableByName(requestParameters: PluginV1alpha1PublicApiQueryPluginAvailableByNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<boolean> {\n            return localVarFp.queryPluginAvailableByName(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for queryPluginAvailableByName operation in PluginV1alpha1PublicApi.\n */\nexport interface PluginV1alpha1PublicApiQueryPluginAvailableByNameRequest {\n    /**\n     * Plugin name\n     */\n    readonly name: string\n}\n\n/**\n * PluginV1alpha1PublicApi - object-oriented interface\n */\nexport class PluginV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Gets plugin available by name.\n     * @param {PluginV1alpha1PublicApiQueryPluginAvailableByNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryPluginAvailableByName(requestParameters: PluginV1alpha1PublicApiQueryPluginAvailableByNameRequest, options?: RawAxiosRequestConfig) {\n        return PluginV1alpha1PublicApiFp(this.configuration).queryPluginAvailableByName(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/policy-alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * PolicyAlpha1ConsoleApi - axios parameter creator\n */\nexport const PolicyAlpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Get policy config by group\n         * @param {string} name Name of the policy\n         * @param {string} group Name of the group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPolicyConfigByGroup: async (name: string, group: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getPolicyConfigByGroup', 'name', name)\n            // verify required parameter 'group' is not null or undefined\n            assertParamExists('getPolicyConfigByGroup', 'group', group)\n            const localVarPath = `/apis/console.api.storage.halo.run/v1alpha1/policies/{name}/configs/{group}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)))\n                .replace(`{${\"group\"}}`, encodeURIComponent(String(group)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update policy config by group\n         * @param {string} name Name of the policy\n         * @param {string} group Name of the group\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePolicyConfigByGroup: async (name: string, group: string, body: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePolicyConfigByGroup', 'name', name)\n            // verify required parameter 'group' is not null or undefined\n            assertParamExists('updatePolicyConfigByGroup', 'group', group)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('updatePolicyConfigByGroup', 'body', body)\n            const localVarPath = `/apis/console.api.storage.halo.run/v1alpha1/policies/{name}/configs/{group}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)))\n                .replace(`{${\"group\"}}`, encodeURIComponent(String(group)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PolicyAlpha1ConsoleApi - functional programming interface\n */\nexport const PolicyAlpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PolicyAlpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Get policy config by group\n         * @param {string} name Name of the policy\n         * @param {string} group Name of the group\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPolicyConfigByGroup(name: string, group: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPolicyConfigByGroup(name, group, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyAlpha1ConsoleApi.getPolicyConfigByGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update policy config by group\n         * @param {string} name Name of the policy\n         * @param {string} group Name of the group\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePolicyConfigByGroup(name: string, group: string, body: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePolicyConfigByGroup(name, group, body, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyAlpha1ConsoleApi.updatePolicyConfigByGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PolicyAlpha1ConsoleApi - factory interface\n */\nexport const PolicyAlpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PolicyAlpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Get policy config by group\n         * @param {PolicyAlpha1ConsoleApiGetPolicyConfigByGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPolicyConfigByGroup(requestParameters: PolicyAlpha1ConsoleApiGetPolicyConfigByGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {\n            return localVarFp.getPolicyConfigByGroup(requestParameters.name, requestParameters.group, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update policy config by group\n         * @param {PolicyAlpha1ConsoleApiUpdatePolicyConfigByGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePolicyConfigByGroup(requestParameters: PolicyAlpha1ConsoleApiUpdatePolicyConfigByGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.updatePolicyConfigByGroup(requestParameters.name, requestParameters.group, requestParameters.body, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for getPolicyConfigByGroup operation in PolicyAlpha1ConsoleApi.\n */\nexport interface PolicyAlpha1ConsoleApiGetPolicyConfigByGroupRequest {\n    /**\n     * Name of the policy\n     */\n    readonly name: string\n\n    /**\n     * Name of the group\n     */\n    readonly group: string\n}\n\n/**\n * Request parameters for updatePolicyConfigByGroup operation in PolicyAlpha1ConsoleApi.\n */\nexport interface PolicyAlpha1ConsoleApiUpdatePolicyConfigByGroupRequest {\n    /**\n     * Name of the policy\n     */\n    readonly name: string\n\n    /**\n     * Name of the group\n     */\n    readonly group: string\n\n    readonly body: object\n}\n\n/**\n * PolicyAlpha1ConsoleApi - object-oriented interface\n */\nexport class PolicyAlpha1ConsoleApi extends BaseAPI {\n    /**\n     * Get policy config by group\n     * @param {PolicyAlpha1ConsoleApiGetPolicyConfigByGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getPolicyConfigByGroup(requestParameters: PolicyAlpha1ConsoleApiGetPolicyConfigByGroupRequest, options?: RawAxiosRequestConfig) {\n        return PolicyAlpha1ConsoleApiFp(this.configuration).getPolicyConfigByGroup(requestParameters.name, requestParameters.group, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update policy config by group\n     * @param {PolicyAlpha1ConsoleApiUpdatePolicyConfigByGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePolicyConfigByGroup(requestParameters: PolicyAlpha1ConsoleApiUpdatePolicyConfigByGroupRequest, options?: RawAxiosRequestConfig) {\n        return PolicyAlpha1ConsoleApiFp(this.configuration).updatePolicyConfigByGroup(requestParameters.name, requestParameters.group, requestParameters.body, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/policy-template-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { PolicyTemplate } from '../models';\n// @ts-ignore\nimport type { PolicyTemplateList } from '../models';\n/**\n * PolicyTemplateV1alpha1Api - axios parameter creator\n */\nexport const PolicyTemplateV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create PolicyTemplate\n         * @param {PolicyTemplate} [policyTemplate] Fresh policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPolicyTemplate: async (policyTemplate?: PolicyTemplate, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policytemplates`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(policyTemplate, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePolicyTemplate: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deletePolicyTemplate', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policytemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPolicyTemplate: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getPolicyTemplate', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policytemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List PolicyTemplate\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPolicyTemplate: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policytemplates`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPolicyTemplate: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchPolicyTemplate', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policytemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {PolicyTemplate} [policyTemplate] Updated policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePolicyTemplate: async (name: string, policyTemplate?: PolicyTemplate, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePolicyTemplate', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policytemplates/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(policyTemplate, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PolicyTemplateV1alpha1Api - functional programming interface\n */\nexport const PolicyTemplateV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PolicyTemplateV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create PolicyTemplate\n         * @param {PolicyTemplate} [policyTemplate] Fresh policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createPolicyTemplate(policyTemplate?: PolicyTemplate, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PolicyTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createPolicyTemplate(policyTemplate, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyTemplateV1alpha1Api.createPolicyTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePolicyTemplate(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePolicyTemplate(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyTemplateV1alpha1Api.deletePolicyTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPolicyTemplate(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PolicyTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPolicyTemplate(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyTemplateV1alpha1Api.getPolicyTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List PolicyTemplate\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPolicyTemplate(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PolicyTemplateList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPolicyTemplate(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyTemplateV1alpha1Api.listPolicyTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchPolicyTemplate(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PolicyTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchPolicyTemplate(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyTemplateV1alpha1Api.patchPolicyTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update PolicyTemplate\n         * @param {string} name Name of policytemplate\n         * @param {PolicyTemplate} [policyTemplate] Updated policytemplate\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePolicyTemplate(name: string, policyTemplate?: PolicyTemplate, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PolicyTemplate>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePolicyTemplate(name, policyTemplate, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyTemplateV1alpha1Api.updatePolicyTemplate']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PolicyTemplateV1alpha1Api - factory interface\n */\nexport const PolicyTemplateV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PolicyTemplateV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create PolicyTemplate\n         * @param {PolicyTemplateV1alpha1ApiCreatePolicyTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiCreatePolicyTemplateRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PolicyTemplate> {\n            return localVarFp.createPolicyTemplate(requestParameters.policyTemplate, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete PolicyTemplate\n         * @param {PolicyTemplateV1alpha1ApiDeletePolicyTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiDeletePolicyTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deletePolicyTemplate(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get PolicyTemplate\n         * @param {PolicyTemplateV1alpha1ApiGetPolicyTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiGetPolicyTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<PolicyTemplate> {\n            return localVarFp.getPolicyTemplate(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List PolicyTemplate\n         * @param {PolicyTemplateV1alpha1ApiListPolicyTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiListPolicyTemplateRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PolicyTemplateList> {\n            return localVarFp.listPolicyTemplate(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch PolicyTemplate\n         * @param {PolicyTemplateV1alpha1ApiPatchPolicyTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiPatchPolicyTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<PolicyTemplate> {\n            return localVarFp.patchPolicyTemplate(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update PolicyTemplate\n         * @param {PolicyTemplateV1alpha1ApiUpdatePolicyTemplateRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiUpdatePolicyTemplateRequest, options?: RawAxiosRequestConfig): AxiosPromise<PolicyTemplate> {\n            return localVarFp.updatePolicyTemplate(requestParameters.name, requestParameters.policyTemplate, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createPolicyTemplate operation in PolicyTemplateV1alpha1Api.\n */\nexport interface PolicyTemplateV1alpha1ApiCreatePolicyTemplateRequest {\n    /**\n     * Fresh policytemplate\n     */\n    readonly policyTemplate?: PolicyTemplate\n}\n\n/**\n * Request parameters for deletePolicyTemplate operation in PolicyTemplateV1alpha1Api.\n */\nexport interface PolicyTemplateV1alpha1ApiDeletePolicyTemplateRequest {\n    /**\n     * Name of policytemplate\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getPolicyTemplate operation in PolicyTemplateV1alpha1Api.\n */\nexport interface PolicyTemplateV1alpha1ApiGetPolicyTemplateRequest {\n    /**\n     * Name of policytemplate\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listPolicyTemplate operation in PolicyTemplateV1alpha1Api.\n */\nexport interface PolicyTemplateV1alpha1ApiListPolicyTemplateRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchPolicyTemplate operation in PolicyTemplateV1alpha1Api.\n */\nexport interface PolicyTemplateV1alpha1ApiPatchPolicyTemplateRequest {\n    /**\n     * Name of policytemplate\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updatePolicyTemplate operation in PolicyTemplateV1alpha1Api.\n */\nexport interface PolicyTemplateV1alpha1ApiUpdatePolicyTemplateRequest {\n    /**\n     * Name of policytemplate\n     */\n    readonly name: string\n\n    /**\n     * Updated policytemplate\n     */\n    readonly policyTemplate?: PolicyTemplate\n}\n\n/**\n * PolicyTemplateV1alpha1Api - object-oriented interface\n */\nexport class PolicyTemplateV1alpha1Api extends BaseAPI {\n    /**\n     * Create PolicyTemplate\n     * @param {PolicyTemplateV1alpha1ApiCreatePolicyTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiCreatePolicyTemplateRequest = {}, options?: RawAxiosRequestConfig) {\n        return PolicyTemplateV1alpha1ApiFp(this.configuration).createPolicyTemplate(requestParameters.policyTemplate, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete PolicyTemplate\n     * @param {PolicyTemplateV1alpha1ApiDeletePolicyTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deletePolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiDeletePolicyTemplateRequest, options?: RawAxiosRequestConfig) {\n        return PolicyTemplateV1alpha1ApiFp(this.configuration).deletePolicyTemplate(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get PolicyTemplate\n     * @param {PolicyTemplateV1alpha1ApiGetPolicyTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiGetPolicyTemplateRequest, options?: RawAxiosRequestConfig) {\n        return PolicyTemplateV1alpha1ApiFp(this.configuration).getPolicyTemplate(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List PolicyTemplate\n     * @param {PolicyTemplateV1alpha1ApiListPolicyTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiListPolicyTemplateRequest = {}, options?: RawAxiosRequestConfig) {\n        return PolicyTemplateV1alpha1ApiFp(this.configuration).listPolicyTemplate(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch PolicyTemplate\n     * @param {PolicyTemplateV1alpha1ApiPatchPolicyTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchPolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiPatchPolicyTemplateRequest, options?: RawAxiosRequestConfig) {\n        return PolicyTemplateV1alpha1ApiFp(this.configuration).patchPolicyTemplate(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update PolicyTemplate\n     * @param {PolicyTemplateV1alpha1ApiUpdatePolicyTemplateRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePolicyTemplate(requestParameters: PolicyTemplateV1alpha1ApiUpdatePolicyTemplateRequest, options?: RawAxiosRequestConfig) {\n        return PolicyTemplateV1alpha1ApiFp(this.configuration).updatePolicyTemplate(requestParameters.name, requestParameters.policyTemplate, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/policy-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Policy } from '../models';\n// @ts-ignore\nimport type { PolicyList } from '../models';\n/**\n * PolicyV1alpha1Api - axios parameter creator\n */\nexport const PolicyV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Policy\n         * @param {Policy} [policy] Fresh policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPolicy: async (policy?: Policy, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policies`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(policy, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Policy\n         * @param {string} name Name of policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePolicy: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deletePolicy', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Policy\n         * @param {string} name Name of policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPolicy: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getPolicy', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Policy\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPolicy: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policies`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Policy\n         * @param {string} name Name of policy\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPolicy: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchPolicy', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Policy\n         * @param {string} name Name of policy\n         * @param {Policy} [policy] Updated policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePolicy: async (name: string, policy?: Policy, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePolicy', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/policies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(policy, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PolicyV1alpha1Api - functional programming interface\n */\nexport const PolicyV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PolicyV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Policy\n         * @param {Policy} [policy] Fresh policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createPolicy(policy?: Policy, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Policy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createPolicy(policy, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyV1alpha1Api.createPolicy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Policy\n         * @param {string} name Name of policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePolicy(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePolicy(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyV1alpha1Api.deletePolicy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Policy\n         * @param {string} name Name of policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPolicy(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Policy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPolicy(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyV1alpha1Api.getPolicy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Policy\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPolicy(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PolicyList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPolicy(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyV1alpha1Api.listPolicy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Policy\n         * @param {string} name Name of policy\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchPolicy(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Policy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchPolicy(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyV1alpha1Api.patchPolicy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Policy\n         * @param {string} name Name of policy\n         * @param {Policy} [policy] Updated policy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePolicy(name: string, policy?: Policy, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Policy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePolicy(name, policy, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PolicyV1alpha1Api.updatePolicy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PolicyV1alpha1Api - factory interface\n */\nexport const PolicyV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PolicyV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Policy\n         * @param {PolicyV1alpha1ApiCreatePolicyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPolicy(requestParameters: PolicyV1alpha1ApiCreatePolicyRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Policy> {\n            return localVarFp.createPolicy(requestParameters.policy, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Policy\n         * @param {PolicyV1alpha1ApiDeletePolicyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePolicy(requestParameters: PolicyV1alpha1ApiDeletePolicyRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deletePolicy(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Policy\n         * @param {PolicyV1alpha1ApiGetPolicyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPolicy(requestParameters: PolicyV1alpha1ApiGetPolicyRequest, options?: RawAxiosRequestConfig): AxiosPromise<Policy> {\n            return localVarFp.getPolicy(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Policy\n         * @param {PolicyV1alpha1ApiListPolicyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPolicy(requestParameters: PolicyV1alpha1ApiListPolicyRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PolicyList> {\n            return localVarFp.listPolicy(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Policy\n         * @param {PolicyV1alpha1ApiPatchPolicyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPolicy(requestParameters: PolicyV1alpha1ApiPatchPolicyRequest, options?: RawAxiosRequestConfig): AxiosPromise<Policy> {\n            return localVarFp.patchPolicy(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Policy\n         * @param {PolicyV1alpha1ApiUpdatePolicyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePolicy(requestParameters: PolicyV1alpha1ApiUpdatePolicyRequest, options?: RawAxiosRequestConfig): AxiosPromise<Policy> {\n            return localVarFp.updatePolicy(requestParameters.name, requestParameters.policy, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createPolicy operation in PolicyV1alpha1Api.\n */\nexport interface PolicyV1alpha1ApiCreatePolicyRequest {\n    /**\n     * Fresh policy\n     */\n    readonly policy?: Policy\n}\n\n/**\n * Request parameters for deletePolicy operation in PolicyV1alpha1Api.\n */\nexport interface PolicyV1alpha1ApiDeletePolicyRequest {\n    /**\n     * Name of policy\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getPolicy operation in PolicyV1alpha1Api.\n */\nexport interface PolicyV1alpha1ApiGetPolicyRequest {\n    /**\n     * Name of policy\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listPolicy operation in PolicyV1alpha1Api.\n */\nexport interface PolicyV1alpha1ApiListPolicyRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchPolicy operation in PolicyV1alpha1Api.\n */\nexport interface PolicyV1alpha1ApiPatchPolicyRequest {\n    /**\n     * Name of policy\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updatePolicy operation in PolicyV1alpha1Api.\n */\nexport interface PolicyV1alpha1ApiUpdatePolicyRequest {\n    /**\n     * Name of policy\n     */\n    readonly name: string\n\n    /**\n     * Updated policy\n     */\n    readonly policy?: Policy\n}\n\n/**\n * PolicyV1alpha1Api - object-oriented interface\n */\nexport class PolicyV1alpha1Api extends BaseAPI {\n    /**\n     * Create Policy\n     * @param {PolicyV1alpha1ApiCreatePolicyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createPolicy(requestParameters: PolicyV1alpha1ApiCreatePolicyRequest = {}, options?: RawAxiosRequestConfig) {\n        return PolicyV1alpha1ApiFp(this.configuration).createPolicy(requestParameters.policy, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Policy\n     * @param {PolicyV1alpha1ApiDeletePolicyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deletePolicy(requestParameters: PolicyV1alpha1ApiDeletePolicyRequest, options?: RawAxiosRequestConfig) {\n        return PolicyV1alpha1ApiFp(this.configuration).deletePolicy(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Policy\n     * @param {PolicyV1alpha1ApiGetPolicyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getPolicy(requestParameters: PolicyV1alpha1ApiGetPolicyRequest, options?: RawAxiosRequestConfig) {\n        return PolicyV1alpha1ApiFp(this.configuration).getPolicy(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Policy\n     * @param {PolicyV1alpha1ApiListPolicyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPolicy(requestParameters: PolicyV1alpha1ApiListPolicyRequest = {}, options?: RawAxiosRequestConfig) {\n        return PolicyV1alpha1ApiFp(this.configuration).listPolicy(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Policy\n     * @param {PolicyV1alpha1ApiPatchPolicyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchPolicy(requestParameters: PolicyV1alpha1ApiPatchPolicyRequest, options?: RawAxiosRequestConfig) {\n        return PolicyV1alpha1ApiFp(this.configuration).patchPolicy(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Policy\n     * @param {PolicyV1alpha1ApiUpdatePolicyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePolicy(requestParameters: PolicyV1alpha1ApiUpdatePolicyRequest, options?: RawAxiosRequestConfig) {\n        return PolicyV1alpha1ApiFp(this.configuration).updatePolicy(requestParameters.name, requestParameters.policy, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/post-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Post } from '../models';\n// @ts-ignore\nimport type { PostList } from '../models';\n/**\n * PostV1alpha1Api - axios parameter creator\n */\nexport const PostV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Post\n         * @param {Post} [post] Fresh post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPost: async (post?: Post, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/posts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(post, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Post\n         * @param {string} name Name of post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deletePost', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Post\n         * @param {string} name Name of post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getPost', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Post\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPost: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/posts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Post\n         * @param {string} name Name of post\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPost: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchPost', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Post\n         * @param {string} name Name of post\n         * @param {Post} [post] Updated post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePost: async (name: string, post?: Post, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePost', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(post, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PostV1alpha1Api - functional programming interface\n */\nexport const PostV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PostV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Post\n         * @param {Post} [post] Fresh post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createPost(post?: Post, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createPost(post, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1Api.createPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Post\n         * @param {string} name Name of post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1Api.deletePost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Post\n         * @param {string} name Name of post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1Api.getPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Post\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPost(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PostList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPost(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1Api.listPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Post\n         * @param {string} name Name of post\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchPost(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchPost(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1Api.patchPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Post\n         * @param {string} name Name of post\n         * @param {Post} [post] Updated post\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePost(name: string, post?: Post, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePost(name, post, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1Api.updatePost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PostV1alpha1Api - factory interface\n */\nexport const PostV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PostV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Post\n         * @param {PostV1alpha1ApiCreatePostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPost(requestParameters: PostV1alpha1ApiCreatePostRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.createPost(requestParameters.post, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Post\n         * @param {PostV1alpha1ApiDeletePostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePost(requestParameters: PostV1alpha1ApiDeletePostRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deletePost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Post\n         * @param {PostV1alpha1ApiGetPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPost(requestParameters: PostV1alpha1ApiGetPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.getPost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Post\n         * @param {PostV1alpha1ApiListPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPost(requestParameters: PostV1alpha1ApiListPostRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PostList> {\n            return localVarFp.listPost(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Post\n         * @param {PostV1alpha1ApiPatchPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchPost(requestParameters: PostV1alpha1ApiPatchPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.patchPost(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Post\n         * @param {PostV1alpha1ApiUpdatePostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePost(requestParameters: PostV1alpha1ApiUpdatePostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.updatePost(requestParameters.name, requestParameters.post, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createPost operation in PostV1alpha1Api.\n */\nexport interface PostV1alpha1ApiCreatePostRequest {\n    /**\n     * Fresh post\n     */\n    readonly post?: Post\n}\n\n/**\n * Request parameters for deletePost operation in PostV1alpha1Api.\n */\nexport interface PostV1alpha1ApiDeletePostRequest {\n    /**\n     * Name of post\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getPost operation in PostV1alpha1Api.\n */\nexport interface PostV1alpha1ApiGetPostRequest {\n    /**\n     * Name of post\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listPost operation in PostV1alpha1Api.\n */\nexport interface PostV1alpha1ApiListPostRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchPost operation in PostV1alpha1Api.\n */\nexport interface PostV1alpha1ApiPatchPostRequest {\n    /**\n     * Name of post\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updatePost operation in PostV1alpha1Api.\n */\nexport interface PostV1alpha1ApiUpdatePostRequest {\n    /**\n     * Name of post\n     */\n    readonly name: string\n\n    /**\n     * Updated post\n     */\n    readonly post?: Post\n}\n\n/**\n * PostV1alpha1Api - object-oriented interface\n */\nexport class PostV1alpha1Api extends BaseAPI {\n    /**\n     * Create Post\n     * @param {PostV1alpha1ApiCreatePostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createPost(requestParameters: PostV1alpha1ApiCreatePostRequest = {}, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ApiFp(this.configuration).createPost(requestParameters.post, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Post\n     * @param {PostV1alpha1ApiDeletePostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deletePost(requestParameters: PostV1alpha1ApiDeletePostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ApiFp(this.configuration).deletePost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Post\n     * @param {PostV1alpha1ApiGetPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getPost(requestParameters: PostV1alpha1ApiGetPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ApiFp(this.configuration).getPost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Post\n     * @param {PostV1alpha1ApiListPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPost(requestParameters: PostV1alpha1ApiListPostRequest = {}, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ApiFp(this.configuration).listPost(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Post\n     * @param {PostV1alpha1ApiPatchPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchPost(requestParameters: PostV1alpha1ApiPatchPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ApiFp(this.configuration).patchPost(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Post\n     * @param {PostV1alpha1ApiUpdatePostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePost(requestParameters: PostV1alpha1ApiUpdatePostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ApiFp(this.configuration).updatePost(requestParameters.name, requestParameters.post, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/post-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Content } from '../models';\n// @ts-ignore\nimport type { ContentWrapper } from '../models';\n// @ts-ignore\nimport type { ListedPostList } from '../models';\n// @ts-ignore\nimport type { ListedSnapshotDto } from '../models';\n// @ts-ignore\nimport type { Post } from '../models';\n// @ts-ignore\nimport type { PostRequest } from '../models';\n// @ts-ignore\nimport type { RevertSnapshotForPostParam } from '../models';\n/**\n * PostV1alpha1ConsoleApi - axios parameter creator\n */\nexport const PostV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Delete a content for post.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePostContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deletePostContent', 'name', name)\n            // verify required parameter 'snapshotName' is not null or undefined\n            assertParamExists('deletePostContent', 'snapshotName', snapshotName)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (snapshotName !== undefined) {\n                localVarQueryParameter['snapshotName'] = snapshotName;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Draft a post.\n         * @param {PostRequest} postRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        draftPost: async (postRequest: PostRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'postRequest' is not null or undefined\n            assertParamExists('draftPost', 'postRequest', postRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(postRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch content of post.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPostContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchPostContent', 'name', name)\n            // verify required parameter 'snapshotName' is not null or undefined\n            assertParamExists('fetchPostContent', 'snapshotName', snapshotName)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (snapshotName !== undefined) {\n                localVarQueryParameter['snapshotName'] = snapshotName;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch head content of post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPostHeadContent: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchPostHeadContent', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/head-content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch release content of post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPostReleaseContent: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchPostReleaseContent', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/release-content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List all snapshots for post content.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPostSnapshots: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('listPostSnapshots', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/snapshot`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List posts.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {ListPostsPublishPhaseEnum} [publishPhase] Posts filtered by publish phase.\n         * @param {string} [keyword] Posts filtered by keyword.\n         * @param {string} [categoryWithChildren] Posts filtered by category including sub-categories.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPosts: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, publishPhase?: ListPostsPublishPhaseEnum, keyword?: string, categoryWithChildren?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (publishPhase !== undefined) {\n                localVarQueryParameter['publishPhase'] = publishPhase;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n            if (categoryWithChildren !== undefined) {\n                localVarQueryParameter['categoryWithChildren'] = categoryWithChildren;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Publish a post.\n         * @param {string} name \n         * @param {string} [headSnapshot] Head snapshot name of content.\n         * @param {boolean} [async] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        publishPost: async (name: string, headSnapshot?: string, async?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('publishPost', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/publish`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (headSnapshot !== undefined) {\n                localVarQueryParameter['headSnapshot'] = headSnapshot;\n            }\n\n            if (async !== undefined) {\n                localVarQueryParameter['async'] = async;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Recycle a post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        recyclePost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('recyclePost', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/recycle`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Revert to specified snapshot for post content.\n         * @param {string} name \n         * @param {RevertSnapshotForPostParam} revertSnapshotForPostParam \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revertToSpecifiedSnapshotForPost: async (name: string, revertSnapshotForPostParam: RevertSnapshotForPostParam, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('revertToSpecifiedSnapshotForPost', 'name', name)\n            // verify required parameter 'revertSnapshotForPostParam' is not null or undefined\n            assertParamExists('revertToSpecifiedSnapshotForPost', 'revertSnapshotForPostParam', revertSnapshotForPostParam)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/revert-content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(revertSnapshotForPostParam, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * UnPublish a post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        unpublishPost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('unpublishPost', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update a post.\n         * @param {string} name \n         * @param {PostRequest} postRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDraftPost: async (name: string, postRequest: PostRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateDraftPost', 'name', name)\n            // verify required parameter 'postRequest' is not null or undefined\n            assertParamExists('updateDraftPost', 'postRequest', postRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(postRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update a post\\'s content.\n         * @param {string} name \n         * @param {Content} content \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePostContent: async (name: string, content: Content, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updatePostContent', 'name', name)\n            // verify required parameter 'content' is not null or undefined\n            assertParamExists('updatePostContent', 'content', content)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(content, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PostV1alpha1ConsoleApi - functional programming interface\n */\nexport const PostV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PostV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Delete a content for post.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePostContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePostContent(name, snapshotName, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.deletePostContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Draft a post.\n         * @param {PostRequest} postRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async draftPost(postRequest: PostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.draftPost(postRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.draftPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch content of post.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchPostContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchPostContent(name, snapshotName, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.fetchPostContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch head content of post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchPostHeadContent(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchPostHeadContent(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.fetchPostHeadContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch release content of post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchPostReleaseContent(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchPostReleaseContent(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.fetchPostReleaseContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List all snapshots for post content.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPostSnapshots(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ListedSnapshotDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPostSnapshots(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.listPostSnapshots']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List posts.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {ListPostsPublishPhaseEnum} [publishPhase] Posts filtered by publish phase.\n         * @param {string} [keyword] Posts filtered by keyword.\n         * @param {string} [categoryWithChildren] Posts filtered by category including sub-categories.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPosts(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, publishPhase?: ListPostsPublishPhaseEnum, keyword?: string, categoryWithChildren?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPosts(page, size, labelSelector, fieldSelector, sort, publishPhase, keyword, categoryWithChildren, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.listPosts']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Publish a post.\n         * @param {string} name \n         * @param {string} [headSnapshot] Head snapshot name of content.\n         * @param {boolean} [async] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async publishPost(name: string, headSnapshot?: string, async?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.publishPost(name, headSnapshot, async, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.publishPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Recycle a post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async recyclePost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.recyclePost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.recyclePost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Revert to specified snapshot for post content.\n         * @param {string} name \n         * @param {RevertSnapshotForPostParam} revertSnapshotForPostParam \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async revertToSpecifiedSnapshotForPost(name: string, revertSnapshotForPostParam: RevertSnapshotForPostParam, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.revertToSpecifiedSnapshotForPost(name, revertSnapshotForPostParam, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.revertToSpecifiedSnapshotForPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * UnPublish a post.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async unpublishPost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.unpublishPost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.unpublishPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update a post.\n         * @param {string} name \n         * @param {PostRequest} postRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateDraftPost(name: string, postRequest: PostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateDraftPost(name, postRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.updateDraftPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update a post\\'s content.\n         * @param {string} name \n         * @param {Content} content \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePostContent(name: string, content: Content, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePostContent(name, content, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1ConsoleApi.updatePostContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PostV1alpha1ConsoleApi - factory interface\n */\nexport const PostV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PostV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Delete a content for post.\n         * @param {PostV1alpha1ConsoleApiDeletePostContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePostContent(requestParameters: PostV1alpha1ConsoleApiDeletePostContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.deletePostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Draft a post.\n         * @param {PostV1alpha1ConsoleApiDraftPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        draftPost(requestParameters: PostV1alpha1ConsoleApiDraftPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.draftPost(requestParameters.postRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch content of post.\n         * @param {PostV1alpha1ConsoleApiFetchPostContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPostContent(requestParameters: PostV1alpha1ConsoleApiFetchPostContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.fetchPostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch head content of post.\n         * @param {PostV1alpha1ConsoleApiFetchPostHeadContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPostHeadContent(requestParameters: PostV1alpha1ConsoleApiFetchPostHeadContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.fetchPostHeadContent(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch release content of post.\n         * @param {PostV1alpha1ConsoleApiFetchPostReleaseContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchPostReleaseContent(requestParameters: PostV1alpha1ConsoleApiFetchPostReleaseContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.fetchPostReleaseContent(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List all snapshots for post content.\n         * @param {PostV1alpha1ConsoleApiListPostSnapshotsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPostSnapshots(requestParameters: PostV1alpha1ConsoleApiListPostSnapshotsRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<ListedSnapshotDto>> {\n            return localVarFp.listPostSnapshots(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List posts.\n         * @param {PostV1alpha1ConsoleApiListPostsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPosts(requestParameters: PostV1alpha1ConsoleApiListPostsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ListedPostList> {\n            return localVarFp.listPosts(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.publishPhase, requestParameters.keyword, requestParameters.categoryWithChildren, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Publish a post.\n         * @param {PostV1alpha1ConsoleApiPublishPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        publishPost(requestParameters: PostV1alpha1ConsoleApiPublishPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.publishPost(requestParameters.name, requestParameters.headSnapshot, requestParameters.async, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Recycle a post.\n         * @param {PostV1alpha1ConsoleApiRecyclePostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        recyclePost(requestParameters: PostV1alpha1ConsoleApiRecyclePostRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.recyclePost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Revert to specified snapshot for post content.\n         * @param {PostV1alpha1ConsoleApiRevertToSpecifiedSnapshotForPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revertToSpecifiedSnapshotForPost(requestParameters: PostV1alpha1ConsoleApiRevertToSpecifiedSnapshotForPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.revertToSpecifiedSnapshotForPost(requestParameters.name, requestParameters.revertSnapshotForPostParam, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * UnPublish a post.\n         * @param {PostV1alpha1ConsoleApiUnpublishPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        unpublishPost(requestParameters: PostV1alpha1ConsoleApiUnpublishPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.unpublishPost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update a post.\n         * @param {PostV1alpha1ConsoleApiUpdateDraftPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDraftPost(requestParameters: PostV1alpha1ConsoleApiUpdateDraftPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.updateDraftPost(requestParameters.name, requestParameters.postRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update a post\\'s content.\n         * @param {PostV1alpha1ConsoleApiUpdatePostContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePostContent(requestParameters: PostV1alpha1ConsoleApiUpdatePostContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.updatePostContent(requestParameters.name, requestParameters.content, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for deletePostContent operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiDeletePostContentRequest {\n    readonly name: string\n\n    readonly snapshotName: string\n}\n\n/**\n * Request parameters for draftPost operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiDraftPostRequest {\n    readonly postRequest: PostRequest\n}\n\n/**\n * Request parameters for fetchPostContent operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiFetchPostContentRequest {\n    readonly name: string\n\n    readonly snapshotName: string\n}\n\n/**\n * Request parameters for fetchPostHeadContent operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiFetchPostHeadContentRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for fetchPostReleaseContent operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiFetchPostReleaseContentRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for listPostSnapshots operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiListPostSnapshotsRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for listPosts operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiListPostsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Posts filtered by publish phase.\n     */\n    readonly publishPhase?: ListPostsPublishPhaseEnum\n\n    /**\n     * Posts filtered by keyword.\n     */\n    readonly keyword?: string\n\n    /**\n     * Posts filtered by category including sub-categories.\n     */\n    readonly categoryWithChildren?: string\n}\n\n/**\n * Request parameters for publishPost operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiPublishPostRequest {\n    readonly name: string\n\n    /**\n     * Head snapshot name of content.\n     */\n    readonly headSnapshot?: string\n\n    readonly async?: boolean\n}\n\n/**\n * Request parameters for recyclePost operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiRecyclePostRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for revertToSpecifiedSnapshotForPost operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiRevertToSpecifiedSnapshotForPostRequest {\n    readonly name: string\n\n    readonly revertSnapshotForPostParam: RevertSnapshotForPostParam\n}\n\n/**\n * Request parameters for unpublishPost operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiUnpublishPostRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for updateDraftPost operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiUpdateDraftPostRequest {\n    readonly name: string\n\n    readonly postRequest: PostRequest\n}\n\n/**\n * Request parameters for updatePostContent operation in PostV1alpha1ConsoleApi.\n */\nexport interface PostV1alpha1ConsoleApiUpdatePostContentRequest {\n    readonly name: string\n\n    readonly content: Content\n}\n\n/**\n * PostV1alpha1ConsoleApi - object-oriented interface\n */\nexport class PostV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Delete a content for post.\n     * @param {PostV1alpha1ConsoleApiDeletePostContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deletePostContent(requestParameters: PostV1alpha1ConsoleApiDeletePostContentRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).deletePostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Draft a post.\n     * @param {PostV1alpha1ConsoleApiDraftPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public draftPost(requestParameters: PostV1alpha1ConsoleApiDraftPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).draftPost(requestParameters.postRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch content of post.\n     * @param {PostV1alpha1ConsoleApiFetchPostContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchPostContent(requestParameters: PostV1alpha1ConsoleApiFetchPostContentRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).fetchPostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch head content of post.\n     * @param {PostV1alpha1ConsoleApiFetchPostHeadContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchPostHeadContent(requestParameters: PostV1alpha1ConsoleApiFetchPostHeadContentRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).fetchPostHeadContent(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch release content of post.\n     * @param {PostV1alpha1ConsoleApiFetchPostReleaseContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchPostReleaseContent(requestParameters: PostV1alpha1ConsoleApiFetchPostReleaseContentRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).fetchPostReleaseContent(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List all snapshots for post content.\n     * @param {PostV1alpha1ConsoleApiListPostSnapshotsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPostSnapshots(requestParameters: PostV1alpha1ConsoleApiListPostSnapshotsRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).listPostSnapshots(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List posts.\n     * @param {PostV1alpha1ConsoleApiListPostsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPosts(requestParameters: PostV1alpha1ConsoleApiListPostsRequest = {}, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).listPosts(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.publishPhase, requestParameters.keyword, requestParameters.categoryWithChildren, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Publish a post.\n     * @param {PostV1alpha1ConsoleApiPublishPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public publishPost(requestParameters: PostV1alpha1ConsoleApiPublishPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).publishPost(requestParameters.name, requestParameters.headSnapshot, requestParameters.async, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Recycle a post.\n     * @param {PostV1alpha1ConsoleApiRecyclePostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public recyclePost(requestParameters: PostV1alpha1ConsoleApiRecyclePostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).recyclePost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Revert to specified snapshot for post content.\n     * @param {PostV1alpha1ConsoleApiRevertToSpecifiedSnapshotForPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public revertToSpecifiedSnapshotForPost(requestParameters: PostV1alpha1ConsoleApiRevertToSpecifiedSnapshotForPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).revertToSpecifiedSnapshotForPost(requestParameters.name, requestParameters.revertSnapshotForPostParam, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * UnPublish a post.\n     * @param {PostV1alpha1ConsoleApiUnpublishPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public unpublishPost(requestParameters: PostV1alpha1ConsoleApiUnpublishPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).unpublishPost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update a post.\n     * @param {PostV1alpha1ConsoleApiUpdateDraftPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateDraftPost(requestParameters: PostV1alpha1ConsoleApiUpdateDraftPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).updateDraftPost(requestParameters.name, requestParameters.postRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update a post\\'s content.\n     * @param {PostV1alpha1ConsoleApiUpdatePostContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updatePostContent(requestParameters: PostV1alpha1ConsoleApiUpdatePostContentRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1ConsoleApiFp(this.configuration).updatePostContent(requestParameters.name, requestParameters.content, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\nexport const ListPostsPublishPhaseEnum = {\n    Draft: 'DRAFT',\n    PendingApproval: 'PENDING_APPROVAL',\n    Published: 'PUBLISHED',\n    Failed: 'FAILED'\n} as const;\nexport type ListPostsPublishPhaseEnum = typeof ListPostsPublishPhaseEnum[keyof typeof ListPostsPublishPhaseEnum];\n"
  },
  {
    "path": "ui/packages/api-client/src/api/post-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ListedPostVoList } from '../models';\n// @ts-ignore\nimport type { NavigationPostVo } from '../models';\n// @ts-ignore\nimport type { PostVo } from '../models';\n/**\n * PostV1alpha1PublicApi - axios parameter creator\n */\nexport const PostV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Gets a post by name.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostByName: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryPostByName', 'name', name)\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Gets a post navigation by name.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostNavigationByName: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryPostNavigationByName', 'name', name)\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/posts/{name}/navigation`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Lists posts.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPosts: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/posts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PostV1alpha1PublicApi - functional programming interface\n */\nexport const PostV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PostV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Gets a post by name.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryPostByName(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PostVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryPostByName(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1PublicApi.queryPostByName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Gets a post navigation by name.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryPostNavigationByName(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NavigationPostVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryPostNavigationByName(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1PublicApi.queryPostNavigationByName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Lists posts.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryPosts(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryPosts(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1PublicApi.queryPosts']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PostV1alpha1PublicApi - factory interface\n */\nexport const PostV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PostV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Gets a post by name.\n         * @param {PostV1alpha1PublicApiQueryPostByNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostByName(requestParameters: PostV1alpha1PublicApiQueryPostByNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<PostVo> {\n            return localVarFp.queryPostByName(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Gets a post navigation by name.\n         * @param {PostV1alpha1PublicApiQueryPostNavigationByNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostNavigationByName(requestParameters: PostV1alpha1PublicApiQueryPostNavigationByNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<NavigationPostVo> {\n            return localVarFp.queryPostNavigationByName(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Lists posts.\n         * @param {PostV1alpha1PublicApiQueryPostsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPosts(requestParameters: PostV1alpha1PublicApiQueryPostsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ListedPostVoList> {\n            return localVarFp.queryPosts(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for queryPostByName operation in PostV1alpha1PublicApi.\n */\nexport interface PostV1alpha1PublicApiQueryPostByNameRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for queryPostNavigationByName operation in PostV1alpha1PublicApi.\n */\nexport interface PostV1alpha1PublicApiQueryPostNavigationByNameRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for queryPosts operation in PostV1alpha1PublicApi.\n */\nexport interface PostV1alpha1PublicApiQueryPostsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * PostV1alpha1PublicApi - object-oriented interface\n */\nexport class PostV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Gets a post by name.\n     * @param {PostV1alpha1PublicApiQueryPostByNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryPostByName(requestParameters: PostV1alpha1PublicApiQueryPostByNameRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1PublicApiFp(this.configuration).queryPostByName(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Gets a post navigation by name.\n     * @param {PostV1alpha1PublicApiQueryPostNavigationByNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryPostNavigationByName(requestParameters: PostV1alpha1PublicApiQueryPostNavigationByNameRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1PublicApiFp(this.configuration).queryPostNavigationByName(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Lists posts.\n     * @param {PostV1alpha1PublicApiQueryPostsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryPosts(requestParameters: PostV1alpha1PublicApiQueryPostsRequest = {}, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1PublicApiFp(this.configuration).queryPosts(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/post-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ListedPostList } from '../models';\n// @ts-ignore\nimport type { Post } from '../models';\n// @ts-ignore\nimport type { Snapshot } from '../models';\n/**\n * PostV1alpha1UcApi - axios parameter creator\n */\nexport const PostV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create my post. If you want to create a post with content, please set  annotation: \\\"content.halo.run/content-json\\\" into annotations and refer  to Content for corresponding data type. \n         * @param {Post} [post] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createMyPost: async (post?: Post, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(post, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get post that belongs to the current user.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyPost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getMyPost', 'name', name)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get my post draft.\n         * @param {string} name Post name\n         * @param {boolean} [patched] Should include patched content and raw or not.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyPostDraft: async (name: string, patched?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getMyPostDraft', 'name', name)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/draft`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (patched !== undefined) {\n                localVarQueryParameter['patched'] = patched;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List posts owned by the current user.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {ListMyPostsPublishPhaseEnum} [publishPhase] Posts filtered by publish phase.\n         * @param {string} [keyword] Posts filtered by keyword.\n         * @param {string} [categoryWithChildren] Posts filtered by category including sub-categories.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMyPosts: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, publishPhase?: ListMyPostsPublishPhaseEnum, keyword?: string, categoryWithChildren?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (publishPhase !== undefined) {\n                localVarQueryParameter['publishPhase'] = publishPhase;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n            if (categoryWithChildren !== undefined) {\n                localVarQueryParameter['categoryWithChildren'] = categoryWithChildren;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Publish my post.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        publishMyPost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('publishMyPost', 'name', name)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/publish`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Move my post to recycle bin.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        recycleMyPost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('recycleMyPost', 'name', name)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/recycle`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Unpublish my post.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        unpublishMyPost: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('unpublishMyPost', 'name', name)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/unpublish`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update my post.\n         * @param {string} name Post name\n         * @param {Post} [post] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMyPost: async (name: string, post?: Post, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateMyPost', 'name', name)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(post, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update draft of my post. Please make sure set annotation: \\\"content.halo.run/content-json\\\" into annotations and refer to Content for corresponding data type. \n         * @param {string} name Post name\n         * @param {Snapshot} [snapshot] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMyPostDraft: async (name: string, snapshot?: Snapshot, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateMyPostDraft', 'name', name)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts/{name}/draft`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(snapshot, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PostV1alpha1UcApi - functional programming interface\n */\nexport const PostV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PostV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create my post. If you want to create a post with content, please set  annotation: \\\"content.halo.run/content-json\\\" into annotations and refer  to Content for corresponding data type. \n         * @param {Post} [post] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createMyPost(post?: Post, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createMyPost(post, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.createMyPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get post that belongs to the current user.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMyPost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMyPost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.getMyPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get my post draft.\n         * @param {string} name Post name\n         * @param {boolean} [patched] Should include patched content and raw or not.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMyPostDraft(name: string, patched?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMyPostDraft(name, patched, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.getMyPostDraft']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List posts owned by the current user.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {ListMyPostsPublishPhaseEnum} [publishPhase] Posts filtered by publish phase.\n         * @param {string} [keyword] Posts filtered by keyword.\n         * @param {string} [categoryWithChildren] Posts filtered by category including sub-categories.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listMyPosts(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, publishPhase?: ListMyPostsPublishPhaseEnum, keyword?: string, categoryWithChildren?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listMyPosts(page, size, labelSelector, fieldSelector, sort, publishPhase, keyword, categoryWithChildren, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.listMyPosts']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Publish my post.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async publishMyPost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.publishMyPost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.publishMyPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Move my post to recycle bin.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async recycleMyPost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.recycleMyPost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.recycleMyPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Unpublish my post.\n         * @param {string} name Post name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async unpublishMyPost(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.unpublishMyPost(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.unpublishMyPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update my post.\n         * @param {string} name Post name\n         * @param {Post} [post] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateMyPost(name: string, post?: Post, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateMyPost(name, post, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.updateMyPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update draft of my post. Please make sure set annotation: \\\"content.halo.run/content-json\\\" into annotations and refer to Content for corresponding data type. \n         * @param {string} name Post name\n         * @param {Snapshot} [snapshot] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateMyPostDraft(name: string, snapshot?: Snapshot, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateMyPostDraft(name, snapshot, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['PostV1alpha1UcApi.updateMyPostDraft']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * PostV1alpha1UcApi - factory interface\n */\nexport const PostV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PostV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Create my post. If you want to create a post with content, please set  annotation: \\\"content.halo.run/content-json\\\" into annotations and refer  to Content for corresponding data type. \n         * @param {PostV1alpha1UcApiCreateMyPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createMyPost(requestParameters: PostV1alpha1UcApiCreateMyPostRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.createMyPost(requestParameters.post, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get post that belongs to the current user.\n         * @param {PostV1alpha1UcApiGetMyPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyPost(requestParameters: PostV1alpha1UcApiGetMyPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.getMyPost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get my post draft.\n         * @param {PostV1alpha1UcApiGetMyPostDraftRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyPostDraft(requestParameters: PostV1alpha1UcApiGetMyPostDraftRequest, options?: RawAxiosRequestConfig): AxiosPromise<Snapshot> {\n            return localVarFp.getMyPostDraft(requestParameters.name, requestParameters.patched, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List posts owned by the current user.\n         * @param {PostV1alpha1UcApiListMyPostsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listMyPosts(requestParameters: PostV1alpha1UcApiListMyPostsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ListedPostList> {\n            return localVarFp.listMyPosts(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.publishPhase, requestParameters.keyword, requestParameters.categoryWithChildren, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Publish my post.\n         * @param {PostV1alpha1UcApiPublishMyPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        publishMyPost(requestParameters: PostV1alpha1UcApiPublishMyPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.publishMyPost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Move my post to recycle bin.\n         * @param {PostV1alpha1UcApiRecycleMyPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        recycleMyPost(requestParameters: PostV1alpha1UcApiRecycleMyPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.recycleMyPost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Unpublish my post.\n         * @param {PostV1alpha1UcApiUnpublishMyPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        unpublishMyPost(requestParameters: PostV1alpha1UcApiUnpublishMyPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.unpublishMyPost(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update my post.\n         * @param {PostV1alpha1UcApiUpdateMyPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMyPost(requestParameters: PostV1alpha1UcApiUpdateMyPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.updateMyPost(requestParameters.name, requestParameters.post, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update draft of my post. Please make sure set annotation: \\\"content.halo.run/content-json\\\" into annotations and refer to Content for corresponding data type. \n         * @param {PostV1alpha1UcApiUpdateMyPostDraftRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMyPostDraft(requestParameters: PostV1alpha1UcApiUpdateMyPostDraftRequest, options?: RawAxiosRequestConfig): AxiosPromise<Snapshot> {\n            return localVarFp.updateMyPostDraft(requestParameters.name, requestParameters.snapshot, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createMyPost operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiCreateMyPostRequest {\n    readonly post?: Post\n}\n\n/**\n * Request parameters for getMyPost operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiGetMyPostRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getMyPostDraft operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiGetMyPostDraftRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n\n    /**\n     * Should include patched content and raw or not.\n     */\n    readonly patched?: boolean\n}\n\n/**\n * Request parameters for listMyPosts operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiListMyPostsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Posts filtered by publish phase.\n     */\n    readonly publishPhase?: ListMyPostsPublishPhaseEnum\n\n    /**\n     * Posts filtered by keyword.\n     */\n    readonly keyword?: string\n\n    /**\n     * Posts filtered by category including sub-categories.\n     */\n    readonly categoryWithChildren?: string\n}\n\n/**\n * Request parameters for publishMyPost operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiPublishMyPostRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for recycleMyPost operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiRecycleMyPostRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for unpublishMyPost operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiUnpublishMyPostRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for updateMyPost operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiUpdateMyPostRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n\n    readonly post?: Post\n}\n\n/**\n * Request parameters for updateMyPostDraft operation in PostV1alpha1UcApi.\n */\nexport interface PostV1alpha1UcApiUpdateMyPostDraftRequest {\n    /**\n     * Post name\n     */\n    readonly name: string\n\n    readonly snapshot?: Snapshot\n}\n\n/**\n * PostV1alpha1UcApi - object-oriented interface\n */\nexport class PostV1alpha1UcApi extends BaseAPI {\n    /**\n     * Create my post. If you want to create a post with content, please set  annotation: \\\"content.halo.run/content-json\\\" into annotations and refer  to Content for corresponding data type. \n     * @param {PostV1alpha1UcApiCreateMyPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createMyPost(requestParameters: PostV1alpha1UcApiCreateMyPostRequest = {}, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).createMyPost(requestParameters.post, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get post that belongs to the current user.\n     * @param {PostV1alpha1UcApiGetMyPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getMyPost(requestParameters: PostV1alpha1UcApiGetMyPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).getMyPost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get my post draft.\n     * @param {PostV1alpha1UcApiGetMyPostDraftRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getMyPostDraft(requestParameters: PostV1alpha1UcApiGetMyPostDraftRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).getMyPostDraft(requestParameters.name, requestParameters.patched, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List posts owned by the current user.\n     * @param {PostV1alpha1UcApiListMyPostsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listMyPosts(requestParameters: PostV1alpha1UcApiListMyPostsRequest = {}, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).listMyPosts(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.publishPhase, requestParameters.keyword, requestParameters.categoryWithChildren, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Publish my post.\n     * @param {PostV1alpha1UcApiPublishMyPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public publishMyPost(requestParameters: PostV1alpha1UcApiPublishMyPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).publishMyPost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Move my post to recycle bin.\n     * @param {PostV1alpha1UcApiRecycleMyPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public recycleMyPost(requestParameters: PostV1alpha1UcApiRecycleMyPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).recycleMyPost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Unpublish my post.\n     * @param {PostV1alpha1UcApiUnpublishMyPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public unpublishMyPost(requestParameters: PostV1alpha1UcApiUnpublishMyPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).unpublishMyPost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update my post.\n     * @param {PostV1alpha1UcApiUpdateMyPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateMyPost(requestParameters: PostV1alpha1UcApiUpdateMyPostRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).updateMyPost(requestParameters.name, requestParameters.post, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update draft of my post. Please make sure set annotation: \\\"content.halo.run/content-json\\\" into annotations and refer to Content for corresponding data type. \n     * @param {PostV1alpha1UcApiUpdateMyPostDraftRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateMyPostDraft(requestParameters: PostV1alpha1UcApiUpdateMyPostDraftRequest, options?: RawAxiosRequestConfig) {\n        return PostV1alpha1UcApiFp(this.configuration).updateMyPostDraft(requestParameters.name, requestParameters.snapshot, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\nexport const ListMyPostsPublishPhaseEnum = {\n    Draft: 'DRAFT',\n    PendingApproval: 'PENDING_APPROVAL',\n    Published: 'PUBLISHED',\n    Failed: 'FAILED'\n} as const;\nexport type ListMyPostsPublishPhaseEnum = typeof ListMyPostsPublishPhaseEnum[keyof typeof ListMyPostsPublishPhaseEnum];\n"
  },
  {
    "path": "ui/packages/api-client/src/api/reason-type-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { ReasonType } from '../models';\n// @ts-ignore\nimport type { ReasonTypeList } from '../models';\n/**\n * ReasonTypeV1alpha1Api - axios parameter creator\n */\nexport const ReasonTypeV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create ReasonType\n         * @param {ReasonType} [reasonType] Fresh reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReasonType: async (reasonType?: ReasonType, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasontypes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reasonType, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete ReasonType\n         * @param {string} name Name of reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReasonType: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteReasonType', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasontypes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get ReasonType\n         * @param {string} name Name of reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReasonType: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getReasonType', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasontypes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List ReasonType\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReasonType: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasontypes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch ReasonType\n         * @param {string} name Name of reasontype\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReasonType: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchReasonType', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasontypes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update ReasonType\n         * @param {string} name Name of reasontype\n         * @param {ReasonType} [reasonType] Updated reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReasonType: async (name: string, reasonType?: ReasonType, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateReasonType', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasontypes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reasonType, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ReasonTypeV1alpha1Api - functional programming interface\n */\nexport const ReasonTypeV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ReasonTypeV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create ReasonType\n         * @param {ReasonType} [reasonType] Fresh reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createReasonType(reasonType?: ReasonType, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonType>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createReasonType(reasonType, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonTypeV1alpha1Api.createReasonType']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete ReasonType\n         * @param {string} name Name of reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteReasonType(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteReasonType(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonTypeV1alpha1Api.deleteReasonType']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get ReasonType\n         * @param {string} name Name of reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getReasonType(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonType>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getReasonType(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonTypeV1alpha1Api.getReasonType']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List ReasonType\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listReasonType(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonTypeList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listReasonType(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonTypeV1alpha1Api.listReasonType']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch ReasonType\n         * @param {string} name Name of reasontype\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchReasonType(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonType>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchReasonType(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonTypeV1alpha1Api.patchReasonType']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update ReasonType\n         * @param {string} name Name of reasontype\n         * @param {ReasonType} [reasonType] Updated reasontype\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateReasonType(name: string, reasonType?: ReasonType, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonType>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateReasonType(name, reasonType, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonTypeV1alpha1Api.updateReasonType']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ReasonTypeV1alpha1Api - factory interface\n */\nexport const ReasonTypeV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ReasonTypeV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create ReasonType\n         * @param {ReasonTypeV1alpha1ApiCreateReasonTypeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReasonType(requestParameters: ReasonTypeV1alpha1ApiCreateReasonTypeRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ReasonType> {\n            return localVarFp.createReasonType(requestParameters.reasonType, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete ReasonType\n         * @param {ReasonTypeV1alpha1ApiDeleteReasonTypeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReasonType(requestParameters: ReasonTypeV1alpha1ApiDeleteReasonTypeRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteReasonType(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get ReasonType\n         * @param {ReasonTypeV1alpha1ApiGetReasonTypeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReasonType(requestParameters: ReasonTypeV1alpha1ApiGetReasonTypeRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReasonType> {\n            return localVarFp.getReasonType(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List ReasonType\n         * @param {ReasonTypeV1alpha1ApiListReasonTypeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReasonType(requestParameters: ReasonTypeV1alpha1ApiListReasonTypeRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ReasonTypeList> {\n            return localVarFp.listReasonType(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch ReasonType\n         * @param {ReasonTypeV1alpha1ApiPatchReasonTypeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReasonType(requestParameters: ReasonTypeV1alpha1ApiPatchReasonTypeRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReasonType> {\n            return localVarFp.patchReasonType(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update ReasonType\n         * @param {ReasonTypeV1alpha1ApiUpdateReasonTypeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReasonType(requestParameters: ReasonTypeV1alpha1ApiUpdateReasonTypeRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReasonType> {\n            return localVarFp.updateReasonType(requestParameters.name, requestParameters.reasonType, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createReasonType operation in ReasonTypeV1alpha1Api.\n */\nexport interface ReasonTypeV1alpha1ApiCreateReasonTypeRequest {\n    /**\n     * Fresh reasontype\n     */\n    readonly reasonType?: ReasonType\n}\n\n/**\n * Request parameters for deleteReasonType operation in ReasonTypeV1alpha1Api.\n */\nexport interface ReasonTypeV1alpha1ApiDeleteReasonTypeRequest {\n    /**\n     * Name of reasontype\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getReasonType operation in ReasonTypeV1alpha1Api.\n */\nexport interface ReasonTypeV1alpha1ApiGetReasonTypeRequest {\n    /**\n     * Name of reasontype\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listReasonType operation in ReasonTypeV1alpha1Api.\n */\nexport interface ReasonTypeV1alpha1ApiListReasonTypeRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchReasonType operation in ReasonTypeV1alpha1Api.\n */\nexport interface ReasonTypeV1alpha1ApiPatchReasonTypeRequest {\n    /**\n     * Name of reasontype\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateReasonType operation in ReasonTypeV1alpha1Api.\n */\nexport interface ReasonTypeV1alpha1ApiUpdateReasonTypeRequest {\n    /**\n     * Name of reasontype\n     */\n    readonly name: string\n\n    /**\n     * Updated reasontype\n     */\n    readonly reasonType?: ReasonType\n}\n\n/**\n * ReasonTypeV1alpha1Api - object-oriented interface\n */\nexport class ReasonTypeV1alpha1Api extends BaseAPI {\n    /**\n     * Create ReasonType\n     * @param {ReasonTypeV1alpha1ApiCreateReasonTypeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createReasonType(requestParameters: ReasonTypeV1alpha1ApiCreateReasonTypeRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReasonTypeV1alpha1ApiFp(this.configuration).createReasonType(requestParameters.reasonType, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete ReasonType\n     * @param {ReasonTypeV1alpha1ApiDeleteReasonTypeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteReasonType(requestParameters: ReasonTypeV1alpha1ApiDeleteReasonTypeRequest, options?: RawAxiosRequestConfig) {\n        return ReasonTypeV1alpha1ApiFp(this.configuration).deleteReasonType(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get ReasonType\n     * @param {ReasonTypeV1alpha1ApiGetReasonTypeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getReasonType(requestParameters: ReasonTypeV1alpha1ApiGetReasonTypeRequest, options?: RawAxiosRequestConfig) {\n        return ReasonTypeV1alpha1ApiFp(this.configuration).getReasonType(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List ReasonType\n     * @param {ReasonTypeV1alpha1ApiListReasonTypeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listReasonType(requestParameters: ReasonTypeV1alpha1ApiListReasonTypeRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReasonTypeV1alpha1ApiFp(this.configuration).listReasonType(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch ReasonType\n     * @param {ReasonTypeV1alpha1ApiPatchReasonTypeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchReasonType(requestParameters: ReasonTypeV1alpha1ApiPatchReasonTypeRequest, options?: RawAxiosRequestConfig) {\n        return ReasonTypeV1alpha1ApiFp(this.configuration).patchReasonType(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update ReasonType\n     * @param {ReasonTypeV1alpha1ApiUpdateReasonTypeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateReasonType(requestParameters: ReasonTypeV1alpha1ApiUpdateReasonTypeRequest, options?: RawAxiosRequestConfig) {\n        return ReasonTypeV1alpha1ApiFp(this.configuration).updateReasonType(requestParameters.name, requestParameters.reasonType, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/reason-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Reason } from '../models';\n// @ts-ignore\nimport type { ReasonList } from '../models';\n/**\n * ReasonV1alpha1Api - axios parameter creator\n */\nexport const ReasonV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Reason\n         * @param {Reason} [reason] Fresh reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReason: async (reason?: Reason, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasons`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reason, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Reason\n         * @param {string} name Name of reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReason: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteReason', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasons/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Reason\n         * @param {string} name Name of reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReason: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getReason', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasons/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Reason\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReason: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasons`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Reason\n         * @param {string} name Name of reason\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReason: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchReason', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasons/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Reason\n         * @param {string} name Name of reason\n         * @param {Reason} [reason] Updated reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReason: async (name: string, reason?: Reason, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateReason', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/reasons/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reason, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ReasonV1alpha1Api - functional programming interface\n */\nexport const ReasonV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ReasonV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Reason\n         * @param {Reason} [reason] Fresh reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createReason(reason?: Reason, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reason>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createReason(reason, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonV1alpha1Api.createReason']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Reason\n         * @param {string} name Name of reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteReason(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteReason(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonV1alpha1Api.deleteReason']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Reason\n         * @param {string} name Name of reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getReason(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reason>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getReason(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonV1alpha1Api.getReason']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Reason\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listReason(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReasonList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listReason(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonV1alpha1Api.listReason']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Reason\n         * @param {string} name Name of reason\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchReason(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reason>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchReason(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonV1alpha1Api.patchReason']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Reason\n         * @param {string} name Name of reason\n         * @param {Reason} [reason] Updated reason\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateReason(name: string, reason?: Reason, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reason>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateReason(name, reason, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReasonV1alpha1Api.updateReason']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ReasonV1alpha1Api - factory interface\n */\nexport const ReasonV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ReasonV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Reason\n         * @param {ReasonV1alpha1ApiCreateReasonRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReason(requestParameters: ReasonV1alpha1ApiCreateReasonRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Reason> {\n            return localVarFp.createReason(requestParameters.reason, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Reason\n         * @param {ReasonV1alpha1ApiDeleteReasonRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReason(requestParameters: ReasonV1alpha1ApiDeleteReasonRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteReason(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Reason\n         * @param {ReasonV1alpha1ApiGetReasonRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReason(requestParameters: ReasonV1alpha1ApiGetReasonRequest, options?: RawAxiosRequestConfig): AxiosPromise<Reason> {\n            return localVarFp.getReason(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Reason\n         * @param {ReasonV1alpha1ApiListReasonRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReason(requestParameters: ReasonV1alpha1ApiListReasonRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ReasonList> {\n            return localVarFp.listReason(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Reason\n         * @param {ReasonV1alpha1ApiPatchReasonRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReason(requestParameters: ReasonV1alpha1ApiPatchReasonRequest, options?: RawAxiosRequestConfig): AxiosPromise<Reason> {\n            return localVarFp.patchReason(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Reason\n         * @param {ReasonV1alpha1ApiUpdateReasonRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReason(requestParameters: ReasonV1alpha1ApiUpdateReasonRequest, options?: RawAxiosRequestConfig): AxiosPromise<Reason> {\n            return localVarFp.updateReason(requestParameters.name, requestParameters.reason, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createReason operation in ReasonV1alpha1Api.\n */\nexport interface ReasonV1alpha1ApiCreateReasonRequest {\n    /**\n     * Fresh reason\n     */\n    readonly reason?: Reason\n}\n\n/**\n * Request parameters for deleteReason operation in ReasonV1alpha1Api.\n */\nexport interface ReasonV1alpha1ApiDeleteReasonRequest {\n    /**\n     * Name of reason\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getReason operation in ReasonV1alpha1Api.\n */\nexport interface ReasonV1alpha1ApiGetReasonRequest {\n    /**\n     * Name of reason\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listReason operation in ReasonV1alpha1Api.\n */\nexport interface ReasonV1alpha1ApiListReasonRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchReason operation in ReasonV1alpha1Api.\n */\nexport interface ReasonV1alpha1ApiPatchReasonRequest {\n    /**\n     * Name of reason\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateReason operation in ReasonV1alpha1Api.\n */\nexport interface ReasonV1alpha1ApiUpdateReasonRequest {\n    /**\n     * Name of reason\n     */\n    readonly name: string\n\n    /**\n     * Updated reason\n     */\n    readonly reason?: Reason\n}\n\n/**\n * ReasonV1alpha1Api - object-oriented interface\n */\nexport class ReasonV1alpha1Api extends BaseAPI {\n    /**\n     * Create Reason\n     * @param {ReasonV1alpha1ApiCreateReasonRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createReason(requestParameters: ReasonV1alpha1ApiCreateReasonRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReasonV1alpha1ApiFp(this.configuration).createReason(requestParameters.reason, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Reason\n     * @param {ReasonV1alpha1ApiDeleteReasonRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteReason(requestParameters: ReasonV1alpha1ApiDeleteReasonRequest, options?: RawAxiosRequestConfig) {\n        return ReasonV1alpha1ApiFp(this.configuration).deleteReason(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Reason\n     * @param {ReasonV1alpha1ApiGetReasonRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getReason(requestParameters: ReasonV1alpha1ApiGetReasonRequest, options?: RawAxiosRequestConfig) {\n        return ReasonV1alpha1ApiFp(this.configuration).getReason(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Reason\n     * @param {ReasonV1alpha1ApiListReasonRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listReason(requestParameters: ReasonV1alpha1ApiListReasonRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReasonV1alpha1ApiFp(this.configuration).listReason(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Reason\n     * @param {ReasonV1alpha1ApiPatchReasonRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchReason(requestParameters: ReasonV1alpha1ApiPatchReasonRequest, options?: RawAxiosRequestConfig) {\n        return ReasonV1alpha1ApiFp(this.configuration).patchReason(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Reason\n     * @param {ReasonV1alpha1ApiUpdateReasonRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateReason(requestParameters: ReasonV1alpha1ApiUpdateReasonRequest, options?: RawAxiosRequestConfig) {\n        return ReasonV1alpha1ApiFp(this.configuration).updateReason(requestParameters.name, requestParameters.reason, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/remember-me-token-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { RememberMeToken } from '../models';\n// @ts-ignore\nimport type { RememberMeTokenList } from '../models';\n/**\n * RememberMeTokenV1alpha1Api - axios parameter creator\n */\nexport const RememberMeTokenV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create RememberMeToken\n         * @param {RememberMeToken} [rememberMeToken] Fresh remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createRememberMeToken: async (rememberMeToken?: RememberMeToken, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/security.halo.run/v1alpha1/remembermetokens`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(rememberMeToken, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteRememberMeToken: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteRememberMeToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/remembermetokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRememberMeToken: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getRememberMeToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/remembermetokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List RememberMeToken\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listRememberMeToken: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/security.halo.run/v1alpha1/remembermetokens`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchRememberMeToken: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchRememberMeToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/remembermetokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {RememberMeToken} [rememberMeToken] Updated remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateRememberMeToken: async (name: string, rememberMeToken?: RememberMeToken, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateRememberMeToken', 'name', name)\n            const localVarPath = `/apis/security.halo.run/v1alpha1/remembermetokens/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(rememberMeToken, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * RememberMeTokenV1alpha1Api - functional programming interface\n */\nexport const RememberMeTokenV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = RememberMeTokenV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create RememberMeToken\n         * @param {RememberMeToken} [rememberMeToken] Fresh remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createRememberMeToken(rememberMeToken?: RememberMeToken, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RememberMeToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createRememberMeToken(rememberMeToken, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RememberMeTokenV1alpha1Api.createRememberMeToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteRememberMeToken(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteRememberMeToken(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RememberMeTokenV1alpha1Api.deleteRememberMeToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getRememberMeToken(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RememberMeToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getRememberMeToken(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RememberMeTokenV1alpha1Api.getRememberMeToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List RememberMeToken\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listRememberMeToken(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RememberMeTokenList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listRememberMeToken(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RememberMeTokenV1alpha1Api.listRememberMeToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchRememberMeToken(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RememberMeToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchRememberMeToken(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RememberMeTokenV1alpha1Api.patchRememberMeToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update RememberMeToken\n         * @param {string} name Name of remembermetoken\n         * @param {RememberMeToken} [rememberMeToken] Updated remembermetoken\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateRememberMeToken(name: string, rememberMeToken?: RememberMeToken, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RememberMeToken>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateRememberMeToken(name, rememberMeToken, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RememberMeTokenV1alpha1Api.updateRememberMeToken']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * RememberMeTokenV1alpha1Api - factory interface\n */\nexport const RememberMeTokenV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = RememberMeTokenV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create RememberMeToken\n         * @param {RememberMeTokenV1alpha1ApiCreateRememberMeTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiCreateRememberMeTokenRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<RememberMeToken> {\n            return localVarFp.createRememberMeToken(requestParameters.rememberMeToken, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete RememberMeToken\n         * @param {RememberMeTokenV1alpha1ApiDeleteRememberMeTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiDeleteRememberMeTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteRememberMeToken(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get RememberMeToken\n         * @param {RememberMeTokenV1alpha1ApiGetRememberMeTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiGetRememberMeTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<RememberMeToken> {\n            return localVarFp.getRememberMeToken(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List RememberMeToken\n         * @param {RememberMeTokenV1alpha1ApiListRememberMeTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiListRememberMeTokenRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<RememberMeTokenList> {\n            return localVarFp.listRememberMeToken(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch RememberMeToken\n         * @param {RememberMeTokenV1alpha1ApiPatchRememberMeTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiPatchRememberMeTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<RememberMeToken> {\n            return localVarFp.patchRememberMeToken(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update RememberMeToken\n         * @param {RememberMeTokenV1alpha1ApiUpdateRememberMeTokenRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiUpdateRememberMeTokenRequest, options?: RawAxiosRequestConfig): AxiosPromise<RememberMeToken> {\n            return localVarFp.updateRememberMeToken(requestParameters.name, requestParameters.rememberMeToken, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createRememberMeToken operation in RememberMeTokenV1alpha1Api.\n */\nexport interface RememberMeTokenV1alpha1ApiCreateRememberMeTokenRequest {\n    /**\n     * Fresh remembermetoken\n     */\n    readonly rememberMeToken?: RememberMeToken\n}\n\n/**\n * Request parameters for deleteRememberMeToken operation in RememberMeTokenV1alpha1Api.\n */\nexport interface RememberMeTokenV1alpha1ApiDeleteRememberMeTokenRequest {\n    /**\n     * Name of remembermetoken\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getRememberMeToken operation in RememberMeTokenV1alpha1Api.\n */\nexport interface RememberMeTokenV1alpha1ApiGetRememberMeTokenRequest {\n    /**\n     * Name of remembermetoken\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listRememberMeToken operation in RememberMeTokenV1alpha1Api.\n */\nexport interface RememberMeTokenV1alpha1ApiListRememberMeTokenRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchRememberMeToken operation in RememberMeTokenV1alpha1Api.\n */\nexport interface RememberMeTokenV1alpha1ApiPatchRememberMeTokenRequest {\n    /**\n     * Name of remembermetoken\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateRememberMeToken operation in RememberMeTokenV1alpha1Api.\n */\nexport interface RememberMeTokenV1alpha1ApiUpdateRememberMeTokenRequest {\n    /**\n     * Name of remembermetoken\n     */\n    readonly name: string\n\n    /**\n     * Updated remembermetoken\n     */\n    readonly rememberMeToken?: RememberMeToken\n}\n\n/**\n * RememberMeTokenV1alpha1Api - object-oriented interface\n */\nexport class RememberMeTokenV1alpha1Api extends BaseAPI {\n    /**\n     * Create RememberMeToken\n     * @param {RememberMeTokenV1alpha1ApiCreateRememberMeTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiCreateRememberMeTokenRequest = {}, options?: RawAxiosRequestConfig) {\n        return RememberMeTokenV1alpha1ApiFp(this.configuration).createRememberMeToken(requestParameters.rememberMeToken, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete RememberMeToken\n     * @param {RememberMeTokenV1alpha1ApiDeleteRememberMeTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiDeleteRememberMeTokenRequest, options?: RawAxiosRequestConfig) {\n        return RememberMeTokenV1alpha1ApiFp(this.configuration).deleteRememberMeToken(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get RememberMeToken\n     * @param {RememberMeTokenV1alpha1ApiGetRememberMeTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiGetRememberMeTokenRequest, options?: RawAxiosRequestConfig) {\n        return RememberMeTokenV1alpha1ApiFp(this.configuration).getRememberMeToken(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List RememberMeToken\n     * @param {RememberMeTokenV1alpha1ApiListRememberMeTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiListRememberMeTokenRequest = {}, options?: RawAxiosRequestConfig) {\n        return RememberMeTokenV1alpha1ApiFp(this.configuration).listRememberMeToken(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch RememberMeToken\n     * @param {RememberMeTokenV1alpha1ApiPatchRememberMeTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiPatchRememberMeTokenRequest, options?: RawAxiosRequestConfig) {\n        return RememberMeTokenV1alpha1ApiFp(this.configuration).patchRememberMeToken(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update RememberMeToken\n     * @param {RememberMeTokenV1alpha1ApiUpdateRememberMeTokenRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateRememberMeToken(requestParameters: RememberMeTokenV1alpha1ApiUpdateRememberMeTokenRequest, options?: RawAxiosRequestConfig) {\n        return RememberMeTokenV1alpha1ApiFp(this.configuration).updateRememberMeToken(requestParameters.name, requestParameters.rememberMeToken, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/reply-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Reply } from '../models';\n// @ts-ignore\nimport type { ReplyList } from '../models';\n/**\n * ReplyV1alpha1Api - axios parameter creator\n */\nexport const ReplyV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Reply\n         * @param {Reply} [reply] Fresh reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReply: async (reply?: Reply, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/replies`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reply, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Reply\n         * @param {string} name Name of reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReply: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteReply', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/replies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Reply\n         * @param {string} name Name of reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReply: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getReply', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/replies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Reply\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReply: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/replies`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Reply\n         * @param {string} name Name of reply\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReply: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchReply', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/replies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Reply\n         * @param {string} name Name of reply\n         * @param {Reply} [reply] Updated reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReply: async (name: string, reply?: Reply, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateReply', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/replies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reply, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ReplyV1alpha1Api - functional programming interface\n */\nexport const ReplyV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ReplyV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Reply\n         * @param {Reply} [reply] Fresh reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createReply(reply?: Reply, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reply>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createReply(reply, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReplyV1alpha1Api.createReply']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Reply\n         * @param {string} name Name of reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteReply(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteReply(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReplyV1alpha1Api.deleteReply']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Reply\n         * @param {string} name Name of reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getReply(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reply>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getReply(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReplyV1alpha1Api.getReply']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Reply\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listReply(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReplyList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listReply(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReplyV1alpha1Api.listReply']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Reply\n         * @param {string} name Name of reply\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchReply(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reply>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchReply(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReplyV1alpha1Api.patchReply']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Reply\n         * @param {string} name Name of reply\n         * @param {Reply} [reply] Updated reply\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateReply(name: string, reply?: Reply, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Reply>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateReply(name, reply, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReplyV1alpha1Api.updateReply']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ReplyV1alpha1Api - factory interface\n */\nexport const ReplyV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ReplyV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Reply\n         * @param {ReplyV1alpha1ApiCreateReplyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReply(requestParameters: ReplyV1alpha1ApiCreateReplyRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Reply> {\n            return localVarFp.createReply(requestParameters.reply, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Reply\n         * @param {ReplyV1alpha1ApiDeleteReplyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReply(requestParameters: ReplyV1alpha1ApiDeleteReplyRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteReply(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Reply\n         * @param {ReplyV1alpha1ApiGetReplyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReply(requestParameters: ReplyV1alpha1ApiGetReplyRequest, options?: RawAxiosRequestConfig): AxiosPromise<Reply> {\n            return localVarFp.getReply(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Reply\n         * @param {ReplyV1alpha1ApiListReplyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReply(requestParameters: ReplyV1alpha1ApiListReplyRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ReplyList> {\n            return localVarFp.listReply(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Reply\n         * @param {ReplyV1alpha1ApiPatchReplyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReply(requestParameters: ReplyV1alpha1ApiPatchReplyRequest, options?: RawAxiosRequestConfig): AxiosPromise<Reply> {\n            return localVarFp.patchReply(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Reply\n         * @param {ReplyV1alpha1ApiUpdateReplyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReply(requestParameters: ReplyV1alpha1ApiUpdateReplyRequest, options?: RawAxiosRequestConfig): AxiosPromise<Reply> {\n            return localVarFp.updateReply(requestParameters.name, requestParameters.reply, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createReply operation in ReplyV1alpha1Api.\n */\nexport interface ReplyV1alpha1ApiCreateReplyRequest {\n    /**\n     * Fresh reply\n     */\n    readonly reply?: Reply\n}\n\n/**\n * Request parameters for deleteReply operation in ReplyV1alpha1Api.\n */\nexport interface ReplyV1alpha1ApiDeleteReplyRequest {\n    /**\n     * Name of reply\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getReply operation in ReplyV1alpha1Api.\n */\nexport interface ReplyV1alpha1ApiGetReplyRequest {\n    /**\n     * Name of reply\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listReply operation in ReplyV1alpha1Api.\n */\nexport interface ReplyV1alpha1ApiListReplyRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchReply operation in ReplyV1alpha1Api.\n */\nexport interface ReplyV1alpha1ApiPatchReplyRequest {\n    /**\n     * Name of reply\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateReply operation in ReplyV1alpha1Api.\n */\nexport interface ReplyV1alpha1ApiUpdateReplyRequest {\n    /**\n     * Name of reply\n     */\n    readonly name: string\n\n    /**\n     * Updated reply\n     */\n    readonly reply?: Reply\n}\n\n/**\n * ReplyV1alpha1Api - object-oriented interface\n */\nexport class ReplyV1alpha1Api extends BaseAPI {\n    /**\n     * Create Reply\n     * @param {ReplyV1alpha1ApiCreateReplyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createReply(requestParameters: ReplyV1alpha1ApiCreateReplyRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReplyV1alpha1ApiFp(this.configuration).createReply(requestParameters.reply, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Reply\n     * @param {ReplyV1alpha1ApiDeleteReplyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteReply(requestParameters: ReplyV1alpha1ApiDeleteReplyRequest, options?: RawAxiosRequestConfig) {\n        return ReplyV1alpha1ApiFp(this.configuration).deleteReply(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Reply\n     * @param {ReplyV1alpha1ApiGetReplyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getReply(requestParameters: ReplyV1alpha1ApiGetReplyRequest, options?: RawAxiosRequestConfig) {\n        return ReplyV1alpha1ApiFp(this.configuration).getReply(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Reply\n     * @param {ReplyV1alpha1ApiListReplyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listReply(requestParameters: ReplyV1alpha1ApiListReplyRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReplyV1alpha1ApiFp(this.configuration).listReply(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Reply\n     * @param {ReplyV1alpha1ApiPatchReplyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchReply(requestParameters: ReplyV1alpha1ApiPatchReplyRequest, options?: RawAxiosRequestConfig) {\n        return ReplyV1alpha1ApiFp(this.configuration).patchReply(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Reply\n     * @param {ReplyV1alpha1ApiUpdateReplyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateReply(requestParameters: ReplyV1alpha1ApiUpdateReplyRequest, options?: RawAxiosRequestConfig) {\n        return ReplyV1alpha1ApiFp(this.configuration).updateReply(requestParameters.name, requestParameters.reply, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/reply-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ListedReplyList } from '../models';\n/**\n * ReplyV1alpha1ConsoleApi - axios parameter creator\n */\nexport const ReplyV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * List replies.\n         * @param {string} commentName Replies filtered by commentName.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReplies: async (commentName: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'commentName' is not null or undefined\n            assertParamExists('listReplies', 'commentName', commentName)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/replies`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (commentName !== undefined) {\n                localVarQueryParameter['commentName'] = commentName;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ReplyV1alpha1ConsoleApi - functional programming interface\n */\nexport const ReplyV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ReplyV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * List replies.\n         * @param {string} commentName Replies filtered by commentName.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listReplies(commentName: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedReplyList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listReplies(commentName, page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReplyV1alpha1ConsoleApi.listReplies']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ReplyV1alpha1ConsoleApi - factory interface\n */\nexport const ReplyV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ReplyV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * List replies.\n         * @param {ReplyV1alpha1ConsoleApiListRepliesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReplies(requestParameters: ReplyV1alpha1ConsoleApiListRepliesRequest, options?: RawAxiosRequestConfig): AxiosPromise<ListedReplyList> {\n            return localVarFp.listReplies(requestParameters.commentName, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for listReplies operation in ReplyV1alpha1ConsoleApi.\n */\nexport interface ReplyV1alpha1ConsoleApiListRepliesRequest {\n    /**\n     * Replies filtered by commentName.\n     */\n    readonly commentName: string\n\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * ReplyV1alpha1ConsoleApi - object-oriented interface\n */\nexport class ReplyV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * List replies.\n     * @param {ReplyV1alpha1ConsoleApiListRepliesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listReplies(requestParameters: ReplyV1alpha1ConsoleApiListRepliesRequest, options?: RawAxiosRequestConfig) {\n        return ReplyV1alpha1ConsoleApiFp(this.configuration).listReplies(requestParameters.commentName, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/reverse-proxy-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { ReverseProxy } from '../models';\n// @ts-ignore\nimport type { ReverseProxyList } from '../models';\n/**\n * ReverseProxyV1alpha1Api - axios parameter creator\n */\nexport const ReverseProxyV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create ReverseProxy\n         * @param {ReverseProxy} [reverseProxy] Fresh reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReverseProxy: async (reverseProxy?: ReverseProxy, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/reverseproxies`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reverseProxy, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReverseProxy: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteReverseProxy', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/reverseproxies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReverseProxy: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getReverseProxy', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/reverseproxies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List ReverseProxy\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReverseProxy: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/reverseproxies`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReverseProxy: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchReverseProxy', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/reverseproxies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {ReverseProxy} [reverseProxy] Updated reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReverseProxy: async (name: string, reverseProxy?: ReverseProxy, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateReverseProxy', 'name', name)\n            const localVarPath = `/apis/plugin.halo.run/v1alpha1/reverseproxies/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(reverseProxy, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ReverseProxyV1alpha1Api - functional programming interface\n */\nexport const ReverseProxyV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ReverseProxyV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create ReverseProxy\n         * @param {ReverseProxy} [reverseProxy] Fresh reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createReverseProxy(reverseProxy?: ReverseProxy, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReverseProxy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createReverseProxy(reverseProxy, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReverseProxyV1alpha1Api.createReverseProxy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteReverseProxy(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteReverseProxy(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReverseProxyV1alpha1Api.deleteReverseProxy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getReverseProxy(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReverseProxy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getReverseProxy(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReverseProxyV1alpha1Api.getReverseProxy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List ReverseProxy\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listReverseProxy(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReverseProxyList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listReverseProxy(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReverseProxyV1alpha1Api.listReverseProxy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchReverseProxy(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReverseProxy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchReverseProxy(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReverseProxyV1alpha1Api.patchReverseProxy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update ReverseProxy\n         * @param {string} name Name of reverseproxy\n         * @param {ReverseProxy} [reverseProxy] Updated reverseproxy\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateReverseProxy(name: string, reverseProxy?: ReverseProxy, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ReverseProxy>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateReverseProxy(name, reverseProxy, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ReverseProxyV1alpha1Api.updateReverseProxy']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ReverseProxyV1alpha1Api - factory interface\n */\nexport const ReverseProxyV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ReverseProxyV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create ReverseProxy\n         * @param {ReverseProxyV1alpha1ApiCreateReverseProxyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createReverseProxy(requestParameters: ReverseProxyV1alpha1ApiCreateReverseProxyRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ReverseProxy> {\n            return localVarFp.createReverseProxy(requestParameters.reverseProxy, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete ReverseProxy\n         * @param {ReverseProxyV1alpha1ApiDeleteReverseProxyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteReverseProxy(requestParameters: ReverseProxyV1alpha1ApiDeleteReverseProxyRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteReverseProxy(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get ReverseProxy\n         * @param {ReverseProxyV1alpha1ApiGetReverseProxyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReverseProxy(requestParameters: ReverseProxyV1alpha1ApiGetReverseProxyRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReverseProxy> {\n            return localVarFp.getReverseProxy(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List ReverseProxy\n         * @param {ReverseProxyV1alpha1ApiListReverseProxyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listReverseProxy(requestParameters: ReverseProxyV1alpha1ApiListReverseProxyRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ReverseProxyList> {\n            return localVarFp.listReverseProxy(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch ReverseProxy\n         * @param {ReverseProxyV1alpha1ApiPatchReverseProxyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchReverseProxy(requestParameters: ReverseProxyV1alpha1ApiPatchReverseProxyRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReverseProxy> {\n            return localVarFp.patchReverseProxy(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update ReverseProxy\n         * @param {ReverseProxyV1alpha1ApiUpdateReverseProxyRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateReverseProxy(requestParameters: ReverseProxyV1alpha1ApiUpdateReverseProxyRequest, options?: RawAxiosRequestConfig): AxiosPromise<ReverseProxy> {\n            return localVarFp.updateReverseProxy(requestParameters.name, requestParameters.reverseProxy, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createReverseProxy operation in ReverseProxyV1alpha1Api.\n */\nexport interface ReverseProxyV1alpha1ApiCreateReverseProxyRequest {\n    /**\n     * Fresh reverseproxy\n     */\n    readonly reverseProxy?: ReverseProxy\n}\n\n/**\n * Request parameters for deleteReverseProxy operation in ReverseProxyV1alpha1Api.\n */\nexport interface ReverseProxyV1alpha1ApiDeleteReverseProxyRequest {\n    /**\n     * Name of reverseproxy\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getReverseProxy operation in ReverseProxyV1alpha1Api.\n */\nexport interface ReverseProxyV1alpha1ApiGetReverseProxyRequest {\n    /**\n     * Name of reverseproxy\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listReverseProxy operation in ReverseProxyV1alpha1Api.\n */\nexport interface ReverseProxyV1alpha1ApiListReverseProxyRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchReverseProxy operation in ReverseProxyV1alpha1Api.\n */\nexport interface ReverseProxyV1alpha1ApiPatchReverseProxyRequest {\n    /**\n     * Name of reverseproxy\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateReverseProxy operation in ReverseProxyV1alpha1Api.\n */\nexport interface ReverseProxyV1alpha1ApiUpdateReverseProxyRequest {\n    /**\n     * Name of reverseproxy\n     */\n    readonly name: string\n\n    /**\n     * Updated reverseproxy\n     */\n    readonly reverseProxy?: ReverseProxy\n}\n\n/**\n * ReverseProxyV1alpha1Api - object-oriented interface\n */\nexport class ReverseProxyV1alpha1Api extends BaseAPI {\n    /**\n     * Create ReverseProxy\n     * @param {ReverseProxyV1alpha1ApiCreateReverseProxyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createReverseProxy(requestParameters: ReverseProxyV1alpha1ApiCreateReverseProxyRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReverseProxyV1alpha1ApiFp(this.configuration).createReverseProxy(requestParameters.reverseProxy, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete ReverseProxy\n     * @param {ReverseProxyV1alpha1ApiDeleteReverseProxyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteReverseProxy(requestParameters: ReverseProxyV1alpha1ApiDeleteReverseProxyRequest, options?: RawAxiosRequestConfig) {\n        return ReverseProxyV1alpha1ApiFp(this.configuration).deleteReverseProxy(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get ReverseProxy\n     * @param {ReverseProxyV1alpha1ApiGetReverseProxyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getReverseProxy(requestParameters: ReverseProxyV1alpha1ApiGetReverseProxyRequest, options?: RawAxiosRequestConfig) {\n        return ReverseProxyV1alpha1ApiFp(this.configuration).getReverseProxy(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List ReverseProxy\n     * @param {ReverseProxyV1alpha1ApiListReverseProxyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listReverseProxy(requestParameters: ReverseProxyV1alpha1ApiListReverseProxyRequest = {}, options?: RawAxiosRequestConfig) {\n        return ReverseProxyV1alpha1ApiFp(this.configuration).listReverseProxy(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch ReverseProxy\n     * @param {ReverseProxyV1alpha1ApiPatchReverseProxyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchReverseProxy(requestParameters: ReverseProxyV1alpha1ApiPatchReverseProxyRequest, options?: RawAxiosRequestConfig) {\n        return ReverseProxyV1alpha1ApiFp(this.configuration).patchReverseProxy(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update ReverseProxy\n     * @param {ReverseProxyV1alpha1ApiUpdateReverseProxyRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateReverseProxy(requestParameters: ReverseProxyV1alpha1ApiUpdateReverseProxyRequest, options?: RawAxiosRequestConfig) {\n        return ReverseProxyV1alpha1ApiFp(this.configuration).updateReverseProxy(requestParameters.name, requestParameters.reverseProxy, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/role-binding-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { RoleBinding } from '../models';\n// @ts-ignore\nimport type { RoleBindingList } from '../models';\n/**\n * RoleBindingV1alpha1Api - axios parameter creator\n */\nexport const RoleBindingV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create RoleBinding\n         * @param {RoleBinding} [roleBinding] Fresh rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createRoleBinding: async (roleBinding?: RoleBinding, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/rolebindings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(roleBinding, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteRoleBinding: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteRoleBinding', 'name', name)\n            const localVarPath = `/api/v1alpha1/rolebindings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRoleBinding: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getRoleBinding', 'name', name)\n            const localVarPath = `/api/v1alpha1/rolebindings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List RoleBinding\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listRoleBinding: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/rolebindings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchRoleBinding: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchRoleBinding', 'name', name)\n            const localVarPath = `/api/v1alpha1/rolebindings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {RoleBinding} [roleBinding] Updated rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateRoleBinding: async (name: string, roleBinding?: RoleBinding, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateRoleBinding', 'name', name)\n            const localVarPath = `/api/v1alpha1/rolebindings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(roleBinding, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * RoleBindingV1alpha1Api - functional programming interface\n */\nexport const RoleBindingV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = RoleBindingV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create RoleBinding\n         * @param {RoleBinding} [roleBinding] Fresh rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createRoleBinding(roleBinding?: RoleBinding, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoleBinding>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createRoleBinding(roleBinding, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleBindingV1alpha1Api.createRoleBinding']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteRoleBinding(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteRoleBinding(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleBindingV1alpha1Api.deleteRoleBinding']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getRoleBinding(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoleBinding>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getRoleBinding(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleBindingV1alpha1Api.getRoleBinding']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List RoleBinding\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listRoleBinding(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoleBindingList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listRoleBinding(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleBindingV1alpha1Api.listRoleBinding']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchRoleBinding(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoleBinding>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchRoleBinding(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleBindingV1alpha1Api.patchRoleBinding']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update RoleBinding\n         * @param {string} name Name of rolebinding\n         * @param {RoleBinding} [roleBinding] Updated rolebinding\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateRoleBinding(name: string, roleBinding?: RoleBinding, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoleBinding>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateRoleBinding(name, roleBinding, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleBindingV1alpha1Api.updateRoleBinding']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * RoleBindingV1alpha1Api - factory interface\n */\nexport const RoleBindingV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = RoleBindingV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create RoleBinding\n         * @param {RoleBindingV1alpha1ApiCreateRoleBindingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createRoleBinding(requestParameters: RoleBindingV1alpha1ApiCreateRoleBindingRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<RoleBinding> {\n            return localVarFp.createRoleBinding(requestParameters.roleBinding, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete RoleBinding\n         * @param {RoleBindingV1alpha1ApiDeleteRoleBindingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteRoleBinding(requestParameters: RoleBindingV1alpha1ApiDeleteRoleBindingRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteRoleBinding(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get RoleBinding\n         * @param {RoleBindingV1alpha1ApiGetRoleBindingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRoleBinding(requestParameters: RoleBindingV1alpha1ApiGetRoleBindingRequest, options?: RawAxiosRequestConfig): AxiosPromise<RoleBinding> {\n            return localVarFp.getRoleBinding(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List RoleBinding\n         * @param {RoleBindingV1alpha1ApiListRoleBindingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listRoleBinding(requestParameters: RoleBindingV1alpha1ApiListRoleBindingRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<RoleBindingList> {\n            return localVarFp.listRoleBinding(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch RoleBinding\n         * @param {RoleBindingV1alpha1ApiPatchRoleBindingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchRoleBinding(requestParameters: RoleBindingV1alpha1ApiPatchRoleBindingRequest, options?: RawAxiosRequestConfig): AxiosPromise<RoleBinding> {\n            return localVarFp.patchRoleBinding(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update RoleBinding\n         * @param {RoleBindingV1alpha1ApiUpdateRoleBindingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateRoleBinding(requestParameters: RoleBindingV1alpha1ApiUpdateRoleBindingRequest, options?: RawAxiosRequestConfig): AxiosPromise<RoleBinding> {\n            return localVarFp.updateRoleBinding(requestParameters.name, requestParameters.roleBinding, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createRoleBinding operation in RoleBindingV1alpha1Api.\n */\nexport interface RoleBindingV1alpha1ApiCreateRoleBindingRequest {\n    /**\n     * Fresh rolebinding\n     */\n    readonly roleBinding?: RoleBinding\n}\n\n/**\n * Request parameters for deleteRoleBinding operation in RoleBindingV1alpha1Api.\n */\nexport interface RoleBindingV1alpha1ApiDeleteRoleBindingRequest {\n    /**\n     * Name of rolebinding\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getRoleBinding operation in RoleBindingV1alpha1Api.\n */\nexport interface RoleBindingV1alpha1ApiGetRoleBindingRequest {\n    /**\n     * Name of rolebinding\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listRoleBinding operation in RoleBindingV1alpha1Api.\n */\nexport interface RoleBindingV1alpha1ApiListRoleBindingRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchRoleBinding operation in RoleBindingV1alpha1Api.\n */\nexport interface RoleBindingV1alpha1ApiPatchRoleBindingRequest {\n    /**\n     * Name of rolebinding\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateRoleBinding operation in RoleBindingV1alpha1Api.\n */\nexport interface RoleBindingV1alpha1ApiUpdateRoleBindingRequest {\n    /**\n     * Name of rolebinding\n     */\n    readonly name: string\n\n    /**\n     * Updated rolebinding\n     */\n    readonly roleBinding?: RoleBinding\n}\n\n/**\n * RoleBindingV1alpha1Api - object-oriented interface\n */\nexport class RoleBindingV1alpha1Api extends BaseAPI {\n    /**\n     * Create RoleBinding\n     * @param {RoleBindingV1alpha1ApiCreateRoleBindingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createRoleBinding(requestParameters: RoleBindingV1alpha1ApiCreateRoleBindingRequest = {}, options?: RawAxiosRequestConfig) {\n        return RoleBindingV1alpha1ApiFp(this.configuration).createRoleBinding(requestParameters.roleBinding, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete RoleBinding\n     * @param {RoleBindingV1alpha1ApiDeleteRoleBindingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteRoleBinding(requestParameters: RoleBindingV1alpha1ApiDeleteRoleBindingRequest, options?: RawAxiosRequestConfig) {\n        return RoleBindingV1alpha1ApiFp(this.configuration).deleteRoleBinding(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get RoleBinding\n     * @param {RoleBindingV1alpha1ApiGetRoleBindingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getRoleBinding(requestParameters: RoleBindingV1alpha1ApiGetRoleBindingRequest, options?: RawAxiosRequestConfig) {\n        return RoleBindingV1alpha1ApiFp(this.configuration).getRoleBinding(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List RoleBinding\n     * @param {RoleBindingV1alpha1ApiListRoleBindingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listRoleBinding(requestParameters: RoleBindingV1alpha1ApiListRoleBindingRequest = {}, options?: RawAxiosRequestConfig) {\n        return RoleBindingV1alpha1ApiFp(this.configuration).listRoleBinding(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch RoleBinding\n     * @param {RoleBindingV1alpha1ApiPatchRoleBindingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchRoleBinding(requestParameters: RoleBindingV1alpha1ApiPatchRoleBindingRequest, options?: RawAxiosRequestConfig) {\n        return RoleBindingV1alpha1ApiFp(this.configuration).patchRoleBinding(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update RoleBinding\n     * @param {RoleBindingV1alpha1ApiUpdateRoleBindingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateRoleBinding(requestParameters: RoleBindingV1alpha1ApiUpdateRoleBindingRequest, options?: RawAxiosRequestConfig) {\n        return RoleBindingV1alpha1ApiFp(this.configuration).updateRoleBinding(requestParameters.name, requestParameters.roleBinding, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/role-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Role } from '../models';\n// @ts-ignore\nimport type { RoleList } from '../models';\n/**\n * RoleV1alpha1Api - axios parameter creator\n */\nexport const RoleV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Role\n         * @param {Role} [role] Fresh role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createRole: async (role?: Role, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/roles`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(role, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Role\n         * @param {string} name Name of role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteRole: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteRole', 'name', name)\n            const localVarPath = `/api/v1alpha1/roles/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Role\n         * @param {string} name Name of role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRole: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getRole', 'name', name)\n            const localVarPath = `/api/v1alpha1/roles/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Role\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listRole: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/roles`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Role\n         * @param {string} name Name of role\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchRole: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchRole', 'name', name)\n            const localVarPath = `/api/v1alpha1/roles/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Role\n         * @param {string} name Name of role\n         * @param {Role} [role] Updated role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateRole: async (name: string, role?: Role, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateRole', 'name', name)\n            const localVarPath = `/api/v1alpha1/roles/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(role, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * RoleV1alpha1Api - functional programming interface\n */\nexport const RoleV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = RoleV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Role\n         * @param {Role} [role] Fresh role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createRole(role?: Role, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Role>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createRole(role, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleV1alpha1Api.createRole']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Role\n         * @param {string} name Name of role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteRole(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteRole(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleV1alpha1Api.deleteRole']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Role\n         * @param {string} name Name of role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getRole(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Role>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getRole(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleV1alpha1Api.getRole']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Role\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listRole(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoleList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listRole(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleV1alpha1Api.listRole']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Role\n         * @param {string} name Name of role\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchRole(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Role>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchRole(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleV1alpha1Api.patchRole']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Role\n         * @param {string} name Name of role\n         * @param {Role} [role] Updated role\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateRole(name: string, role?: Role, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Role>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateRole(name, role, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['RoleV1alpha1Api.updateRole']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * RoleV1alpha1Api - factory interface\n */\nexport const RoleV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = RoleV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Role\n         * @param {RoleV1alpha1ApiCreateRoleRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createRole(requestParameters: RoleV1alpha1ApiCreateRoleRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Role> {\n            return localVarFp.createRole(requestParameters.role, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Role\n         * @param {RoleV1alpha1ApiDeleteRoleRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteRole(requestParameters: RoleV1alpha1ApiDeleteRoleRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteRole(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Role\n         * @param {RoleV1alpha1ApiGetRoleRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRole(requestParameters: RoleV1alpha1ApiGetRoleRequest, options?: RawAxiosRequestConfig): AxiosPromise<Role> {\n            return localVarFp.getRole(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Role\n         * @param {RoleV1alpha1ApiListRoleRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listRole(requestParameters: RoleV1alpha1ApiListRoleRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<RoleList> {\n            return localVarFp.listRole(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Role\n         * @param {RoleV1alpha1ApiPatchRoleRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchRole(requestParameters: RoleV1alpha1ApiPatchRoleRequest, options?: RawAxiosRequestConfig): AxiosPromise<Role> {\n            return localVarFp.patchRole(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Role\n         * @param {RoleV1alpha1ApiUpdateRoleRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateRole(requestParameters: RoleV1alpha1ApiUpdateRoleRequest, options?: RawAxiosRequestConfig): AxiosPromise<Role> {\n            return localVarFp.updateRole(requestParameters.name, requestParameters.role, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createRole operation in RoleV1alpha1Api.\n */\nexport interface RoleV1alpha1ApiCreateRoleRequest {\n    /**\n     * Fresh role\n     */\n    readonly role?: Role\n}\n\n/**\n * Request parameters for deleteRole operation in RoleV1alpha1Api.\n */\nexport interface RoleV1alpha1ApiDeleteRoleRequest {\n    /**\n     * Name of role\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getRole operation in RoleV1alpha1Api.\n */\nexport interface RoleV1alpha1ApiGetRoleRequest {\n    /**\n     * Name of role\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listRole operation in RoleV1alpha1Api.\n */\nexport interface RoleV1alpha1ApiListRoleRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchRole operation in RoleV1alpha1Api.\n */\nexport interface RoleV1alpha1ApiPatchRoleRequest {\n    /**\n     * Name of role\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateRole operation in RoleV1alpha1Api.\n */\nexport interface RoleV1alpha1ApiUpdateRoleRequest {\n    /**\n     * Name of role\n     */\n    readonly name: string\n\n    /**\n     * Updated role\n     */\n    readonly role?: Role\n}\n\n/**\n * RoleV1alpha1Api - object-oriented interface\n */\nexport class RoleV1alpha1Api extends BaseAPI {\n    /**\n     * Create Role\n     * @param {RoleV1alpha1ApiCreateRoleRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createRole(requestParameters: RoleV1alpha1ApiCreateRoleRequest = {}, options?: RawAxiosRequestConfig) {\n        return RoleV1alpha1ApiFp(this.configuration).createRole(requestParameters.role, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Role\n     * @param {RoleV1alpha1ApiDeleteRoleRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteRole(requestParameters: RoleV1alpha1ApiDeleteRoleRequest, options?: RawAxiosRequestConfig) {\n        return RoleV1alpha1ApiFp(this.configuration).deleteRole(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Role\n     * @param {RoleV1alpha1ApiGetRoleRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getRole(requestParameters: RoleV1alpha1ApiGetRoleRequest, options?: RawAxiosRequestConfig) {\n        return RoleV1alpha1ApiFp(this.configuration).getRole(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Role\n     * @param {RoleV1alpha1ApiListRoleRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listRole(requestParameters: RoleV1alpha1ApiListRoleRequest = {}, options?: RawAxiosRequestConfig) {\n        return RoleV1alpha1ApiFp(this.configuration).listRole(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Role\n     * @param {RoleV1alpha1ApiPatchRoleRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchRole(requestParameters: RoleV1alpha1ApiPatchRoleRequest, options?: RawAxiosRequestConfig) {\n        return RoleV1alpha1ApiFp(this.configuration).patchRole(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Role\n     * @param {RoleV1alpha1ApiUpdateRoleRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateRole(requestParameters: RoleV1alpha1ApiUpdateRoleRequest, options?: RawAxiosRequestConfig) {\n        return RoleV1alpha1ApiFp(this.configuration).updateRole(requestParameters.name, requestParameters.role, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/secret-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Secret } from '../models';\n// @ts-ignore\nimport type { SecretList } from '../models';\n/**\n * SecretV1alpha1Api - axios parameter creator\n */\nexport const SecretV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Secret\n         * @param {Secret} [secret] Fresh secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSecret: async (secret?: Secret, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/secrets`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(secret, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Secret\n         * @param {string} name Name of secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSecret: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteSecret', 'name', name)\n            const localVarPath = `/api/v1alpha1/secrets/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Secret\n         * @param {string} name Name of secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSecret: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getSecret', 'name', name)\n            const localVarPath = `/api/v1alpha1/secrets/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Secret\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSecret: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/secrets`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Secret\n         * @param {string} name Name of secret\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSecret: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchSecret', 'name', name)\n            const localVarPath = `/api/v1alpha1/secrets/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Secret\n         * @param {string} name Name of secret\n         * @param {Secret} [secret] Updated secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSecret: async (name: string, secret?: Secret, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateSecret', 'name', name)\n            const localVarPath = `/api/v1alpha1/secrets/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(secret, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SecretV1alpha1Api - functional programming interface\n */\nexport const SecretV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SecretV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Secret\n         * @param {Secret} [secret] Fresh secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createSecret(secret?: Secret, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Secret>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createSecret(secret, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SecretV1alpha1Api.createSecret']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Secret\n         * @param {string} name Name of secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteSecret(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSecret(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SecretV1alpha1Api.deleteSecret']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Secret\n         * @param {string} name Name of secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSecret(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Secret>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSecret(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SecretV1alpha1Api.getSecret']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Secret\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listSecret(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SecretList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listSecret(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SecretV1alpha1Api.listSecret']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Secret\n         * @param {string} name Name of secret\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchSecret(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Secret>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchSecret(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SecretV1alpha1Api.patchSecret']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Secret\n         * @param {string} name Name of secret\n         * @param {Secret} [secret] Updated secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateSecret(name: string, secret?: Secret, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Secret>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSecret(name, secret, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SecretV1alpha1Api.updateSecret']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SecretV1alpha1Api - factory interface\n */\nexport const SecretV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SecretV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Secret\n         * @param {SecretV1alpha1ApiCreateSecretRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSecret(requestParameters: SecretV1alpha1ApiCreateSecretRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Secret> {\n            return localVarFp.createSecret(requestParameters.secret, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Secret\n         * @param {SecretV1alpha1ApiDeleteSecretRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSecret(requestParameters: SecretV1alpha1ApiDeleteSecretRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteSecret(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Secret\n         * @param {SecretV1alpha1ApiGetSecretRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSecret(requestParameters: SecretV1alpha1ApiGetSecretRequest, options?: RawAxiosRequestConfig): AxiosPromise<Secret> {\n            return localVarFp.getSecret(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Secret\n         * @param {SecretV1alpha1ApiListSecretRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSecret(requestParameters: SecretV1alpha1ApiListSecretRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SecretList> {\n            return localVarFp.listSecret(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Secret\n         * @param {SecretV1alpha1ApiPatchSecretRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSecret(requestParameters: SecretV1alpha1ApiPatchSecretRequest, options?: RawAxiosRequestConfig): AxiosPromise<Secret> {\n            return localVarFp.patchSecret(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Secret\n         * @param {SecretV1alpha1ApiUpdateSecretRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSecret(requestParameters: SecretV1alpha1ApiUpdateSecretRequest, options?: RawAxiosRequestConfig): AxiosPromise<Secret> {\n            return localVarFp.updateSecret(requestParameters.name, requestParameters.secret, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createSecret operation in SecretV1alpha1Api.\n */\nexport interface SecretV1alpha1ApiCreateSecretRequest {\n    /**\n     * Fresh secret\n     */\n    readonly secret?: Secret\n}\n\n/**\n * Request parameters for deleteSecret operation in SecretV1alpha1Api.\n */\nexport interface SecretV1alpha1ApiDeleteSecretRequest {\n    /**\n     * Name of secret\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getSecret operation in SecretV1alpha1Api.\n */\nexport interface SecretV1alpha1ApiGetSecretRequest {\n    /**\n     * Name of secret\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listSecret operation in SecretV1alpha1Api.\n */\nexport interface SecretV1alpha1ApiListSecretRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchSecret operation in SecretV1alpha1Api.\n */\nexport interface SecretV1alpha1ApiPatchSecretRequest {\n    /**\n     * Name of secret\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateSecret operation in SecretV1alpha1Api.\n */\nexport interface SecretV1alpha1ApiUpdateSecretRequest {\n    /**\n     * Name of secret\n     */\n    readonly name: string\n\n    /**\n     * Updated secret\n     */\n    readonly secret?: Secret\n}\n\n/**\n * SecretV1alpha1Api - object-oriented interface\n */\nexport class SecretV1alpha1Api extends BaseAPI {\n    /**\n     * Create Secret\n     * @param {SecretV1alpha1ApiCreateSecretRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createSecret(requestParameters: SecretV1alpha1ApiCreateSecretRequest = {}, options?: RawAxiosRequestConfig) {\n        return SecretV1alpha1ApiFp(this.configuration).createSecret(requestParameters.secret, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Secret\n     * @param {SecretV1alpha1ApiDeleteSecretRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteSecret(requestParameters: SecretV1alpha1ApiDeleteSecretRequest, options?: RawAxiosRequestConfig) {\n        return SecretV1alpha1ApiFp(this.configuration).deleteSecret(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Secret\n     * @param {SecretV1alpha1ApiGetSecretRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getSecret(requestParameters: SecretV1alpha1ApiGetSecretRequest, options?: RawAxiosRequestConfig) {\n        return SecretV1alpha1ApiFp(this.configuration).getSecret(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Secret\n     * @param {SecretV1alpha1ApiListSecretRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listSecret(requestParameters: SecretV1alpha1ApiListSecretRequest = {}, options?: RawAxiosRequestConfig) {\n        return SecretV1alpha1ApiFp(this.configuration).listSecret(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Secret\n     * @param {SecretV1alpha1ApiPatchSecretRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchSecret(requestParameters: SecretV1alpha1ApiPatchSecretRequest, options?: RawAxiosRequestConfig) {\n        return SecretV1alpha1ApiFp(this.configuration).patchSecret(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Secret\n     * @param {SecretV1alpha1ApiUpdateSecretRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateSecret(requestParameters: SecretV1alpha1ApiUpdateSecretRequest, options?: RawAxiosRequestConfig) {\n        return SecretV1alpha1ApiFp(this.configuration).updateSecret(requestParameters.name, requestParameters.secret, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/setting-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Setting } from '../models';\n// @ts-ignore\nimport type { SettingList } from '../models';\n/**\n * SettingV1alpha1Api - axios parameter creator\n */\nexport const SettingV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Setting\n         * @param {Setting} [setting] Fresh setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSetting: async (setting?: Setting, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/settings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(setting, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Setting\n         * @param {string} name Name of setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSetting: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/settings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Setting\n         * @param {string} name Name of setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSetting: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/settings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Setting\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSetting: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/settings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Setting\n         * @param {string} name Name of setting\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSetting: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/settings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Setting\n         * @param {string} name Name of setting\n         * @param {Setting} [setting] Updated setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSetting: async (name: string, setting?: Setting, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateSetting', 'name', name)\n            const localVarPath = `/api/v1alpha1/settings/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(setting, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SettingV1alpha1Api - functional programming interface\n */\nexport const SettingV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SettingV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Setting\n         * @param {Setting} [setting] Fresh setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createSetting(setting?: Setting, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Setting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createSetting(setting, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SettingV1alpha1Api.createSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Setting\n         * @param {string} name Name of setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteSetting(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSetting(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SettingV1alpha1Api.deleteSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Setting\n         * @param {string} name Name of setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSetting(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Setting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSetting(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SettingV1alpha1Api.getSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Setting\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listSetting(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SettingList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listSetting(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SettingV1alpha1Api.listSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Setting\n         * @param {string} name Name of setting\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchSetting(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Setting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchSetting(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SettingV1alpha1Api.patchSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Setting\n         * @param {string} name Name of setting\n         * @param {Setting} [setting] Updated setting\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateSetting(name: string, setting?: Setting, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Setting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSetting(name, setting, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SettingV1alpha1Api.updateSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SettingV1alpha1Api - factory interface\n */\nexport const SettingV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SettingV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Setting\n         * @param {SettingV1alpha1ApiCreateSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSetting(requestParameters: SettingV1alpha1ApiCreateSettingRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Setting> {\n            return localVarFp.createSetting(requestParameters.setting, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Setting\n         * @param {SettingV1alpha1ApiDeleteSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSetting(requestParameters: SettingV1alpha1ApiDeleteSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteSetting(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Setting\n         * @param {SettingV1alpha1ApiGetSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSetting(requestParameters: SettingV1alpha1ApiGetSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<Setting> {\n            return localVarFp.getSetting(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Setting\n         * @param {SettingV1alpha1ApiListSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSetting(requestParameters: SettingV1alpha1ApiListSettingRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SettingList> {\n            return localVarFp.listSetting(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Setting\n         * @param {SettingV1alpha1ApiPatchSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSetting(requestParameters: SettingV1alpha1ApiPatchSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<Setting> {\n            return localVarFp.patchSetting(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Setting\n         * @param {SettingV1alpha1ApiUpdateSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSetting(requestParameters: SettingV1alpha1ApiUpdateSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<Setting> {\n            return localVarFp.updateSetting(requestParameters.name, requestParameters.setting, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createSetting operation in SettingV1alpha1Api.\n */\nexport interface SettingV1alpha1ApiCreateSettingRequest {\n    /**\n     * Fresh setting\n     */\n    readonly setting?: Setting\n}\n\n/**\n * Request parameters for deleteSetting operation in SettingV1alpha1Api.\n */\nexport interface SettingV1alpha1ApiDeleteSettingRequest {\n    /**\n     * Name of setting\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getSetting operation in SettingV1alpha1Api.\n */\nexport interface SettingV1alpha1ApiGetSettingRequest {\n    /**\n     * Name of setting\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listSetting operation in SettingV1alpha1Api.\n */\nexport interface SettingV1alpha1ApiListSettingRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchSetting operation in SettingV1alpha1Api.\n */\nexport interface SettingV1alpha1ApiPatchSettingRequest {\n    /**\n     * Name of setting\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateSetting operation in SettingV1alpha1Api.\n */\nexport interface SettingV1alpha1ApiUpdateSettingRequest {\n    /**\n     * Name of setting\n     */\n    readonly name: string\n\n    /**\n     * Updated setting\n     */\n    readonly setting?: Setting\n}\n\n/**\n * SettingV1alpha1Api - object-oriented interface\n */\nexport class SettingV1alpha1Api extends BaseAPI {\n    /**\n     * Create Setting\n     * @param {SettingV1alpha1ApiCreateSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createSetting(requestParameters: SettingV1alpha1ApiCreateSettingRequest = {}, options?: RawAxiosRequestConfig) {\n        return SettingV1alpha1ApiFp(this.configuration).createSetting(requestParameters.setting, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Setting\n     * @param {SettingV1alpha1ApiDeleteSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteSetting(requestParameters: SettingV1alpha1ApiDeleteSettingRequest, options?: RawAxiosRequestConfig) {\n        return SettingV1alpha1ApiFp(this.configuration).deleteSetting(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Setting\n     * @param {SettingV1alpha1ApiGetSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getSetting(requestParameters: SettingV1alpha1ApiGetSettingRequest, options?: RawAxiosRequestConfig) {\n        return SettingV1alpha1ApiFp(this.configuration).getSetting(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Setting\n     * @param {SettingV1alpha1ApiListSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listSetting(requestParameters: SettingV1alpha1ApiListSettingRequest = {}, options?: RawAxiosRequestConfig) {\n        return SettingV1alpha1ApiFp(this.configuration).listSetting(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Setting\n     * @param {SettingV1alpha1ApiPatchSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchSetting(requestParameters: SettingV1alpha1ApiPatchSettingRequest, options?: RawAxiosRequestConfig) {\n        return SettingV1alpha1ApiFp(this.configuration).patchSetting(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Setting\n     * @param {SettingV1alpha1ApiUpdateSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateSetting(requestParameters: SettingV1alpha1ApiUpdateSettingRequest, options?: RawAxiosRequestConfig) {\n        return SettingV1alpha1ApiFp(this.configuration).updateSetting(requestParameters.name, requestParameters.setting, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/single-page-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { SinglePage } from '../models';\n// @ts-ignore\nimport type { SinglePageList } from '../models';\n/**\n * SinglePageV1alpha1Api - axios parameter creator\n */\nexport const SinglePageV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create SinglePage\n         * @param {SinglePage} [singlePage] Fresh singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSinglePage: async (singlePage?: SinglePage, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/singlepages`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(singlePage, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete SinglePage\n         * @param {string} name Name of singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSinglePage: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteSinglePage', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/singlepages/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get SinglePage\n         * @param {string} name Name of singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSinglePage: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getSinglePage', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/singlepages/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List SinglePage\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSinglePage: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/singlepages`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch SinglePage\n         * @param {string} name Name of singlepage\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSinglePage: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchSinglePage', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/singlepages/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update SinglePage\n         * @param {string} name Name of singlepage\n         * @param {SinglePage} [singlePage] Updated singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSinglePage: async (name: string, singlePage?: SinglePage, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateSinglePage', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/singlepages/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(singlePage, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SinglePageV1alpha1Api - functional programming interface\n */\nexport const SinglePageV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SinglePageV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create SinglePage\n         * @param {SinglePage} [singlePage] Fresh singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createSinglePage(singlePage?: SinglePage, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePage>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createSinglePage(singlePage, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1Api.createSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete SinglePage\n         * @param {string} name Name of singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteSinglePage(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSinglePage(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1Api.deleteSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get SinglePage\n         * @param {string} name Name of singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSinglePage(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePage>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSinglePage(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1Api.getSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List SinglePage\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listSinglePage(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePageList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listSinglePage(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1Api.listSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch SinglePage\n         * @param {string} name Name of singlepage\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchSinglePage(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePage>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchSinglePage(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1Api.patchSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update SinglePage\n         * @param {string} name Name of singlepage\n         * @param {SinglePage} [singlePage] Updated singlepage\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateSinglePage(name: string, singlePage?: SinglePage, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePage>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSinglePage(name, singlePage, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1Api.updateSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SinglePageV1alpha1Api - factory interface\n */\nexport const SinglePageV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SinglePageV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create SinglePage\n         * @param {SinglePageV1alpha1ApiCreateSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSinglePage(requestParameters: SinglePageV1alpha1ApiCreateSinglePageRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {\n            return localVarFp.createSinglePage(requestParameters.singlePage, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete SinglePage\n         * @param {SinglePageV1alpha1ApiDeleteSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSinglePage(requestParameters: SinglePageV1alpha1ApiDeleteSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteSinglePage(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get SinglePage\n         * @param {SinglePageV1alpha1ApiGetSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSinglePage(requestParameters: SinglePageV1alpha1ApiGetSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {\n            return localVarFp.getSinglePage(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List SinglePage\n         * @param {SinglePageV1alpha1ApiListSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSinglePage(requestParameters: SinglePageV1alpha1ApiListSinglePageRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SinglePageList> {\n            return localVarFp.listSinglePage(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch SinglePage\n         * @param {SinglePageV1alpha1ApiPatchSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSinglePage(requestParameters: SinglePageV1alpha1ApiPatchSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {\n            return localVarFp.patchSinglePage(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update SinglePage\n         * @param {SinglePageV1alpha1ApiUpdateSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSinglePage(requestParameters: SinglePageV1alpha1ApiUpdateSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {\n            return localVarFp.updateSinglePage(requestParameters.name, requestParameters.singlePage, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createSinglePage operation in SinglePageV1alpha1Api.\n */\nexport interface SinglePageV1alpha1ApiCreateSinglePageRequest {\n    /**\n     * Fresh singlepage\n     */\n    readonly singlePage?: SinglePage\n}\n\n/**\n * Request parameters for deleteSinglePage operation in SinglePageV1alpha1Api.\n */\nexport interface SinglePageV1alpha1ApiDeleteSinglePageRequest {\n    /**\n     * Name of singlepage\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getSinglePage operation in SinglePageV1alpha1Api.\n */\nexport interface SinglePageV1alpha1ApiGetSinglePageRequest {\n    /**\n     * Name of singlepage\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listSinglePage operation in SinglePageV1alpha1Api.\n */\nexport interface SinglePageV1alpha1ApiListSinglePageRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchSinglePage operation in SinglePageV1alpha1Api.\n */\nexport interface SinglePageV1alpha1ApiPatchSinglePageRequest {\n    /**\n     * Name of singlepage\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateSinglePage operation in SinglePageV1alpha1Api.\n */\nexport interface SinglePageV1alpha1ApiUpdateSinglePageRequest {\n    /**\n     * Name of singlepage\n     */\n    readonly name: string\n\n    /**\n     * Updated singlepage\n     */\n    readonly singlePage?: SinglePage\n}\n\n/**\n * SinglePageV1alpha1Api - object-oriented interface\n */\nexport class SinglePageV1alpha1Api extends BaseAPI {\n    /**\n     * Create SinglePage\n     * @param {SinglePageV1alpha1ApiCreateSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createSinglePage(requestParameters: SinglePageV1alpha1ApiCreateSinglePageRequest = {}, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ApiFp(this.configuration).createSinglePage(requestParameters.singlePage, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete SinglePage\n     * @param {SinglePageV1alpha1ApiDeleteSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteSinglePage(requestParameters: SinglePageV1alpha1ApiDeleteSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ApiFp(this.configuration).deleteSinglePage(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get SinglePage\n     * @param {SinglePageV1alpha1ApiGetSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getSinglePage(requestParameters: SinglePageV1alpha1ApiGetSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ApiFp(this.configuration).getSinglePage(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List SinglePage\n     * @param {SinglePageV1alpha1ApiListSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listSinglePage(requestParameters: SinglePageV1alpha1ApiListSinglePageRequest = {}, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ApiFp(this.configuration).listSinglePage(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch SinglePage\n     * @param {SinglePageV1alpha1ApiPatchSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchSinglePage(requestParameters: SinglePageV1alpha1ApiPatchSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ApiFp(this.configuration).patchSinglePage(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update SinglePage\n     * @param {SinglePageV1alpha1ApiUpdateSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateSinglePage(requestParameters: SinglePageV1alpha1ApiUpdateSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ApiFp(this.configuration).updateSinglePage(requestParameters.name, requestParameters.singlePage, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/single-page-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Content } from '../models';\n// @ts-ignore\nimport type { ContentWrapper } from '../models';\n// @ts-ignore\nimport type { ListedSinglePageList } from '../models';\n// @ts-ignore\nimport type { ListedSnapshotDto } from '../models';\n// @ts-ignore\nimport type { Post } from '../models';\n// @ts-ignore\nimport type { RevertSnapshotForSingleParam } from '../models';\n// @ts-ignore\nimport type { SinglePage } from '../models';\n// @ts-ignore\nimport type { SinglePageRequest } from '../models';\n/**\n * SinglePageV1alpha1ConsoleApi - axios parameter creator\n */\nexport const SinglePageV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Delete a content for post.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSinglePageContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteSinglePageContent', 'name', name)\n            // verify required parameter 'snapshotName' is not null or undefined\n            assertParamExists('deleteSinglePageContent', 'snapshotName', snapshotName)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (snapshotName !== undefined) {\n                localVarQueryParameter['snapshotName'] = snapshotName;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Draft a single page.\n         * @param {SinglePageRequest} singlePageRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        draftSinglePage: async (singlePageRequest: SinglePageRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'singlePageRequest' is not null or undefined\n            assertParamExists('draftSinglePage', 'singlePageRequest', singlePageRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(singlePageRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch content of single page.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSinglePageContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchSinglePageContent', 'name', name)\n            // verify required parameter 'snapshotName' is not null or undefined\n            assertParamExists('fetchSinglePageContent', 'snapshotName', snapshotName)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (snapshotName !== undefined) {\n                localVarQueryParameter['snapshotName'] = snapshotName;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch head content of single page.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSinglePageHeadContent: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchSinglePageHeadContent', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/head-content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch release content of single page.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSinglePageReleaseContent: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchSinglePageReleaseContent', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/release-content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List all snapshots for single page content.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSinglePageSnapshots: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('listSinglePageSnapshots', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/snapshot`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List single pages.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {Array<string>} [contributor] SinglePages filtered by contributor.\n         * @param {ListSinglePagesPublishPhaseEnum} [publishPhase] SinglePages filtered by publish phase.\n         * @param {ListSinglePagesVisibleEnum} [visible] SinglePages filtered by visibility.\n         * @param {string} [keyword] SinglePages filtered by keyword.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSinglePages: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, contributor?: Array<string>, publishPhase?: ListSinglePagesPublishPhaseEnum, visible?: ListSinglePagesVisibleEnum, keyword?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (contributor) {\n                localVarQueryParameter['contributor'] = contributor;\n            }\n\n            if (publishPhase !== undefined) {\n                localVarQueryParameter['publishPhase'] = publishPhase;\n            }\n\n            if (visible !== undefined) {\n                localVarQueryParameter['visible'] = visible;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Publish a single page.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        publishSinglePage: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('publishSinglePage', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/publish`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Revert to specified snapshot for single page content.\n         * @param {string} name \n         * @param {RevertSnapshotForSingleParam} revertSnapshotForSingleParam \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revertToSpecifiedSnapshotForSinglePage: async (name: string, revertSnapshotForSingleParam: RevertSnapshotForSingleParam, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('revertToSpecifiedSnapshotForSinglePage', 'name', name)\n            // verify required parameter 'revertSnapshotForSingleParam' is not null or undefined\n            assertParamExists('revertToSpecifiedSnapshotForSinglePage', 'revertSnapshotForSingleParam', revertSnapshotForSingleParam)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/revert-content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(revertSnapshotForSingleParam, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update a single page.\n         * @param {string} name \n         * @param {SinglePageRequest} singlePageRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDraftSinglePage: async (name: string, singlePageRequest: SinglePageRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateDraftSinglePage', 'name', name)\n            // verify required parameter 'singlePageRequest' is not null or undefined\n            assertParamExists('updateDraftSinglePage', 'singlePageRequest', singlePageRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(singlePageRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update a single page\\'s content.\n         * @param {string} name \n         * @param {Content} content \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSinglePageContent: async (name: string, content: Content, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateSinglePageContent', 'name', name)\n            // verify required parameter 'content' is not null or undefined\n            assertParamExists('updateSinglePageContent', 'content', content)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(content, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SinglePageV1alpha1ConsoleApi - functional programming interface\n */\nexport const SinglePageV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SinglePageV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Delete a content for post.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteSinglePageContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSinglePageContent(name, snapshotName, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.deleteSinglePageContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Draft a single page.\n         * @param {SinglePageRequest} singlePageRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async draftSinglePage(singlePageRequest: SinglePageRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePage>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.draftSinglePage(singlePageRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.draftSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch content of single page.\n         * @param {string} name \n         * @param {string} snapshotName \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchSinglePageContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchSinglePageContent(name, snapshotName, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.fetchSinglePageContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch head content of single page.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchSinglePageHeadContent(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchSinglePageHeadContent(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.fetchSinglePageHeadContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch release content of single page.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchSinglePageReleaseContent(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchSinglePageReleaseContent(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.fetchSinglePageReleaseContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List all snapshots for single page content.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listSinglePageSnapshots(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ListedSnapshotDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listSinglePageSnapshots(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.listSinglePageSnapshots']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List single pages.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {Array<string>} [contributor] SinglePages filtered by contributor.\n         * @param {ListSinglePagesPublishPhaseEnum} [publishPhase] SinglePages filtered by publish phase.\n         * @param {ListSinglePagesVisibleEnum} [visible] SinglePages filtered by visibility.\n         * @param {string} [keyword] SinglePages filtered by keyword.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listSinglePages(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, contributor?: Array<string>, publishPhase?: ListSinglePagesPublishPhaseEnum, visible?: ListSinglePagesVisibleEnum, keyword?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedSinglePageList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listSinglePages(page, size, labelSelector, fieldSelector, sort, contributor, publishPhase, visible, keyword, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.listSinglePages']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Publish a single page.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async publishSinglePage(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePage>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.publishSinglePage(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.publishSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Revert to specified snapshot for single page content.\n         * @param {string} name \n         * @param {RevertSnapshotForSingleParam} revertSnapshotForSingleParam \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async revertToSpecifiedSnapshotForSinglePage(name: string, revertSnapshotForSingleParam: RevertSnapshotForSingleParam, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.revertToSpecifiedSnapshotForSinglePage(name, revertSnapshotForSingleParam, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.revertToSpecifiedSnapshotForSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update a single page.\n         * @param {string} name \n         * @param {SinglePageRequest} singlePageRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateDraftSinglePage(name: string, singlePageRequest: SinglePageRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePage>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateDraftSinglePage(name, singlePageRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.updateDraftSinglePage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update a single page\\'s content.\n         * @param {string} name \n         * @param {Content} content \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateSinglePageContent(name: string, content: Content, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSinglePageContent(name, content, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1ConsoleApi.updateSinglePageContent']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SinglePageV1alpha1ConsoleApi - factory interface\n */\nexport const SinglePageV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SinglePageV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Delete a content for post.\n         * @param {SinglePageV1alpha1ConsoleApiDeleteSinglePageContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSinglePageContent(requestParameters: SinglePageV1alpha1ConsoleApiDeleteSinglePageContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.deleteSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Draft a single page.\n         * @param {SinglePageV1alpha1ConsoleApiDraftSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        draftSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiDraftSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {\n            return localVarFp.draftSinglePage(requestParameters.singlePageRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch content of single page.\n         * @param {SinglePageV1alpha1ConsoleApiFetchSinglePageContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSinglePageContent(requestParameters: SinglePageV1alpha1ConsoleApiFetchSinglePageContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.fetchSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch head content of single page.\n         * @param {SinglePageV1alpha1ConsoleApiFetchSinglePageHeadContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSinglePageHeadContent(requestParameters: SinglePageV1alpha1ConsoleApiFetchSinglePageHeadContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.fetchSinglePageHeadContent(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch release content of single page.\n         * @param {SinglePageV1alpha1ConsoleApiFetchSinglePageReleaseContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchSinglePageReleaseContent(requestParameters: SinglePageV1alpha1ConsoleApiFetchSinglePageReleaseContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {\n            return localVarFp.fetchSinglePageReleaseContent(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List all snapshots for single page content.\n         * @param {SinglePageV1alpha1ConsoleApiListSinglePageSnapshotsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSinglePageSnapshots(requestParameters: SinglePageV1alpha1ConsoleApiListSinglePageSnapshotsRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<ListedSnapshotDto>> {\n            return localVarFp.listSinglePageSnapshots(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List single pages.\n         * @param {SinglePageV1alpha1ConsoleApiListSinglePagesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSinglePages(requestParameters: SinglePageV1alpha1ConsoleApiListSinglePagesRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ListedSinglePageList> {\n            return localVarFp.listSinglePages(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.contributor, requestParameters.publishPhase, requestParameters.visible, requestParameters.keyword, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Publish a single page.\n         * @param {SinglePageV1alpha1ConsoleApiPublishSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        publishSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiPublishSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {\n            return localVarFp.publishSinglePage(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Revert to specified snapshot for single page content.\n         * @param {SinglePageV1alpha1ConsoleApiRevertToSpecifiedSnapshotForSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        revertToSpecifiedSnapshotForSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiRevertToSpecifiedSnapshotForSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.revertToSpecifiedSnapshotForSinglePage(requestParameters.name, requestParameters.revertSnapshotForSingleParam, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update a single page.\n         * @param {SinglePageV1alpha1ConsoleApiUpdateDraftSinglePageRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDraftSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiUpdateDraftSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {\n            return localVarFp.updateDraftSinglePage(requestParameters.name, requestParameters.singlePageRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update a single page\\'s content.\n         * @param {SinglePageV1alpha1ConsoleApiUpdateSinglePageContentRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSinglePageContent(requestParameters: SinglePageV1alpha1ConsoleApiUpdateSinglePageContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {\n            return localVarFp.updateSinglePageContent(requestParameters.name, requestParameters.content, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for deleteSinglePageContent operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiDeleteSinglePageContentRequest {\n    readonly name: string\n\n    readonly snapshotName: string\n}\n\n/**\n * Request parameters for draftSinglePage operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiDraftSinglePageRequest {\n    readonly singlePageRequest: SinglePageRequest\n}\n\n/**\n * Request parameters for fetchSinglePageContent operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiFetchSinglePageContentRequest {\n    readonly name: string\n\n    readonly snapshotName: string\n}\n\n/**\n * Request parameters for fetchSinglePageHeadContent operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiFetchSinglePageHeadContentRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for fetchSinglePageReleaseContent operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiFetchSinglePageReleaseContentRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for listSinglePageSnapshots operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiListSinglePageSnapshotsRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for listSinglePages operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiListSinglePagesRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * SinglePages filtered by contributor.\n     */\n    readonly contributor?: Array<string>\n\n    /**\n     * SinglePages filtered by publish phase.\n     */\n    readonly publishPhase?: ListSinglePagesPublishPhaseEnum\n\n    /**\n     * SinglePages filtered by visibility.\n     */\n    readonly visible?: ListSinglePagesVisibleEnum\n\n    /**\n     * SinglePages filtered by keyword.\n     */\n    readonly keyword?: string\n}\n\n/**\n * Request parameters for publishSinglePage operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiPublishSinglePageRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for revertToSpecifiedSnapshotForSinglePage operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiRevertToSpecifiedSnapshotForSinglePageRequest {\n    readonly name: string\n\n    readonly revertSnapshotForSingleParam: RevertSnapshotForSingleParam\n}\n\n/**\n * Request parameters for updateDraftSinglePage operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiUpdateDraftSinglePageRequest {\n    readonly name: string\n\n    readonly singlePageRequest: SinglePageRequest\n}\n\n/**\n * Request parameters for updateSinglePageContent operation in SinglePageV1alpha1ConsoleApi.\n */\nexport interface SinglePageV1alpha1ConsoleApiUpdateSinglePageContentRequest {\n    readonly name: string\n\n    readonly content: Content\n}\n\n/**\n * SinglePageV1alpha1ConsoleApi - object-oriented interface\n */\nexport class SinglePageV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Delete a content for post.\n     * @param {SinglePageV1alpha1ConsoleApiDeleteSinglePageContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteSinglePageContent(requestParameters: SinglePageV1alpha1ConsoleApiDeleteSinglePageContentRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).deleteSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Draft a single page.\n     * @param {SinglePageV1alpha1ConsoleApiDraftSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public draftSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiDraftSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).draftSinglePage(requestParameters.singlePageRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch content of single page.\n     * @param {SinglePageV1alpha1ConsoleApiFetchSinglePageContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchSinglePageContent(requestParameters: SinglePageV1alpha1ConsoleApiFetchSinglePageContentRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).fetchSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch head content of single page.\n     * @param {SinglePageV1alpha1ConsoleApiFetchSinglePageHeadContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchSinglePageHeadContent(requestParameters: SinglePageV1alpha1ConsoleApiFetchSinglePageHeadContentRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).fetchSinglePageHeadContent(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch release content of single page.\n     * @param {SinglePageV1alpha1ConsoleApiFetchSinglePageReleaseContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchSinglePageReleaseContent(requestParameters: SinglePageV1alpha1ConsoleApiFetchSinglePageReleaseContentRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).fetchSinglePageReleaseContent(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List all snapshots for single page content.\n     * @param {SinglePageV1alpha1ConsoleApiListSinglePageSnapshotsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listSinglePageSnapshots(requestParameters: SinglePageV1alpha1ConsoleApiListSinglePageSnapshotsRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).listSinglePageSnapshots(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List single pages.\n     * @param {SinglePageV1alpha1ConsoleApiListSinglePagesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listSinglePages(requestParameters: SinglePageV1alpha1ConsoleApiListSinglePagesRequest = {}, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).listSinglePages(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.contributor, requestParameters.publishPhase, requestParameters.visible, requestParameters.keyword, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Publish a single page.\n     * @param {SinglePageV1alpha1ConsoleApiPublishSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public publishSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiPublishSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).publishSinglePage(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Revert to specified snapshot for single page content.\n     * @param {SinglePageV1alpha1ConsoleApiRevertToSpecifiedSnapshotForSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public revertToSpecifiedSnapshotForSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiRevertToSpecifiedSnapshotForSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).revertToSpecifiedSnapshotForSinglePage(requestParameters.name, requestParameters.revertSnapshotForSingleParam, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update a single page.\n     * @param {SinglePageV1alpha1ConsoleApiUpdateDraftSinglePageRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateDraftSinglePage(requestParameters: SinglePageV1alpha1ConsoleApiUpdateDraftSinglePageRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).updateDraftSinglePage(requestParameters.name, requestParameters.singlePageRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update a single page\\'s content.\n     * @param {SinglePageV1alpha1ConsoleApiUpdateSinglePageContentRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateSinglePageContent(requestParameters: SinglePageV1alpha1ConsoleApiUpdateSinglePageContentRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1ConsoleApiFp(this.configuration).updateSinglePageContent(requestParameters.name, requestParameters.content, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\nexport const ListSinglePagesPublishPhaseEnum = {\n    Draft: 'DRAFT',\n    PendingApproval: 'PENDING_APPROVAL',\n    Published: 'PUBLISHED',\n    Failed: 'FAILED'\n} as const;\nexport type ListSinglePagesPublishPhaseEnum = typeof ListSinglePagesPublishPhaseEnum[keyof typeof ListSinglePagesPublishPhaseEnum];\nexport const ListSinglePagesVisibleEnum = {\n    Public: 'PUBLIC',\n    Internal: 'INTERNAL',\n    Private: 'PRIVATE'\n} as const;\nexport type ListSinglePagesVisibleEnum = typeof ListSinglePagesVisibleEnum[keyof typeof ListSinglePagesVisibleEnum];\n"
  },
  {
    "path": "ui/packages/api-client/src/api/single-page-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ListedSinglePageVoList } from '../models';\n// @ts-ignore\nimport type { SinglePageVo } from '../models';\n/**\n * SinglePageV1alpha1PublicApi - axios parameter creator\n */\nexport const SinglePageV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Gets single page by name\n         * @param {string} name SinglePage name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        querySinglePageByName: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('querySinglePageByName', 'name', name)\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/singlepages/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Lists single pages\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        querySinglePages: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/singlepages`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SinglePageV1alpha1PublicApi - functional programming interface\n */\nexport const SinglePageV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SinglePageV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Gets single page by name\n         * @param {string} name SinglePage name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async querySinglePageByName(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SinglePageVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.querySinglePageByName(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1PublicApi.querySinglePageByName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Lists single pages\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async querySinglePages(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedSinglePageVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.querySinglePages(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SinglePageV1alpha1PublicApi.querySinglePages']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SinglePageV1alpha1PublicApi - factory interface\n */\nexport const SinglePageV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SinglePageV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Gets single page by name\n         * @param {SinglePageV1alpha1PublicApiQuerySinglePageByNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        querySinglePageByName(requestParameters: SinglePageV1alpha1PublicApiQuerySinglePageByNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePageVo> {\n            return localVarFp.querySinglePageByName(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Lists single pages\n         * @param {SinglePageV1alpha1PublicApiQuerySinglePagesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        querySinglePages(requestParameters: SinglePageV1alpha1PublicApiQuerySinglePagesRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ListedSinglePageVoList> {\n            return localVarFp.querySinglePages(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for querySinglePageByName operation in SinglePageV1alpha1PublicApi.\n */\nexport interface SinglePageV1alpha1PublicApiQuerySinglePageByNameRequest {\n    /**\n     * SinglePage name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for querySinglePages operation in SinglePageV1alpha1PublicApi.\n */\nexport interface SinglePageV1alpha1PublicApiQuerySinglePagesRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * SinglePageV1alpha1PublicApi - object-oriented interface\n */\nexport class SinglePageV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Gets single page by name\n     * @param {SinglePageV1alpha1PublicApiQuerySinglePageByNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public querySinglePageByName(requestParameters: SinglePageV1alpha1PublicApiQuerySinglePageByNameRequest, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1PublicApiFp(this.configuration).querySinglePageByName(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Lists single pages\n     * @param {SinglePageV1alpha1PublicApiQuerySinglePagesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public querySinglePages(requestParameters: SinglePageV1alpha1PublicApiQuerySinglePagesRequest = {}, options?: RawAxiosRequestConfig) {\n        return SinglePageV1alpha1PublicApiFp(this.configuration).querySinglePages(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/snapshot-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Snapshot } from '../models';\n// @ts-ignore\nimport type { SnapshotList } from '../models';\n/**\n * SnapshotV1alpha1Api - axios parameter creator\n */\nexport const SnapshotV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Snapshot\n         * @param {Snapshot} [snapshot] Fresh snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSnapshot: async (snapshot?: Snapshot, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/snapshots`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(snapshot, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Snapshot\n         * @param {string} name Name of snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSnapshot: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteSnapshot', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/snapshots/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Snapshot\n         * @param {string} name Name of snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSnapshot: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getSnapshot', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/snapshots/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Snapshot\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSnapshot: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/snapshots`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Snapshot\n         * @param {string} name Name of snapshot\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSnapshot: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchSnapshot', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/snapshots/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Snapshot\n         * @param {string} name Name of snapshot\n         * @param {Snapshot} [snapshot] Updated snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSnapshot: async (name: string, snapshot?: Snapshot, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateSnapshot', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/snapshots/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(snapshot, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SnapshotV1alpha1Api - functional programming interface\n */\nexport const SnapshotV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SnapshotV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Snapshot\n         * @param {Snapshot} [snapshot] Fresh snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createSnapshot(snapshot?: Snapshot, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createSnapshot(snapshot, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SnapshotV1alpha1Api.createSnapshot']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Snapshot\n         * @param {string} name Name of snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteSnapshot(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSnapshot(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SnapshotV1alpha1Api.deleteSnapshot']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Snapshot\n         * @param {string} name Name of snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSnapshot(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSnapshot(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SnapshotV1alpha1Api.getSnapshot']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Snapshot\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listSnapshot(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SnapshotList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listSnapshot(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SnapshotV1alpha1Api.listSnapshot']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Snapshot\n         * @param {string} name Name of snapshot\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchSnapshot(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchSnapshot(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SnapshotV1alpha1Api.patchSnapshot']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Snapshot\n         * @param {string} name Name of snapshot\n         * @param {Snapshot} [snapshot] Updated snapshot\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateSnapshot(name: string, snapshot?: Snapshot, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSnapshot(name, snapshot, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SnapshotV1alpha1Api.updateSnapshot']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SnapshotV1alpha1Api - factory interface\n */\nexport const SnapshotV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SnapshotV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Snapshot\n         * @param {SnapshotV1alpha1ApiCreateSnapshotRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSnapshot(requestParameters: SnapshotV1alpha1ApiCreateSnapshotRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Snapshot> {\n            return localVarFp.createSnapshot(requestParameters.snapshot, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Snapshot\n         * @param {SnapshotV1alpha1ApiDeleteSnapshotRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSnapshot(requestParameters: SnapshotV1alpha1ApiDeleteSnapshotRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteSnapshot(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Snapshot\n         * @param {SnapshotV1alpha1ApiGetSnapshotRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSnapshot(requestParameters: SnapshotV1alpha1ApiGetSnapshotRequest, options?: RawAxiosRequestConfig): AxiosPromise<Snapshot> {\n            return localVarFp.getSnapshot(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Snapshot\n         * @param {SnapshotV1alpha1ApiListSnapshotRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSnapshot(requestParameters: SnapshotV1alpha1ApiListSnapshotRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SnapshotList> {\n            return localVarFp.listSnapshot(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Snapshot\n         * @param {SnapshotV1alpha1ApiPatchSnapshotRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSnapshot(requestParameters: SnapshotV1alpha1ApiPatchSnapshotRequest, options?: RawAxiosRequestConfig): AxiosPromise<Snapshot> {\n            return localVarFp.patchSnapshot(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Snapshot\n         * @param {SnapshotV1alpha1ApiUpdateSnapshotRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSnapshot(requestParameters: SnapshotV1alpha1ApiUpdateSnapshotRequest, options?: RawAxiosRequestConfig): AxiosPromise<Snapshot> {\n            return localVarFp.updateSnapshot(requestParameters.name, requestParameters.snapshot, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createSnapshot operation in SnapshotV1alpha1Api.\n */\nexport interface SnapshotV1alpha1ApiCreateSnapshotRequest {\n    /**\n     * Fresh snapshot\n     */\n    readonly snapshot?: Snapshot\n}\n\n/**\n * Request parameters for deleteSnapshot operation in SnapshotV1alpha1Api.\n */\nexport interface SnapshotV1alpha1ApiDeleteSnapshotRequest {\n    /**\n     * Name of snapshot\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getSnapshot operation in SnapshotV1alpha1Api.\n */\nexport interface SnapshotV1alpha1ApiGetSnapshotRequest {\n    /**\n     * Name of snapshot\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listSnapshot operation in SnapshotV1alpha1Api.\n */\nexport interface SnapshotV1alpha1ApiListSnapshotRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchSnapshot operation in SnapshotV1alpha1Api.\n */\nexport interface SnapshotV1alpha1ApiPatchSnapshotRequest {\n    /**\n     * Name of snapshot\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateSnapshot operation in SnapshotV1alpha1Api.\n */\nexport interface SnapshotV1alpha1ApiUpdateSnapshotRequest {\n    /**\n     * Name of snapshot\n     */\n    readonly name: string\n\n    /**\n     * Updated snapshot\n     */\n    readonly snapshot?: Snapshot\n}\n\n/**\n * SnapshotV1alpha1Api - object-oriented interface\n */\nexport class SnapshotV1alpha1Api extends BaseAPI {\n    /**\n     * Create Snapshot\n     * @param {SnapshotV1alpha1ApiCreateSnapshotRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createSnapshot(requestParameters: SnapshotV1alpha1ApiCreateSnapshotRequest = {}, options?: RawAxiosRequestConfig) {\n        return SnapshotV1alpha1ApiFp(this.configuration).createSnapshot(requestParameters.snapshot, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Snapshot\n     * @param {SnapshotV1alpha1ApiDeleteSnapshotRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteSnapshot(requestParameters: SnapshotV1alpha1ApiDeleteSnapshotRequest, options?: RawAxiosRequestConfig) {\n        return SnapshotV1alpha1ApiFp(this.configuration).deleteSnapshot(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Snapshot\n     * @param {SnapshotV1alpha1ApiGetSnapshotRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getSnapshot(requestParameters: SnapshotV1alpha1ApiGetSnapshotRequest, options?: RawAxiosRequestConfig) {\n        return SnapshotV1alpha1ApiFp(this.configuration).getSnapshot(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Snapshot\n     * @param {SnapshotV1alpha1ApiListSnapshotRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listSnapshot(requestParameters: SnapshotV1alpha1ApiListSnapshotRequest = {}, options?: RawAxiosRequestConfig) {\n        return SnapshotV1alpha1ApiFp(this.configuration).listSnapshot(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Snapshot\n     * @param {SnapshotV1alpha1ApiPatchSnapshotRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchSnapshot(requestParameters: SnapshotV1alpha1ApiPatchSnapshotRequest, options?: RawAxiosRequestConfig) {\n        return SnapshotV1alpha1ApiFp(this.configuration).patchSnapshot(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Snapshot\n     * @param {SnapshotV1alpha1ApiUpdateSnapshotRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateSnapshot(requestParameters: SnapshotV1alpha1ApiUpdateSnapshotRequest, options?: RawAxiosRequestConfig) {\n        return SnapshotV1alpha1ApiFp(this.configuration).updateSnapshot(requestParameters.name, requestParameters.snapshot, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/snapshot-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { Snapshot } from '../models';\n/**\n * SnapshotV1alpha1UcApi - axios parameter creator\n */\nexport const SnapshotV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Get snapshot for one post.\n         * @param {string} name Snapshot name.\n         * @param {string} postName Post name.\n         * @param {boolean} [patched] Should include patched content and raw or not.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSnapshotForPost: async (name: string, postName: string, patched?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getSnapshotForPost', 'name', name)\n            // verify required parameter 'postName' is not null or undefined\n            assertParamExists('getSnapshotForPost', 'postName', postName)\n            const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/snapshots/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (postName !== undefined) {\n                localVarQueryParameter['postName'] = postName;\n            }\n\n            if (patched !== undefined) {\n                localVarQueryParameter['patched'] = patched;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SnapshotV1alpha1UcApi - functional programming interface\n */\nexport const SnapshotV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SnapshotV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Get snapshot for one post.\n         * @param {string} name Snapshot name.\n         * @param {string} postName Post name.\n         * @param {boolean} [patched] Should include patched content and raw or not.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSnapshotForPost(name: string, postName: string, patched?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSnapshotForPost(name, postName, patched, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SnapshotV1alpha1UcApi.getSnapshotForPost']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SnapshotV1alpha1UcApi - factory interface\n */\nexport const SnapshotV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SnapshotV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Get snapshot for one post.\n         * @param {SnapshotV1alpha1UcApiGetSnapshotForPostRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSnapshotForPost(requestParameters: SnapshotV1alpha1UcApiGetSnapshotForPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Snapshot> {\n            return localVarFp.getSnapshotForPost(requestParameters.name, requestParameters.postName, requestParameters.patched, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for getSnapshotForPost operation in SnapshotV1alpha1UcApi.\n */\nexport interface SnapshotV1alpha1UcApiGetSnapshotForPostRequest {\n    /**\n     * Snapshot name.\n     */\n    readonly name: string\n\n    /**\n     * Post name.\n     */\n    readonly postName: string\n\n    /**\n     * Should include patched content and raw or not.\n     */\n    readonly patched?: boolean\n}\n\n/**\n * SnapshotV1alpha1UcApi - object-oriented interface\n */\nexport class SnapshotV1alpha1UcApi extends BaseAPI {\n    /**\n     * Get snapshot for one post.\n     * @param {SnapshotV1alpha1UcApiGetSnapshotForPostRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getSnapshotForPost(requestParameters: SnapshotV1alpha1UcApiGetSnapshotForPostRequest, options?: RawAxiosRequestConfig) {\n        return SnapshotV1alpha1UcApiFp(this.configuration).getSnapshotForPost(requestParameters.name, requestParameters.postName, requestParameters.patched, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/subscription-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Subscription } from '../models';\n// @ts-ignore\nimport type { SubscriptionList } from '../models';\n/**\n * SubscriptionV1alpha1Api - axios parameter creator\n */\nexport const SubscriptionV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Subscription\n         * @param {Subscription} [subscription] Fresh subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSubscription: async (subscription?: Subscription, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/subscriptions`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(subscription, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Subscription\n         * @param {string} name Name of subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSubscription: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteSubscription', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/subscriptions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Subscription\n         * @param {string} name Name of subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSubscription: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getSubscription', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/subscriptions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Subscription\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSubscription: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/subscriptions`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Subscription\n         * @param {string} name Name of subscription\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSubscription: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchSubscription', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/subscriptions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Subscription\n         * @param {string} name Name of subscription\n         * @param {Subscription} [subscription] Updated subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSubscription: async (name: string, subscription?: Subscription, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateSubscription', 'name', name)\n            const localVarPath = `/apis/notification.halo.run/v1alpha1/subscriptions/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(subscription, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SubscriptionV1alpha1Api - functional programming interface\n */\nexport const SubscriptionV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SubscriptionV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Subscription\n         * @param {Subscription} [subscription] Fresh subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createSubscription(subscription?: Subscription, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Subscription>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createSubscription(subscription, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SubscriptionV1alpha1Api.createSubscription']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Subscription\n         * @param {string} name Name of subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteSubscription(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSubscription(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SubscriptionV1alpha1Api.deleteSubscription']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Subscription\n         * @param {string} name Name of subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSubscription(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Subscription>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSubscription(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SubscriptionV1alpha1Api.getSubscription']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Subscription\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listSubscription(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SubscriptionList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listSubscription(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SubscriptionV1alpha1Api.listSubscription']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Subscription\n         * @param {string} name Name of subscription\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchSubscription(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Subscription>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchSubscription(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SubscriptionV1alpha1Api.patchSubscription']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Subscription\n         * @param {string} name Name of subscription\n         * @param {Subscription} [subscription] Updated subscription\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateSubscription(name: string, subscription?: Subscription, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Subscription>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSubscription(name, subscription, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SubscriptionV1alpha1Api.updateSubscription']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SubscriptionV1alpha1Api - factory interface\n */\nexport const SubscriptionV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SubscriptionV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Subscription\n         * @param {SubscriptionV1alpha1ApiCreateSubscriptionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createSubscription(requestParameters: SubscriptionV1alpha1ApiCreateSubscriptionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Subscription> {\n            return localVarFp.createSubscription(requestParameters.subscription, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Subscription\n         * @param {SubscriptionV1alpha1ApiDeleteSubscriptionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteSubscription(requestParameters: SubscriptionV1alpha1ApiDeleteSubscriptionRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteSubscription(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Subscription\n         * @param {SubscriptionV1alpha1ApiGetSubscriptionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSubscription(requestParameters: SubscriptionV1alpha1ApiGetSubscriptionRequest, options?: RawAxiosRequestConfig): AxiosPromise<Subscription> {\n            return localVarFp.getSubscription(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Subscription\n         * @param {SubscriptionV1alpha1ApiListSubscriptionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listSubscription(requestParameters: SubscriptionV1alpha1ApiListSubscriptionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SubscriptionList> {\n            return localVarFp.listSubscription(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Subscription\n         * @param {SubscriptionV1alpha1ApiPatchSubscriptionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchSubscription(requestParameters: SubscriptionV1alpha1ApiPatchSubscriptionRequest, options?: RawAxiosRequestConfig): AxiosPromise<Subscription> {\n            return localVarFp.patchSubscription(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Subscription\n         * @param {SubscriptionV1alpha1ApiUpdateSubscriptionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSubscription(requestParameters: SubscriptionV1alpha1ApiUpdateSubscriptionRequest, options?: RawAxiosRequestConfig): AxiosPromise<Subscription> {\n            return localVarFp.updateSubscription(requestParameters.name, requestParameters.subscription, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createSubscription operation in SubscriptionV1alpha1Api.\n */\nexport interface SubscriptionV1alpha1ApiCreateSubscriptionRequest {\n    /**\n     * Fresh subscription\n     */\n    readonly subscription?: Subscription\n}\n\n/**\n * Request parameters for deleteSubscription operation in SubscriptionV1alpha1Api.\n */\nexport interface SubscriptionV1alpha1ApiDeleteSubscriptionRequest {\n    /**\n     * Name of subscription\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getSubscription operation in SubscriptionV1alpha1Api.\n */\nexport interface SubscriptionV1alpha1ApiGetSubscriptionRequest {\n    /**\n     * Name of subscription\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listSubscription operation in SubscriptionV1alpha1Api.\n */\nexport interface SubscriptionV1alpha1ApiListSubscriptionRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchSubscription operation in SubscriptionV1alpha1Api.\n */\nexport interface SubscriptionV1alpha1ApiPatchSubscriptionRequest {\n    /**\n     * Name of subscription\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateSubscription operation in SubscriptionV1alpha1Api.\n */\nexport interface SubscriptionV1alpha1ApiUpdateSubscriptionRequest {\n    /**\n     * Name of subscription\n     */\n    readonly name: string\n\n    /**\n     * Updated subscription\n     */\n    readonly subscription?: Subscription\n}\n\n/**\n * SubscriptionV1alpha1Api - object-oriented interface\n */\nexport class SubscriptionV1alpha1Api extends BaseAPI {\n    /**\n     * Create Subscription\n     * @param {SubscriptionV1alpha1ApiCreateSubscriptionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createSubscription(requestParameters: SubscriptionV1alpha1ApiCreateSubscriptionRequest = {}, options?: RawAxiosRequestConfig) {\n        return SubscriptionV1alpha1ApiFp(this.configuration).createSubscription(requestParameters.subscription, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Subscription\n     * @param {SubscriptionV1alpha1ApiDeleteSubscriptionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteSubscription(requestParameters: SubscriptionV1alpha1ApiDeleteSubscriptionRequest, options?: RawAxiosRequestConfig) {\n        return SubscriptionV1alpha1ApiFp(this.configuration).deleteSubscription(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Subscription\n     * @param {SubscriptionV1alpha1ApiGetSubscriptionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getSubscription(requestParameters: SubscriptionV1alpha1ApiGetSubscriptionRequest, options?: RawAxiosRequestConfig) {\n        return SubscriptionV1alpha1ApiFp(this.configuration).getSubscription(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Subscription\n     * @param {SubscriptionV1alpha1ApiListSubscriptionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listSubscription(requestParameters: SubscriptionV1alpha1ApiListSubscriptionRequest = {}, options?: RawAxiosRequestConfig) {\n        return SubscriptionV1alpha1ApiFp(this.configuration).listSubscription(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Subscription\n     * @param {SubscriptionV1alpha1ApiPatchSubscriptionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchSubscription(requestParameters: SubscriptionV1alpha1ApiPatchSubscriptionRequest, options?: RawAxiosRequestConfig) {\n        return SubscriptionV1alpha1ApiFp(this.configuration).patchSubscription(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Subscription\n     * @param {SubscriptionV1alpha1ApiUpdateSubscriptionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateSubscription(requestParameters: SubscriptionV1alpha1ApiUpdateSubscriptionRequest, options?: RawAxiosRequestConfig) {\n        return SubscriptionV1alpha1ApiFp(this.configuration).updateSubscription(requestParameters.name, requestParameters.subscription, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/system-config-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * SystemConfigV1alpha1ConsoleApi - axios parameter creator\n */\nexport const SystemConfigV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Get system config by group\n         * @param {string} group Group of the system config\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSystemConfigByGroup: async (group: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'group' is not null or undefined\n            assertParamExists('getSystemConfigByGroup', 'group', group)\n            const localVarPath = `/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}`\n                .replace(`{${\"group\"}}`, encodeURIComponent(String(group)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update system config by group\n         * @param {string} group Group of the system config\n         * @param {object} [body] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSystemConfigByGroup: async (group: string, body?: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'group' is not null or undefined\n            assertParamExists('updateSystemConfigByGroup', 'group', group)\n            const localVarPath = `/apis/console.api.halo.run/v1alpha1/systemconfigs/{group}`\n                .replace(`{${\"group\"}}`, encodeURIComponent(String(group)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SystemConfigV1alpha1ConsoleApi - functional programming interface\n */\nexport const SystemConfigV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SystemConfigV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Get system config by group\n         * @param {string} group Group of the system config\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSystemConfigByGroup(group: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSystemConfigByGroup(group, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SystemConfigV1alpha1ConsoleApi.getSystemConfigByGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update system config by group\n         * @param {string} group Group of the system config\n         * @param {object} [body] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateSystemConfigByGroup(group: string, body?: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateSystemConfigByGroup(group, body, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SystemConfigV1alpha1ConsoleApi.updateSystemConfigByGroup']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SystemConfigV1alpha1ConsoleApi - factory interface\n */\nexport const SystemConfigV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SystemConfigV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Get system config by group\n         * @param {SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {\n            return localVarFp.getSystemConfigByGroup(requestParameters.group, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update system config by group\n         * @param {SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.updateSystemConfigByGroup(requestParameters.group, requestParameters.body, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for getSystemConfigByGroup operation in SystemConfigV1alpha1ConsoleApi.\n */\nexport interface SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest {\n    /**\n     * Group of the system config\n     */\n    readonly group: string\n}\n\n/**\n * Request parameters for updateSystemConfigByGroup operation in SystemConfigV1alpha1ConsoleApi.\n */\nexport interface SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest {\n    /**\n     * Group of the system config\n     */\n    readonly group: string\n\n    readonly body?: object\n}\n\n/**\n * SystemConfigV1alpha1ConsoleApi - object-oriented interface\n */\nexport class SystemConfigV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Get system config by group\n     * @param {SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiGetSystemConfigByGroupRequest, options?: RawAxiosRequestConfig) {\n        return SystemConfigV1alpha1ConsoleApiFp(this.configuration).getSystemConfigByGroup(requestParameters.group, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update system config by group\n     * @param {SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateSystemConfigByGroup(requestParameters: SystemConfigV1alpha1ConsoleApiUpdateSystemConfigByGroupRequest, options?: RawAxiosRequestConfig) {\n        return SystemConfigV1alpha1ConsoleApiFp(this.configuration).updateSystemConfigByGroup(requestParameters.group, requestParameters.body, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/system-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { DashboardStats } from '../models';\n/**\n * SystemV1alpha1ConsoleApi - axios parameter creator\n */\nexport const SystemV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Get stats.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStats: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/stats`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SystemV1alpha1ConsoleApi - functional programming interface\n */\nexport const SystemV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SystemV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Get stats.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getStats(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DashboardStats>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getStats(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SystemV1alpha1ConsoleApi.getStats']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SystemV1alpha1ConsoleApi - factory interface\n */\nexport const SystemV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SystemV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Get stats.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStats(options?: RawAxiosRequestConfig): AxiosPromise<DashboardStats> {\n            return localVarFp.getStats(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * SystemV1alpha1ConsoleApi - object-oriented interface\n */\nexport class SystemV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Get stats.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getStats(options?: RawAxiosRequestConfig) {\n        return SystemV1alpha1ConsoleApiFp(this.configuration).getStats(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/system-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { SetupRequest } from '../models';\n// @ts-ignore\nimport type { SiteStatsVo } from '../models';\n/**\n * SystemV1alpha1PublicApi - axios parameter creator\n */\nexport const SystemV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Jump to setup page\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        jumpToSetupPage: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/system/setup`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Gets site stats\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryStats: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.halo.run/v1alpha1/stats/-`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Setup system\n         * @param {SetupRequest} setupRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        setupSystem: async (setupRequest: SetupRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'setupRequest' is not null or undefined\n            assertParamExists('setupSystem', 'setupRequest', setupRequest)\n            const localVarPath = `/system/setup`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(setupRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SystemV1alpha1PublicApi - functional programming interface\n */\nexport const SystemV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SystemV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Jump to setup page\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async jumpToSetupPage(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.jumpToSetupPage(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SystemV1alpha1PublicApi.jumpToSetupPage']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Gets site stats\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryStats(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SiteStatsVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryStats(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SystemV1alpha1PublicApi.queryStats']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Setup system\n         * @param {SetupRequest} setupRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async setupSystem(setupRequest: SetupRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.setupSystem(setupRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['SystemV1alpha1PublicApi.setupSystem']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * SystemV1alpha1PublicApi - factory interface\n */\nexport const SystemV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SystemV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Jump to setup page\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        jumpToSetupPage(options?: RawAxiosRequestConfig): AxiosPromise<string> {\n            return localVarFp.jumpToSetupPage(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Gets site stats\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryStats(options?: RawAxiosRequestConfig): AxiosPromise<SiteStatsVo> {\n            return localVarFp.queryStats(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Setup system\n         * @param {SystemV1alpha1PublicApiSetupSystemRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        setupSystem(requestParameters: SystemV1alpha1PublicApiSetupSystemRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.setupSystem(requestParameters.setupRequest, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for setupSystem operation in SystemV1alpha1PublicApi.\n */\nexport interface SystemV1alpha1PublicApiSetupSystemRequest {\n    readonly setupRequest: SetupRequest\n}\n\n/**\n * SystemV1alpha1PublicApi - object-oriented interface\n */\nexport class SystemV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Jump to setup page\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public jumpToSetupPage(options?: RawAxiosRequestConfig) {\n        return SystemV1alpha1PublicApiFp(this.configuration).jumpToSetupPage(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Gets site stats\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryStats(options?: RawAxiosRequestConfig) {\n        return SystemV1alpha1PublicApiFp(this.configuration).queryStats(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Setup system\n     * @param {SystemV1alpha1PublicApiSetupSystemRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public setupSystem(requestParameters: SystemV1alpha1PublicApiSetupSystemRequest, options?: RawAxiosRequestConfig) {\n        return SystemV1alpha1PublicApiFp(this.configuration).setupSystem(requestParameters.setupRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/tag-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Tag } from '../models';\n// @ts-ignore\nimport type { TagList } from '../models';\n/**\n * TagV1alpha1Api - axios parameter creator\n */\nexport const TagV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Tag\n         * @param {Tag} [tag] Fresh tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTag: async (tag?: Tag, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/tags`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(tag, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Tag\n         * @param {string} name Name of tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTag: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteTag', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/tags/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Tag\n         * @param {string} name Name of tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTag: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getTag', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/tags/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Tag\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listTag: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/content.halo.run/v1alpha1/tags`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Tag\n         * @param {string} name Name of tag\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchTag: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchTag', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/tags/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Tag\n         * @param {string} name Name of tag\n         * @param {Tag} [tag] Updated tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTag: async (name: string, tag?: Tag, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateTag', 'name', name)\n            const localVarPath = `/apis/content.halo.run/v1alpha1/tags/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(tag, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TagV1alpha1Api - functional programming interface\n */\nexport const TagV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TagV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Tag\n         * @param {Tag} [tag] Fresh tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTag(tag?: Tag, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Tag>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTag(tag, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1Api.createTag']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Tag\n         * @param {string} name Name of tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteTag(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTag(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1Api.deleteTag']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Tag\n         * @param {string} name Name of tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTag(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Tag>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTag(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1Api.getTag']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Tag\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listTag(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TagList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listTag(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1Api.listTag']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Tag\n         * @param {string} name Name of tag\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchTag(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Tag>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchTag(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1Api.patchTag']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Tag\n         * @param {string} name Name of tag\n         * @param {Tag} [tag] Updated tag\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateTag(name: string, tag?: Tag, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Tag>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateTag(name, tag, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1Api.updateTag']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * TagV1alpha1Api - factory interface\n */\nexport const TagV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TagV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Tag\n         * @param {TagV1alpha1ApiCreateTagRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTag(requestParameters: TagV1alpha1ApiCreateTagRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Tag> {\n            return localVarFp.createTag(requestParameters.tag, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Tag\n         * @param {TagV1alpha1ApiDeleteTagRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTag(requestParameters: TagV1alpha1ApiDeleteTagRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteTag(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Tag\n         * @param {TagV1alpha1ApiGetTagRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTag(requestParameters: TagV1alpha1ApiGetTagRequest, options?: RawAxiosRequestConfig): AxiosPromise<Tag> {\n            return localVarFp.getTag(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Tag\n         * @param {TagV1alpha1ApiListTagRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listTag(requestParameters: TagV1alpha1ApiListTagRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TagList> {\n            return localVarFp.listTag(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Tag\n         * @param {TagV1alpha1ApiPatchTagRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchTag(requestParameters: TagV1alpha1ApiPatchTagRequest, options?: RawAxiosRequestConfig): AxiosPromise<Tag> {\n            return localVarFp.patchTag(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Tag\n         * @param {TagV1alpha1ApiUpdateTagRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTag(requestParameters: TagV1alpha1ApiUpdateTagRequest, options?: RawAxiosRequestConfig): AxiosPromise<Tag> {\n            return localVarFp.updateTag(requestParameters.name, requestParameters.tag, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createTag operation in TagV1alpha1Api.\n */\nexport interface TagV1alpha1ApiCreateTagRequest {\n    /**\n     * Fresh tag\n     */\n    readonly tag?: Tag\n}\n\n/**\n * Request parameters for deleteTag operation in TagV1alpha1Api.\n */\nexport interface TagV1alpha1ApiDeleteTagRequest {\n    /**\n     * Name of tag\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getTag operation in TagV1alpha1Api.\n */\nexport interface TagV1alpha1ApiGetTagRequest {\n    /**\n     * Name of tag\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listTag operation in TagV1alpha1Api.\n */\nexport interface TagV1alpha1ApiListTagRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchTag operation in TagV1alpha1Api.\n */\nexport interface TagV1alpha1ApiPatchTagRequest {\n    /**\n     * Name of tag\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateTag operation in TagV1alpha1Api.\n */\nexport interface TagV1alpha1ApiUpdateTagRequest {\n    /**\n     * Name of tag\n     */\n    readonly name: string\n\n    /**\n     * Updated tag\n     */\n    readonly tag?: Tag\n}\n\n/**\n * TagV1alpha1Api - object-oriented interface\n */\nexport class TagV1alpha1Api extends BaseAPI {\n    /**\n     * Create Tag\n     * @param {TagV1alpha1ApiCreateTagRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createTag(requestParameters: TagV1alpha1ApiCreateTagRequest = {}, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1ApiFp(this.configuration).createTag(requestParameters.tag, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Tag\n     * @param {TagV1alpha1ApiDeleteTagRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteTag(requestParameters: TagV1alpha1ApiDeleteTagRequest, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1ApiFp(this.configuration).deleteTag(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Tag\n     * @param {TagV1alpha1ApiGetTagRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getTag(requestParameters: TagV1alpha1ApiGetTagRequest, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1ApiFp(this.configuration).getTag(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Tag\n     * @param {TagV1alpha1ApiListTagRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listTag(requestParameters: TagV1alpha1ApiListTagRequest = {}, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1ApiFp(this.configuration).listTag(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Tag\n     * @param {TagV1alpha1ApiPatchTagRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchTag(requestParameters: TagV1alpha1ApiPatchTagRequest, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1ApiFp(this.configuration).patchTag(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Tag\n     * @param {TagV1alpha1ApiUpdateTagRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateTag(requestParameters: TagV1alpha1ApiUpdateTagRequest, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1ApiFp(this.configuration).updateTag(requestParameters.name, requestParameters.tag, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/tag-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { TagList } from '../models';\n/**\n * TagV1alpha1ConsoleApi - axios parameter creator\n */\nexport const TagV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * List Post Tags.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Post tags filtered by keyword.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPostTags: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/tags`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TagV1alpha1ConsoleApi - functional programming interface\n */\nexport const TagV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TagV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * List Post Tags.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Post tags filtered by keyword.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listPostTags(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TagList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listPostTags(page, size, labelSelector, fieldSelector, sort, keyword, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1ConsoleApi.listPostTags']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * TagV1alpha1ConsoleApi - factory interface\n */\nexport const TagV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TagV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * List Post Tags.\n         * @param {TagV1alpha1ConsoleApiListPostTagsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listPostTags(requestParameters: TagV1alpha1ConsoleApiListPostTagsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TagList> {\n            return localVarFp.listPostTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for listPostTags operation in TagV1alpha1ConsoleApi.\n */\nexport interface TagV1alpha1ConsoleApiListPostTagsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Post tags filtered by keyword.\n     */\n    readonly keyword?: string\n}\n\n/**\n * TagV1alpha1ConsoleApi - object-oriented interface\n */\nexport class TagV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * List Post Tags.\n     * @param {TagV1alpha1ConsoleApiListPostTagsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listPostTags(requestParameters: TagV1alpha1ConsoleApiListPostTagsRequest = {}, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1ConsoleApiFp(this.configuration).listPostTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/tag-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ListedPostVo } from '../models';\n// @ts-ignore\nimport type { TagVo } from '../models';\n// @ts-ignore\nimport type { TagVoList } from '../models';\n/**\n * TagV1alpha1PublicApi - axios parameter creator\n */\nexport const TagV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Lists posts by tag name\n         * @param {string} name Tag name\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostsByTagName: async (name: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryPostsByTagName', 'name', name)\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/tags/{name}/posts`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Gets tag by name\n         * @param {string} name Tag name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryTagByName: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('queryTagByName', 'name', name)\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/tags/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Lists tags\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryTags: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.content.halo.run/v1alpha1/tags`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TagV1alpha1PublicApi - functional programming interface\n */\nexport const TagV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TagV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Lists posts by tag name\n         * @param {string} name Tag name\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryPostsByTagName(name: string, page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryPostsByTagName(name, page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1PublicApi.queryPostsByTagName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Gets tag by name\n         * @param {string} name Tag name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryTagByName(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TagVo>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryTagByName(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1PublicApi.queryTagByName']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Lists tags\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async queryTags(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TagVoList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.queryTags(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TagV1alpha1PublicApi.queryTags']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * TagV1alpha1PublicApi - factory interface\n */\nexport const TagV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TagV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Lists posts by tag name\n         * @param {TagV1alpha1PublicApiQueryPostsByTagNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryPostsByTagName(requestParameters: TagV1alpha1PublicApiQueryPostsByTagNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<ListedPostVo> {\n            return localVarFp.queryPostsByTagName(requestParameters.name, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Gets tag by name\n         * @param {TagV1alpha1PublicApiQueryTagByNameRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryTagByName(requestParameters: TagV1alpha1PublicApiQueryTagByNameRequest, options?: RawAxiosRequestConfig): AxiosPromise<TagVo> {\n            return localVarFp.queryTagByName(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Lists tags\n         * @param {TagV1alpha1PublicApiQueryTagsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        queryTags(requestParameters: TagV1alpha1PublicApiQueryTagsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TagVoList> {\n            return localVarFp.queryTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for queryPostsByTagName operation in TagV1alpha1PublicApi.\n */\nexport interface TagV1alpha1PublicApiQueryPostsByTagNameRequest {\n    /**\n     * Tag name\n     */\n    readonly name: string\n\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for queryTagByName operation in TagV1alpha1PublicApi.\n */\nexport interface TagV1alpha1PublicApiQueryTagByNameRequest {\n    /**\n     * Tag name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for queryTags operation in TagV1alpha1PublicApi.\n */\nexport interface TagV1alpha1PublicApiQueryTagsRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * TagV1alpha1PublicApi - object-oriented interface\n */\nexport class TagV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Lists posts by tag name\n     * @param {TagV1alpha1PublicApiQueryPostsByTagNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryPostsByTagName(requestParameters: TagV1alpha1PublicApiQueryPostsByTagNameRequest, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1PublicApiFp(this.configuration).queryPostsByTagName(requestParameters.name, requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Gets tag by name\n     * @param {TagV1alpha1PublicApiQueryTagByNameRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryTagByName(requestParameters: TagV1alpha1PublicApiQueryTagByNameRequest, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1PublicApiFp(this.configuration).queryTagByName(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Lists tags\n     * @param {TagV1alpha1PublicApiQueryTagsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public queryTags(requestParameters: TagV1alpha1PublicApiQueryTagsRequest = {}, options?: RawAxiosRequestConfig) {\n        return TagV1alpha1PublicApiFp(this.configuration).queryTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/theme-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Theme } from '../models';\n// @ts-ignore\nimport type { ThemeList } from '../models';\n/**\n * ThemeV1alpha1Api - axios parameter creator\n */\nexport const ThemeV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Theme\n         * @param {Theme} [theme] Fresh theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTheme: async (theme?: Theme, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/theme.halo.run/v1alpha1/themes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(theme, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Theme\n         * @param {string} name Name of theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTheme: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteTheme', 'name', name)\n            const localVarPath = `/apis/theme.halo.run/v1alpha1/themes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Theme\n         * @param {string} name Name of theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTheme: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getTheme', 'name', name)\n            const localVarPath = `/apis/theme.halo.run/v1alpha1/themes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Theme\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listTheme: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/theme.halo.run/v1alpha1/themes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Theme\n         * @param {string} name Name of theme\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchTheme: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchTheme', 'name', name)\n            const localVarPath = `/apis/theme.halo.run/v1alpha1/themes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Theme\n         * @param {string} name Name of theme\n         * @param {Theme} [theme] Updated theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTheme: async (name: string, theme?: Theme, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateTheme', 'name', name)\n            const localVarPath = `/apis/theme.halo.run/v1alpha1/themes/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(theme, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ThemeV1alpha1Api - functional programming interface\n */\nexport const ThemeV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ThemeV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Theme\n         * @param {Theme} [theme] Fresh theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTheme(theme?: Theme, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTheme(theme, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1Api.createTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Theme\n         * @param {string} name Name of theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteTheme(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTheme(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1Api.deleteTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Theme\n         * @param {string} name Name of theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTheme(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTheme(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1Api.getTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Theme\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listTheme(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ThemeList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listTheme(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1Api.listTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Theme\n         * @param {string} name Name of theme\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchTheme(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchTheme(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1Api.patchTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Theme\n         * @param {string} name Name of theme\n         * @param {Theme} [theme] Updated theme\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateTheme(name: string, theme?: Theme, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateTheme(name, theme, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1Api.updateTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ThemeV1alpha1Api - factory interface\n */\nexport const ThemeV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ThemeV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Theme\n         * @param {ThemeV1alpha1ApiCreateThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTheme(requestParameters: ThemeV1alpha1ApiCreateThemeRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.createTheme(requestParameters.theme, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Theme\n         * @param {ThemeV1alpha1ApiDeleteThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTheme(requestParameters: ThemeV1alpha1ApiDeleteThemeRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteTheme(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Theme\n         * @param {ThemeV1alpha1ApiGetThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTheme(requestParameters: ThemeV1alpha1ApiGetThemeRequest, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.getTheme(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Theme\n         * @param {ThemeV1alpha1ApiListThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listTheme(requestParameters: ThemeV1alpha1ApiListThemeRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ThemeList> {\n            return localVarFp.listTheme(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Theme\n         * @param {ThemeV1alpha1ApiPatchThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchTheme(requestParameters: ThemeV1alpha1ApiPatchThemeRequest, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.patchTheme(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Theme\n         * @param {ThemeV1alpha1ApiUpdateThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTheme(requestParameters: ThemeV1alpha1ApiUpdateThemeRequest, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.updateTheme(requestParameters.name, requestParameters.theme, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createTheme operation in ThemeV1alpha1Api.\n */\nexport interface ThemeV1alpha1ApiCreateThemeRequest {\n    /**\n     * Fresh theme\n     */\n    readonly theme?: Theme\n}\n\n/**\n * Request parameters for deleteTheme operation in ThemeV1alpha1Api.\n */\nexport interface ThemeV1alpha1ApiDeleteThemeRequest {\n    /**\n     * Name of theme\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getTheme operation in ThemeV1alpha1Api.\n */\nexport interface ThemeV1alpha1ApiGetThemeRequest {\n    /**\n     * Name of theme\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listTheme operation in ThemeV1alpha1Api.\n */\nexport interface ThemeV1alpha1ApiListThemeRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchTheme operation in ThemeV1alpha1Api.\n */\nexport interface ThemeV1alpha1ApiPatchThemeRequest {\n    /**\n     * Name of theme\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateTheme operation in ThemeV1alpha1Api.\n */\nexport interface ThemeV1alpha1ApiUpdateThemeRequest {\n    /**\n     * Name of theme\n     */\n    readonly name: string\n\n    /**\n     * Updated theme\n     */\n    readonly theme?: Theme\n}\n\n/**\n * ThemeV1alpha1Api - object-oriented interface\n */\nexport class ThemeV1alpha1Api extends BaseAPI {\n    /**\n     * Create Theme\n     * @param {ThemeV1alpha1ApiCreateThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createTheme(requestParameters: ThemeV1alpha1ApiCreateThemeRequest = {}, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ApiFp(this.configuration).createTheme(requestParameters.theme, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Theme\n     * @param {ThemeV1alpha1ApiDeleteThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteTheme(requestParameters: ThemeV1alpha1ApiDeleteThemeRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ApiFp(this.configuration).deleteTheme(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Theme\n     * @param {ThemeV1alpha1ApiGetThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getTheme(requestParameters: ThemeV1alpha1ApiGetThemeRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ApiFp(this.configuration).getTheme(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Theme\n     * @param {ThemeV1alpha1ApiListThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listTheme(requestParameters: ThemeV1alpha1ApiListThemeRequest = {}, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ApiFp(this.configuration).listTheme(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Theme\n     * @param {ThemeV1alpha1ApiPatchThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchTheme(requestParameters: ThemeV1alpha1ApiPatchThemeRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ApiFp(this.configuration).patchTheme(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Theme\n     * @param {ThemeV1alpha1ApiUpdateThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateTheme(requestParameters: ThemeV1alpha1ApiUpdateThemeRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ApiFp(this.configuration).updateTheme(requestParameters.name, requestParameters.theme, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/theme-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ConfigMap } from '../models';\n// @ts-ignore\nimport type { InstallFromUriRequest } from '../models';\n// @ts-ignore\nimport type { Setting } from '../models';\n// @ts-ignore\nimport type { Theme } from '../models';\n// @ts-ignore\nimport type { ThemeList } from '../models';\n// @ts-ignore\nimport type { UpgradeFromUriRequest } from '../models';\n/**\n * ThemeV1alpha1ConsoleApi - axios parameter creator\n */\nexport const ThemeV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Activate a theme by name.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        activateTheme: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('activateTheme', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/activation`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch the activated theme.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchActivatedTheme: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/-/activation`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch converted json config of theme by configured configMapName.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchThemeJsonConfig: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchThemeJsonConfig', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/json-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Fetch setting of theme.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchThemeSetting: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('fetchThemeSetting', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/setting`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Install a theme by uploading a zip file.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installTheme: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/install`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Install a theme from uri.\n         * @param {InstallFromUriRequest} installFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installThemeFromUri: async (installFromUriRequest: InstallFromUriRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'installFromUriRequest' is not null or undefined\n            assertParamExists('installThemeFromUri', 'installFromUriRequest', installFromUriRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/-/install-from-uri`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(installFromUriRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Invalidate theme template cache.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        invalidateCache: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('invalidateCache', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/invalidate-cache`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List themes.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {boolean} [uninstalled] Whether to list uninstalled themes.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listThemes: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, uninstalled?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (uninstalled !== undefined) {\n                localVarQueryParameter['uninstalled'] = uninstalled;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Reload theme setting.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        reload: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('reload', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/reload`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Reset the configMap of theme setting.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        resetThemeConfig: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('resetThemeConfig', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/reset-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update the configMap of theme setting.\n         * @param {string} name \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateThemeJsonConfig: async (name: string, body: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateThemeJsonConfig', 'name', name)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('updateThemeJsonConfig', 'body', body)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/json-config`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upgrade theme\n         * @param {string} name \n         * @param {File} file \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradeTheme: async (name: string, file: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('upgradeTheme', 'name', name)\n            // verify required parameter 'file' is not null or undefined\n            assertParamExists('upgradeTheme', 'file', file)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Upgrade a theme from uri.\n         * @param {string} name \n         * @param {UpgradeFromUriRequest} upgradeFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradeThemeFromUri: async (name: string, upgradeFromUriRequest: UpgradeFromUriRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('upgradeThemeFromUri', 'name', name)\n            // verify required parameter 'upgradeFromUriRequest' is not null or undefined\n            assertParamExists('upgradeThemeFromUri', 'upgradeFromUriRequest', upgradeFromUriRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade-from-uri`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(upgradeFromUriRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ThemeV1alpha1ConsoleApi - functional programming interface\n */\nexport const ThemeV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ThemeV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Activate a theme by name.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async activateTheme(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.activateTheme(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.activateTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch the activated theme.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchActivatedTheme(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchActivatedTheme(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.fetchActivatedTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch converted json config of theme by configured configMapName.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchThemeJsonConfig(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchThemeJsonConfig(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.fetchThemeJsonConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Fetch setting of theme.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async fetchThemeSetting(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Setting>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.fetchThemeSetting(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.fetchThemeSetting']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Install a theme by uploading a zip file.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async installTheme(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.installTheme(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.installTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Install a theme from uri.\n         * @param {InstallFromUriRequest} installFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async installThemeFromUri(installFromUriRequest: InstallFromUriRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.installThemeFromUri(installFromUriRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.installThemeFromUri']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Invalidate theme template cache.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async invalidateCache(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.invalidateCache(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.invalidateCache']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List themes.\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {boolean} [uninstalled] Whether to list uninstalled themes.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listThemes(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, uninstalled?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ThemeList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listThemes(page, size, labelSelector, fieldSelector, uninstalled, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.listThemes']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Reload theme setting.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async reload(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.reload(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.reload']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Reset the configMap of theme setting.\n         * @param {string} name \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async resetThemeConfig(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConfigMap>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.resetThemeConfig(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.resetThemeConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update the configMap of theme setting.\n         * @param {string} name \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateThemeJsonConfig(name: string, body: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateThemeJsonConfig(name, body, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.updateThemeJsonConfig']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upgrade theme\n         * @param {string} name \n         * @param {File} file \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async upgradeTheme(name: string, file: File, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.upgradeTheme(name, file, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.upgradeTheme']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Upgrade a theme from uri.\n         * @param {string} name \n         * @param {UpgradeFromUriRequest} upgradeFromUriRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async upgradeThemeFromUri(name: string, upgradeFromUriRequest: UpgradeFromUriRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.upgradeThemeFromUri(name, upgradeFromUriRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThemeV1alpha1ConsoleApi.upgradeThemeFromUri']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ThemeV1alpha1ConsoleApi - factory interface\n */\nexport const ThemeV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ThemeV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Activate a theme by name.\n         * @param {ThemeV1alpha1ConsoleApiActivateThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        activateTheme(requestParameters: ThemeV1alpha1ConsoleApiActivateThemeRequest, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.activateTheme(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch the activated theme.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchActivatedTheme(options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.fetchActivatedTheme(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch converted json config of theme by configured configMapName.\n         * @param {ThemeV1alpha1ConsoleApiFetchThemeJsonConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchThemeJsonConfig(requestParameters: ThemeV1alpha1ConsoleApiFetchThemeJsonConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {\n            return localVarFp.fetchThemeJsonConfig(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Fetch setting of theme.\n         * @param {ThemeV1alpha1ConsoleApiFetchThemeSettingRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        fetchThemeSetting(requestParameters: ThemeV1alpha1ConsoleApiFetchThemeSettingRequest, options?: RawAxiosRequestConfig): AxiosPromise<Setting> {\n            return localVarFp.fetchThemeSetting(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Install a theme by uploading a zip file.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installTheme(options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.installTheme(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Install a theme from uri.\n         * @param {ThemeV1alpha1ConsoleApiInstallThemeFromUriRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        installThemeFromUri(requestParameters: ThemeV1alpha1ConsoleApiInstallThemeFromUriRequest, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.installThemeFromUri(requestParameters.installFromUriRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Invalidate theme template cache.\n         * @param {ThemeV1alpha1ConsoleApiInvalidateCacheRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        invalidateCache(requestParameters: ThemeV1alpha1ConsoleApiInvalidateCacheRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.invalidateCache(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List themes.\n         * @param {ThemeV1alpha1ConsoleApiListThemesRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listThemes(requestParameters: ThemeV1alpha1ConsoleApiListThemesRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ThemeList> {\n            return localVarFp.listThemes(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.uninstalled, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Reload theme setting.\n         * @param {ThemeV1alpha1ConsoleApiReloadRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        reload(requestParameters: ThemeV1alpha1ConsoleApiReloadRequest, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.reload(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Reset the configMap of theme setting.\n         * @param {ThemeV1alpha1ConsoleApiResetThemeConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        resetThemeConfig(requestParameters: ThemeV1alpha1ConsoleApiResetThemeConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<ConfigMap> {\n            return localVarFp.resetThemeConfig(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update the configMap of theme setting.\n         * @param {ThemeV1alpha1ConsoleApiUpdateThemeJsonConfigRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateThemeJsonConfig(requestParameters: ThemeV1alpha1ConsoleApiUpdateThemeJsonConfigRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.updateThemeJsonConfig(requestParameters.name, requestParameters.body, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upgrade theme\n         * @param {ThemeV1alpha1ConsoleApiUpgradeThemeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradeTheme(requestParameters: ThemeV1alpha1ConsoleApiUpgradeThemeRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.upgradeTheme(requestParameters.name, requestParameters.file, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Upgrade a theme from uri.\n         * @param {ThemeV1alpha1ConsoleApiUpgradeThemeFromUriRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        upgradeThemeFromUri(requestParameters: ThemeV1alpha1ConsoleApiUpgradeThemeFromUriRequest, options?: RawAxiosRequestConfig): AxiosPromise<Theme> {\n            return localVarFp.upgradeThemeFromUri(requestParameters.name, requestParameters.upgradeFromUriRequest, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for activateTheme operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiActivateThemeRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for fetchThemeJsonConfig operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiFetchThemeJsonConfigRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for fetchThemeSetting operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiFetchThemeSettingRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for installThemeFromUri operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiInstallThemeFromUriRequest {\n    readonly installFromUriRequest: InstallFromUriRequest\n}\n\n/**\n * Request parameters for invalidateCache operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiInvalidateCacheRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for listThemes operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiListThemesRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Whether to list uninstalled themes.\n     */\n    readonly uninstalled?: boolean\n}\n\n/**\n * Request parameters for reload operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiReloadRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for resetThemeConfig operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiResetThemeConfigRequest {\n    readonly name: string\n}\n\n/**\n * Request parameters for updateThemeJsonConfig operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiUpdateThemeJsonConfigRequest {\n    readonly name: string\n\n    readonly body: object\n}\n\n/**\n * Request parameters for upgradeTheme operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiUpgradeThemeRequest {\n    readonly name: string\n\n    readonly file: File\n}\n\n/**\n * Request parameters for upgradeThemeFromUri operation in ThemeV1alpha1ConsoleApi.\n */\nexport interface ThemeV1alpha1ConsoleApiUpgradeThemeFromUriRequest {\n    readonly name: string\n\n    readonly upgradeFromUriRequest: UpgradeFromUriRequest\n}\n\n/**\n * ThemeV1alpha1ConsoleApi - object-oriented interface\n */\nexport class ThemeV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Activate a theme by name.\n     * @param {ThemeV1alpha1ConsoleApiActivateThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public activateTheme(requestParameters: ThemeV1alpha1ConsoleApiActivateThemeRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).activateTheme(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch the activated theme.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchActivatedTheme(options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).fetchActivatedTheme(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch converted json config of theme by configured configMapName.\n     * @param {ThemeV1alpha1ConsoleApiFetchThemeJsonConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchThemeJsonConfig(requestParameters: ThemeV1alpha1ConsoleApiFetchThemeJsonConfigRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).fetchThemeJsonConfig(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Fetch setting of theme.\n     * @param {ThemeV1alpha1ConsoleApiFetchThemeSettingRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public fetchThemeSetting(requestParameters: ThemeV1alpha1ConsoleApiFetchThemeSettingRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).fetchThemeSetting(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Install a theme by uploading a zip file.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public installTheme(options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).installTheme(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Install a theme from uri.\n     * @param {ThemeV1alpha1ConsoleApiInstallThemeFromUriRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public installThemeFromUri(requestParameters: ThemeV1alpha1ConsoleApiInstallThemeFromUriRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).installThemeFromUri(requestParameters.installFromUriRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Invalidate theme template cache.\n     * @param {ThemeV1alpha1ConsoleApiInvalidateCacheRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public invalidateCache(requestParameters: ThemeV1alpha1ConsoleApiInvalidateCacheRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).invalidateCache(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List themes.\n     * @param {ThemeV1alpha1ConsoleApiListThemesRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listThemes(requestParameters: ThemeV1alpha1ConsoleApiListThemesRequest = {}, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).listThemes(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.uninstalled, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Reload theme setting.\n     * @param {ThemeV1alpha1ConsoleApiReloadRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public reload(requestParameters: ThemeV1alpha1ConsoleApiReloadRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).reload(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Reset the configMap of theme setting.\n     * @param {ThemeV1alpha1ConsoleApiResetThemeConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public resetThemeConfig(requestParameters: ThemeV1alpha1ConsoleApiResetThemeConfigRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).resetThemeConfig(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update the configMap of theme setting.\n     * @param {ThemeV1alpha1ConsoleApiUpdateThemeJsonConfigRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateThemeJsonConfig(requestParameters: ThemeV1alpha1ConsoleApiUpdateThemeJsonConfigRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).updateThemeJsonConfig(requestParameters.name, requestParameters.body, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upgrade theme\n     * @param {ThemeV1alpha1ConsoleApiUpgradeThemeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public upgradeTheme(requestParameters: ThemeV1alpha1ConsoleApiUpgradeThemeRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).upgradeTheme(requestParameters.name, requestParameters.file, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Upgrade a theme from uri.\n     * @param {ThemeV1alpha1ConsoleApiUpgradeThemeFromUriRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public upgradeThemeFromUri(requestParameters: ThemeV1alpha1ConsoleApiUpgradeThemeFromUriRequest, options?: RawAxiosRequestConfig) {\n        return ThemeV1alpha1ConsoleApiFp(this.configuration).upgradeThemeFromUri(requestParameters.name, requestParameters.upgradeFromUriRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/thumbnail-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { Thumbnail } from '../models';\n// @ts-ignore\nimport type { ThumbnailList } from '../models';\n/**\n * ThumbnailV1alpha1Api - axios parameter creator\n */\nexport const ThumbnailV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create Thumbnail\n         * @param {Thumbnail} [thumbnail] Fresh thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createThumbnail: async (thumbnail?: Thumbnail, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/thumbnails`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(thumbnail, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteThumbnail: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/thumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getThumbnail: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/thumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List Thumbnail\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listThumbnail: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/thumbnails`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchThumbnail: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/thumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {Thumbnail} [thumbnail] Updated thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateThumbnail: async (name: string, thumbnail?: Thumbnail, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateThumbnail', 'name', name)\n            const localVarPath = `/apis/storage.halo.run/v1alpha1/thumbnails/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(thumbnail, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ThumbnailV1alpha1Api - functional programming interface\n */\nexport const ThumbnailV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ThumbnailV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create Thumbnail\n         * @param {Thumbnail} [thumbnail] Fresh thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createThumbnail(thumbnail?: Thumbnail, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Thumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createThumbnail(thumbnail, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThumbnailV1alpha1Api.createThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteThumbnail(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteThumbnail(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThumbnailV1alpha1Api.deleteThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getThumbnail(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Thumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getThumbnail(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThumbnailV1alpha1Api.getThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List Thumbnail\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listThumbnail(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ThumbnailList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listThumbnail(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThumbnailV1alpha1Api.listThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchThumbnail(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Thumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchThumbnail(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThumbnailV1alpha1Api.patchThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update Thumbnail\n         * @param {string} name Name of thumbnail\n         * @param {Thumbnail} [thumbnail] Updated thumbnail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateThumbnail(name: string, thumbnail?: Thumbnail, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Thumbnail>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateThumbnail(name, thumbnail, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThumbnailV1alpha1Api.updateThumbnail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ThumbnailV1alpha1Api - factory interface\n */\nexport const ThumbnailV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ThumbnailV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create Thumbnail\n         * @param {ThumbnailV1alpha1ApiCreateThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createThumbnail(requestParameters: ThumbnailV1alpha1ApiCreateThumbnailRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<Thumbnail> {\n            return localVarFp.createThumbnail(requestParameters.thumbnail, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete Thumbnail\n         * @param {ThumbnailV1alpha1ApiDeleteThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteThumbnail(requestParameters: ThumbnailV1alpha1ApiDeleteThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteThumbnail(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Thumbnail\n         * @param {ThumbnailV1alpha1ApiGetThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getThumbnail(requestParameters: ThumbnailV1alpha1ApiGetThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<Thumbnail> {\n            return localVarFp.getThumbnail(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List Thumbnail\n         * @param {ThumbnailV1alpha1ApiListThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listThumbnail(requestParameters: ThumbnailV1alpha1ApiListThumbnailRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<ThumbnailList> {\n            return localVarFp.listThumbnail(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch Thumbnail\n         * @param {ThumbnailV1alpha1ApiPatchThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchThumbnail(requestParameters: ThumbnailV1alpha1ApiPatchThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<Thumbnail> {\n            return localVarFp.patchThumbnail(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update Thumbnail\n         * @param {ThumbnailV1alpha1ApiUpdateThumbnailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateThumbnail(requestParameters: ThumbnailV1alpha1ApiUpdateThumbnailRequest, options?: RawAxiosRequestConfig): AxiosPromise<Thumbnail> {\n            return localVarFp.updateThumbnail(requestParameters.name, requestParameters.thumbnail, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createThumbnail operation in ThumbnailV1alpha1Api.\n */\nexport interface ThumbnailV1alpha1ApiCreateThumbnailRequest {\n    /**\n     * Fresh thumbnail\n     */\n    readonly thumbnail?: Thumbnail\n}\n\n/**\n * Request parameters for deleteThumbnail operation in ThumbnailV1alpha1Api.\n */\nexport interface ThumbnailV1alpha1ApiDeleteThumbnailRequest {\n    /**\n     * Name of thumbnail\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getThumbnail operation in ThumbnailV1alpha1Api.\n */\nexport interface ThumbnailV1alpha1ApiGetThumbnailRequest {\n    /**\n     * Name of thumbnail\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listThumbnail operation in ThumbnailV1alpha1Api.\n */\nexport interface ThumbnailV1alpha1ApiListThumbnailRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchThumbnail operation in ThumbnailV1alpha1Api.\n */\nexport interface ThumbnailV1alpha1ApiPatchThumbnailRequest {\n    /**\n     * Name of thumbnail\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateThumbnail operation in ThumbnailV1alpha1Api.\n */\nexport interface ThumbnailV1alpha1ApiUpdateThumbnailRequest {\n    /**\n     * Name of thumbnail\n     */\n    readonly name: string\n\n    /**\n     * Updated thumbnail\n     */\n    readonly thumbnail?: Thumbnail\n}\n\n/**\n * ThumbnailV1alpha1Api - object-oriented interface\n */\nexport class ThumbnailV1alpha1Api extends BaseAPI {\n    /**\n     * Create Thumbnail\n     * @param {ThumbnailV1alpha1ApiCreateThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createThumbnail(requestParameters: ThumbnailV1alpha1ApiCreateThumbnailRequest = {}, options?: RawAxiosRequestConfig) {\n        return ThumbnailV1alpha1ApiFp(this.configuration).createThumbnail(requestParameters.thumbnail, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete Thumbnail\n     * @param {ThumbnailV1alpha1ApiDeleteThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteThumbnail(requestParameters: ThumbnailV1alpha1ApiDeleteThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return ThumbnailV1alpha1ApiFp(this.configuration).deleteThumbnail(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Thumbnail\n     * @param {ThumbnailV1alpha1ApiGetThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getThumbnail(requestParameters: ThumbnailV1alpha1ApiGetThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return ThumbnailV1alpha1ApiFp(this.configuration).getThumbnail(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List Thumbnail\n     * @param {ThumbnailV1alpha1ApiListThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listThumbnail(requestParameters: ThumbnailV1alpha1ApiListThumbnailRequest = {}, options?: RawAxiosRequestConfig) {\n        return ThumbnailV1alpha1ApiFp(this.configuration).listThumbnail(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch Thumbnail\n     * @param {ThumbnailV1alpha1ApiPatchThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchThumbnail(requestParameters: ThumbnailV1alpha1ApiPatchThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return ThumbnailV1alpha1ApiFp(this.configuration).patchThumbnail(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update Thumbnail\n     * @param {ThumbnailV1alpha1ApiUpdateThumbnailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateThumbnail(requestParameters: ThumbnailV1alpha1ApiUpdateThumbnailRequest, options?: RawAxiosRequestConfig) {\n        return ThumbnailV1alpha1ApiFp(this.configuration).updateThumbnail(requestParameters.name, requestParameters.thumbnail, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/thumbnail-v1alpha1-public-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * ThumbnailV1alpha1PublicApi - axios parameter creator\n */\nexport const ThumbnailV1alpha1PublicApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Get thumbnail by URI\n         * @param {string} uri The URI of the image\n         * @param {GetThumbnailByUriSizeEnum} size The size of the thumbnail\n         * @param {GetThumbnailByUriWidthEnum} [width] The width of the thumbnail, if \\&#39;size\\&#39; is not provided, this parameter will be used to determine the size\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getThumbnailByUri: async (uri: string, size: GetThumbnailByUriSizeEnum, width?: GetThumbnailByUriWidthEnum, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'uri' is not null or undefined\n            assertParamExists('getThumbnailByUri', 'uri', uri)\n            // verify required parameter 'size' is not null or undefined\n            assertParamExists('getThumbnailByUri', 'size', size)\n            const localVarPath = `/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (uri !== undefined) {\n                localVarQueryParameter['uri'] = uri;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (width !== undefined) {\n                localVarQueryParameter['width'] = width;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ThumbnailV1alpha1PublicApi - functional programming interface\n */\nexport const ThumbnailV1alpha1PublicApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ThumbnailV1alpha1PublicApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Get thumbnail by URI\n         * @param {string} uri The URI of the image\n         * @param {GetThumbnailByUriSizeEnum} size The size of the thumbnail\n         * @param {GetThumbnailByUriWidthEnum} [width] The width of the thumbnail, if \\&#39;size\\&#39; is not provided, this parameter will be used to determine the size\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getThumbnailByUri(uri: string, size: GetThumbnailByUriSizeEnum, width?: GetThumbnailByUriWidthEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getThumbnailByUri(uri, size, width, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['ThumbnailV1alpha1PublicApi.getThumbnailByUri']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * ThumbnailV1alpha1PublicApi - factory interface\n */\nexport const ThumbnailV1alpha1PublicApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ThumbnailV1alpha1PublicApiFp(configuration)\n    return {\n        /**\n         * Get thumbnail by URI\n         * @param {ThumbnailV1alpha1PublicApiGetThumbnailByUriRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getThumbnailByUri(requestParameters: ThumbnailV1alpha1PublicApiGetThumbnailByUriRequest, options?: RawAxiosRequestConfig): AxiosPromise<File> {\n            return localVarFp.getThumbnailByUri(requestParameters.uri, requestParameters.size, requestParameters.width, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for getThumbnailByUri operation in ThumbnailV1alpha1PublicApi.\n */\nexport interface ThumbnailV1alpha1PublicApiGetThumbnailByUriRequest {\n    /**\n     * The URI of the image\n     */\n    readonly uri: string\n\n    /**\n     * The size of the thumbnail\n     */\n    readonly size: GetThumbnailByUriSizeEnum\n\n    /**\n     * The width of the thumbnail, if \\&#39;size\\&#39; is not provided, this parameter will be used to determine the size\n     */\n    readonly width?: GetThumbnailByUriWidthEnum\n}\n\n/**\n * ThumbnailV1alpha1PublicApi - object-oriented interface\n */\nexport class ThumbnailV1alpha1PublicApi extends BaseAPI {\n    /**\n     * Get thumbnail by URI\n     * @param {ThumbnailV1alpha1PublicApiGetThumbnailByUriRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getThumbnailByUri(requestParameters: ThumbnailV1alpha1PublicApiGetThumbnailByUriRequest, options?: RawAxiosRequestConfig) {\n        return ThumbnailV1alpha1PublicApiFp(this.configuration).getThumbnailByUri(requestParameters.uri, requestParameters.size, requestParameters.width, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\nexport const GetThumbnailByUriSizeEnum = {\n    S: 'S',\n    M: 'M',\n    L: 'L',\n    Xl: 'XL'\n} as const;\nexport type GetThumbnailByUriSizeEnum = typeof GetThumbnailByUriSizeEnum[keyof typeof GetThumbnailByUriSizeEnum];\nexport const GetThumbnailByUriWidthEnum = {\n    NUMBER_400: 400,\n    NUMBER_800: 800,\n    NUMBER_1200: 1200,\n    NUMBER_1600: 1600\n} as const;\nexport type GetThumbnailByUriWidthEnum = typeof GetThumbnailByUriWidthEnum[keyof typeof GetThumbnailByUriWidthEnum];\n"
  },
  {
    "path": "ui/packages/api-client/src/api/two-factor-auth-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { PasswordRequest } from '../models';\n// @ts-ignore\nimport type { TotpAuthLinkResponse } from '../models';\n// @ts-ignore\nimport type { TotpRequest } from '../models';\n// @ts-ignore\nimport type { TwoFactorAuthSettings } from '../models';\n/**\n * TwoFactorAuthV1alpha1UcApi - axios parameter creator\n */\nexport const TwoFactorAuthV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Configure a TOTP\n         * @param {TotpRequest} [totpRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        configurerTotp: async (totpRequest?: TotpRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(totpRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {PasswordRequest} [passwordRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTotp: async (passwordRequest?: PasswordRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp/-`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(passwordRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Disable Two-factor authentication\n         * @param {PasswordRequest} [passwordRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disableTwoFactor: async (passwordRequest?: PasswordRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings/disabled`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(passwordRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Enable Two-factor authentication\n         * @param {PasswordRequest} [passwordRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        enableTwoFactor: async (passwordRequest?: PasswordRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings/enabled`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(passwordRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get TOTP auth link, including secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTotpAuthLink: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/totp/auth-link`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get Two-factor authentication settings.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTwoFactorAuthenticationSettings: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/uc.api.security.halo.run/v1alpha1/authentications/two-factor/settings`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TwoFactorAuthV1alpha1UcApi - functional programming interface\n */\nexport const TwoFactorAuthV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TwoFactorAuthV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Configure a TOTP\n         * @param {TotpRequest} [totpRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async configurerTotp(totpRequest?: TotpRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TwoFactorAuthSettings>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.configurerTotp(totpRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TwoFactorAuthV1alpha1UcApi.configurerTotp']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * \n         * @param {PasswordRequest} [passwordRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteTotp(passwordRequest?: PasswordRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TwoFactorAuthSettings>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTotp(passwordRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TwoFactorAuthV1alpha1UcApi.deleteTotp']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Disable Two-factor authentication\n         * @param {PasswordRequest} [passwordRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async disableTwoFactor(passwordRequest?: PasswordRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TwoFactorAuthSettings>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.disableTwoFactor(passwordRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TwoFactorAuthV1alpha1UcApi.disableTwoFactor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Enable Two-factor authentication\n         * @param {PasswordRequest} [passwordRequest] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async enableTwoFactor(passwordRequest?: PasswordRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TwoFactorAuthSettings>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.enableTwoFactor(passwordRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TwoFactorAuthV1alpha1UcApi.enableTwoFactor']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get TOTP auth link, including secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTotpAuthLink(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TotpAuthLinkResponse>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTotpAuthLink(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TwoFactorAuthV1alpha1UcApi.getTotpAuthLink']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get Two-factor authentication settings.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTwoFactorAuthenticationSettings(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TwoFactorAuthSettings>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTwoFactorAuthenticationSettings(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['TwoFactorAuthV1alpha1UcApi.getTwoFactorAuthenticationSettings']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * TwoFactorAuthV1alpha1UcApi - factory interface\n */\nexport const TwoFactorAuthV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TwoFactorAuthV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Configure a TOTP\n         * @param {TwoFactorAuthV1alpha1UcApiConfigurerTotpRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        configurerTotp(requestParameters: TwoFactorAuthV1alpha1UcApiConfigurerTotpRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TwoFactorAuthSettings> {\n            return localVarFp.configurerTotp(requestParameters.totpRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {TwoFactorAuthV1alpha1UcApiDeleteTotpRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTotp(requestParameters: TwoFactorAuthV1alpha1UcApiDeleteTotpRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TwoFactorAuthSettings> {\n            return localVarFp.deleteTotp(requestParameters.passwordRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Disable Two-factor authentication\n         * @param {TwoFactorAuthV1alpha1UcApiDisableTwoFactorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disableTwoFactor(requestParameters: TwoFactorAuthV1alpha1UcApiDisableTwoFactorRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TwoFactorAuthSettings> {\n            return localVarFp.disableTwoFactor(requestParameters.passwordRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Enable Two-factor authentication\n         * @param {TwoFactorAuthV1alpha1UcApiEnableTwoFactorRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        enableTwoFactor(requestParameters: TwoFactorAuthV1alpha1UcApiEnableTwoFactorRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TwoFactorAuthSettings> {\n            return localVarFp.enableTwoFactor(requestParameters.passwordRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get TOTP auth link, including secret\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTotpAuthLink(options?: RawAxiosRequestConfig): AxiosPromise<TotpAuthLinkResponse> {\n            return localVarFp.getTotpAuthLink(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get Two-factor authentication settings.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTwoFactorAuthenticationSettings(options?: RawAxiosRequestConfig): AxiosPromise<TwoFactorAuthSettings> {\n            return localVarFp.getTwoFactorAuthenticationSettings(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for configurerTotp operation in TwoFactorAuthV1alpha1UcApi.\n */\nexport interface TwoFactorAuthV1alpha1UcApiConfigurerTotpRequest {\n    readonly totpRequest?: TotpRequest\n}\n\n/**\n * Request parameters for deleteTotp operation in TwoFactorAuthV1alpha1UcApi.\n */\nexport interface TwoFactorAuthV1alpha1UcApiDeleteTotpRequest {\n    readonly passwordRequest?: PasswordRequest\n}\n\n/**\n * Request parameters for disableTwoFactor operation in TwoFactorAuthV1alpha1UcApi.\n */\nexport interface TwoFactorAuthV1alpha1UcApiDisableTwoFactorRequest {\n    readonly passwordRequest?: PasswordRequest\n}\n\n/**\n * Request parameters for enableTwoFactor operation in TwoFactorAuthV1alpha1UcApi.\n */\nexport interface TwoFactorAuthV1alpha1UcApiEnableTwoFactorRequest {\n    readonly passwordRequest?: PasswordRequest\n}\n\n/**\n * TwoFactorAuthV1alpha1UcApi - object-oriented interface\n */\nexport class TwoFactorAuthV1alpha1UcApi extends BaseAPI {\n    /**\n     * Configure a TOTP\n     * @param {TwoFactorAuthV1alpha1UcApiConfigurerTotpRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public configurerTotp(requestParameters: TwoFactorAuthV1alpha1UcApiConfigurerTotpRequest = {}, options?: RawAxiosRequestConfig) {\n        return TwoFactorAuthV1alpha1UcApiFp(this.configuration).configurerTotp(requestParameters.totpRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {TwoFactorAuthV1alpha1UcApiDeleteTotpRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteTotp(requestParameters: TwoFactorAuthV1alpha1UcApiDeleteTotpRequest = {}, options?: RawAxiosRequestConfig) {\n        return TwoFactorAuthV1alpha1UcApiFp(this.configuration).deleteTotp(requestParameters.passwordRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Disable Two-factor authentication\n     * @param {TwoFactorAuthV1alpha1UcApiDisableTwoFactorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public disableTwoFactor(requestParameters: TwoFactorAuthV1alpha1UcApiDisableTwoFactorRequest = {}, options?: RawAxiosRequestConfig) {\n        return TwoFactorAuthV1alpha1UcApiFp(this.configuration).disableTwoFactor(requestParameters.passwordRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Enable Two-factor authentication\n     * @param {TwoFactorAuthV1alpha1UcApiEnableTwoFactorRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public enableTwoFactor(requestParameters: TwoFactorAuthV1alpha1UcApiEnableTwoFactorRequest = {}, options?: RawAxiosRequestConfig) {\n        return TwoFactorAuthV1alpha1UcApiFp(this.configuration).enableTwoFactor(requestParameters.passwordRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get TOTP auth link, including secret\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getTotpAuthLink(options?: RawAxiosRequestConfig) {\n        return TwoFactorAuthV1alpha1UcApiFp(this.configuration).getTotpAuthLink(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get Two-factor authentication settings.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getTwoFactorAuthenticationSettings(options?: RawAxiosRequestConfig) {\n        return TwoFactorAuthV1alpha1UcApiFp(this.configuration).getTwoFactorAuthenticationSettings(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/user-connection-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { UserConnection } from '../models';\n// @ts-ignore\nimport type { UserConnectionList } from '../models';\n/**\n * UserConnectionV1alpha1Api - axios parameter creator\n */\nexport const UserConnectionV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create UserConnection\n         * @param {UserConnection} [userConnection] Fresh userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUserConnection: async (userConnection?: UserConnection, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/userconnections`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(userConnection, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete UserConnection\n         * @param {string} name Name of userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUserConnection: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteUserConnection', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/userconnections/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get UserConnection\n         * @param {string} name Name of userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserConnection: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getUserConnection', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/userconnections/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List UserConnection\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUserConnection: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/userconnections`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch UserConnection\n         * @param {string} name Name of userconnection\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchUserConnection: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchUserConnection', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/userconnections/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update UserConnection\n         * @param {string} name Name of userconnection\n         * @param {UserConnection} [userConnection] Updated userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUserConnection: async (name: string, userConnection?: UserConnection, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateUserConnection', 'name', name)\n            const localVarPath = `/apis/auth.halo.run/v1alpha1/userconnections/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(userConnection, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UserConnectionV1alpha1Api - functional programming interface\n */\nexport const UserConnectionV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UserConnectionV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create UserConnection\n         * @param {UserConnection} [userConnection] Fresh userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createUserConnection(userConnection?: UserConnection, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserConnection>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createUserConnection(userConnection, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1Api.createUserConnection']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete UserConnection\n         * @param {string} name Name of userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteUserConnection(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUserConnection(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1Api.deleteUserConnection']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get UserConnection\n         * @param {string} name Name of userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUserConnection(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserConnection>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserConnection(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1Api.getUserConnection']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List UserConnection\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listUserConnection(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserConnectionList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listUserConnection(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1Api.listUserConnection']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch UserConnection\n         * @param {string} name Name of userconnection\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchUserConnection(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserConnection>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchUserConnection(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1Api.patchUserConnection']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update UserConnection\n         * @param {string} name Name of userconnection\n         * @param {UserConnection} [userConnection] Updated userconnection\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateUserConnection(name: string, userConnection?: UserConnection, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserConnection>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateUserConnection(name, userConnection, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1Api.updateUserConnection']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * UserConnectionV1alpha1Api - factory interface\n */\nexport const UserConnectionV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UserConnectionV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create UserConnection\n         * @param {UserConnectionV1alpha1ApiCreateUserConnectionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUserConnection(requestParameters: UserConnectionV1alpha1ApiCreateUserConnectionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<UserConnection> {\n            return localVarFp.createUserConnection(requestParameters.userConnection, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete UserConnection\n         * @param {UserConnectionV1alpha1ApiDeleteUserConnectionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUserConnection(requestParameters: UserConnectionV1alpha1ApiDeleteUserConnectionRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteUserConnection(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get UserConnection\n         * @param {UserConnectionV1alpha1ApiGetUserConnectionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserConnection(requestParameters: UserConnectionV1alpha1ApiGetUserConnectionRequest, options?: RawAxiosRequestConfig): AxiosPromise<UserConnection> {\n            return localVarFp.getUserConnection(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List UserConnection\n         * @param {UserConnectionV1alpha1ApiListUserConnectionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUserConnection(requestParameters: UserConnectionV1alpha1ApiListUserConnectionRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<UserConnectionList> {\n            return localVarFp.listUserConnection(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch UserConnection\n         * @param {UserConnectionV1alpha1ApiPatchUserConnectionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchUserConnection(requestParameters: UserConnectionV1alpha1ApiPatchUserConnectionRequest, options?: RawAxiosRequestConfig): AxiosPromise<UserConnection> {\n            return localVarFp.patchUserConnection(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update UserConnection\n         * @param {UserConnectionV1alpha1ApiUpdateUserConnectionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUserConnection(requestParameters: UserConnectionV1alpha1ApiUpdateUserConnectionRequest, options?: RawAxiosRequestConfig): AxiosPromise<UserConnection> {\n            return localVarFp.updateUserConnection(requestParameters.name, requestParameters.userConnection, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createUserConnection operation in UserConnectionV1alpha1Api.\n */\nexport interface UserConnectionV1alpha1ApiCreateUserConnectionRequest {\n    /**\n     * Fresh userconnection\n     */\n    readonly userConnection?: UserConnection\n}\n\n/**\n * Request parameters for deleteUserConnection operation in UserConnectionV1alpha1Api.\n */\nexport interface UserConnectionV1alpha1ApiDeleteUserConnectionRequest {\n    /**\n     * Name of userconnection\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getUserConnection operation in UserConnectionV1alpha1Api.\n */\nexport interface UserConnectionV1alpha1ApiGetUserConnectionRequest {\n    /**\n     * Name of userconnection\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listUserConnection operation in UserConnectionV1alpha1Api.\n */\nexport interface UserConnectionV1alpha1ApiListUserConnectionRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchUserConnection operation in UserConnectionV1alpha1Api.\n */\nexport interface UserConnectionV1alpha1ApiPatchUserConnectionRequest {\n    /**\n     * Name of userconnection\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateUserConnection operation in UserConnectionV1alpha1Api.\n */\nexport interface UserConnectionV1alpha1ApiUpdateUserConnectionRequest {\n    /**\n     * Name of userconnection\n     */\n    readonly name: string\n\n    /**\n     * Updated userconnection\n     */\n    readonly userConnection?: UserConnection\n}\n\n/**\n * UserConnectionV1alpha1Api - object-oriented interface\n */\nexport class UserConnectionV1alpha1Api extends BaseAPI {\n    /**\n     * Create UserConnection\n     * @param {UserConnectionV1alpha1ApiCreateUserConnectionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createUserConnection(requestParameters: UserConnectionV1alpha1ApiCreateUserConnectionRequest = {}, options?: RawAxiosRequestConfig) {\n        return UserConnectionV1alpha1ApiFp(this.configuration).createUserConnection(requestParameters.userConnection, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete UserConnection\n     * @param {UserConnectionV1alpha1ApiDeleteUserConnectionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteUserConnection(requestParameters: UserConnectionV1alpha1ApiDeleteUserConnectionRequest, options?: RawAxiosRequestConfig) {\n        return UserConnectionV1alpha1ApiFp(this.configuration).deleteUserConnection(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get UserConnection\n     * @param {UserConnectionV1alpha1ApiGetUserConnectionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getUserConnection(requestParameters: UserConnectionV1alpha1ApiGetUserConnectionRequest, options?: RawAxiosRequestConfig) {\n        return UserConnectionV1alpha1ApiFp(this.configuration).getUserConnection(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List UserConnection\n     * @param {UserConnectionV1alpha1ApiListUserConnectionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listUserConnection(requestParameters: UserConnectionV1alpha1ApiListUserConnectionRequest = {}, options?: RawAxiosRequestConfig) {\n        return UserConnectionV1alpha1ApiFp(this.configuration).listUserConnection(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch UserConnection\n     * @param {UserConnectionV1alpha1ApiPatchUserConnectionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchUserConnection(requestParameters: UserConnectionV1alpha1ApiPatchUserConnectionRequest, options?: RawAxiosRequestConfig) {\n        return UserConnectionV1alpha1ApiFp(this.configuration).patchUserConnection(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update UserConnection\n     * @param {UserConnectionV1alpha1ApiUpdateUserConnectionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateUserConnection(requestParameters: UserConnectionV1alpha1ApiUpdateUserConnectionRequest, options?: RawAxiosRequestConfig) {\n        return UserConnectionV1alpha1ApiFp(this.configuration).updateUserConnection(requestParameters.name, requestParameters.userConnection, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/user-connection-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { UserConnection } from '../models';\n/**\n * UserConnectionV1alpha1UcApi - axios parameter creator\n */\nexport const UserConnectionV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Disconnect my connection from a third-party platform.\n         * @param {string} registerId The registration ID of the third-party platform.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disconnectMyConnection: async (registerId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'registerId' is not null or undefined\n            assertParamExists('disconnectMyConnection', 'registerId', registerId)\n            const localVarPath = `/apis/uc.api.auth.halo.run/v1alpha1/user-connections/{registerId}/disconnect`\n                .replace(`{${\"registerId\"}}`, encodeURIComponent(String(registerId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UserConnectionV1alpha1UcApi - functional programming interface\n */\nexport const UserConnectionV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UserConnectionV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Disconnect my connection from a third-party platform.\n         * @param {string} registerId The registration ID of the third-party platform.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async disconnectMyConnection(registerId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserConnection>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.disconnectMyConnection(registerId, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1UcApi.disconnectMyConnection']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * UserConnectionV1alpha1UcApi - factory interface\n */\nexport const UserConnectionV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UserConnectionV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Disconnect my connection from a third-party platform.\n         * @param {UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disconnectMyConnection(requestParameters: UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<UserConnection>> {\n            return localVarFp.disconnectMyConnection(requestParameters.registerId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for disconnectMyConnection operation in UserConnectionV1alpha1UcApi.\n */\nexport interface UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest {\n    /**\n     * The registration ID of the third-party platform.\n     */\n    readonly registerId: string\n}\n\n/**\n * UserConnectionV1alpha1UcApi - object-oriented interface\n */\nexport class UserConnectionV1alpha1UcApi extends BaseAPI {\n    /**\n     * Disconnect my connection from a third-party platform.\n     * @param {UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public disconnectMyConnection(requestParameters: UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest, options?: RawAxiosRequestConfig) {\n        return UserConnectionV1alpha1UcApiFp(this.configuration).disconnectMyConnection(requestParameters.registerId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/user-preference-v1alpha1-uc-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n/**\n * UserPreferenceV1alpha1UcApi - axios parameter creator\n */\nexport const UserPreferenceV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Get my preference by group.\n         * @param {string} group Group of user preference, e.g. &#x60;notification&#x60;.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyPreference: async (group: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'group' is not null or undefined\n            assertParamExists('getMyPreference', 'group', group)\n            const localVarPath = `/apis/uc.api.halo.run/v1alpha1/user-preferences/{group}`\n                .replace(`{${\"group\"}}`, encodeURIComponent(String(group)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Create or update my preference by group.\n         * @param {string} group Group of user preference, e.g. &#x60;notification&#x60;.\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMyPreference: async (group: string, body: object, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'group' is not null or undefined\n            assertParamExists('updateMyPreference', 'group', group)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('updateMyPreference', 'body', body)\n            const localVarPath = `/apis/uc.api.halo.run/v1alpha1/user-preferences/{group}`\n                .replace(`{${\"group\"}}`, encodeURIComponent(String(group)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UserPreferenceV1alpha1UcApi - functional programming interface\n */\nexport const UserPreferenceV1alpha1UcApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UserPreferenceV1alpha1UcApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Get my preference by group.\n         * @param {string} group Group of user preference, e.g. &#x60;notification&#x60;.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMyPreference(group: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMyPreference(group, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserPreferenceV1alpha1UcApi.getMyPreference']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Create or update my preference by group.\n         * @param {string} group Group of user preference, e.g. &#x60;notification&#x60;.\n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateMyPreference(group: string, body: object, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateMyPreference(group, body, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserPreferenceV1alpha1UcApi.updateMyPreference']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * UserPreferenceV1alpha1UcApi - factory interface\n */\nexport const UserPreferenceV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UserPreferenceV1alpha1UcApiFp(configuration)\n    return {\n        /**\n         * Get my preference by group.\n         * @param {UserPreferenceV1alpha1UcApiGetMyPreferenceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyPreference(requestParameters: UserPreferenceV1alpha1UcApiGetMyPreferenceRequest, options?: RawAxiosRequestConfig): AxiosPromise<object> {\n            return localVarFp.getMyPreference(requestParameters.group, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Create or update my preference by group.\n         * @param {UserPreferenceV1alpha1UcApiUpdateMyPreferenceRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateMyPreference(requestParameters: UserPreferenceV1alpha1UcApiUpdateMyPreferenceRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.updateMyPreference(requestParameters.group, requestParameters.body, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for getMyPreference operation in UserPreferenceV1alpha1UcApi.\n */\nexport interface UserPreferenceV1alpha1UcApiGetMyPreferenceRequest {\n    /**\n     * Group of user preference, e.g. &#x60;notification&#x60;.\n     */\n    readonly group: string\n}\n\n/**\n * Request parameters for updateMyPreference operation in UserPreferenceV1alpha1UcApi.\n */\nexport interface UserPreferenceV1alpha1UcApiUpdateMyPreferenceRequest {\n    /**\n     * Group of user preference, e.g. &#x60;notification&#x60;.\n     */\n    readonly group: string\n\n    readonly body: object\n}\n\n/**\n * UserPreferenceV1alpha1UcApi - object-oriented interface\n */\nexport class UserPreferenceV1alpha1UcApi extends BaseAPI {\n    /**\n     * Get my preference by group.\n     * @param {UserPreferenceV1alpha1UcApiGetMyPreferenceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getMyPreference(requestParameters: UserPreferenceV1alpha1UcApiGetMyPreferenceRequest, options?: RawAxiosRequestConfig) {\n        return UserPreferenceV1alpha1UcApiFp(this.configuration).getMyPreference(requestParameters.group, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Create or update my preference by group.\n     * @param {UserPreferenceV1alpha1UcApiUpdateMyPreferenceRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateMyPreference(requestParameters: UserPreferenceV1alpha1UcApiUpdateMyPreferenceRequest, options?: RawAxiosRequestConfig) {\n        return UserPreferenceV1alpha1UcApiFp(this.configuration).updateMyPreference(requestParameters.group, requestParameters.body, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/user-v1alpha1-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { JsonPatchInner } from '../models';\n// @ts-ignore\nimport type { User } from '../models';\n// @ts-ignore\nimport type { UserList } from '../models';\n/**\n * UserV1alpha1Api - axios parameter creator\n */\nexport const UserV1alpha1ApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Create User\n         * @param {User} [user] Fresh user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUser: async (user?: User, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/users`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Delete User\n         * @param {string} name Name of user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUser: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteUser', 'name', name)\n            const localVarPath = `/api/v1alpha1/users/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get User\n         * @param {string} name Name of user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUser: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getUser', 'name', name)\n            const localVarPath = `/api/v1alpha1/users/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List User\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUser: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/api/v1alpha1/users`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Patch User\n         * @param {string} name Name of user\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchUser: async (name: string, jsonPatchInner?: Array<JsonPatchInner>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('patchUser', 'name', name)\n            const localVarPath = `/api/v1alpha1/users/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(jsonPatchInner, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update User\n         * @param {string} name Name of user\n         * @param {User} [user] Updated user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUser: async (name: string, user?: User, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('updateUser', 'name', name)\n            const localVarPath = `/api/v1alpha1/users/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UserV1alpha1Api - functional programming interface\n */\nexport const UserV1alpha1ApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UserV1alpha1ApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Create User\n         * @param {User} [user] Fresh user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createUser(user?: User, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(user, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1Api.createUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Delete User\n         * @param {string} name Name of user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteUser(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1Api.deleteUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get User\n         * @param {string} name Name of user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUser(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUser(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1Api.getUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List User\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listUser(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listUser(page, size, labelSelector, fieldSelector, sort, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1Api.listUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Patch User\n         * @param {string} name Name of user\n         * @param {Array<JsonPatchInner>} [jsonPatchInner] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async patchUser(name: string, jsonPatchInner?: Array<JsonPatchInner>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.patchUser(name, jsonPatchInner, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1Api.patchUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update User\n         * @param {string} name Name of user\n         * @param {User} [user] Updated user\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateUser(name: string, user?: User, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateUser(name, user, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1Api.updateUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * UserV1alpha1Api - factory interface\n */\nexport const UserV1alpha1ApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UserV1alpha1ApiFp(configuration)\n    return {\n        /**\n         * Create User\n         * @param {UserV1alpha1ApiCreateUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUser(requestParameters: UserV1alpha1ApiCreateUserRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.createUser(requestParameters.user, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Delete User\n         * @param {UserV1alpha1ApiDeleteUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUser(requestParameters: UserV1alpha1ApiDeleteUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.deleteUser(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get User\n         * @param {UserV1alpha1ApiGetUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUser(requestParameters: UserV1alpha1ApiGetUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.getUser(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List User\n         * @param {UserV1alpha1ApiListUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUser(requestParameters: UserV1alpha1ApiListUserRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<UserList> {\n            return localVarFp.listUser(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Patch User\n         * @param {UserV1alpha1ApiPatchUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        patchUser(requestParameters: UserV1alpha1ApiPatchUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.patchUser(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update User\n         * @param {UserV1alpha1ApiUpdateUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUser(requestParameters: UserV1alpha1ApiUpdateUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.updateUser(requestParameters.name, requestParameters.user, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for createUser operation in UserV1alpha1Api.\n */\nexport interface UserV1alpha1ApiCreateUserRequest {\n    /**\n     * Fresh user\n     */\n    readonly user?: User\n}\n\n/**\n * Request parameters for deleteUser operation in UserV1alpha1Api.\n */\nexport interface UserV1alpha1ApiDeleteUserRequest {\n    /**\n     * Name of user\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getUser operation in UserV1alpha1Api.\n */\nexport interface UserV1alpha1ApiGetUserRequest {\n    /**\n     * Name of user\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for listUser operation in UserV1alpha1Api.\n */\nexport interface UserV1alpha1ApiListUserRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n}\n\n/**\n * Request parameters for patchUser operation in UserV1alpha1Api.\n */\nexport interface UserV1alpha1ApiPatchUserRequest {\n    /**\n     * Name of user\n     */\n    readonly name: string\n\n    readonly jsonPatchInner?: Array<JsonPatchInner>\n}\n\n/**\n * Request parameters for updateUser operation in UserV1alpha1Api.\n */\nexport interface UserV1alpha1ApiUpdateUserRequest {\n    /**\n     * Name of user\n     */\n    readonly name: string\n\n    /**\n     * Updated user\n     */\n    readonly user?: User\n}\n\n/**\n * UserV1alpha1Api - object-oriented interface\n */\nexport class UserV1alpha1Api extends BaseAPI {\n    /**\n     * Create User\n     * @param {UserV1alpha1ApiCreateUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createUser(requestParameters: UserV1alpha1ApiCreateUserRequest = {}, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ApiFp(this.configuration).createUser(requestParameters.user, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Delete User\n     * @param {UserV1alpha1ApiDeleteUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteUser(requestParameters: UserV1alpha1ApiDeleteUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ApiFp(this.configuration).deleteUser(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get User\n     * @param {UserV1alpha1ApiGetUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getUser(requestParameters: UserV1alpha1ApiGetUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ApiFp(this.configuration).getUser(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List User\n     * @param {UserV1alpha1ApiListUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listUser(requestParameters: UserV1alpha1ApiListUserRequest = {}, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ApiFp(this.configuration).listUser(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Patch User\n     * @param {UserV1alpha1ApiPatchUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public patchUser(requestParameters: UserV1alpha1ApiPatchUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ApiFp(this.configuration).patchUser(requestParameters.name, requestParameters.jsonPatchInner, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update User\n     * @param {UserV1alpha1ApiUpdateUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateUser(requestParameters: UserV1alpha1ApiUpdateUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ApiFp(this.configuration).updateUser(requestParameters.name, requestParameters.user, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api/user-v1alpha1-console-api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from '../configuration';\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';\n// @ts-ignore\nimport type { ChangeOwnPasswordRequest } from '../models';\n// @ts-ignore\nimport type { ChangePasswordRequest } from '../models';\n// @ts-ignore\nimport type { CreateUserRequest } from '../models';\n// @ts-ignore\nimport type { DetailedUser } from '../models';\n// @ts-ignore\nimport type { EmailVerifyRequest } from '../models';\n// @ts-ignore\nimport type { GrantRequest } from '../models';\n// @ts-ignore\nimport type { User } from '../models';\n// @ts-ignore\nimport type { UserEndpointListedUserList } from '../models';\n// @ts-ignore\nimport type { UserPermission } from '../models';\n// @ts-ignore\nimport type { VerifyCodeRequest } from '../models';\n/**\n * UserV1alpha1ConsoleApi - axios parameter creator\n */\nexport const UserV1alpha1ConsoleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * Change anyone password of user for admin.\n         * @param {string} name Name of user. If the name is equal to \\&#39;-\\&#39;, it will change the password of current user.\n         * @param {ChangePasswordRequest} changePasswordRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changeAnyonePassword: async (name: string, changePasswordRequest: ChangePasswordRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('changeAnyonePassword', 'name', name)\n            // verify required parameter 'changePasswordRequest' is not null or undefined\n            assertParamExists('changeAnyonePassword', 'changePasswordRequest', changePasswordRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/{name}/password`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(changePasswordRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Change own password of user.\n         * @param {ChangeOwnPasswordRequest} changeOwnPasswordRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changeOwnPassword: async (changeOwnPasswordRequest: ChangeOwnPasswordRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'changeOwnPasswordRequest' is not null or undefined\n            assertParamExists('changeOwnPassword', 'changeOwnPasswordRequest', changeOwnPasswordRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/-/password`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(changeOwnPasswordRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Creates a new user.\n         * @param {CreateUserRequest} createUserRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUser: async (createUserRequest: CreateUserRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createUserRequest' is not null or undefined\n            assertParamExists('createUser', 'createUserRequest', createUserRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createUserRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * delete user avatar\n         * @param {string} name User name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUserAvatar: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('deleteUserAvatar', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/{name}/avatar`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Disable user by username\n         * @param {string} username Username\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disableUser: async (username: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('disableUser', 'username', username)\n            const localVarPath = `/apis/console.api.security.halo.run/v1alpha1/users/{username}/disable`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Enable user by username\n         * @param {string} username Username\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        enableUser: async (username: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('enableUser', 'username', username)\n            const localVarPath = `/apis/console.api.security.halo.run/v1alpha1/users/{username}/enable`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get current user detail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCurrentUserDetail: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/-`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get permissions of user\n         * @param {string} name User name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPermissions: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getPermissions', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/{name}/permissions`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Get user detail by name\n         * @param {string} name User name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserDetail: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('getUserDetail', 'name', name)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/{name}`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Grant permissions to user\n         * @param {string} name User name\n         * @param {GrantRequest} grantRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        grantPermission: async (name: string, grantRequest: GrantRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('grantPermission', 'name', name)\n            // verify required parameter 'grantRequest' is not null or undefined\n            assertParamExists('grantPermission', 'grantRequest', grantRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/{name}/permissions`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(grantRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * List users\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Keyword to search\n         * @param {string} [role] Role name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUsers: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, role?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n            if (page !== undefined) {\n                localVarQueryParameter['page'] = page;\n            }\n\n            if (size !== undefined) {\n                localVarQueryParameter['size'] = size;\n            }\n\n            if (labelSelector) {\n                localVarQueryParameter['labelSelector'] = labelSelector;\n            }\n\n            if (fieldSelector) {\n                localVarQueryParameter['fieldSelector'] = fieldSelector;\n            }\n\n            if (sort) {\n                localVarQueryParameter['sort'] = sort;\n            }\n\n            if (keyword !== undefined) {\n                localVarQueryParameter['keyword'] = keyword;\n            }\n\n            if (role !== undefined) {\n                localVarQueryParameter['role'] = role;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Send email verification code for user\n         * @param {EmailVerifyRequest} emailVerifyRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        sendEmailVerificationCode: async (emailVerifyRequest: EmailVerifyRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'emailVerifyRequest' is not null or undefined\n            assertParamExists('sendEmailVerificationCode', 'emailVerifyRequest', emailVerifyRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/-/send-email-verification-code`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(emailVerifyRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Update current user profile, but password.\n         * @param {User} user \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCurrentUser: async (user: User, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'user' is not null or undefined\n            assertParamExists('updateCurrentUser', 'user', user)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/-`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * upload user avatar\n         * @param {string} name User name\n         * @param {File} file \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadUserAvatar: async (name: string, file: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'name' is not null or undefined\n            assertParamExists('uploadUserAvatar', 'name', name)\n            // verify required parameter 'file' is not null or undefined\n            assertParamExists('uploadUserAvatar', 'file', file)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/{name}/avatar`\n                .replace(`{${\"name\"}}`, encodeURIComponent(String(name)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n            const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)();\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n            if (file !== undefined) { \n                localVarFormParams.append('file', file as any);\n            }\n    \n    \n            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = localVarFormParams;\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * Verify email for user by code.\n         * @param {VerifyCodeRequest} verifyCodeRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        verifyEmail: async (verifyCodeRequest: VerifyCodeRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'verifyCodeRequest' is not null or undefined\n            assertParamExists('verifyEmail', 'verifyCodeRequest', verifyCodeRequest)\n            const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/-/verify-email`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            // authentication basicAuth required\n            // http basic authentication required\n            setBasicAuthToObject(localVarRequestOptions, configuration)\n\n            // authentication bearerAuth required\n            // http bearer authentication required\n            await setBearerAuthToObject(localVarHeaderParameter, configuration)\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(verifyCodeRequest, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UserV1alpha1ConsoleApi - functional programming interface\n */\nexport const UserV1alpha1ConsoleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UserV1alpha1ConsoleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * Change anyone password of user for admin.\n         * @param {string} name Name of user. If the name is equal to \\&#39;-\\&#39;, it will change the password of current user.\n         * @param {ChangePasswordRequest} changePasswordRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async changeAnyonePassword(name: string, changePasswordRequest: ChangePasswordRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.changeAnyonePassword(name, changePasswordRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.changeAnyonePassword']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Change own password of user.\n         * @param {ChangeOwnPasswordRequest} changeOwnPasswordRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async changeOwnPassword(changeOwnPasswordRequest: ChangeOwnPasswordRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.changeOwnPassword(changeOwnPasswordRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.changeOwnPassword']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Creates a new user.\n         * @param {CreateUserRequest} createUserRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createUser(createUserRequest: CreateUserRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.createUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * delete user avatar\n         * @param {string} name User name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteUserAvatar(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUserAvatar(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.deleteUserAvatar']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Disable user by username\n         * @param {string} username Username\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async disableUser(username: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.disableUser(username, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.disableUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Enable user by username\n         * @param {string} username Username\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async enableUser(username: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.enableUser(username, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.enableUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get current user detail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCurrentUserDetail(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DetailedUser>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCurrentUserDetail(options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.getCurrentUserDetail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get permissions of user\n         * @param {string} name User name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPermissions(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserPermission>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPermissions(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.getPermissions']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Get user detail by name\n         * @param {string} name User name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUserDetail(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DetailedUser>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserDetail(name, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.getUserDetail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Grant permissions to user\n         * @param {string} name User name\n         * @param {GrantRequest} grantRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async grantPermission(name: string, grantRequest: GrantRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.grantPermission(name, grantRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.grantPermission']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * List users\n         * @param {number} [page] Page number. Default is 0.\n         * @param {number} [size] Size number. Default is 0.\n         * @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!&#x3D;true\n         * @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n         * @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n         * @param {string} [keyword] Keyword to search\n         * @param {string} [role] Role name\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async listUsers(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, role?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserEndpointListedUserList>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.listUsers(page, size, labelSelector, fieldSelector, sort, keyword, role, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.listUsers']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Send email verification code for user\n         * @param {EmailVerifyRequest} emailVerifyRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async sendEmailVerificationCode(emailVerifyRequest: EmailVerifyRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.sendEmailVerificationCode(emailVerifyRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.sendEmailVerificationCode']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Update current user profile, but password.\n         * @param {User} user \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateCurrentUser(user: User, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateCurrentUser(user, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.updateCurrentUser']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * upload user avatar\n         * @param {string} name User name\n         * @param {File} file \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async uploadUserAvatar(name: string, file: File, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadUserAvatar(name, file, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.uploadUserAvatar']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n        /**\n         * Verify email for user by code.\n         * @param {VerifyCodeRequest} verifyCodeRequest \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async verifyEmail(verifyCodeRequest: VerifyCodeRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.verifyEmail(verifyCodeRequest, options);\n            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;\n            const localVarOperationServerBasePath = operationServerMap['UserV1alpha1ConsoleApi.verifyEmail']?.[localVarOperationServerIndex]?.url;\n            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);\n        },\n    }\n};\n\n/**\n * UserV1alpha1ConsoleApi - factory interface\n */\nexport const UserV1alpha1ConsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UserV1alpha1ConsoleApiFp(configuration)\n    return {\n        /**\n         * Change anyone password of user for admin.\n         * @param {UserV1alpha1ConsoleApiChangeAnyonePasswordRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changeAnyonePassword(requestParameters: UserV1alpha1ConsoleApiChangeAnyonePasswordRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.changeAnyonePassword(requestParameters.name, requestParameters.changePasswordRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Change own password of user.\n         * @param {UserV1alpha1ConsoleApiChangeOwnPasswordRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changeOwnPassword(requestParameters: UserV1alpha1ConsoleApiChangeOwnPasswordRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.changeOwnPassword(requestParameters.changeOwnPasswordRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Creates a new user.\n         * @param {UserV1alpha1ConsoleApiCreateUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUser(requestParameters: UserV1alpha1ConsoleApiCreateUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.createUser(requestParameters.createUserRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * delete user avatar\n         * @param {UserV1alpha1ConsoleApiDeleteUserAvatarRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUserAvatar(requestParameters: UserV1alpha1ConsoleApiDeleteUserAvatarRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.deleteUserAvatar(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Disable user by username\n         * @param {UserV1alpha1ConsoleApiDisableUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        disableUser(requestParameters: UserV1alpha1ConsoleApiDisableUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.disableUser(requestParameters.username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Enable user by username\n         * @param {UserV1alpha1ConsoleApiEnableUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        enableUser(requestParameters: UserV1alpha1ConsoleApiEnableUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.enableUser(requestParameters.username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get current user detail\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCurrentUserDetail(options?: RawAxiosRequestConfig): AxiosPromise<DetailedUser> {\n            return localVarFp.getCurrentUserDetail(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get permissions of user\n         * @param {UserV1alpha1ConsoleApiGetPermissionsRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPermissions(requestParameters: UserV1alpha1ConsoleApiGetPermissionsRequest, options?: RawAxiosRequestConfig): AxiosPromise<UserPermission> {\n            return localVarFp.getPermissions(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Get user detail by name\n         * @param {UserV1alpha1ConsoleApiGetUserDetailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserDetail(requestParameters: UserV1alpha1ConsoleApiGetUserDetailRequest, options?: RawAxiosRequestConfig): AxiosPromise<DetailedUser> {\n            return localVarFp.getUserDetail(requestParameters.name, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Grant permissions to user\n         * @param {UserV1alpha1ConsoleApiGrantPermissionRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        grantPermission(requestParameters: UserV1alpha1ConsoleApiGrantPermissionRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.grantPermission(requestParameters.name, requestParameters.grantRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * List users\n         * @param {UserV1alpha1ConsoleApiListUsersRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        listUsers(requestParameters: UserV1alpha1ConsoleApiListUsersRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<UserEndpointListedUserList> {\n            return localVarFp.listUsers(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, requestParameters.role, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Send email verification code for user\n         * @param {UserV1alpha1ConsoleApiSendEmailVerificationCodeRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        sendEmailVerificationCode(requestParameters: UserV1alpha1ConsoleApiSendEmailVerificationCodeRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.sendEmailVerificationCode(requestParameters.emailVerifyRequest, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Update current user profile, but password.\n         * @param {UserV1alpha1ConsoleApiUpdateCurrentUserRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCurrentUser(requestParameters: UserV1alpha1ConsoleApiUpdateCurrentUserRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.updateCurrentUser(requestParameters.user, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * upload user avatar\n         * @param {UserV1alpha1ConsoleApiUploadUserAvatarRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        uploadUserAvatar(requestParameters: UserV1alpha1ConsoleApiUploadUserAvatarRequest, options?: RawAxiosRequestConfig): AxiosPromise<User> {\n            return localVarFp.uploadUserAvatar(requestParameters.name, requestParameters.file, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * Verify email for user by code.\n         * @param {UserV1alpha1ConsoleApiVerifyEmailRequest} requestParameters Request parameters.\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        verifyEmail(requestParameters: UserV1alpha1ConsoleApiVerifyEmailRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {\n            return localVarFp.verifyEmail(requestParameters.verifyCodeRequest, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * Request parameters for changeAnyonePassword operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiChangeAnyonePasswordRequest {\n    /**\n     * Name of user. If the name is equal to \\&#39;-\\&#39;, it will change the password of current user.\n     */\n    readonly name: string\n\n    readonly changePasswordRequest: ChangePasswordRequest\n}\n\n/**\n * Request parameters for changeOwnPassword operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiChangeOwnPasswordRequest {\n    readonly changeOwnPasswordRequest: ChangeOwnPasswordRequest\n}\n\n/**\n * Request parameters for createUser operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiCreateUserRequest {\n    readonly createUserRequest: CreateUserRequest\n}\n\n/**\n * Request parameters for deleteUserAvatar operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiDeleteUserAvatarRequest {\n    /**\n     * User name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for disableUser operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiDisableUserRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n}\n\n/**\n * Request parameters for enableUser operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiEnableUserRequest {\n    /**\n     * Username\n     */\n    readonly username: string\n}\n\n/**\n * Request parameters for getPermissions operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiGetPermissionsRequest {\n    /**\n     * User name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for getUserDetail operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiGetUserDetailRequest {\n    /**\n     * User name\n     */\n    readonly name: string\n}\n\n/**\n * Request parameters for grantPermission operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiGrantPermissionRequest {\n    /**\n     * User name\n     */\n    readonly name: string\n\n    readonly grantRequest: GrantRequest\n}\n\n/**\n * Request parameters for listUsers operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiListUsersRequest {\n    /**\n     * Page number. Default is 0.\n     */\n    readonly page?: number\n\n    /**\n     * Size number. Default is 0.\n     */\n    readonly size?: number\n\n    /**\n     * Label selector. e.g.: hidden!&#x3D;true\n     */\n    readonly labelSelector?: Array<string>\n\n    /**\n     * Field selector. e.g.: metadata.name&#x3D;&#x3D;halo\n     */\n    readonly fieldSelector?: Array<string>\n\n    /**\n     * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.\n     */\n    readonly sort?: Array<string>\n\n    /**\n     * Keyword to search\n     */\n    readonly keyword?: string\n\n    /**\n     * Role name\n     */\n    readonly role?: string\n}\n\n/**\n * Request parameters for sendEmailVerificationCode operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiSendEmailVerificationCodeRequest {\n    readonly emailVerifyRequest: EmailVerifyRequest\n}\n\n/**\n * Request parameters for updateCurrentUser operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiUpdateCurrentUserRequest {\n    readonly user: User\n}\n\n/**\n * Request parameters for uploadUserAvatar operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiUploadUserAvatarRequest {\n    /**\n     * User name\n     */\n    readonly name: string\n\n    readonly file: File\n}\n\n/**\n * Request parameters for verifyEmail operation in UserV1alpha1ConsoleApi.\n */\nexport interface UserV1alpha1ConsoleApiVerifyEmailRequest {\n    readonly verifyCodeRequest: VerifyCodeRequest\n}\n\n/**\n * UserV1alpha1ConsoleApi - object-oriented interface\n */\nexport class UserV1alpha1ConsoleApi extends BaseAPI {\n    /**\n     * Change anyone password of user for admin.\n     * @param {UserV1alpha1ConsoleApiChangeAnyonePasswordRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public changeAnyonePassword(requestParameters: UserV1alpha1ConsoleApiChangeAnyonePasswordRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).changeAnyonePassword(requestParameters.name, requestParameters.changePasswordRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Change own password of user.\n     * @param {UserV1alpha1ConsoleApiChangeOwnPasswordRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public changeOwnPassword(requestParameters: UserV1alpha1ConsoleApiChangeOwnPasswordRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).changeOwnPassword(requestParameters.changeOwnPasswordRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Creates a new user.\n     * @param {UserV1alpha1ConsoleApiCreateUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public createUser(requestParameters: UserV1alpha1ConsoleApiCreateUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).createUser(requestParameters.createUserRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * delete user avatar\n     * @param {UserV1alpha1ConsoleApiDeleteUserAvatarRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public deleteUserAvatar(requestParameters: UserV1alpha1ConsoleApiDeleteUserAvatarRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).deleteUserAvatar(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Disable user by username\n     * @param {UserV1alpha1ConsoleApiDisableUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public disableUser(requestParameters: UserV1alpha1ConsoleApiDisableUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).disableUser(requestParameters.username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Enable user by username\n     * @param {UserV1alpha1ConsoleApiEnableUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public enableUser(requestParameters: UserV1alpha1ConsoleApiEnableUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).enableUser(requestParameters.username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get current user detail\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getCurrentUserDetail(options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).getCurrentUserDetail(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get permissions of user\n     * @param {UserV1alpha1ConsoleApiGetPermissionsRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getPermissions(requestParameters: UserV1alpha1ConsoleApiGetPermissionsRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).getPermissions(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Get user detail by name\n     * @param {UserV1alpha1ConsoleApiGetUserDetailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public getUserDetail(requestParameters: UserV1alpha1ConsoleApiGetUserDetailRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).getUserDetail(requestParameters.name, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Grant permissions to user\n     * @param {UserV1alpha1ConsoleApiGrantPermissionRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public grantPermission(requestParameters: UserV1alpha1ConsoleApiGrantPermissionRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).grantPermission(requestParameters.name, requestParameters.grantRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * List users\n     * @param {UserV1alpha1ConsoleApiListUsersRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public listUsers(requestParameters: UserV1alpha1ConsoleApiListUsersRequest = {}, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).listUsers(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, requestParameters.role, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Send email verification code for user\n     * @param {UserV1alpha1ConsoleApiSendEmailVerificationCodeRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public sendEmailVerificationCode(requestParameters: UserV1alpha1ConsoleApiSendEmailVerificationCodeRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).sendEmailVerificationCode(requestParameters.emailVerifyRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Update current user profile, but password.\n     * @param {UserV1alpha1ConsoleApiUpdateCurrentUserRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public updateCurrentUser(requestParameters: UserV1alpha1ConsoleApiUpdateCurrentUserRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).updateCurrentUser(requestParameters.user, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * upload user avatar\n     * @param {UserV1alpha1ConsoleApiUploadUserAvatarRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public uploadUserAvatar(requestParameters: UserV1alpha1ConsoleApiUploadUserAvatarRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).uploadUserAvatar(requestParameters.name, requestParameters.file, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * Verify email for user by code.\n     * @param {UserV1alpha1ConsoleApiVerifyEmailRequest} requestParameters Request parameters.\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     */\n    public verifyEmail(requestParameters: UserV1alpha1ConsoleApiVerifyEmailRequest, options?: RawAxiosRequestConfig) {\n        return UserV1alpha1ConsoleApiFp(this.configuration).verifyEmail(requestParameters.verifyCodeRequest, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport * from './api/annotation-setting-v1-alpha-uc-api';\nexport * from './api/annotation-setting-v1alpha1-api';\nexport * from './api/attachment-v1alpha1-api';\nexport * from './api/attachment-v1alpha1-console-api';\nexport * from './api/attachment-v1alpha1-uc-api';\nexport * from './api/auth-provider-v1alpha1-api';\nexport * from './api/auth-provider-v1alpha1-console-api';\nexport * from './api/backup-v1alpha1-api';\nexport * from './api/category-v1alpha1-api';\nexport * from './api/category-v1alpha1-public-api';\nexport * from './api/comment-v1alpha1-api';\nexport * from './api/comment-v1alpha1-console-api';\nexport * from './api/comment-v1alpha1-public-api';\nexport * from './api/config-map-v1alpha1-api';\nexport * from './api/counter-v1alpha1-api';\nexport * from './api/device-v1alpha1-api';\nexport * from './api/device-v1alpha1-uc-api';\nexport * from './api/extension-definition-v1alpha1-api';\nexport * from './api/extension-point-definition-v1alpha1-api';\nexport * from './api/group-v1alpha1-api';\nexport * from './api/index-v1alpha1-public-api';\nexport * from './api/indices-v1alpha1-console-api';\nexport * from './api/local-thumbnail-v1alpha1-api';\nexport * from './api/menu-item-v1alpha1-api';\nexport * from './api/menu-v1alpha1-api';\nexport * from './api/menu-v1alpha1-public-api';\nexport * from './api/metrics-v1alpha1-public-api';\nexport * from './api/migration-v1alpha1-console-api';\nexport * from './api/notification-template-v1alpha1-api';\nexport * from './api/notification-v1alpha1-api';\nexport * from './api/notification-v1alpha1-public-api';\nexport * from './api/notification-v1alpha1-uc-api';\nexport * from './api/notifier-descriptor-v1alpha1-api';\nexport * from './api/notifier-v1alpha1-console-api';\nexport * from './api/notifier-v1alpha1-uc-api';\nexport * from './api/personal-access-token-v1alpha1-api';\nexport * from './api/personal-access-token-v1alpha1-uc-api';\nexport * from './api/plugin-v1alpha1-api';\nexport * from './api/plugin-v1alpha1-console-api';\nexport * from './api/plugin-v1alpha1-public-api';\nexport * from './api/policy-alpha1-console-api';\nexport * from './api/policy-template-v1alpha1-api';\nexport * from './api/policy-v1alpha1-api';\nexport * from './api/post-v1alpha1-api';\nexport * from './api/post-v1alpha1-console-api';\nexport * from './api/post-v1alpha1-public-api';\nexport * from './api/post-v1alpha1-uc-api';\nexport * from './api/reason-type-v1alpha1-api';\nexport * from './api/reason-v1alpha1-api';\nexport * from './api/remember-me-token-v1alpha1-api';\nexport * from './api/reply-v1alpha1-api';\nexport * from './api/reply-v1alpha1-console-api';\nexport * from './api/reverse-proxy-v1alpha1-api';\nexport * from './api/role-binding-v1alpha1-api';\nexport * from './api/role-v1alpha1-api';\nexport * from './api/secret-v1alpha1-api';\nexport * from './api/setting-v1alpha1-api';\nexport * from './api/single-page-v1alpha1-api';\nexport * from './api/single-page-v1alpha1-console-api';\nexport * from './api/single-page-v1alpha1-public-api';\nexport * from './api/snapshot-v1alpha1-api';\nexport * from './api/snapshot-v1alpha1-uc-api';\nexport * from './api/subscription-v1alpha1-api';\nexport * from './api/system-config-v1alpha1-console-api';\nexport * from './api/system-v1alpha1-console-api';\nexport * from './api/system-v1alpha1-public-api';\nexport * from './api/tag-v1alpha1-api';\nexport * from './api/tag-v1alpha1-console-api';\nexport * from './api/tag-v1alpha1-public-api';\nexport * from './api/theme-v1alpha1-api';\nexport * from './api/theme-v1alpha1-console-api';\nexport * from './api/thumbnail-v1alpha1-api';\nexport * from './api/thumbnail-v1alpha1-public-api';\nexport * from './api/two-factor-auth-v1alpha1-uc-api';\nexport * from './api/user-connection-v1alpha1-api';\nexport * from './api/user-connection-v1alpha1-uc-api';\nexport * from './api/user-preference-v1alpha1-uc-api';\nexport * from './api/user-v1alpha1-api';\nexport * from './api/user-v1alpha1-console-api';\n\n"
  },
  {
    "path": "ui/packages/api-client/src/base.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport type { Configuration } from './configuration';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';\nimport globalAxios from 'axios';\n\nexport const BASE_PATH = \"http://localhost:8091\".replace(/\\/+$/, \"\");\n\nexport const COLLECTION_FORMATS = {\n    csv: \",\",\n    ssv: \" \",\n    tsv: \"\\t\",\n    pipes: \"|\",\n};\n\nexport interface RequestArgs {\n    url: string;\n    options: RawAxiosRequestConfig;\n}\n\nexport class BaseAPI {\n    protected configuration: Configuration | undefined;\n\n    constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {\n        if (configuration) {\n            this.configuration = configuration;\n            this.basePath = configuration.basePath ?? basePath;\n        }\n    }\n};\n\nexport class RequiredError extends Error {\n    constructor(public field: string, msg?: string) {\n        super(msg);\n        this.name = \"RequiredError\"\n    }\n}\n\ninterface ServerMap {\n    [key: string]: {\n        url: string,\n        description: string,\n    }[];\n}\n\nexport const operationServerMap: ServerMap = {\n}\n"
  },
  {
    "path": "ui/packages/api-client/src/common.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport type { Configuration } from \"./configuration\";\nimport type { RequestArgs } from \"./base\";\nimport type { AxiosInstance, AxiosResponse } from 'axios';\nimport { RequiredError } from \"./base\";\n\nexport const DUMMY_BASE_URL = 'https://example.com'\n\n/**\n *\n * @throws {RequiredError}\n */\nexport const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {\n    if (paramValue === null || paramValue === undefined) {\n        throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);\n    }\n}\n\nexport const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {\n    if (configuration && configuration.apiKey) {\n        const localVarApiKeyValue = typeof configuration.apiKey === 'function'\n            ? await configuration.apiKey(keyParamName)\n            : await configuration.apiKey;\n        object[keyParamName] = localVarApiKeyValue;\n    }\n}\n\nexport const setBasicAuthToObject = function (object: any, configuration?: Configuration) {\n    if (configuration && (configuration.username || configuration.password)) {\n        object[\"auth\"] = { username: configuration.username, password: configuration.password };\n    }\n}\n\nexport const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {\n    if (configuration && configuration.accessToken) {\n        const accessToken = typeof configuration.accessToken === 'function'\n            ? await configuration.accessToken()\n            : await configuration.accessToken;\n        object[\"Authorization\"] = \"Bearer \" + accessToken;\n    }\n}\n\nexport const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {\n    if (configuration && configuration.accessToken) {\n        const localVarAccessTokenValue = typeof configuration.accessToken === 'function'\n            ? await configuration.accessToken(name, scopes)\n            : await configuration.accessToken;\n        object[\"Authorization\"] = \"Bearer \" + localVarAccessTokenValue;\n    }\n}\n\n\nfunction setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = \"\"): void {\n    if (parameter == null) return;\n    if (typeof parameter === \"object\") {\n        if (Array.isArray(parameter)) {\n            (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));\n        }\n        else {\n            Object.keys(parameter).forEach(currentKey =>\n                setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)\n            );\n        }\n    }\n    else {\n        if (urlSearchParams.has(key)) {\n            urlSearchParams.append(key, parameter);\n        }\n        else {\n            urlSearchParams.set(key, parameter);\n        }\n    }\n}\n\nexport const setSearchParams = function (url: URL, ...objects: any[]) {\n    const searchParams = new URLSearchParams(url.search);\n    setFlattenedQueryParams(searchParams, objects);\n    url.search = searchParams.toString();\n}\n\nexport const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {\n    const nonString = typeof value !== 'string';\n    const needsSerialization = nonString && configuration && configuration.isJsonMime\n        ? configuration.isJsonMime(requestOptions.headers['Content-Type'])\n        : nonString;\n    return needsSerialization\n        ? JSON.stringify(value !== undefined ? value : {})\n        : (value || \"\");\n}\n\nexport const toPathString = function (url: URL) {\n    return url.pathname + url.search + url.hash\n}\n\nexport const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {\n    return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {\n        const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url};\n        return axios.request<T, R>(axiosRequestArgs);\n    };\n}\n"
  },
  {
    "path": "ui/packages/api-client/src/configuration.ts",
    "content": "/* tslint:disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\ninterface AWSv4Configuration {\n  options?: {\n    region?: string\n    service?: string\n  }\n  credentials?: {\n    accessKeyId?: string\n    secretAccessKey?: string,\n    sessionToken?: string\n  }\n}\n\nexport interface ConfigurationParameters {\n    apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);\n    username?: string;\n    password?: string;\n    accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);\n    awsv4?: AWSv4Configuration;\n    basePath?: string;\n    serverIndex?: number;\n    baseOptions?: any;\n    formDataCtor?: new () => any;\n}\n\nexport class Configuration {\n    /**\n     * parameter for apiKey security\n     * @param name security name\n     */\n    apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);\n    /**\n     * parameter for basic security\n     */\n    username?: string;\n    /**\n     * parameter for basic security\n     */\n    password?: string;\n    /**\n     * parameter for oauth2 security\n     * @param name security name\n     * @param scopes oauth2 scope\n     */\n    accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);\n    /**\n     * parameter for aws4 signature security\n     * @param {Object} AWS4Signature - AWS4 Signature security\n     * @param {string} options.region - aws region\n     * @param {string} options.service - name of the service.\n     * @param {string} credentials.accessKeyId - aws access key id\n     * @param {string} credentials.secretAccessKey - aws access key\n     * @param {string} credentials.sessionToken - aws session token\n     * @memberof Configuration\n     */\n    awsv4?: AWSv4Configuration;\n    /**\n     * override base path\n     */\n    basePath?: string;\n    /**\n     * override server index\n     */\n    serverIndex?: number;\n    /**\n     * base options for axios calls\n     */\n    baseOptions?: any;\n    /**\n     * The FormData constructor that will be used to create multipart form data\n     * requests. You can inject this here so that execution environments that\n     * do not support the FormData class can still run the generated client.\n     *\n     * @type {new () => FormData}\n     */\n    formDataCtor?: new () => any;\n\n    constructor(param: ConfigurationParameters = {}) {\n        this.apiKey = param.apiKey;\n        this.username = param.username;\n        this.password = param.password;\n        this.accessToken = param.accessToken;\n        this.awsv4 = param.awsv4;\n        this.basePath = param.basePath;\n        this.serverIndex = param.serverIndex;\n        this.baseOptions = {\n            ...param.baseOptions,\n            headers: {\n                ...param.baseOptions?.headers,\n            },\n        };\n        this.formDataCtor = param.formDataCtor;\n    }\n\n    /**\n     * Check if the given MIME is a JSON MIME.\n     * JSON MIME examples:\n     *   application/json\n     *   application/json; charset=UTF8\n     *   APPLICATION/JSON\n     *   application/vnd.company+json\n     * @param mime - MIME (Multipurpose Internet Mail Extensions)\n     * @return True if the given MIME is JSON, false otherwise.\n     */\n    public isJsonMime(mime: string): boolean {\n        const jsonMime: RegExp = new RegExp('^(application\\/json|[^;/ \\t]+\\/[^;/ \\t]+[+]json)[ \\t]*(;.*)?$', 'i');\n        return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');\n    }\n}\n"
  },
  {
    "path": "ui/packages/api-client/src/git_push.sh",
    "content": "#!/bin/sh\n# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/\n#\n# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl \"minor update\" \"gitlab.com\"\n\ngit_user_id=$1\ngit_repo_id=$2\nrelease_note=$3\ngit_host=$4\n\nif [ \"$git_host\" = \"\" ]; then\n    git_host=\"github.com\"\n    echo \"[INFO] No command line input provided. Set \\$git_host to $git_host\"\nfi\n\nif [ \"$git_user_id\" = \"\" ]; then\n    git_user_id=\"GIT_USER_ID\"\n    echo \"[INFO] No command line input provided. Set \\$git_user_id to $git_user_id\"\nfi\n\nif [ \"$git_repo_id\" = \"\" ]; then\n    git_repo_id=\"GIT_REPO_ID\"\n    echo \"[INFO] No command line input provided. Set \\$git_repo_id to $git_repo_id\"\nfi\n\nif [ \"$release_note\" = \"\" ]; then\n    release_note=\"Minor update\"\n    echo \"[INFO] No command line input provided. Set \\$release_note to $release_note\"\nfi\n\n# Initialize the local directory as a Git repository\ngit init\n\n# Adds the files in the local repository and stages them for commit.\ngit add .\n\n# Commits the tracked changes and prepares them to be pushed to a remote repository.\ngit commit -m \"$release_note\"\n\n# Sets the new remote\ngit_remote=$(git remote)\nif [ \"$git_remote\" = \"\" ]; then # git remote not defined\n\n    if [ \"$GIT_TOKEN\" = \"\" ]; then\n        echo \"[INFO] \\$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment.\"\n        git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git\n    else\n        git remote add origin https://${git_user_id}:\"${GIT_TOKEN}\"@${git_host}/${git_user_id}/${git_repo_id}.git\n    fi\n\nfi\n\ngit pull origin master\n\n# Pushes (Forces) the changes in the local repository up to the remote repository\necho \"Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git\"\ngit push origin master 2>&1 | grep -v 'To https'\n"
  },
  {
    "path": "ui/packages/api-client/src/index.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nexport * from \"./api\";\nexport * from \"./configuration\";\nexport * from \"./models\";\n"
  },
  {
    "path": "ui/packages/api-client/src/models/add-operation.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface AddOperation {\n    'op': AddOperationOpEnum;\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'path': string;\n    /**\n     * Value can be any JSON value\n     */\n    'value': any;\n}\n\nexport const AddOperationOpEnum = {\n    Add: 'add'\n} as const;\n\nexport type AddOperationOpEnum = typeof AddOperationOpEnum[keyof typeof AddOperationOpEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/annotation-setting-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { AnnotationSetting } from './annotation-setting';\n\nexport interface AnnotationSettingList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<AnnotationSetting>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/annotation-setting-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { GroupKind } from './group-kind';\n\nexport interface AnnotationSettingSpec {\n    'formSchema': Array<object>;\n    'targetRef': GroupKind;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/annotation-setting.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { AnnotationSettingSpec } from './annotation-setting-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface AnnotationSetting {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': AnnotationSettingSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/attachment-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Attachment } from './attachment';\n\nexport interface AttachmentList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Attachment>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/attachment-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface AttachmentSpec {\n    /**\n     * Display name of attachment\n     */\n    'displayName'?: string;\n    /**\n     * Group name\n     */\n    'groupName'?: string;\n    /**\n     * Media type of attachment\n     */\n    'mediaType'?: string;\n    /**\n     * Name of User who uploads the attachment\n     */\n    'ownerName'?: string;\n    /**\n     * Policy name\n     */\n    'policyName'?: string;\n    /**\n     * Size of attachment. Unit is Byte\n     */\n    'size'?: number;\n    /**\n     * Tags of attachment\n     */\n    'tags'?: Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/attachment-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface AttachmentStatus {\n    /**\n     * Permalink of attachment. If it is in local storage, the public URL will be set. If it is in s3 storage, the Object URL will be set. \n     */\n    'permalink'?: string;\n    'thumbnails'?: { [key: string]: string; };\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/attachment.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { AttachmentSpec } from './attachment-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { AttachmentStatus } from './attachment-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface Attachment {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': AttachmentSpec;\n    'status'?: AttachmentStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/auth-provider-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { AuthProvider } from './auth-provider';\n\nexport interface AuthProviderList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<AuthProvider>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/auth-provider-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ConfigMapRef } from './config-map-ref';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SettingRef } from './setting-ref';\n\nexport interface AuthProviderSpec {\n    /**\n     * Auth type: form or oauth2.\n     */\n    'authType': AuthProviderSpecAuthTypeEnum;\n    /**\n     * Authentication url of the auth provider\n     */\n    'authenticationUrl': string;\n    'bindingUrl'?: string;\n    'configMapRef'?: ConfigMapRef;\n    'description'?: string;\n    /**\n     * Display name of the auth provider\n     */\n    'displayName': string;\n    'helpPage'?: string;\n    'logo'?: string;\n    'method'?: string;\n    'rememberMeSupport'?: boolean;\n    'settingRef'?: SettingRef;\n    'unbindUrl'?: string;\n    'website'?: string;\n}\n\nexport const AuthProviderSpecAuthTypeEnum = {\n    Form: 'FORM',\n    Oauth2: 'OAUTH2'\n} as const;\n\nexport type AuthProviderSpecAuthTypeEnum = typeof AuthProviderSpecAuthTypeEnum[keyof typeof AuthProviderSpecAuthTypeEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/auth-provider.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { AuthProviderSpec } from './auth-provider-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * Auth provider extension.\n */\nexport interface AuthProvider {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': AuthProviderSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/author.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface Author {\n    'name': string;\n    'website'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/backup-file.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Backup file.\n */\nexport interface BackupFile {\n    /**\n     * Filename of backup file.\n     */\n    'filename'?: string;\n    /**\n     * Last modified time of backup file.\n     */\n    'lastModifiedTime'?: string;\n    /**\n     * Size of backup file.\n     */\n    'size'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/backup-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Backup } from './backup';\n\nexport interface BackupList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Backup>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/backup-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface BackupSpec {\n    'expiresAt'?: string;\n    /**\n     * Backup file format. Currently, only zip format is supported.\n     */\n    'format'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/backup-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface BackupStatus {\n    'completionTimestamp'?: string;\n    'failureMessage'?: string;\n    'failureReason'?: string;\n    /**\n     * Name of backup file.\n     */\n    'filename'?: string;\n    'phase'?: BackupStatusPhaseEnum;\n    /**\n     * Size of backup file. Data unit: byte\n     */\n    'size'?: number;\n    'startTimestamp'?: string;\n}\n\nexport const BackupStatusPhaseEnum = {\n    Pending: 'PENDING',\n    Running: 'RUNNING',\n    Succeeded: 'SUCCEEDED',\n    Failed: 'FAILED'\n} as const;\n\nexport type BackupStatusPhaseEnum = typeof BackupStatusPhaseEnum[keyof typeof BackupStatusPhaseEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/backup.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { BackupSpec } from './backup-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { BackupStatus } from './backup-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface Backup {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: BackupSpec;\n    'status'?: BackupStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/category-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Category } from './category';\n\nexport interface CategoryList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Category>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/category-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface CategorySpec {\n    'children'?: Array<string>;\n    'cover'?: string;\n    'description'?: string;\n    'displayName': string;\n    /**\n     * <p>Whether to hide the category from the category list.</p>  <p>When set to true, the category including its subcategories and related posts will  not be displayed in the category list, but it can still be accessed by permalink.</p>  <p>Limitation: It only takes effect on the theme-side categorized list and it only  allows to be set to true on the first level(root node) of categories.</p>\n     */\n    'hideFromList'?: boolean;\n    /**\n     * <p>Used to specify the template for the posts associated with the category.</p>  <p>The priority is not as high as that of the post.</p>  <p>If the post also specifies a template, the post\\'s template will prevail.</p>\n     */\n    'postTemplate'?: string;\n    /**\n     * <p>if a category is queried for related posts, the default behavior is to  query all posts under the category including its subcategories, but if this field is  set to true, cascade query behavior will be terminated here.</p>  <p>For example, if a category has subcategories A and B, and A has subcategories C and  D and C marked this field as true, when querying posts under A category,all posts under A  and B will be queried, but C and D will not be queried.</p>\n     */\n    'preventParentPostCascadeQuery'?: boolean;\n    'priority': number;\n    'slug': string;\n    'template'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/category-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface CategoryStatus {\n    'permalink'?: string;\n    /**\n     * 包括当前和其下所有层级的文章数量 (depth=max).\n     */\n    'postCount'?: number;\n    /**\n     * 包括当前和其下所有层级的已发布且公开的文章数量 (depth=max).\n     */\n    'visiblePostCount'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/category-vo-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CategoryVo } from './category-vo';\n\nexport interface CategoryVoList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<CategoryVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/category-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CategorySpec } from './category-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CategoryStatus } from './category-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * A value object for {@link Category Category}.\n */\nexport interface CategoryVo {\n    'metadata': Metadata;\n    'postCount'?: number;\n    'spec'?: CategorySpec;\n    'status'?: CategoryStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/category.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CategorySpec } from './category-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CategoryStatus } from './category-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface Category {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': CategorySpec;\n    'status'?: CategoryStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/change-own-password-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ChangeOwnPasswordRequest {\n    /**\n     * Old password.\n     */\n    'oldPassword': string;\n    /**\n     * New password.\n     */\n    'password': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/change-password-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ChangePasswordRequest {\n    /**\n     * New password.\n     */\n    'password': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-email-owner.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * <p>The creator info of the comment.</p>  This {@link CommentEmailOwner CommentEmailOwner} is only applicable to the user who is allowed to comment  without logging in.\n */\nexport interface CommentEmailOwner {\n    /**\n     * avatar for comment owner\n     */\n    'avatar'?: string;\n    /**\n     * display name for comment owner\n     */\n    'displayName'?: string;\n    /**\n     * email for comment owner\n     */\n    'email'?: string;\n    /**\n     * website for comment owner\n     */\n    'website'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Comment } from './comment';\n\nexport interface CommentList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Comment>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-owner.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface CommentOwner {\n    'annotations'?: { [key: string]: string; };\n    'displayName'?: string;\n    'kind': string;\n    'name': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentEmailOwner } from './comment-email-owner';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Ref } from './ref';\n\n/**\n * Request parameter object for {@link Comment Comment}.\n */\nexport interface CommentRequest {\n    'allowNotification'?: boolean;\n    'content': string;\n    'hidden'?: boolean;\n    'owner'?: CommentEmailOwner;\n    'raw': string;\n    'subjectRef': Ref;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentOwner } from './comment-owner';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Ref } from './ref';\n\nexport interface CommentSpec {\n    'allowNotification': boolean;\n    'approved': boolean;\n    'approvedTime'?: string;\n    'content': string;\n    /**\n     * The user-defined creation time default is <code>metadata.creationTimestamp</code>.\n     */\n    'creationTime'?: string;\n    'hidden': boolean;\n    'ipAddress'?: string;\n    'lastReadTime'?: string;\n    'owner': CommentOwner;\n    'priority': number;\n    'raw': string;\n    'subjectRef': Ref;\n    'top': boolean;\n    'userAgent'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-stats-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * comment stats value object.\n */\nexport interface CommentStatsVo {\n    'upvote'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-stats.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * comment stats value object.\n */\nexport interface CommentStats {\n    'upvote'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface CommentStatus {\n    'hasNewReply'?: boolean;\n    'lastReplyTime'?: string;\n    'observedVersion'?: number;\n    'replyCount'?: number;\n    'unreadReplyCount'?: number;\n    'visibleReplyCount'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-vo-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentVo } from './comment-vo';\n\nexport interface CommentVoList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<CommentVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentSpec } from './comment-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStatsVo } from './comment-stats-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStatus } from './comment-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { OwnerInfo } from './owner-info';\n\n/**\n * A chunk of items.\n */\nexport interface CommentVo {\n    'metadata': Metadata;\n    'owner': OwnerInfo;\n    'spec': CommentSpec;\n    'stats': CommentStatsVo;\n    'status'?: CommentStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-with-reply-vo-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentWithReplyVo } from './comment-with-reply-vo';\n\nexport interface CommentWithReplyVoList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<CommentWithReplyVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment-with-reply-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentSpec } from './comment-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStatsVo } from './comment-stats-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStatus } from './comment-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListResultReplyVo } from './list-result-reply-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { OwnerInfo } from './owner-info';\n\n/**\n * A chunk of items.\n */\nexport interface CommentWithReplyVo {\n    'metadata': Metadata;\n    'owner': OwnerInfo;\n    'replies'?: ListResultReplyVo;\n    'spec': CommentSpec;\n    'stats': CommentStatsVo;\n    'status'?: CommentStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/comment.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentSpec } from './comment-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStatus } from './comment-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface Comment {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': CommentSpec;\n    'status'?: CommentStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/condition.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * EqualsAndHashCode 排除了lastTransitionTime否则失败时，lastTransitionTime 会被更新  导致 equals 为 false，一直被加入队列.\n */\nexport interface Condition {\n    /**\n     * Last time the condition transitioned from one status to another.\n     */\n    'lastTransitionTime': string;\n    /**\n     * Human-readable message indicating details about last transition.  This may be an empty string.\n     */\n    'message'?: string;\n    /**\n     * Unique, one-word, CamelCase reason for the condition\\'s last transition.\n     */\n    'reason'?: string;\n    /**\n     * Status is the status of the condition. Can be True, False, Unknown.\n     */\n    'status': ConditionStatusEnum;\n    /**\n     * type of condition in CamelCase or in foo.example.com/CamelCase.  example: Ready, Initialized.  maxLength: 316.\n     */\n    'type': string;\n}\n\nexport const ConditionStatusEnum = {\n    True: 'TRUE',\n    False: 'FALSE',\n    Unknown: 'UNKNOWN'\n} as const;\n\nexport type ConditionStatusEnum = typeof ConditionStatusEnum[keyof typeof ConditionStatusEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/config-map-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ConfigMap } from './config-map';\n\nexport interface ConfigMapList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ConfigMap>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/config-map-ref.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ConfigMapRef {\n    'name': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/config-map.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * <p>ConfigMap holds configuration data to consume.</p>\n */\nexport interface ConfigMap {\n    'apiVersion': string;\n    'data'?: { [key: string]: string; };\n    'kind': string;\n    'metadata': Metadata;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/content-update-param.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ContentUpdateParam {\n    'content': string;\n    'raw': string;\n    'rawType': string;\n    'version'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/content-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * A value object for Content from {@link Snapshot Snapshot}.\n */\nexport interface ContentVo {\n    'content'?: string;\n    'raw'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/content-wrapper.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ContentWrapper {\n    'content'?: string;\n    'raw'?: string;\n    'rawType'?: string;\n    'snapshotName'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/content.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface Content {\n    'content': string;\n    'raw': string;\n    'rawType': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/contributor-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * A value object for {@link run.halo.app.core.extension.User run.halo.app.core.extension.User}.\n */\nexport interface ContributorVo {\n    'avatar'?: string;\n    'bio'?: string;\n    'displayName'?: string;\n    'metadata': Metadata;\n    'name'?: string;\n    'permalink'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/contributor.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Contributor from user.\n */\nexport interface Contributor {\n    'avatar'?: string;\n    'displayName'?: string;\n    'name'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/copy-operation.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface CopyOperation {\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'from': string;\n    'op': CopyOperationOpEnum;\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'path': string;\n}\n\nexport const CopyOperationOpEnum = {\n    Copy: 'copy'\n} as const;\n\nexport type CopyOperationOpEnum = typeof CopyOperationOpEnum[keyof typeof CopyOperationOpEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/counter-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Counter } from './counter';\n\nexport interface CounterList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Counter>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/counter-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface CounterRequest {\n    'group'?: string;\n    'hostname'?: string;\n    'language'?: string;\n    'name'?: string;\n    'plural'?: string;\n    'referrer'?: string;\n    'screen'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/counter.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * A counter for number of requests by extension resource name.\n */\nexport interface Counter {\n    'apiVersion': string;\n    'approvedComment'?: number;\n    'downvote'?: number;\n    'kind': string;\n    'metadata': Metadata;\n    'totalComment'?: number;\n    'upvote'?: number;\n    'visit'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/create-user-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface CreateUserRequest {\n    'annotations'?: { [key: string]: string; };\n    'avatar'?: string;\n    'bio'?: string;\n    'displayName'?: string;\n    'email': string;\n    'name': string;\n    'password'?: string;\n    'phone'?: string;\n    'roles'?: Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/custom-templates.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TemplateDescriptor } from './template-descriptor';\n\nexport interface CustomTemplates {\n    'category'?: Array<TemplateDescriptor>;\n    'page'?: Array<TemplateDescriptor>;\n    'post'?: Array<TemplateDescriptor>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/dashboard-stats.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface DashboardStats {\n    'approvedComments'?: number;\n    'comments'?: number;\n    'posts'?: number;\n    'upvotes'?: number;\n    'users'?: number;\n    'visits'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/detailed-user.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Role } from './role';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { User } from './user';\n\nexport interface DetailedUser {\n    'roles': Array<Role>;\n    'user': User;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/device-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Device } from './device';\n\nexport interface DeviceList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Device>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/device-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface DeviceSpec {\n    'ipAddress': string;\n    'lastAccessedTime'?: string;\n    'lastAuthenticatedTime'?: string;\n    'principalName': string;\n    'rememberMeSeriesId'?: string;\n    'sessionId': string;\n    'userAgent'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/device-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface DeviceStatus {\n    'browser'?: string;\n    'os'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/device.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { DeviceSpec } from './device-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { DeviceStatus } from './device-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface Device {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': DeviceSpec;\n    'status': DeviceStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/email-config-validation-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface EmailConfigValidationRequest {\n    /**\n     * Gets email display name.\n     */\n    'displayName'?: string;\n    'enable'?: boolean;\n    'encryption'?: string;\n    'host'?: string;\n    'password'?: string;\n    'port'?: number;\n    /**\n     * Gets email sender address.\n     */\n    'sender'?: string;\n    'username'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/email-verify-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface EmailVerifyRequest {\n    'email': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/excerpt.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface Excerpt {\n    'autoGenerate': boolean;\n    'raw'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/extension-definition-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ExtensionDefinition } from './extension-definition';\n\nexport interface ExtensionDefinitionList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ExtensionDefinition>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/extension-definition.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ExtensionSpec } from './extension-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * Extension definition.  An {@link ExtensionDefinition ExtensionDefinition} is a type of metadata that provides additional information about  an extension. An extension is a way to add new functionality to an existing class, structure,  enumeration, or protocol type without needing to subclass it.\n */\nexport interface ExtensionDefinition {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': ExtensionSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/extension-point-definition-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ExtensionPointDefinition } from './extension-point-definition';\n\nexport interface ExtensionPointDefinitionList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ExtensionPointDefinition>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/extension-point-definition.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ExtensionPointSpec } from './extension-point-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * Extension point definition.  An {@link ExtensionPointDefinition ExtensionPointDefinition} is a concept used in <code>Halo</code> to allow for the  dynamic extension of system. It defines a location within <code>Halo</code> where  additional functionality can be added through the use of plugins or extensions.\n */\nexport interface ExtensionPointDefinition {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': ExtensionPointSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/extension-point-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ExtensionPointSpec {\n    'className': string;\n    'description'?: string;\n    'displayName': string;\n    'icon'?: string;\n    'type': ExtensionPointSpecTypeEnum;\n}\n\nexport const ExtensionPointSpecTypeEnum = {\n    Singleton: 'SINGLETON',\n    MultiInstance: 'MULTI_INSTANCE'\n} as const;\n\nexport type ExtensionPointSpecTypeEnum = typeof ExtensionPointSpecTypeEnum[keyof typeof ExtensionPointSpecTypeEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/extension-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ExtensionSpec {\n    'className': string;\n    'description'?: string;\n    'displayName': string;\n    'extensionPointName': string;\n    'icon'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/extension.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * Extension is an interface which represents an Extension. It contains setters and getters of  GroupVersionKind and Metadata.\n */\nexport interface Extension {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/file-reverse-proxy-provider.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface FileReverseProxyProvider {\n    'directory'?: string;\n    'filename'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/grant-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface GrantRequest {\n    'roles'?: Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/group-kind.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * GroupKind contains group and kind data only.\n */\nexport interface GroupKind {\n    /**\n     * is group name of Extension.\n     */\n    'group'?: string;\n    /**\n     * is kind name of Extension.\n     */\n    'kind'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/group-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Group } from './group';\n\nexport interface GroupList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Group>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/group-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface GroupSpec {\n    /**\n     * Display name of group\n     */\n    'displayName': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/group-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface GroupStatus {\n    /**\n     * Total of attachments under the current group\n     */\n    'totalAttachments'?: number;\n    /**\n     * Update timestamp of the group\n     */\n    'updateTimestamp'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/group.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { GroupSpec } from './group-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { GroupStatus } from './group-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface Group {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': GroupSpec;\n    'status'?: GroupStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/halo-document.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Document for search.\n */\nexport interface HaloDocument {\n    /**\n     * Custom metadata. Make sure the map is serializable.\n     */\n    'annotations'?: { [key: string]: string; };\n    /**\n     * Document categories. The item in the list is the category metadata name.\n     */\n    'categories'?: Array<string>;\n    /**\n     * Document content. Safety content, without HTML tag.\n     */\n    'content': string;\n    /**\n     * Document creation timestamp.\n     */\n    'creationTimestamp'?: string;\n    /**\n     * Document description.\n     */\n    'description'?: string;\n    /**\n     * Whether the document is exposed to the public.\n     */\n    'exposed'?: boolean;\n    /**\n     * Document ID. It should be unique globally.\n     */\n    'id': string;\n    /**\n     * Metadata name of the corresponding extension.\n     */\n    'metadataName': string;\n    /**\n     * Document owner metadata name.\n     */\n    'ownerName': string;\n    /**\n     * Document permalink.\n     */\n    'permalink': string;\n    /**\n     * Whether the document is published.\n     */\n    'published'?: boolean;\n    /**\n     * Whether the document is recycled.\n     */\n    'recycled'?: boolean;\n    /**\n     * Document tags. The item in the list is the tag metadata name.\n     */\n    'tags'?: Array<string>;\n    /**\n     * Document title.\n     */\n    'title': string;\n    /**\n     * Document type. e.g.: post.content.halo.run, singlepage.content.halo.run, moment.moment  .halo.run, doc.doc.halo.run.\n     */\n    'type': string;\n    /**\n     * Document update timestamp.\n     */\n    'updateTimestamp'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/index.ts",
    "content": "export * from './add-operation';\nexport * from './annotation-setting';\nexport * from './annotation-setting-list';\nexport * from './annotation-setting-spec';\nexport * from './attachment';\nexport * from './attachment-list';\nexport * from './attachment-spec';\nexport * from './attachment-status';\nexport * from './auth-provider';\nexport * from './auth-provider-list';\nexport * from './auth-provider-spec';\nexport * from './author';\nexport * from './backup';\nexport * from './backup-file';\nexport * from './backup-list';\nexport * from './backup-spec';\nexport * from './backup-status';\nexport * from './category';\nexport * from './category-list';\nexport * from './category-spec';\nexport * from './category-status';\nexport * from './category-vo';\nexport * from './category-vo-list';\nexport * from './change-own-password-request';\nexport * from './change-password-request';\nexport * from './comment';\nexport * from './comment-email-owner';\nexport * from './comment-list';\nexport * from './comment-owner';\nexport * from './comment-request';\nexport * from './comment-spec';\nexport * from './comment-stats';\nexport * from './comment-stats-vo';\nexport * from './comment-status';\nexport * from './comment-vo';\nexport * from './comment-vo-list';\nexport * from './comment-with-reply-vo';\nexport * from './comment-with-reply-vo-list';\nexport * from './condition';\nexport * from './config-map';\nexport * from './config-map-list';\nexport * from './config-map-ref';\nexport * from './content';\nexport * from './content-update-param';\nexport * from './content-vo';\nexport * from './content-wrapper';\nexport * from './contributor';\nexport * from './contributor-vo';\nexport * from './copy-operation';\nexport * from './counter';\nexport * from './counter-list';\nexport * from './counter-request';\nexport * from './create-user-request';\nexport * from './custom-templates';\nexport * from './dashboard-stats';\nexport * from './detailed-user';\nexport * from './device';\nexport * from './device-list';\nexport * from './device-spec';\nexport * from './device-status';\nexport * from './email-config-validation-request';\nexport * from './email-verify-request';\nexport * from './excerpt';\nexport * from './extension';\nexport * from './extension-definition';\nexport * from './extension-definition-list';\nexport * from './extension-point-definition';\nexport * from './extension-point-definition-list';\nexport * from './extension-point-spec';\nexport * from './extension-spec';\nexport * from './file-reverse-proxy-provider';\nexport * from './grant-request';\nexport * from './group';\nexport * from './group-kind';\nexport * from './group-list';\nexport * from './group-spec';\nexport * from './group-status';\nexport * from './halo-document';\nexport * from './install-from-uri-request';\nexport * from './interest-reason';\nexport * from './interest-reason-subject';\nexport * from './json-patch-inner';\nexport * from './license';\nexport * from './list-result-reply-vo';\nexport * from './listed-auth-provider';\nexport * from './listed-comment';\nexport * from './listed-comment-list';\nexport * from './listed-post';\nexport * from './listed-post-list';\nexport * from './listed-post-vo';\nexport * from './listed-post-vo-list';\nexport * from './listed-reply';\nexport * from './listed-reply-list';\nexport * from './listed-single-page';\nexport * from './listed-single-page-list';\nexport * from './listed-single-page-vo';\nexport * from './listed-single-page-vo-list';\nexport * from './listed-snapshot-dto';\nexport * from './listed-snapshot-spec';\nexport * from './listed-user';\nexport * from './local-thumbnail';\nexport * from './local-thumbnail-list';\nexport * from './local-thumbnail-spec';\nexport * from './local-thumbnail-status';\nexport * from './mark-specified-request';\nexport * from './menu';\nexport * from './menu-item';\nexport * from './menu-item-list';\nexport * from './menu-item-spec';\nexport * from './menu-item-status';\nexport * from './menu-item-vo';\nexport * from './menu-list';\nexport * from './menu-spec';\nexport * from './menu-vo';\nexport * from './metadata';\nexport * from './move-operation';\nexport * from './navigation-post-vo';\nexport * from './notification';\nexport * from './notification-list';\nexport * from './notification-spec';\nexport * from './notification-template';\nexport * from './notification-template-list';\nexport * from './notification-template-spec';\nexport * from './notifier-descriptor';\nexport * from './notifier-descriptor-list';\nexport * from './notifier-descriptor-spec';\nexport * from './notifier-info';\nexport * from './notifier-setting-ref';\nexport * from './owner-info';\nexport * from './password-request';\nexport * from './pat-spec';\nexport * from './personal-access-token';\nexport * from './personal-access-token-list';\nexport * from './plugin';\nexport * from './plugin-author';\nexport * from './plugin-list';\nexport * from './plugin-running-state-request';\nexport * from './plugin-spec';\nexport * from './plugin-status';\nexport * from './policy';\nexport * from './policy-list';\nexport * from './policy-rule';\nexport * from './policy-spec';\nexport * from './policy-template';\nexport * from './policy-template-list';\nexport * from './policy-template-spec';\nexport * from './post';\nexport * from './post-list';\nexport * from './post-request';\nexport * from './post-spec';\nexport * from './post-status';\nexport * from './post-vo';\nexport * from './reason';\nexport * from './reason-attributes';\nexport * from './reason-list';\nexport * from './reason-property';\nexport * from './reason-selector';\nexport * from './reason-spec';\nexport * from './reason-subject';\nexport * from './reason-type';\nexport * from './reason-type-info';\nexport * from './reason-type-list';\nexport * from './reason-type-notifier-collection-request';\nexport * from './reason-type-notifier-matrix';\nexport * from './reason-type-notifier-request';\nexport * from './reason-type-spec';\nexport * from './ref';\nexport * from './remember-me-token';\nexport * from './remember-me-token-list';\nexport * from './remember-me-token-spec';\nexport * from './remove-operation';\nexport * from './replace-operation';\nexport * from './reply';\nexport * from './reply-list';\nexport * from './reply-request';\nexport * from './reply-spec';\nexport * from './reply-status';\nexport * from './reply-vo';\nexport * from './reply-vo-list';\nexport * from './reverse-proxy';\nexport * from './reverse-proxy-list';\nexport * from './reverse-proxy-rule';\nexport * from './revert-snapshot-for-post-param';\nexport * from './revert-snapshot-for-single-param';\nexport * from './role';\nexport * from './role-binding';\nexport * from './role-binding-list';\nexport * from './role-list';\nexport * from './role-ref';\nexport * from './search-option';\nexport * from './search-result';\nexport * from './secret';\nexport * from './secret-list';\nexport * from './setting';\nexport * from './setting-form';\nexport * from './setting-list';\nexport * from './setting-ref';\nexport * from './setting-spec';\nexport * from './setup-request';\nexport * from './single-page';\nexport * from './single-page-list';\nexport * from './single-page-request';\nexport * from './single-page-spec';\nexport * from './single-page-status';\nexport * from './single-page-vo';\nexport * from './site-stats-vo';\nexport * from './snap-shot-spec';\nexport * from './snapshot';\nexport * from './snapshot-list';\nexport * from './stats';\nexport * from './stats-vo';\nexport * from './subject';\nexport * from './subscription';\nexport * from './subscription-list';\nexport * from './subscription-spec';\nexport * from './subscription-subscriber';\nexport * from './tag';\nexport * from './tag-list';\nexport * from './tag-spec';\nexport * from './tag-status';\nexport * from './tag-vo';\nexport * from './tag-vo-list';\nexport * from './template-content';\nexport * from './template-descriptor';\nexport * from './test-operation';\nexport * from './theme';\nexport * from './theme-list';\nexport * from './theme-spec';\nexport * from './theme-status';\nexport * from './thumbnail';\nexport * from './thumbnail-list';\nexport * from './thumbnail-spec';\nexport * from './totp-auth-link-response';\nexport * from './totp-request';\nexport * from './two-factor-auth-settings';\nexport * from './uc-upload-from-url-request';\nexport * from './upgrade-from-uri-request';\nexport * from './upload-from-url-request';\nexport * from './user';\nexport * from './user-connection';\nexport * from './user-connection-list';\nexport * from './user-connection-spec';\nexport * from './user-device';\nexport * from './user-endpoint-listed-user-list';\nexport * from './user-list';\nexport * from './user-permission';\nexport * from './user-spec';\nexport * from './user-status';\nexport * from './verify-code-request';\nexport * from './vote-request';\n"
  },
  {
    "path": "ui/packages/api-client/src/models/install-from-uri-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface InstallFromUriRequest {\n    'uri': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/interest-reason-subject.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * The subject name of reason type to be interested in\n */\nexport interface InterestReasonSubject {\n    'apiVersion': string;\n    'kind': string;\n    /**\n     * if name is not specified, it presents all subjects of the specified reason type and custom resources\n     */\n    'name'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/interest-reason.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { InterestReasonSubject } from './interest-reason-subject';\n\n/**\n * The reason to be interested in\n */\nexport interface InterestReason {\n    /**\n     * The expression to be interested in\n     */\n    'expression'?: string;\n    /**\n     * The name of the reason definition to be interested in\n     */\n    'reasonType': string;\n    'subject': InterestReasonSubject;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/json-patch-inner.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { AddOperation } from './add-operation';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CopyOperation } from './copy-operation';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MoveOperation } from './move-operation';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { RemoveOperation } from './remove-operation';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReplaceOperation } from './replace-operation';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TestOperation } from './test-operation';\n\n/**\n * @type JsonPatchInner\n */\nexport type JsonPatchInner = AddOperation | CopyOperation | MoveOperation | RemoveOperation | ReplaceOperation | TestOperation;\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/license.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Common data objects for license.\n */\nexport interface License {\n    'name'?: string;\n    'url'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/list-result-reply-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReplyVo } from './reply-vo';\n\nexport interface ListResultReplyVo {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ReplyVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-auth-provider.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * A listed value object for {@link run.halo.app.core.extension.AuthProvider run.halo.app.core.extension.AuthProvider}.\n */\nexport interface ListedAuthProvider {\n    'authType'?: ListedAuthProviderAuthTypeEnum;\n    'authenticationUrl'?: string;\n    'bindingUrl'?: string;\n    'description'?: string;\n    'displayName': string;\n    'enabled'?: boolean;\n    'helpPage'?: string;\n    'isBound'?: boolean;\n    'logo'?: string;\n    'name': string;\n    'priority'?: number;\n    'privileged'?: boolean;\n    'supportsBinding'?: boolean;\n    'unbindingUrl'?: string;\n    'website'?: string;\n}\n\nexport const ListedAuthProviderAuthTypeEnum = {\n    Form: 'FORM',\n    Oauth2: 'OAUTH2'\n} as const;\n\nexport type ListedAuthProviderAuthTypeEnum = typeof ListedAuthProviderAuthTypeEnum[keyof typeof ListedAuthProviderAuthTypeEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-comment-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedComment } from './listed-comment';\n\nexport interface ListedCommentList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ListedComment>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-comment.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Comment } from './comment';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStats } from './comment-stats';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Extension } from './extension';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { OwnerInfo } from './owner-info';\n\n/**\n * A chunk of items.\n */\nexport interface ListedComment {\n    'comment': Comment;\n    'owner': OwnerInfo;\n    'stats': CommentStats;\n    'subject'?: Extension;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-post-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedPost } from './listed-post';\n\nexport interface ListedPostList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ListedPost>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-post-vo-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedPostVo } from './listed-post-vo';\n\nexport interface ListedPostVoList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ListedPostVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-post-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CategoryVo } from './category-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContributorVo } from './contributor-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PostSpec } from './post-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PostStatus } from './post-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { StatsVo } from './stats-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TagVo } from './tag-vo';\n\n/**\n * A value object for {@link Post Post}.\n */\nexport interface ListedPostVo {\n    'categories'?: Array<CategoryVo>;\n    'contributors'?: Array<ContributorVo>;\n    'metadata': Metadata;\n    'owner'?: ContributorVo;\n    'spec'?: PostSpec;\n    'stats'?: StatsVo;\n    'status'?: PostStatus;\n    'tags'?: Array<TagVo>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-post.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Category } from './category';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Contributor } from './contributor';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Post } from './post';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Stats } from './stats';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Tag } from './tag';\n\n/**\n * A chunk of items.\n */\nexport interface ListedPost {\n    'categories': Array<Category>;\n    'contributors': Array<Contributor>;\n    'owner': Contributor;\n    'post': Post;\n    'stats': Stats;\n    'tags': Array<Tag>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-reply-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedReply } from './listed-reply';\n\nexport interface ListedReplyList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ListedReply>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-reply.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStats } from './comment-stats';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { OwnerInfo } from './owner-info';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Reply } from './reply';\n\n/**\n * A chunk of items.\n */\nexport interface ListedReply {\n    'owner': OwnerInfo;\n    'reply': Reply;\n    'stats': CommentStats;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-single-page-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedSinglePage } from './listed-single-page';\n\nexport interface ListedSinglePageList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ListedSinglePage>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-single-page-vo-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedSinglePageVo } from './listed-single-page-vo';\n\nexport interface ListedSinglePageVoList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ListedSinglePageVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-single-page-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContributorVo } from './contributor-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePageSpec } from './single-page-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePageStatus } from './single-page-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { StatsVo } from './stats-vo';\n\n/**\n * A chunk of items.\n */\nexport interface ListedSinglePageVo {\n    'contributors'?: Array<ContributorVo>;\n    'metadata': Metadata;\n    'owner'?: ContributorVo;\n    'spec'?: SinglePageSpec;\n    'stats'?: StatsVo;\n    'status'?: SinglePageStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-single-page.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Contributor } from './contributor';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePage } from './single-page';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Stats } from './stats';\n\n/**\n * A chunk of items.\n */\nexport interface ListedSinglePage {\n    'contributors': Array<Contributor>;\n    'owner': Contributor;\n    'page': SinglePage;\n    'stats': Stats;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-snapshot-dto.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedSnapshotSpec } from './listed-snapshot-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface ListedSnapshotDto {\n    'metadata': Metadata;\n    'spec': ListedSnapshotSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-snapshot-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ListedSnapshotSpec {\n    'modifyTime'?: string;\n    'owner': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/listed-user.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Role } from './role';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { User } from './user';\n\n/**\n * A chunk of items.\n */\nexport interface ListedUser {\n    'roles': Array<Role>;\n    'user': User;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/local-thumbnail-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { LocalThumbnail } from './local-thumbnail';\n\nexport interface LocalThumbnailList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<LocalThumbnail>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/local-thumbnail-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface LocalThumbnailSpec {\n    /**\n     * Consider the compatibility of the system and migration, use unix-style relative paths  here.\n     */\n    'filePath': string;\n    /**\n     * A hash signature for the image uri.\n     */\n    'imageSignature': string;\n    'imageUri': string;\n    'size': LocalThumbnailSpecSizeEnum;\n    /**\n     * A hash signature for the thumbnail uri.\n     */\n    'thumbSignature': string;\n    'thumbnailUri': string;\n}\n\nexport const LocalThumbnailSpecSizeEnum = {\n    S: 'S',\n    M: 'M',\n    L: 'L',\n    Xl: 'XL'\n} as const;\n\nexport type LocalThumbnailSpecSizeEnum = typeof LocalThumbnailSpecSizeEnum[keyof typeof LocalThumbnailSpecSizeEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/local-thumbnail-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface LocalThumbnailStatus {\n    'phase'?: LocalThumbnailStatusPhaseEnum;\n}\n\nexport const LocalThumbnailStatusPhaseEnum = {\n    Pending: 'PENDING',\n    Succeeded: 'SUCCEEDED',\n    Failed: 'FAILED'\n} as const;\n\nexport type LocalThumbnailStatusPhaseEnum = typeof LocalThumbnailStatusPhaseEnum[keyof typeof LocalThumbnailStatusPhaseEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/local-thumbnail.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { LocalThumbnailSpec } from './local-thumbnail-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { LocalThumbnailStatus } from './local-thumbnail-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface LocalThumbnail {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': LocalThumbnailSpec;\n    'status': LocalThumbnailStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/mark-specified-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface MarkSpecifiedRequest {\n    'names'?: Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-item-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuItem } from './menu-item';\n\nexport interface MenuItemList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<MenuItem>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-item-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Ref } from './ref';\n\nexport interface MenuItemSpec {\n    /**\n     * Children of this menu item\n     */\n    'children'?: Array<string>;\n    /**\n     * The display name of menu item.\n     */\n    'displayName'?: string;\n    /**\n     * The href of this menu item.\n     */\n    'href'?: string;\n    /**\n     * The priority is for ordering.\n     */\n    'priority'?: number;\n    /**\n     * The <a> target attribute of this menu item.\n     */\n    'target'?: MenuItemSpecTargetEnum;\n    'targetRef'?: Ref;\n}\n\nexport const MenuItemSpecTargetEnum = {\n    Blank: '_blank',\n    Self: '_self',\n    Parent: '_parent',\n    Top: '_top'\n} as const;\n\nexport type MenuItemSpecTargetEnum = typeof MenuItemSpecTargetEnum[keyof typeof MenuItemSpecTargetEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-item-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface MenuItemStatus {\n    /**\n     * Calculated Display name of menu item.\n     */\n    'displayName'?: string;\n    /**\n     * Calculated href of manu item.\n     */\n    'href'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-item-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuItemSpec } from './menu-item-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuItemStatus } from './menu-item-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * A value object for {@link MenuItem MenuItem}.\n */\nexport interface MenuItemVo {\n    'children'?: Array<MenuItemVo>;\n    /**\n     * Gets menu item\\'s display name.\n     */\n    'displayName'?: string;\n    'metadata': Metadata;\n    'parentName'?: string;\n    'spec'?: MenuItemSpec;\n    'status'?: MenuItemStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-item.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuItemSpec } from './menu-item-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuItemStatus } from './menu-item-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface MenuItem {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': MenuItemSpec;\n    'status'?: MenuItemStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Menu } from './menu';\n\nexport interface MenuList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Menu>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface MenuSpec {\n    /**\n     * The display name of the menu.\n     */\n    'displayName': string;\n    /**\n     * Menu items of this menu.\n     */\n    'menuItems'?: Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuItemVo } from './menu-item-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuSpec } from './menu-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * A value object for {@link Menu Menu}.\n */\nexport interface MenuVo {\n    'menuItems'?: Array<MenuItemVo>;\n    'metadata': Metadata;\n    'spec'?: MenuSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/menu.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { MenuSpec } from './menu-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\nexport interface Menu {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': MenuSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/metadata.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Metadata of Extension.\n */\nexport interface Metadata {\n    /**\n     * Annotations are like key-value format.\n     */\n    'annotations'?: { [key: string]: string; };\n    /**\n     * Creation timestamp of the Extension.\n     */\n    'creationTimestamp'?: string | null;\n    /**\n     * Deletion timestamp of the Extension.\n     */\n    'deletionTimestamp'?: string | null;\n    'finalizers'?: Array<string | null> | null;\n    /**\n     * The name field will be generated automatically according to the given generateName field\n     */\n    'generateName'?: string;\n    /**\n     * Labels are like key-value format.\n     */\n    'labels'?: { [key: string]: string; };\n    /**\n     * Metadata name\n     */\n    'name': string;\n    /**\n     * Current version of the Extension. It will be bumped up every update.\n     */\n    'version'?: number | null;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/move-operation.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface MoveOperation {\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'from': string;\n    'op': MoveOperationOpEnum;\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'path': string;\n}\n\nexport const MoveOperationOpEnum = {\n    Move: 'move'\n} as const;\n\nexport type MoveOperationOpEnum = typeof MoveOperationOpEnum[keyof typeof MoveOperationOpEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/navigation-post-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedPostVo } from './listed-post-vo';\n\n/**\n * Post navigation vo to hold previous and next item.\n */\nexport interface NavigationPostVo {\n    'next'?: ListedPostVo;\n    'previous'?: ListedPostVo;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notification-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Notification } from './notification';\n\nexport interface NotificationList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Notification>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notification-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface NotificationSpec {\n    'htmlContent': string;\n    'lastReadAt'?: string;\n    'rawContent': string;\n    /**\n     * The name of reason\n     */\n    'reason': string;\n    /**\n     * The name of user\n     */\n    'recipient': string;\n    'title': string;\n    'unread'?: boolean;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notification-template-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { NotificationTemplate } from './notification-template';\n\nexport interface NotificationTemplateList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<NotificationTemplate>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notification-template-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonSelector } from './reason-selector';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TemplateContent } from './template-content';\n\nexport interface NotificationTemplateSpec {\n    'reasonSelector'?: ReasonSelector;\n    'template'?: TemplateContent;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notification-template.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { NotificationTemplateSpec } from './notification-template-spec';\n\n/**\n * <p>{@link NotificationTemplate NotificationTemplate} is a custom extension that defines a notification template.</p>  <p>It describes the notification template\\'s name, description, and the template content.</p>  <p>{@link Spec#getReasonSelector Spec#getReasonSelector()} is used to select the template by reasonType and language,  if multiple templates are matched, the best match will be selected. This is useful when you  want to override the default template.</p>\n */\nexport interface NotificationTemplate {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: NotificationTemplateSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notification.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { NotificationSpec } from './notification-spec';\n\n/**\n * <p>{@link Notification Notification} is a custom extension that used to store notification information for  inner use, it\\'s on-site notification.</p>   <p>Supports the following operations:</p>  <ul>     <li>Marked as read: {@link NotificationSpec#setUnread(boolean) NotificationSpec#setUnread(boolean)}</li>     <li>Get the last read time: {@link NotificationSpec#getLastReadAt NotificationSpec#getLastReadAt()}</li>     <li>Filter by recipient: {@link NotificationSpec#getRecipient NotificationSpec#getRecipient()}</li>  </ul>\n */\nexport interface Notification {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: NotificationSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notifier-descriptor-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { NotifierDescriptor } from './notifier-descriptor';\n\nexport interface NotifierDescriptorList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<NotifierDescriptor>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notifier-descriptor-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { NotifierSettingRef } from './notifier-setting-ref';\n\nexport interface NotifierDescriptorSpec {\n    'description'?: string;\n    'displayName': string;\n    'notifierExtName': string;\n    'receiverSettingRef'?: NotifierSettingRef;\n    'senderSettingRef'?: NotifierSettingRef;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notifier-descriptor.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { NotifierDescriptorSpec } from './notifier-descriptor-spec';\n\n/**\n * <p>{@link NotifierDescriptor NotifierDescriptor} is a custom extension that defines a notifier.</p>  <p>It describes the notifier\\'s name, description, and the extension name of the notifier to  let the user know what the notifier is and what it can do in the UI and also let the  <code>NotificationCenter</code> know how to load the notifier and prepare the notifier\\'s settings.</p>\n */\nexport interface NotifierDescriptor {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: NotifierDescriptorSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notifier-info.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface NotifierInfo {\n    'description'?: string;\n    'displayName'?: string;\n    'name'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/notifier-setting-ref.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface NotifierSettingRef {\n    'group': string;\n    'name': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/owner-info.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Comment owner info.\n */\nexport interface OwnerInfo {\n    'avatar'?: string;\n    'displayName'?: string;\n    'email'?: string;\n    'kind'?: string;\n    'name'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/password-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface PasswordRequest {\n    'password': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/pat-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface PatSpec {\n    'description'?: string;\n    'expiresAt'?: string;\n    'lastUsed'?: string;\n    'name': string;\n    'revoked'?: boolean;\n    'revokesAt'?: string;\n    'roles'?: Array<string>;\n    'scopes'?: Array<string>;\n    'tokenId': string;\n    'username': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/personal-access-token-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PersonalAccessToken } from './personal-access-token';\n\nexport interface PersonalAccessTokenList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<PersonalAccessToken>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/personal-access-token.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PatSpec } from './pat-spec';\n\nexport interface PersonalAccessToken {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: PatSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/plugin-author.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface PluginAuthor {\n    'name': string;\n    'website'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/plugin-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Plugin } from './plugin';\n\nexport interface PluginList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Plugin>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/plugin-running-state-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface PluginRunningStateRequest {\n    'async'?: boolean;\n    'enable'?: boolean;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/plugin-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { License } from './license';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PluginAuthor } from './plugin-author';\n\nexport interface PluginSpec {\n    'author'?: PluginAuthor;\n    'configMapName'?: string;\n    'description'?: string;\n    'displayName'?: string;\n    'enabled'?: boolean;\n    'homepage'?: string;\n    'issues'?: string;\n    'license'?: Array<License>;\n    'logo'?: string;\n    'pluginDependencies'?: { [key: string]: string; };\n    'repo'?: string;\n    /**\n     * SemVer format.\n     */\n    'requires'?: string;\n    'settingName'?: string;\n    /**\n     * plugin version.\n     */\n    'version': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/plugin-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Condition } from './condition';\n\nexport interface PluginStatus {\n    'conditions'?: Array<Condition>;\n    'entry'?: string;\n    'lastProbeState'?: PluginStatusLastProbeStateEnum;\n    'lastStartTime'?: string;\n    /**\n     * Load location of the plugin, often a path.\n     */\n    'loadLocation'?: string;\n    'logo'?: string;\n    'phase'?: PluginStatusPhaseEnum;\n    'stylesheet'?: string;\n}\n\nexport const PluginStatusLastProbeStateEnum = {\n    Created: 'CREATED',\n    Disabled: 'DISABLED',\n    Resolved: 'RESOLVED',\n    Started: 'STARTED',\n    Stopped: 'STOPPED',\n    Failed: 'FAILED',\n    Unloaded: 'UNLOADED'\n} as const;\n\nexport type PluginStatusLastProbeStateEnum = typeof PluginStatusLastProbeStateEnum[keyof typeof PluginStatusLastProbeStateEnum];\nexport const PluginStatusPhaseEnum = {\n    Pending: 'PENDING',\n    Starting: 'STARTING',\n    Created: 'CREATED',\n    Disabling: 'DISABLING',\n    Disabled: 'DISABLED',\n    Resolved: 'RESOLVED',\n    Started: 'STARTED',\n    Stopped: 'STOPPED',\n    Failed: 'FAILED',\n    Unknown: 'UNKNOWN'\n} as const;\n\nexport type PluginStatusPhaseEnum = typeof PluginStatusPhaseEnum[keyof typeof PluginStatusPhaseEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/plugin.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PluginSpec } from './plugin-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PluginStatus } from './plugin-status';\n\n/**\n * A custom resource for Plugin.\n */\nexport interface Plugin {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': PluginSpec;\n    'status'?: PluginStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/policy-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Policy } from './policy';\n\nexport interface PolicyList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Policy>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/policy-rule.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * PolicyRule holds information that describes a policy rule, but does not contain information  about whom the rule applies to or which namespace the rule applies to.\n */\nexport interface PolicyRule {\n    /**\n     * APIGroups is the name of the APIGroup that contains the resources.  If multiple API groups are specified, any action requested against one of the enumerated  resources in any API group will be allowed.\n     */\n    'apiGroups'?: Array<string>;\n    /**\n     * NonResourceURLs is a set of partial urls that a user should have access to.  *s are allowed, but only as the full, final step in the path  If an action is not a resource API request, then the URL is split on \\'/\\' and is checked  against the NonResourceURLs to look for a match.  Since non-resource URLs are not namespaced, this field is only applicable for  ClusterRoles referenced from a ClusterRoleBinding.  Rules can either apply to API resources (such as \\\"pods\\\" or \\\"secrets\\\") or non-resource  URL paths (such as \\\"/api\\\"),  but not both.\n     */\n    'nonResourceURLs'?: Array<string>;\n    /**\n     * ResourceNames is an optional white list of names that the rule applies to.  An empty set  means that everything is allowed.\n     */\n    'resourceNames'?: Array<string>;\n    /**\n     * Resources is a list of resources this rule applies to.  \\'*\\' represents all resources in  the specified apiGroups.  \\'*&#47;foo\\' represents the subresource \\'foo\\' for all resources in the specified  apiGroups.\n     */\n    'resources'?: Array<string>;\n    /**\n     * about who the rule applies to or which namespace the rule applies to.\n     */\n    'verbs'?: Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/policy-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface PolicySpec {\n    /**\n     * Reference name of ConfigMap extension\n     */\n    'configMapName'?: string;\n    /**\n     * Display name of policy\n     */\n    'displayName': string;\n    /**\n     * Reference name of PolicyTemplate\n     */\n    'templateName': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/policy-template-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PolicyTemplate } from './policy-template';\n\nexport interface PolicyTemplateList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<PolicyTemplate>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/policy-template-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface PolicyTemplateSpec {\n    'displayName'?: string;\n    'settingName': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/policy-template.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PolicyTemplateSpec } from './policy-template-spec';\n\nexport interface PolicyTemplate {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: PolicyTemplateSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/policy.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PolicySpec } from './policy-spec';\n\nexport interface Policy {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': PolicySpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/post-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Post } from './post';\n\nexport interface PostList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Post>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/post-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContentUpdateParam } from './content-update-param';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Post } from './post';\n\n/**\n * Post and content data for creating and updating post.\n */\nexport interface PostRequest {\n    'content': ContentUpdateParam;\n    'post': Post;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/post-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Excerpt } from './excerpt';\n\nexport interface PostSpec {\n    'allowComment': boolean;\n    'baseSnapshot'?: string;\n    'categories'?: Array<string>;\n    'cover'?: string;\n    'deleted': boolean;\n    'excerpt': Excerpt;\n    'headSnapshot'?: string;\n    'htmlMetas'?: Array<{ [key: string]: string; }>;\n    'owner'?: string;\n    'pinned': boolean;\n    'priority': number;\n    'publish': boolean;\n    'publishTime'?: string;\n    /**\n     * 文章引用到的已发布的内容，用于主题端显示.\n     */\n    'releaseSnapshot'?: string;\n    'slug': string;\n    'tags'?: Array<string>;\n    'template'?: string;\n    'title': string;\n    'visible': PostSpecVisibleEnum;\n}\n\nexport const PostSpecVisibleEnum = {\n    Public: 'PUBLIC',\n    Internal: 'INTERNAL',\n    Private: 'PRIVATE'\n} as const;\n\nexport type PostSpecVisibleEnum = typeof PostSpecVisibleEnum[keyof typeof PostSpecVisibleEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/post-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Condition } from './condition';\n\nexport interface PostStatus {\n    'commentsCount'?: number;\n    'conditions'?: Array<Condition>;\n    'contributors'?: Array<string>;\n    'excerpt'?: string;\n    /**\n     * see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\n     */\n    'hideFromList'?: boolean;\n    'inProgress'?: boolean;\n    'lastModifyTime'?: string;\n    'observedVersion'?: number;\n    'permalink'?: string;\n    'phase'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/post-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CategoryVo } from './category-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContentVo } from './content-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContributorVo } from './contributor-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PostSpec } from './post-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PostStatus } from './post-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { StatsVo } from './stats-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TagVo } from './tag-vo';\n\n/**\n * A value object for {@link Post Post}.\n */\nexport interface PostVo {\n    'categories'?: Array<CategoryVo>;\n    'content'?: ContentVo;\n    'contributors'?: Array<ContributorVo>;\n    'metadata': Metadata;\n    'owner'?: ContributorVo;\n    'spec'?: PostSpec;\n    'stats'?: StatsVo;\n    'status'?: PostStatus;\n    'tags'?: Array<TagVo>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/post.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PostSpec } from './post-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PostStatus } from './post-status';\n\n/**\n * <p>Post extension.</p>\n */\nexport interface Post {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': PostSpec;\n    'status'?: PostStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-attributes.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Attributes used to transfer data\n */\nexport interface ReasonAttributes {\n    'empty'?: boolean;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Reason } from './reason';\n\nexport interface ReasonList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Reason>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-property.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ReasonProperty {\n    'description'?: string;\n    'name': string;\n    'optional'?: boolean;\n    'type': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-selector.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ReasonSelector {\n    'language': string;\n    'reasonType': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonAttributes } from './reason-attributes';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonSubject } from './reason-subject';\n\nexport interface ReasonSpec {\n    'attributes'?: ReasonAttributes;\n    'author': string;\n    'reasonType': string;\n    'subject': ReasonSubject;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-subject.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ReasonSubject {\n    'apiVersion': string;\n    'kind': string;\n    'name': string;\n    'title': string;\n    'url'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-type-info.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ReasonTypeInfo {\n    'description'?: string;\n    'displayName'?: string;\n    'name'?: string;\n    'uiPermissions'?: Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-type-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonType } from './reason-type';\n\nexport interface ReasonTypeList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ReasonType>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-type-notifier-collection-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonTypeNotifierRequest } from './reason-type-notifier-request';\n\nexport interface ReasonTypeNotifierCollectionRequest {\n    'reasonTypeNotifiers': Array<ReasonTypeNotifierRequest>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-type-notifier-matrix.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { NotifierInfo } from './notifier-info';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonTypeInfo } from './reason-type-info';\n\nexport interface ReasonTypeNotifierMatrix {\n    'notifiers'?: Array<NotifierInfo>;\n    'reasonTypes'?: Array<ReasonTypeInfo>;\n    'stateMatrix'?: Array<Array<boolean>>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-type-notifier-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ReasonTypeNotifierRequest {\n    'notifiers'?: Array<string>;\n    'reasonType'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-type-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonProperty } from './reason-property';\n\nexport interface ReasonTypeSpec {\n    'description': string;\n    'displayName': string;\n    'properties'?: Array<ReasonProperty>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason-type.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonTypeSpec } from './reason-type-spec';\n\n/**\n * <p>{@link ReasonType ReasonType} is a custom extension that defines a type of reason.</p>  <p>One {@link ReasonType ReasonType} can have multiple {@link Reason Reason}s to notify.</p>\n */\nexport interface ReasonType {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: ReasonTypeSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reason.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReasonSpec } from './reason-spec';\n\n/**\n * <p>{@link Reason Reason} is a custom extension that defines a reason for a notification, It represents  an instance of a {@link ReasonType ReasonType}.</p>  <p>It can be understood as an event that triggers a notification.</p>\n */\nexport interface Reason {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: ReasonSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/ref.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Extension reference object. The name is mandatory\n */\nexport interface Ref {\n    /**\n     * Extension group\n     */\n    'group': string;\n    /**\n     * Extension kind\n     */\n    'kind': string;\n    /**\n     * Extension name. This field is mandatory\n     */\n    'name': string;\n    /**\n     * Extension version\n     */\n    'version'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/remember-me-token-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { RememberMeToken } from './remember-me-token';\n\nexport interface RememberMeTokenList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<RememberMeToken>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/remember-me-token-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface RememberMeTokenSpec {\n    'lastUsed'?: string;\n    'series': string;\n    'tokenValue': string;\n    'username': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/remember-me-token.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { RememberMeTokenSpec } from './remember-me-token-spec';\n\nexport interface RememberMeToken {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': RememberMeTokenSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/remove-operation.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface RemoveOperation {\n    'op': RemoveOperationOpEnum;\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'path': string;\n}\n\nexport const RemoveOperationOpEnum = {\n    Remove: 'remove'\n} as const;\n\nexport type RemoveOperationOpEnum = typeof RemoveOperationOpEnum[keyof typeof RemoveOperationOpEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/replace-operation.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ReplaceOperation {\n    'op': ReplaceOperationOpEnum;\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'path': string;\n    /**\n     * Value can be any JSON value\n     */\n    'value': any;\n}\n\nexport const ReplaceOperationOpEnum = {\n    Replace: 'replace'\n} as const;\n\nexport type ReplaceOperationOpEnum = typeof ReplaceOperationOpEnum[keyof typeof ReplaceOperationOpEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reply-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Reply } from './reply';\n\nexport interface ReplyList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Reply>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reply-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentEmailOwner } from './comment-email-owner';\n\n/**\n * A request parameter object for {@link Reply Reply}.\n */\nexport interface ReplyRequest {\n    'allowNotification'?: boolean;\n    'content': string;\n    'hidden'?: boolean;\n    'owner'?: CommentEmailOwner;\n    'quoteReply'?: string;\n    'raw': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reply-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentOwner } from './comment-owner';\n\nexport interface ReplySpec {\n    'allowNotification': boolean;\n    'approved': boolean;\n    'approvedTime'?: string;\n    'commentName': string;\n    'content': string;\n    /**\n     * The user-defined creation time default is <code>metadata.creationTimestamp</code>.\n     */\n    'creationTime'?: string;\n    'hidden': boolean;\n    'ipAddress'?: string;\n    'owner': CommentOwner;\n    'priority': number;\n    'quoteReply'?: string;\n    'raw': string;\n    'top': boolean;\n    'userAgent'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reply-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ReplyStatus {\n    'observedVersion'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reply-vo-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReplyVo } from './reply-vo';\n\nexport interface ReplyVoList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ReplyVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reply-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CommentStatsVo } from './comment-stats-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { OwnerInfo } from './owner-info';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReplySpec } from './reply-spec';\n\n/**\n * A chunk of items.\n */\nexport interface ReplyVo {\n    'metadata': Metadata;\n    'owner': OwnerInfo;\n    'spec': ReplySpec;\n    'stats': CommentStatsVo;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reply.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReplySpec } from './reply-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReplyStatus } from './reply-status';\n\nexport interface Reply {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': ReplySpec;\n    'status': ReplyStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reverse-proxy-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReverseProxy } from './reverse-proxy';\n\nexport interface ReverseProxyList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ReverseProxy>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reverse-proxy-rule.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { FileReverseProxyProvider } from './file-reverse-proxy-provider';\n\nexport interface ReverseProxyRule {\n    'file'?: FileReverseProxyProvider;\n    'path'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/reverse-proxy.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ReverseProxyRule } from './reverse-proxy-rule';\n\n/**\n * <p>The reverse proxy custom resource is used to configure a path to proxy it to a directory or  file.</p>  <p>HTTP proxy may be added in the future.</p>\n */\nexport interface ReverseProxy {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'rules'?: Array<ReverseProxyRule>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/revert-snapshot-for-post-param.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface RevertSnapshotForPostParam {\n    'snapshotName': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/revert-snapshot-for-single-param.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface RevertSnapshotForSingleParam {\n    'snapshotName': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/role-binding-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { RoleBinding } from './role-binding';\n\nexport interface RoleBindingList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<RoleBinding>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/role-binding.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { RoleRef } from './role-ref';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Subject } from './subject';\n\n/**\n * RoleBinding references a role, but does not contain it.  It can reference a Role in the global.  It adds who information via Subjects.\n */\nexport interface RoleBinding {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'roleRef'?: RoleRef;\n    /**\n     * Subjects holds references to the objects the role applies to.\n     */\n    'subjects'?: Array<Subject>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/role-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Role } from './role';\n\nexport interface RoleList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Role>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/role-ref.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * RoleRef contains information that points to the role being used.\n */\nexport interface RoleRef {\n    /**\n     * APIGroup is the group for the resource being referenced.\n     */\n    'apiGroup'?: string;\n    /**\n     * Kind is the type of resource being referenced.\n     */\n    'kind'?: string;\n    /**\n     * Name is the name of resource being referenced.\n     */\n    'name'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/role.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { PolicyRule } from './policy-rule';\n\nexport interface Role {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'rules': Array<PolicyRule>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/search-option.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Search option. It is used to control search behavior.\n */\nexport interface SearchOption {\n    /**\n     * Additional annotations for extending search option by other search engines.\n     */\n    'annotations'?: { [key: string]: string; };\n    /**\n     * Whether to filter exposed content. If null, it will not filter.\n     */\n    'filterExposed'?: boolean;\n    /**\n     * Whether to filter published content. If null, it will not filter.\n     */\n    'filterPublished'?: boolean;\n    /**\n     * Whether to filter recycled content. If null, it will not filter.\n     */\n    'filterRecycled'?: boolean;\n    /**\n     * Post HTML tag of highlighted fragment.\n     */\n    'highlightPostTag'?: string;\n    /**\n     * Pre HTML tag of highlighted fragment.\n     */\n    'highlightPreTag'?: string;\n    /**\n     * Category names to include(and). If null, it will include all categories.\n     */\n    'includeCategoryNames'?: Array<string>;\n    /**\n     * Owner names to include(or). If null, it will include all owners.\n     */\n    'includeOwnerNames'?: Array<string>;\n    /**\n     * Tag names to include(and). If null, it will include all tags.\n     */\n    'includeTagNames'?: Array<string>;\n    /**\n     * Types to include(or). If null, it will include all types.\n     */\n    'includeTypes'?: Array<string>;\n    /**\n     * Search keyword.\n     */\n    'keyword': string;\n    /**\n     * Limit of result.\n     */\n    'limit'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/search-result.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { HaloDocument } from './halo-document';\n\nexport interface SearchResult {\n    'hits'?: Array<HaloDocument>;\n    'keyword'?: string;\n    'limit'?: number;\n    'processingTimeMillis'?: number;\n    'total'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/secret-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Secret } from './secret';\n\nexport interface SecretList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Secret>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/secret.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n\n/**\n * Secret is a small piece of sensitive data which should be kept secret, such as a password,  a token, or a key.\n */\nexport interface Secret {\n    'apiVersion': string;\n    /**\n     * <p>The total bytes of the values in  the Data field must be less than {@link run.halo.app.extension.Secret#MAX_SECRET_SIZE #MAX_SECRET_SIZE} bytes.</p>  <p><code>data</code> contains the secret data. Each key must consist of alphanumeric  characters, \\'-\\', \\'_\\' or \\'.\\'. The serialized form of the secret data is a  base64 encoded string, representing the arbitrary (possibly non-string)  data value here. Described in  <a href=\\\"https://tools.ietf.org/html/rfc4648#section-4\\\">rfc4648#section-4</a>  </p>\n     */\n    'data'?: { [key: string]: string; };\n    'kind': string;\n    'metadata': Metadata;\n    /**\n     * <code>stringData</code> allows specifying non-binary secret data in string form.  It is provided as a write-only input field for convenience.  All keys and values are merged into the data field on write, overwriting any existing  values.  The stringData field is never output when reading from the API.\n     */\n    'stringData'?: { [key: string]: string; };\n    /**\n     * Used to facilitate programmatic handling of secret data.  More info:  <a href=\\\"https://kubernetes.io/docs/concepts/configuration/secret/#secret-types\\\">secret-types</a>\n     */\n    'type'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/setting-form.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface SettingForm {\n    'formSchema': Array<object>;\n    'group': string;\n    'label'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/setting-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Setting } from './setting';\n\nexport interface SettingList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Setting>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/setting-ref.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface SettingRef {\n    'group': string;\n    'name': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/setting-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SettingForm } from './setting-form';\n\nexport interface SettingSpec {\n    'forms': Array<SettingForm>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/setting.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SettingSpec } from './setting-spec';\n\n/**\n * {@link Setting Setting} is a custom extension to generate forms based on configuration.\n */\nexport interface Setting {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': SettingSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/setup-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface SetupRequest {\n    'email'?: string;\n    'externalUrl': string;\n    'language'?: string;\n    'password': string;\n    'siteTitle': string;\n    'username': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/single-page-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePage } from './single-page';\n\nexport interface SinglePageList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<SinglePage>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/single-page-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContentUpdateParam } from './content-update-param';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePage } from './single-page';\n\n/**\n * A request parameter for {@link SinglePage SinglePage}.\n */\nexport interface SinglePageRequest {\n    'content': ContentUpdateParam;\n    'page': SinglePage;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/single-page-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Excerpt } from './excerpt';\n\nexport interface SinglePageSpec {\n    'allowComment': boolean;\n    'baseSnapshot'?: string;\n    'cover'?: string;\n    'deleted': boolean;\n    'excerpt': Excerpt;\n    'headSnapshot'?: string;\n    'htmlMetas'?: Array<{ [key: string]: string; }>;\n    'owner'?: string;\n    'pinned': boolean;\n    'priority': number;\n    'publish': boolean;\n    'publishTime'?: string;\n    /**\n     * 引用到的已发布的内容，用于主题端显示.\n     */\n    'releaseSnapshot'?: string;\n    'slug': string;\n    'template'?: string;\n    'title': string;\n    'visible': SinglePageSpecVisibleEnum;\n}\n\nexport const SinglePageSpecVisibleEnum = {\n    Public: 'PUBLIC',\n    Internal: 'INTERNAL',\n    Private: 'PRIVATE'\n} as const;\n\nexport type SinglePageSpecVisibleEnum = typeof SinglePageSpecVisibleEnum[keyof typeof SinglePageSpecVisibleEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/single-page-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Condition } from './condition';\n\nexport interface SinglePageStatus {\n    'commentsCount'?: number;\n    'conditions'?: Array<Condition>;\n    'contributors'?: Array<string>;\n    'excerpt'?: string;\n    /**\n     * see {@link Category.CategorySpec#isHideFromList Category.CategorySpec#isHideFromList()}.\n     */\n    'hideFromList'?: boolean;\n    'inProgress'?: boolean;\n    'lastModifyTime'?: string;\n    'observedVersion'?: number;\n    'permalink'?: string;\n    'phase'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/single-page-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContentVo } from './content-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ContributorVo } from './contributor-vo';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePageSpec } from './single-page-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePageStatus } from './single-page-status';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { StatsVo } from './stats-vo';\n\n/**\n * A value object for {@link SinglePage SinglePage}.\n */\nexport interface SinglePageVo {\n    'content'?: ContentVo;\n    'contributors'?: Array<ContributorVo>;\n    'metadata': Metadata;\n    'owner'?: ContributorVo;\n    'spec'?: SinglePageSpec;\n    'stats'?: StatsVo;\n    'status'?: SinglePageStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/single-page.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePageSpec } from './single-page-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SinglePageStatus } from './single-page-status';\n\n/**\n * <p>Single page extension.</p>\n */\nexport interface SinglePage {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': SinglePageSpec;\n    'status'?: SinglePageStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/site-stats-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * A value object for site stats.\n */\nexport interface SiteStatsVo {\n    'category'?: number;\n    'comment'?: number;\n    'post'?: number;\n    'upvote'?: number;\n    'visit'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/snap-shot-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Ref } from './ref';\n\nexport interface SnapShotSpec {\n    'contentPatch'?: string;\n    'contributors'?: Array<string>;\n    'lastModifyTime'?: string;\n    'owner': string;\n    'parentSnapshotName'?: string;\n    'rawPatch'?: string;\n    /**\n     * such as: markdown | html | json | asciidoc | latex.\n     */\n    'rawType': string;\n    'subjectRef': Ref;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/snapshot-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Snapshot } from './snapshot';\n\nexport interface SnapshotList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Snapshot>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/snapshot.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SnapShotSpec } from './snap-shot-spec';\n\nexport interface Snapshot {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': SnapShotSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/stats-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Stats value object.\n */\nexport interface StatsVo {\n    'comment'?: number;\n    'upvote'?: number;\n    'visit'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/stats.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Stats value object.\n */\nexport interface Stats {\n    'approvedComment'?: number;\n    'totalComment'?: number;\n    'upvote'?: number;\n    'visit'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/subject.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface Subject {\n    /**\n     * APIGroup holds the API group of the referenced subject.  Defaults to \\\"\\\" for ServiceAccount subjects.  Defaults to \\\"rbac.authorization.halo.run\\\" for User and Group subjects.\n     */\n    'apiGroup'?: string;\n    /**\n     * Kind of object being referenced. Values defined by this API group are \\\"User\\\", \\\"Group\\\",  and \\\"ServiceAccount\\\".  If the Authorizer does not recognize the kind value, the Authorizer should report  an error.\n     */\n    'kind'?: string;\n    /**\n     * Name of the object being referenced.\n     */\n    'name'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/subscription-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Subscription } from './subscription';\n\nexport interface SubscriptionList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Subscription>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/subscription-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { InterestReason } from './interest-reason';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SubscriptionSubscriber } from './subscription-subscriber';\n\nexport interface SubscriptionSpec {\n    /**\n     * Perhaps users need to unsubscribe and interact without receiving notifications again\n     */\n    'disabled'?: boolean;\n    'reason': InterestReason;\n    'subscriber': SubscriptionSubscriber;\n    /**\n     * The token to unsubscribe\n     */\n    'unsubscribeToken': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/subscription-subscriber.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * The subscriber to be notified\n */\nexport interface SubscriptionSubscriber {\n    'name': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/subscription.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { SubscriptionSpec } from './subscription-spec';\n\n/**\n * <p>{@link Subscription Subscription} is a custom extension that defines a subscriber to be notified when a  certain {@link Reason Reason} is triggered.</p>  <p>It holds a {@link Subscriber Subscriber} to the user to be notified, a {@link InterestReason InterestReason} to  subscribe to.</p>\n */\nexport interface Subscription {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec'?: SubscriptionSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/tag-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Tag } from './tag';\n\nexport interface TagList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Tag>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/tag-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface TagSpec {\n    /**\n     * Color regex explanation.  <pre>  ^                 # start of the line  #                 # start with a number sign `#`  (                 # start of (group 1)    [a-fA-F0-9]{6}  # support z-f, A-F and 0-9, with a length of 6    |               # or    [a-fA-F0-9]{3}  # support z-f, A-F and 0-9, with a length of 3  )                 # end of (group 1)  $                 # end of the line  </pre>\n     */\n    'color'?: string;\n    'cover'?: string;\n    'description'?: string;\n    'displayName': string;\n    'slug': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/tag-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface TagStatus {\n    'observedVersion'?: number;\n    'permalink'?: string;\n    'postCount'?: number;\n    'visiblePostCount'?: number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/tag-vo-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TagVo } from './tag-vo';\n\nexport interface TagVoList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<TagVo>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/tag-vo.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TagSpec } from './tag-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TagStatus } from './tag-status';\n\n/**\n * A value object for {@link Tag Tag}.\n */\nexport interface TagVo {\n    'metadata': Metadata;\n    'postCount'?: number;\n    'spec'?: TagSpec;\n    'status'?: TagStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/tag.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TagSpec } from './tag-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { TagStatus } from './tag-status';\n\nexport interface Tag {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': TagSpec;\n    'status'?: TagStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/template-content.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface TemplateContent {\n    'htmlBody'?: string;\n    'rawBody'?: string;\n    'title': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/template-descriptor.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\n/**\n * Type used to describe custom template page.\n */\nexport interface TemplateDescriptor {\n    'description'?: string;\n    'file': string;\n    'name': string;\n    'screenshot'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/test-operation.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface TestOperation {\n    'op': TestOperationOpEnum;\n    /**\n     * A JSON Pointer path pointing to the location to move/copy from.\n     */\n    'path': string;\n    /**\n     * Value can be any JSON value\n     */\n    'value': any;\n}\n\nexport const TestOperationOpEnum = {\n    Test: 'test'\n} as const;\n\nexport type TestOperationOpEnum = typeof TestOperationOpEnum[keyof typeof TestOperationOpEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/theme-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Theme } from './theme';\n\nexport interface ThemeList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Theme>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/theme-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Author } from './author';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { CustomTemplates } from './custom-templates';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { License } from './license';\n\nexport interface ThemeSpec {\n    'author': Author;\n    'configMapName'?: string;\n    'customTemplates'?: CustomTemplates;\n    'description'?: string;\n    'displayName': string;\n    'homepage'?: string;\n    'issues'?: string;\n    'license'?: Array<License>;\n    'logo'?: string;\n    'repo'?: string;\n    'requires'?: string;\n    'settingName'?: string;\n    'version'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/theme-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Condition } from './condition';\n\nexport interface ThemeStatus {\n    'conditions'?: Array<Condition>;\n    'location'?: string;\n    'phase'?: ThemeStatusPhaseEnum;\n}\n\nexport const ThemeStatusPhaseEnum = {\n    Ready: 'READY',\n    Failed: 'FAILED',\n    Unknown: 'UNKNOWN'\n} as const;\n\nexport type ThemeStatusPhaseEnum = typeof ThemeStatusPhaseEnum[keyof typeof ThemeStatusPhaseEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/theme.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ThemeSpec } from './theme-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ThemeStatus } from './theme-status';\n\n/**\n * <p>Theme extension.</p>\n */\nexport interface Theme {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': ThemeSpec;\n    'status'?: ThemeStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/thumbnail-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Thumbnail } from './thumbnail';\n\nexport interface ThumbnailList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<Thumbnail>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/thumbnail-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface ThumbnailSpec {\n    'imageSignature': string;\n    'imageUri': string;\n    'size': ThumbnailSpecSizeEnum;\n    'thumbnailUri': string;\n}\n\nexport const ThumbnailSpecSizeEnum = {\n    S: 'S',\n    M: 'M',\n    L: 'L',\n    Xl: 'XL'\n} as const;\n\nexport type ThumbnailSpecSizeEnum = typeof ThumbnailSpecSizeEnum[keyof typeof ThumbnailSpecSizeEnum];\n\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/thumbnail.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ThumbnailSpec } from './thumbnail-spec';\n\nexport interface Thumbnail {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': ThumbnailSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/totp-auth-link-response.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface TotpAuthLinkResponse {\n    /**\n     * QR Code with base64 encoded.\n     */\n    'authLink'?: string;\n    'rawSecret'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/totp-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface TotpRequest {\n    'code': string;\n    'password': string;\n    'secret': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/two-factor-auth-settings.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface TwoFactorAuthSettings {\n    /**\n     * Check if 2FA is available.\n     */\n    'available'?: boolean;\n    'emailVerified'?: boolean;\n    'enabled'?: boolean;\n    'totpConfigured'?: boolean;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/uc-upload-from-url-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface UcUploadFromUrlRequest {\n    /**\n     * Custom file name\n     */\n    'filename'?: string;\n    'url': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/upgrade-from-uri-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface UpgradeFromUriRequest {\n    'uri': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/upload-from-url-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface UploadFromUrlRequest {\n    /**\n     * Custom file name\n     */\n    'filename'?: string;\n    /**\n     * The name of the group to which the attachment belongs\n     */\n    'groupName'?: string;\n    /**\n     * Storage policy name\n     */\n    'policyName': string;\n    'url': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-connection-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { UserConnection } from './user-connection';\n\nexport interface UserConnectionList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<UserConnection>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-connection-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface UserConnectionSpec {\n    /**\n     * The unique identifier for the user\\'s connection to the OAuth provider.  for example, the user\\'s GitHub id.\n     */\n    'providerUserId': string;\n    /**\n     * The name of the OAuth provider (e.g. Google, Facebook, Twitter).\n     */\n    'registrationId': string;\n    /**\n     * The time when the user connection was last updated.\n     */\n    'updatedAt'?: string;\n    /**\n     * The {@link Metadata#getName Metadata#getName()} of the user associated with the OAuth connection.\n     */\n    'username': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-connection.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { UserConnectionSpec } from './user-connection-spec';\n\n/**\n * User connection extension.\n */\nexport interface UserConnection {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': UserConnectionSpec;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-device.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Device } from './device';\n\nexport interface UserDevice {\n    'active': boolean;\n    'currentDevice': boolean;\n    'device': Device;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-endpoint-listed-user-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { ListedUser } from './listed-user';\n\nexport interface UserEndpointListedUserList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<ListedUser>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-list.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { User } from './user';\n\nexport interface UserList {\n    /**\n     * Indicates whether current page is the first page.\n     */\n    'first': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasNext': boolean;\n    /**\n     * Indicates whether current page has previous page.\n     */\n    'hasPrevious': boolean;\n    /**\n     * A chunk of items.\n     */\n    'items': Array<User>;\n    /**\n     * Indicates whether current page is the last page.\n     */\n    'last': boolean;\n    /**\n     * Page number, starts from 1. If not set or equal to 0, it means no pagination.\n     */\n    'page': number;\n    /**\n     * Size of each page. If not set or equal to 0, it means no pagination.\n     */\n    'size': number;\n    /**\n     * Total elements.\n     */\n    'total': number;\n    /**\n     * Indicates total pages.\n     */\n    'totalPages': number;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-permission.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Role } from './role';\n\nexport interface UserPermission {\n    'permissions': Array<Role>;\n    'roles': Array<Role>;\n    'uiPermissions': Array<string>;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-spec.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface UserSpec {\n    'avatar'?: string;\n    'bio'?: string;\n    'disabled'?: boolean;\n    'displayName': string;\n    'email': string;\n    'emailVerified'?: boolean;\n    'loginHistoryLimit'?: number;\n    'password'?: string;\n    'phone'?: string;\n    'registeredAt'?: string;\n    'totpEncryptedSecret'?: string;\n    'twoFactorAuthEnabled'?: boolean;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user-status.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface UserStatus {\n    'permalink'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/user.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { Metadata } from './metadata';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { UserSpec } from './user-spec';\n// May contain unused imports in some cases\n// @ts-ignore\nimport type { UserStatus } from './user-status';\n\n/**\n * The extension represents user details of Halo.\n */\nexport interface User {\n    'apiVersion': string;\n    'kind': string;\n    'metadata': Metadata;\n    'spec': UserSpec;\n    'status'?: UserStatus;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/verify-code-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface VerifyCodeRequest {\n    'code': string;\n    'password': string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/src/models/vote-request.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * Halo\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 2.23.0-SNAPSHOT\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\n\nexport interface VoteRequest {\n    'group'?: string;\n    'name'?: string;\n    'plural'?: string;\n}\n\n"
  },
  {
    "path": "ui/packages/api-client/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2018\",\n    \"module\": \"esnext\",\n    \"lib\": [\"esnext\", \"dom\"],\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"strictNullChecks\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"skipDefaultLibCheck\": true\n  },\n  \"include\": [\"src\", \"entry\"]\n}\n"
  },
  {
    "path": "ui/packages/api-client/tsdown.config.ts",
    "content": "import { defineConfig, type UserConfig } from \"tsdown\";\n\nconst sharedConfig: UserConfig = {\n  entry: [\"./entry/index.ts\"],\n  deps: {\n    neverBundle: [\"axios\"],\n    alwaysBundle: [\"qs\"],\n    onlyAllowBundle: false,\n  },\n  outputOptions: {\n    globals: {\n      axios: \"axios\",\n    },\n  },\n  platform: \"browser\",\n  globalName: \"HaloApiClient\",\n  tsconfig: \"./tsconfig.json\",\n  exports: true,\n  dts: {\n    tsgo: true,\n  },\n};\n\nexport default defineConfig([\n  {\n    ...sharedConfig,\n    format: \"esm\",\n  },\n  {\n    ...sharedConfig,\n    format: \"iife\",\n    minify: true,\n  },\n]);\n"
  },
  {
    "path": "ui/packages/components/.storybook/main.ts",
    "content": "import { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { StorybookConfig } from \"@storybook/vue3-vite\";\n\nconst config: StorybookConfig = {\n  stories: [\"../src/**/*.mdx\", \"../src/**/*.stories.@(js|jsx|ts|tsx)\"],\n\n  addons: [\n    getAbsolutePath(\"@storybook/addon-links\"),\n    getAbsolutePath(\"@storybook/addon-docs\"),\n  ],\n\n  framework: {\n    name: getAbsolutePath(\"@storybook/vue3-vite\"),\n    options: {},\n  },\n\n  async viteFinal(config) {\n    const { mergeConfig } = await import(\"vite\");\n\n    return mergeConfig(config, {\n      assetsInclude: [\"/sb-preview/runtime.js\"],\n    });\n  },\n};\nexport default config;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction getAbsolutePath(value: string): any {\n  return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));\n}\n"
  },
  {
    "path": "ui/packages/components/.storybook/preview.ts",
    "content": "import type { Preview } from \"@storybook/vue3-vite\";\nimport \"overlayscrollbars/overlayscrollbars.css\";\nimport \"../src/styles/tailwind.css\";\n\nconst preview: Preview = {\n  parameters: {\n    actions: { argTypesRegex: \"^on[A-Z].*\" },\n    controls: {\n      matchers: {\n        color: /(background|color)$/i,\n        date: /Date$/,\n      },\n    },\n    layout: \"padded\",\n  },\n};\n\nexport default preview;\n"
  },
  {
    "path": "ui/packages/components/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"histoire\" />\n/// <reference types=\"unplugin-icons/types/vue\" />\n\ndeclare module \"*.vue\" {\n  import type { DefineComponent } from \"vue\";\n  // eslint-disable-next-line\n  const component: DefineComponent<{}, {}, any>;\n  export default component;\n}\n"
  },
  {
    "path": "ui/packages/components/package.json",
    "content": "{\n  \"name\": \"@halo-dev/components\",\n  \"version\": \"2.23.0\",\n  \"description\": \"\",\n  \"keywords\": [\n    \"@halo-dev/components\",\n    \"halo\",\n    \"halo-components\",\n    \"halo-dev\"\n  ],\n  \"homepage\": \"https://github.com/halo-dev/halo/tree/main/ui/packages/components#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/halo-dev/halo/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"@halo-dev\",\n    \"url\": \"https://github.com/halo-dev\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/halo-dev/halo.git\",\n    \"directory\": \"ui/packages/components\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"./dist/index.iife.js\",\n  \"module\": \"./dist/index.es.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"unpkg\": \"./dist/index.iife.js\",\n  \"jsdelivr\": \"./dist/index.iife.js\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.es.js\"\n    },\n    \"./dist/style.css\": \"./dist/style.css\"\n  },\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"build-storybook\": \"storybook build\",\n    \"dev\": \"vite build --watch\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"test:unit\": \"vitest --run\",\n    \"test:unit:coverage\": \"vitest run --coverage\",\n    \"test:unit:ui\": \"vitest --watch --ui\",\n    \"test:unit:watch\": \"vitest --watch\",\n    \"typecheck\": \"vue-tsc --noEmit -p tsconfig.app.json --composite false\",\n    \"prepublishOnly\": \"pnpm run build\"\n  },\n  \"dependencies\": {\n    \"floating-vue\": \"^5.2.2\"\n  },\n  \"devDependencies\": {\n    \"@storybook/addon-docs\": \"^10.0.8\",\n    \"@storybook/addon-links\": \"^10.0.8\",\n    \"@storybook/testing-library\": \"^0.0.14-next.2\",\n    \"@storybook/vue3-vite\": \"^10.0.8\",\n    \"eslint-plugin-storybook\": \"10.0.8\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"storybook\": \"^10.0.8\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"^3.5.x\",\n    \"vue-router\": \"^5.0.x\"\n  }\n}\n"
  },
  {
    "path": "ui/packages/components/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/alert/Alert.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VAlert } from \".\";\n\nconst meta: Meta<typeof VAlert> = {\n  title: \"Alert\",\n  component: VAlert,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VAlert },\n    setup() {\n      return { args };\n    },\n    template: `<VAlert v-bind=\"args\"></VAlert>`,\n  }),\n  argTypes: {\n    type: {\n      control: { type: \"select\" },\n      options: [\"default\", \"success\", \"info\", \"warning\", \"error\"],\n    },\n    closable: {\n      control: { type: \"boolean\" },\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VAlert>;\n\nexport const Default: Story = {\n  args: {\n    type: \"default\",\n    title: \"default\",\n    description: \"Halo\",\n  },\n};\n\nexport const Success: Story = {\n  args: {\n    type: \"success\",\n    title: \"success\",\n    description: \"Halo\",\n  },\n};\n\nexport const Info: Story = {\n  args: {\n    type: \"info\",\n    title: \"info\",\n    description: \"Halo\",\n  },\n};\n\nexport const Warning: Story = {\n  args: {\n    type: \"warning\",\n    title: \"warning\",\n    description: \"Halo\",\n  },\n};\n\nexport const Error: Story = {\n  args: {\n    type: \"error\",\n    title: \"error\",\n    description: \"Halo\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/alert/Alert.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FunctionalComponent, Raw, SVGAttributes } from \"vue\";\nimport { computed, markRaw } from \"vue\";\nimport {\n  IconCheckboxCircle,\n  IconClose,\n  IconCloseCircle,\n  IconErrorWarning,\n  IconInformation,\n} from \"../../icons/icons\";\nimport type { AlertType } from \"./types\";\n\nconst TypeIcons: Record<AlertType, Raw<FunctionalComponent<SVGAttributes>>> = {\n  success: markRaw(IconCheckboxCircle),\n  info: markRaw(IconInformation),\n  default: markRaw(IconInformation),\n  warning: markRaw(IconErrorWarning),\n  error: markRaw(IconCloseCircle),\n};\n\nconst props = withDefaults(\n  defineProps<{\n    type?: AlertType;\n    title?: string;\n    description?: string;\n    closable?: boolean;\n  }>(),\n  {\n    type: \"default\",\n    title: \"\",\n    description: \"\",\n    closable: true,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst classes = computed(() => {\n  return [`alert-${props.type}`];\n});\n\nconst handleClose = () => {\n  emit(\"close\");\n};\n</script>\n\n<template>\n  <div :class=\"classes\" class=\"alert-wrapper\">\n    <div class=\"alert-header\">\n      <div class=\"alert-icon\">\n        <slot name=\"icon\">\n          <component :is=\"TypeIcons[type]\" />\n        </slot>\n      </div>\n      <div v-if=\"title || $slots.title\" class=\"alert-title\">\n        <slot name=\"title\">\n          {{ title }}\n        </slot>\n      </div>\n      <div v-if=\"closable\" class=\"alert-close\" @click=\"handleClose\">\n        <IconClose />\n      </div>\n    </div>\n    <div v-if=\"description || $slots.description\" class=\"alert-description\">\n      <slot name=\"description\">\n        {{ description }}\n      </slot>\n    </div>\n    <div v-if=\"$slots.actions\" class=\"alert-actions\">\n      <slot name=\"actions\" />\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.alert-wrapper {\n  @apply box-border flex flex-col rounded-base border;\n  padding: 12px 16px;\n\n  .alert-header {\n    @apply flex;\n\n    .alert-icon {\n      @apply mr-3 self-center text-lg;\n    }\n\n    .alert-title {\n      @apply mr-3 flex-1 self-center text-sm font-medium;\n    }\n\n    .alert-close {\n      @apply cursor-pointer self-center rounded-full p-0.5;\n\n      &:hover {\n        @apply bg-gray-300 text-white transition-all;\n      }\n    }\n  }\n\n  .alert-description {\n    @apply mt-2 text-xs;\n  }\n\n  .alert-actions {\n    @apply mt-3 border-t pt-2;\n  }\n\n  &.alert-default {\n    @apply border-gray-300 bg-gray-50;\n\n    .alert-icon,\n    .alert-description {\n      @apply text-gray-600;\n    }\n\n    .alert-close,\n    .alert-title {\n      @apply text-gray-700;\n    }\n  }\n\n  &.alert-success {\n    @apply border-green-300 bg-green-50;\n\n    .alert-icon,\n    .alert-description {\n      @apply text-green-600;\n    }\n\n    .alert-close,\n    .alert-title {\n      @apply text-green-700;\n    }\n  }\n\n  &.alert-info {\n    @apply border-sky-300 bg-sky-50;\n\n    .alert-icon,\n    .alert-description {\n      @apply text-sky-600;\n    }\n\n    .alert-close,\n    .alert-title {\n      @apply text-sky-700;\n    }\n  }\n\n  &.alert-warning {\n    @apply border-orange-300 bg-orange-50;\n\n    .alert-icon,\n    .alert-description {\n      @apply text-orange-600;\n    }\n\n    .alert-close,\n    .alert-title {\n      @apply text-orange-700;\n    }\n  }\n\n  &.alert-error {\n    @apply border-red-300 bg-red-50;\n\n    .alert-icon,\n    .alert-description {\n      @apply text-red-600;\n    }\n\n    .alert-close,\n    .alert-title {\n      @apply text-red-700;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/alert/__tests__/Alert.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { VAlert } from \"../index\";\n\ndescribe(\"Alert\", () => {\n  it(\"should render\", () => {\n    expect(VAlert).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/alert/index.ts",
    "content": "export { default as VAlert } from \"./Alert.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/alert/types.ts",
    "content": "export type AlertType = \"default\" | \"success\" | \"info\" | \"warning\" | \"error\";\n"
  },
  {
    "path": "ui/packages/components/src/components/avatar/Avatar.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VAvatar } from \".\";\n\nconst meta: Meta<typeof VAvatar> = {\n  title: \"Avatar\",\n  component: VAvatar,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VAvatar },\n    setup() {\n      return { args };\n    },\n    template: `<VAvatar v-bind=\"args\" />`,\n  }),\n  argTypes: {\n    size: {\n      control: { type: \"select\" },\n      options: [\"lg\", \"md\", \"sm\", \"xs\"],\n      defaultValue: \"md\",\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VAvatar>;\n\nexport const Default: Story = {\n  args: {\n    src: \"https://www.halo.run/logo\",\n    alt: \"Hello\",\n  },\n};\n\nexport const Circle: Story = {\n  args: {\n    src: \"https://www.halo.run/logo\",\n    alt: \"Hello\",\n    circle: true,\n  },\n};\n\nexport const Text: Story = {\n  args: {\n    alt: \"Ryan Wang\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/avatar/Avatar.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, inject, onMounted, ref, watch } from \"vue\";\nimport { IconErrorWarning } from \"../../icons/icons\";\nimport { AvatarGroupContextInjectionKey, type AvatarProps } from \"./types\";\n\nconst props = withDefaults(defineProps<AvatarProps>(), {\n  size: \"md\",\n  circle: false,\n});\n\nconst groupProps = inject(AvatarGroupContextInjectionKey, undefined);\n\nconst size = computed(() => groupProps?.size || props.size);\nconst circle = computed(() => groupProps?.circle || props.circle);\nconst width = computed(() => groupProps?.width || props.width);\nconst height = computed(() => groupProps?.height || props.height);\n\nconst isLoading = ref(false);\nconst error = ref(false);\nlet init = true;\n\nconst loadImage = async (isInit: boolean) => {\n  if (!props.src) {\n    error.value = true;\n    return;\n  }\n\n  isLoading.value = true;\n  try {\n    if (!props.src) {\n      error.value = true;\n      return Promise.reject();\n    }\n    if (!isInit) {\n      error.value = false;\n    }\n    const image = new Image();\n    image.src = props.src;\n    return new Promise((resolve, reject) => {\n      image.onload = () => resolve(image);\n      image.onerror = (err) => {\n        error.value = true;\n        reject(err);\n      };\n    });\n  } catch (_) {\n    error.value = true;\n  } finally {\n    isLoading.value = false;\n  }\n};\n\nwatch([() => props.alt, () => props.src], async () => loadImage(init));\n\nonMounted(async () => {\n  loadImage(init);\n  init = false;\n});\n\nconst classes = computed(() => {\n  const result = [`avatar-${circle.value ? \"circle\" : \"square\"}`];\n  if (size.value) {\n    result.push(`avatar-${size.value}`);\n  }\n  return result;\n});\n\nconst styles = computed(() => {\n  const result: Record<string, string> = {};\n  if (width.value) {\n    result.width = width.value;\n  }\n  if (height.value) {\n    result.height = height.value;\n  }\n  return result;\n});\n\nconst placeholderText = computed(() => {\n  if (!props.alt) {\n    return undefined;\n  }\n  const words = props.alt.split(\" \");\n  if (words.length === 1) {\n    return words[0].charAt(0).toUpperCase();\n  }\n  if (words.length > 1) {\n    return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase();\n  }\n  return undefined;\n});\n</script>\n\n<template>\n  <div class=\"avatar-wrapper\" :class=\"classes\" :style=\"styles\">\n    <div v-if=\"isLoading || error\" class=\"avatar-fallback\">\n      <svg\n        v-if=\"isLoading\"\n        class=\"avatar-loading\"\n        fill=\"none\"\n        viewBox=\"0 0 24 24\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <circle\n          class=\"opacity-25\"\n          cx=\"12\"\n          cy=\"12\"\n          r=\"10\"\n          stroke=\"currentColor\"\n          stroke-width=\"4\"\n        ></circle>\n        <path\n          class=\"opacity-75\"\n          d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n          fill=\"currentColor\"\n        ></path>\n      </svg>\n      <span v-else-if=\"placeholderText\" class=\"avatar-placeholder\">\n        {{ placeholderText }}\n      </span>\n      <IconErrorWarning v-else class=\"avatar-error\" />\n    </div>\n    <img v-else :src=\"src\" :alt=\"alt\" />\n  </div>\n</template>\n\n<style lang=\"scss\">\n.avatar-wrapper {\n  @apply inline-flex items-center justify-center overflow-hidden bg-gray-100;\n\n  img {\n    @apply h-full w-full object-cover;\n  }\n\n  .avatar-fallback {\n    @apply flex h-full w-full items-center justify-center;\n  }\n\n  .avatar-loading {\n    @apply h-5 w-5 animate-spin;\n  }\n\n  .avatar-placeholder {\n    @apply text-sm font-medium text-gray-800;\n  }\n\n  .avatar-error {\n    @apply h-5 w-5 text-red-500;\n  }\n\n  &.avatar-circle {\n    @apply rounded-full;\n  }\n\n  &.avatar-square {\n    @apply rounded-base;\n  }\n\n  &.avatar-xs {\n    @apply h-6 w-6;\n\n    .avatar-placeholder {\n      @apply text-xs;\n    }\n  }\n\n  &.avatar-sm {\n    @apply h-8 w-8;\n\n    .avatar-placeholder {\n      @apply text-xs;\n    }\n  }\n\n  &.avatar-md {\n    @apply h-10 w-10;\n  }\n\n  &.avatar-lg {\n    @apply h-12 w-12;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/avatar/AvatarGroup.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VAvatar, VAvatarGroup } from \".\";\n\nconst meta: Meta<typeof VAvatarGroup> = {\n  title: \"AvatarGroup\",\n  component: VAvatarGroup,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VAvatarGroup, VAvatar },\n    setup() {\n      return { args };\n    },\n    template: `<VAvatarGroup v-bind=\"args\">\n      <VAvatar src=\"https://avatar.iran.liara.run/public?id=1\" />\n      <VAvatar src=\"https://avatar.iran.liara.run/public?id=2\" />\n      <VAvatar src=\"https://avatar.iran.liara.run/public?id=3\" />\n      <VAvatar src=\"https://avatar.iran.liara.run/public?id=4\" />\n    </VAvatarGroup>`,\n  }),\n  argTypes: {\n    size: {\n      control: { type: \"select\" },\n      options: [\"lg\", \"md\", \"sm\", \"xs\"],\n      defaultValue: \"md\",\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VAvatarGroup>;\n\nexport const Default: Story = {\n  args: {},\n};\n\nexport const Circle: Story = {\n  args: {\n    circle: true,\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/avatar/AvatarGroup.vue",
    "content": "<script lang=\"ts\" setup>\nimport { provide } from \"vue\";\nimport { AvatarGroupContextInjectionKey, type AvatarGroupProps } from \"./types\";\n\nconst props = withDefaults(defineProps<AvatarGroupProps>(), {\n  size: \"md\",\n  circle: false,\n});\n\nprovide(AvatarGroupContextInjectionKey, props);\n</script>\n\n<template>\n  <div class=\"avatar-group-wrapper\">\n    <slot />\n  </div>\n</template>\n\n<style lang=\"scss\">\n.avatar-group-wrapper {\n  @apply inline-flex -space-x-2.5;\n\n  > * {\n    @apply ring-2 ring-white transition-all hover:z-10;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/avatar/index.ts",
    "content": "export { default as VAvatar } from \"./Avatar.vue\";\nexport { default as VAvatarGroup } from \"./AvatarGroup.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/avatar/types.ts",
    "content": "import type { InjectionKey } from \"vue\";\n\nexport type AvatarSize = \"lg\" | \"md\" | \"sm\" | \"xs\";\n\nexport interface AvatarProps {\n  src?: string;\n  alt?: string;\n  size?: AvatarSize;\n  width?: string;\n  height?: string;\n  circle?: boolean;\n}\n\nexport type AvatarGroupProps = Omit<AvatarProps, \"src\" | \"alt\">;\n\nexport const AvatarGroupContextInjectionKey: InjectionKey<AvatarGroupProps> =\n  Symbol(\"avatar-group-context\");\n"
  },
  {
    "path": "ui/packages/components/src/components/button/Button.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VButton } from \".\";\n\nconst meta: Meta<typeof VButton> = {\n  title: \"Button\",\n  component: VButton,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VButton },\n    setup() {\n      return { args };\n    },\n    template: `<VButton v-bind=\"args\">${args.default}</VButton>`,\n  }),\n  argTypes: {\n    default: {\n      control: { type: \"text\" },\n    },\n    type: {\n      control: { type: \"select\" },\n      options: [\"default\", \"primary\", \"secondary\", \"danger\"],\n    },\n    size: {\n      control: {\n        type: \"select\",\n      },\n      options: [\"lg\", \"md\", \"sm\", \"xs\"],\n    },\n  },\n  args: {\n    default: \"Button\",\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VButton>;\n\nexport const Default: Story = {\n  args: {\n    type: \"default\",\n  },\n};\n\nexport const Primary: Story = {\n  args: {\n    type: \"primary\",\n  },\n};\n\nexport const Secondary: Story = {\n  args: {\n    type: \"secondary\",\n  },\n};\n\nexport const Danger: Story = {\n  args: {\n    type: \"danger\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/button/Button.vue",
    "content": "<template>\n  <button\n    :class=\"classes\"\n    :disabled=\"disabled\"\n    class=\"btn\"\n    type=\"button\"\n    @click=\"handleClick\"\n  >\n    <span v-if=\"$slots.icon || loading\" class=\"btn-icon\">\n      <svg\n        v-if=\"loading\"\n        class=\"animate-spin\"\n        fill=\"none\"\n        viewBox=\"0 0 24 24\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <circle\n          class=\"opacity-25\"\n          cx=\"12\"\n          cy=\"12\"\n          r=\"10\"\n          stroke=\"currentColor\"\n          stroke-width=\"4\"\n        ></circle>\n        <path\n          class=\"opacity-75\"\n          d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n          fill=\"currentColor\"\n        ></path>\n      </svg>\n      <slot v-else name=\"icon\" />\n    </span>\n    <span class=\"btn-content\">\n      <slot />\n    </span>\n  </button>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport type { RouteLocationRaw } from \"vue-router\";\nimport { useRouter } from \"vue-router\";\nimport type { ButtonSize, ButtonType } from \"./types\";\n\nconst props = withDefaults(\n  defineProps<{\n    type?: ButtonType;\n    size?: ButtonSize;\n    circle?: boolean;\n    block?: boolean;\n    disabled?: boolean;\n    loading?: boolean;\n    route?: RouteLocationRaw | undefined;\n    ghost?: boolean;\n  }>(),\n  {\n    type: \"default\",\n    size: \"md\",\n    circle: false,\n    block: false,\n    disabled: false,\n    loading: false,\n    route: undefined,\n    ghost: false,\n  }\n);\n\nconst router = useRouter();\nconst emit = defineEmits<{\n  (event: \"click\"): void;\n}>();\n\nconst classes = computed(() => {\n  return [\n    `btn-${props.size}`,\n    `btn-${props.type}`,\n    { \"btn-circle\": props.circle },\n    { \"btn-block\": props.block },\n    { \"btn-loading\": props.loading },\n    { \"btn-ghost\": props.ghost },\n  ];\n});\n\nfunction handleClick() {\n  if (props.disabled || props.loading) return;\n  if (props.route) {\n    router.push(props.route);\n  }\n  emit(\"click\");\n}\n</script>\n\n<style lang=\"scss\">\n@use \"sass:map\";\n$btn-sizes: (\n  xs: (\n    height: theme(\"spacing.6\"),\n    padding-x: theme(\"spacing.2\"),\n    font-size: theme(\"fontSize.xs\"),\n    icon-size: theme(\"spacing.3\"),\n    icon-margin: theme(\"spacing.2\"),\n  ),\n  sm: (\n    height: theme(\"spacing.7\"),\n    padding-x: theme(\"spacing.3\"),\n    font-size: theme(\"fontSize.xs\"),\n    icon-size: theme(\"spacing.3\"),\n    icon-margin: theme(\"spacing.2\"),\n  ),\n  md: (\n    height: theme(\"spacing.9\"),\n    padding-x: theme(\"spacing.4\"),\n    font-size: theme(\"fontSize.sm\"),\n    icon-size: theme(\"spacing.5\"),\n    icon-margin: theme(\"spacing.3\"),\n  ),\n  lg: (\n    height: theme(\"spacing.11\"),\n    padding-x: theme(\"spacing.5\"),\n    font-size: theme(\"fontSize.lg\"),\n    icon-size: theme(\"spacing.5\"),\n    icon-margin: theme(\"spacing.3\"),\n  ),\n);\n\n$btn-themes: (\n  default: (\n    bg: transparent,\n    color: inherit,\n    border: 1px solid #d9d9d9,\n    hover-bg: theme(\"colors.gray.100\"),\n    icon-color: theme(\"colors.secondary\"),\n    ghost-color: inherit,\n    ghost-hover-bg: theme(\"colors.gray.100\"),\n    ghost-icon-color: theme(\"colors.secondary\"),\n  ),\n  primary: (\n    bg: theme(\"colors.primary\"),\n    color: #fff,\n    border: none,\n    hover-bg: theme(\"colors.primary\"),\n    icon-color: #fff,\n    ghost-color: theme(\"colors.primary\"),\n    ghost-hover-bg: theme(\"colors.primary / 10%\"),\n    ghost-icon-color: theme(\"colors.primary\"),\n  ),\n  secondary: (\n    bg: theme(\"colors.secondary\"),\n    color: #fff,\n    border: none,\n    hover-bg: theme(\"colors.secondary\"),\n    icon-color: #fff,\n    ghost-color: theme(\"colors.secondary\"),\n    ghost-hover-bg: theme(\"colors.secondary / 10%\"),\n    ghost-icon-color: theme(\"colors.secondary\"),\n  ),\n  danger: (\n    bg: theme(\"colors.danger\"),\n    color: #fff,\n    border: none,\n    hover-bg: theme(\"colors.danger\"),\n    icon-color: #fff,\n    ghost-color: theme(\"colors.danger\"),\n    ghost-hover-bg: theme(\"colors.danger / 10%\"),\n    ghost-icon-color: theme(\"colors.danger\"),\n  ),\n);\n\n.btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n  flex-wrap: wrap;\n  cursor: pointer;\n  user-select: none;\n  appearance: none;\n  border-radius: theme(\"borderRadius.base\");\n  transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);\n  text-align: center;\n  text-decoration: none;\n  vertical-align: middle;\n  outline-width: 0;\n  border-style: none;\n  $md-config: map.get($btn-sizes, md);\n  height: map.get($md-config, height);\n  padding-left: map.get($md-config, padding-x);\n  padding-right: map.get($md-config, padding-x);\n  font-size: map.get($md-config, font-size);\n\n  &:hover {\n    opacity: 0.9;\n  }\n\n  &:active {\n    opacity: 1;\n  }\n\n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n\n  &.btn-loading {\n    cursor: not-allowed;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n\n  &.btn-block {\n    width: 100%;\n  }\n\n  .btn-icon {\n    height: map.get($md-config, icon-size);\n    width: map.get($md-config, icon-size);\n    margin-right: map.get($md-config, icon-margin);\n    color: #fff;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n\n    > * {\n      height: 100%;\n      width: 100%;\n    }\n  }\n}\n\n@each $size, $config in $btn-sizes {\n  .btn-#{$size} {\n    height: map.get($config, height);\n    padding-left: map.get($config, padding-x);\n    padding-right: map.get($config, padding-x);\n    font-size: map.get($config, font-size);\n\n    .btn-icon {\n      height: map.get($config, icon-size);\n      width: map.get($config, icon-size);\n      margin-right: map.get($config, icon-margin);\n    }\n\n    &.btn-circle {\n      width: map.get($config, height);\n      padding: 0;\n      border-radius: 9999px;\n    }\n  }\n}\n\n@each $theme, $config in $btn-themes {\n  .btn-#{$theme} {\n    background-color: map.get($config, bg) !important;\n    color: map.get($config, color);\n    border: map.get($config, border);\n\n    &:hover {\n      background-color: map.get($config, hover-bg) !important;\n    }\n\n    .btn-icon {\n      color: map.get($config, icon-color);\n    }\n  }\n}\n\n.btn-ghost {\n  background-color: transparent !important;\n\n  @each $theme, $config in $btn-themes {\n    &.btn-#{$theme} {\n      color: map.get($config, ghost-color);\n      border: none;\n\n      &:hover {\n        background-color: map.get($config, ghost-hover-bg) !important;\n      }\n\n      .btn-icon {\n        color: map.get($config, ghost-icon-color);\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/button/__tests__/Button.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport { IconSettings } from \"../../../icons/icons\";\nimport { VButton } from \"../index\";\n\ndescribe(\"Button\", () => {\n  it(\"should render\", () => {\n    expect(mount(VButton).html()).contains(\"button\");\n    expect(mount(VButton).html()).toMatchSnapshot();\n  });\n\n  it(\"should render with text\", () => {\n    expect(mount(VButton, { slots: { default: \"Hello Halo\" } }).text()).toBe(\n      \"Hello Halo\"\n    );\n  });\n\n  it(\"should work with type prop\", () => {\n    // default button type is default\n    expect(mount(VButton).find(\".btn\").classes()).toContain(\"btn-default\");\n\n    [\"primary\", \"secondary\", \"danger\"].forEach((type) => {\n      const button = mount(VButton, { props: { type } });\n\n      expect(button.html()).toMatchSnapshot();\n      expect(button.find(\".btn\").classes()).toContain(`btn-${type}`);\n      button.unmount();\n    });\n  });\n\n  it(\"should work with size prop\", async () => {\n    // default button size is md\n    expect(mount(VButton).find(\".btn\").classes()).toContain(\"btn-md\");\n\n    [\"lg\", \"sm\", \"xs\"].forEach((size) => {\n      const button = mount(VButton, { props: { size } });\n\n      expect(button.html()).toMatchSnapshot();\n      expect(button.find(\".btn\").classes()).toContain(`btn-${size}`);\n      button.unmount();\n    });\n  });\n\n  it(\"should work with circle prop\", async () => {\n    const button = mount(VButton);\n\n    // default: false\n    expect(button.find(\".btn\").classes()).not.toContain(\"btn-circle\");\n\n    await button.setProps({ circle: true });\n    expect(button.html()).toMatchSnapshot();\n    expect(button.find(\".btn\").classes()).toContain(\"btn-circle\");\n  });\n\n  it(\"should work with block prop\", async () => {\n    const button = mount(VButton);\n\n    // default: false\n    expect(button.find(\".btn\").classes()).not.toContain(\"btn-block\");\n\n    await button.setProps({ block: true });\n    expect(button.html()).toMatchSnapshot();\n    expect(button.find(\".btn\").classes()).toContain(\"btn-block\");\n  });\n\n  it(\"should work with disabled prop\", async () => {\n    const onClick = vi.fn(() => 1);\n\n    // default: false\n    const button = mount(VButton, {\n      emits: { click: onClick() },\n    });\n\n    await button.trigger(\"click\");\n    expect(onClick).toHaveBeenCalled();\n\n    onClick.mockReset();\n    await button.setProps({ disabled: true });\n    await button.trigger(\"click\");\n\n    expect(button.html()).toMatchSnapshot();\n    expect(onClick).not.toHaveBeenCalled();\n  });\n\n  it(\"should work with loading prop\", async function () {\n    const wrapper = mount({\n      data() {\n        return {\n          loading: true,\n        };\n      },\n      template: `\n        <v-button :loading=\"loading\" >\n          Hello\n        </v-button>\n      `,\n      components: {\n        VButton,\n      },\n    });\n\n    expect(wrapper.find(\".btn\").classes()).toContain(\"btn-loading\");\n    expect(wrapper.find(\".btn-icon\").exists()).toBe(true);\n\n    // set loading = false\n    await wrapper.setData({ loading: false });\n    expect(wrapper.find(\".btn\").classes()).not.toContain(\"btn-loading\");\n    expect(wrapper.find(\".btn-icon\").exists()).toBe(false);\n  });\n\n  it(\"should work with loading prop and icon slot\", async function () {\n    const wrapper = mount({\n      data() {\n        return {\n          loading: false,\n        };\n      },\n      template: `\n        <v-button :loading=\"loading\">\n        <template #icon>\n          IconSettings\n        </template>\n        Hello\n        </v-button>\n      `,\n      components: {\n        VButton,\n        IconSettings,\n      },\n    });\n\n    expect(wrapper.find(\".btn-icon\").exists()).toBe(true);\n    expect(wrapper.find(\".btn\").classes()).not.toContain(\"btn-loading\");\n\n    await wrapper.setData({ loading: true });\n\n    expect(wrapper.find(\".btn-icon\").exists()).toBe(true);\n    expect(wrapper.find(\".btn\").classes()).toContain(\"btn-loading\");\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/button/__tests__/__snapshots__/Button.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Button > should render 1`] = `\n\"<button class=\"btn-md btn-default btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with block prop 1`] = `\n\"<button class=\"btn-md btn-default btn-block btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with circle prop 1`] = `\n\"<button class=\"btn-md btn-default btn-circle btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with disabled prop 1`] = `\n\"<button class=\"btn-md btn-default btn\" type=\"button\" disabled=\"\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with size prop 1`] = `\n\"<button class=\"btn-lg btn-default btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with size prop 2`] = `\n\"<button class=\"btn-sm btn-default btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with size prop 3`] = `\n\"<button class=\"btn-xs btn-default btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with type prop 1`] = `\n\"<button class=\"btn-md btn-primary btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with type prop 2`] = `\n\"<button class=\"btn-md btn-secondary btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n\nexports[`Button > should work with type prop 3`] = `\n\"<button class=\"btn-md btn-danger btn\" type=\"button\">\n  <!--v-if--><span class=\"btn-content\"></span>\n</button>\"\n`;\n"
  },
  {
    "path": "ui/packages/components/src/components/button/index.ts",
    "content": "export { default as VButton } from \"./Button.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/button/types.ts",
    "content": "export type ButtonType = \"default\" | \"primary\" | \"secondary\" | \"danger\";\nexport type ButtonSize = \"lg\" | \"md\" | \"sm\" | \"xs\";\n"
  },
  {
    "path": "ui/packages/components/src/components/card/Card.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VCard } from \".\";\n\nconst meta: Meta<typeof VCard> = {\n  title: \"Card\",\n  component: VCard,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VCard },\n    setup() {\n      return { args };\n    },\n    template: `<VCard v-bind=\"args\">${args.default}</VCard>`,\n  }),\n  argTypes: {\n    default: {\n      control: { type: \"text\" },\n    },\n  },\n  args: {\n    default: `      <!--      https://lofiui.co/-->\n    <div class=\"p-3\">\n      <VCard title=\"Page Views\">\n        <div class=\"flex w-full flex-col items-center bg-white\">\n          <div class=\"flex w-full flex-grow items-end space-x-2 sm:space-x-3\">\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$37,500</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-8 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-6 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-16 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Jan</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$45,000</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-10 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-6 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-20 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Feb</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$47,500</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-10 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-8 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-20 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Mar</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$50,000</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-10 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-6 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-24 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Apr</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$47,500</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-10 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-8 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-20 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">May</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$55,000</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-12 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-8 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-24 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Jun</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$60,000</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-12 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-16 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-20 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Jul</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$57,500</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-12 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-10 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-24 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Aug</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$67,500</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-12 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-10 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-32 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Sep</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$65,000</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-12 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-12 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-28 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Oct</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$70,000</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-8 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-8 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-40 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Nov</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$75,000</span\n              >\n              <div class=\"flex w-full items-end\">\n                <div\n                  class=\"relative flex h-12 flex-grow justify-center bg-indigo-200\"\n                ></div>\n                <div\n                  class=\"relative flex h-8 flex-grow justify-center bg-indigo-300\"\n                ></div>\n                <div\n                  class=\"relative flex h-40 flex-grow justify-center bg-indigo-400\"\n                ></div>\n              </div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Dec</span>\n            </div>\n          </div>\n          <div class=\"mt-3 flex w-full\">\n            <div class=\"ml-auto flex items-center\">\n              <span class=\"block h-4 w-4 bg-indigo-400\"></span>\n              <span class=\"ml-1 text-xs font-medium\">Existing</span>\n            </div>\n            <div class=\"ml-4 flex items-center\">\n              <span class=\"block h-4 w-4 bg-indigo-300\"></span>\n              <span class=\"ml-1 text-xs font-medium\">Upgrades</span>\n            </div>\n            <div class=\"ml-4 flex items-center\">\n              <span class=\"block h-4 w-4 bg-indigo-200\"></span>\n              <span class=\"ml-1 text-xs font-medium\">New</span>\n            </div>\n          </div>\n        </div>\n      </VCard>\n    </div>\n\n    <!--      https://lofiui.co/-->\n    <div class=\"p-3\">\n      <VCard title=\"Page Views\">\n        <div class=\"flex w-full flex-col items-center bg-white\">\n          <div class=\"flex w-full flex-grow items-end space-x-2 sm:space-x-3\">\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$37,500</span\n              >\n              <div\n                class=\"relative flex h-8 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-6 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-16 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Jan</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$45,000</span\n              >\n              <div\n                class=\"relative flex h-10 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-6 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-20 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Feb</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$47,500</span\n              >\n              <div\n                class=\"relative flex h-10 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-8 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-20 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Mar</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$50,000</span\n              >\n              <div\n                class=\"relative flex h-10 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-6 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-24 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Apr</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$47,500</span\n              >\n              <div\n                class=\"relative flex h-10 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-8 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-20 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">May</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$55,000</span\n              >\n              <div\n                class=\"relative flex h-12 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-8 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-24 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Jun</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$60,000</span\n              >\n              <div\n                class=\"relative flex h-12 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-16 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-20 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Jul</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$57,500</span\n              >\n              <div\n                class=\"relative flex h-12 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-10 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-24 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Aug</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$67,500</span\n              >\n              <div\n                class=\"relative flex h-12 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-10 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-32 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Sep</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$65,000</span\n              >\n              <div\n                class=\"relative flex h-12 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-12 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-28 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Oct</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$70,000</span\n              >\n              <div\n                class=\"relative flex h-8 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-8 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-40 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Nov</span>\n            </div>\n            <div\n              class=\"group relative flex flex-grow flex-col items-center pb-5\"\n            >\n              <span\n                class=\"absolute top-0 -mt-6 hidden text-xs font-bold group-hover:block\"\n                >$75,000</span\n              >\n              <div\n                class=\"relative flex h-12 w-full justify-center bg-indigo-200\"\n              ></div>\n              <div\n                class=\"relative flex h-8 w-full justify-center bg-indigo-300\"\n              ></div>\n              <div\n                class=\"relative flex h-40 w-full justify-center bg-indigo-400\"\n              ></div>\n              <span class=\"absolute bottom-0 text-xs font-bold\">Dec</span>\n            </div>\n          </div>\n          <div class=\"mt-3 flex w-full\">\n            <div class=\"ml-auto flex items-center\">\n              <span class=\"block h-4 w-4 bg-indigo-400\"></span>\n              <span class=\"ml-1 text-xs font-medium\">Existing</span>\n            </div>\n            <div class=\"ml-4 flex items-center\">\n              <span class=\"block h-4 w-4 bg-indigo-300\"></span>\n              <span class=\"ml-1 text-xs font-medium\">Upgrades</span>\n            </div>\n            <div class=\"ml-4 flex items-center\">\n              <span class=\"block h-4 w-4 bg-indigo-200\"></span>\n              <span class=\"ml-1 text-xs font-medium\">New</span>\n            </div>\n          </div>\n        </div>\n      </VCard>\n    </div>\n\n    <div class=\"p-3\">\n      <div class=\"grid w-full gap-6 md:grid-cols-2 lg:grid-cols-3\">\n        <VCard>\n          <div class=\"flex items-center bg-white\">\n            <div\n              class=\"flex h-16 w-16 flex-shrink-0 items-center justify-center rounded bg-green-200\"\n            >\n              <svg\n                class=\"h-6 w-6 fill-current text-green-700\"\n                fill=\"currentColor\"\n                viewBox=\"0 0 20 20\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n              >\n                <path\n                  clip-rule=\"evenodd\"\n                  d=\"M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z\"\n                  fill-rule=\"evenodd\"\n                />\n              </svg>\n            </div>\n            <div class=\"ml-4 flex flex-grow flex-col\">\n              <span class=\"text-xl font-bold\">$8,430</span>\n              <div class=\"flex items-center justify-between\">\n                <span class=\"text-gray-500\">Revenue last 30 days</span>\n                <span class=\"ml-2 text-sm font-semibold text-green-500\"\n                  >+12.6%</span\n                >\n              </div>\n            </div>\n          </div>\n        </VCard>\n\n        <VCard>\n          <div class=\"flex items-center bg-white\">\n            <div\n              class=\"flex h-16 w-16 flex-shrink-0 items-center justify-center rounded bg-red-200\"\n            >\n              <svg\n                class=\"h-6 w-6 fill-current text-red-700\"\n                fill=\"currentColor\"\n                viewBox=\"0 0 20 20\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n              >\n                <path\n                  clip-rule=\"evenodd\"\n                  d=\"M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z\"\n                  fill-rule=\"evenodd\"\n                />\n              </svg>\n            </div>\n            <div class=\"ml-4 flex flex-grow flex-col\">\n              <span class=\"text-xl font-bold\">211</span>\n              <div class=\"flex items-center justify-between\">\n                <span class=\"text-gray-500\">Sales last 30 days</span>\n                <span class=\"ml-2 text-sm font-semibold text-red-500\"\n                  >-8.1%</span\n                >\n              </div>\n            </div>\n          </div>\n        </VCard>\n\n        <VCard>\n          <div class=\"flex items-center bg-white\">\n            <div\n              class=\"flex h-16 w-16 flex-shrink-0 items-center justify-center rounded bg-green-200\"\n            >\n              <svg\n                class=\"h-6 w-6 fill-current text-green-700\"\n                fill=\"currentColor\"\n                viewBox=\"0 0 20 20\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n              >\n                <path\n                  clip-rule=\"evenodd\"\n                  d=\"M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z\"\n                  fill-rule=\"evenodd\"\n                />\n              </svg>\n            </div>\n            <div class=\"ml-4 flex flex-grow flex-col\">\n              <span class=\"text-xl font-bold\">140</span>\n              <div class=\"flex items-center justify-between\">\n                <span class=\"text-gray-500\">Customers last 30 days</span>\n                <span class=\"ml-2 text-sm font-semibold text-green-500\"\n                  >+28.4%</span\n                >\n              </div>\n            </div>\n          </div>\n        </VCard>\n      </div>\n    </div>`,\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VCard>;\n\nexport const Default: Story = {\n  args: {\n    title: \"Card\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/card/Card.vue",
    "content": "<script lang=\"ts\" setup>\ndefineProps<{\n  title?: string;\n  bodyClass?: string[];\n}>();\n</script>\n\n<template>\n  <div class=\"card-wrapper\">\n    <div v-if=\"title || $slots.header\" class=\"card-header\">\n      <slot name=\"header\">\n        <div class=\"card-header-title\">\n          {{ title }}\n        </div>\n        <div class=\"card-header-actions\">\n          <slot name=\"actions\" />\n        </div>\n      </slot>\n    </div>\n    <div :class=\"bodyClass\" class=\"card-body\">\n      <slot />\n    </div>\n    <div v-if=\"$slots.footer\" class=\"card-footer\">\n      <slot name=\"footer\" />\n    </div>\n  </div>\n</template>\n<style lang=\"scss\">\n.card-wrapper {\n  @apply box-border flex flex-col rounded-base bg-white shadow-sm ring-1 ring-[#eaecf0];\n\n  .card-header {\n    @apply flex justify-between overflow-hidden rounded-t-base;\n    border-bottom: 1px solid #eaecf0;\n\n    .card-header-title {\n      @apply self-center text-base font-bold;\n      padding: 12px 16px;\n    }\n\n    .card-header-actions {\n      @apply self-center;\n    }\n  }\n\n  .card-body {\n    @apply overflow-hidden rounded-base;\n    padding: 12px 16px;\n  }\n\n  .card-footer {\n    @apply overflow-hidden rounded-b-base;\n    border-top: 1px solid #eaecf0;\n    padding: 12px 16px;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/card/__tests__/Card.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { VCard } from \"../index\";\n\ndescribe(\"Card\", () => {\n  it(\"should render\", () => {\n    expect(VCard).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/card/index.ts",
    "content": "export { default as VCard } from \"./Card.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/description/Description.vue",
    "content": "<script lang=\"ts\" setup></script>\n\n<template>\n  <dl class=\"description-wrapper\">\n    <slot />\n  </dl>\n</template>\n\n<style>\n.description-wrapper {\n  @apply divide-y divide-gray-100;\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/description/DescriptionItem.vue",
    "content": "<script lang=\"ts\" setup>\nwithDefaults(\n  defineProps<{\n    label: string;\n    content?: string;\n    verticalCenter?: boolean;\n  }>(),\n  {\n    content: undefined,\n    verticalCenter: false,\n  }\n);\n</script>\n\n<template>\n  <div\n    class=\"description-item-wrapper\"\n    :class=\"{ 'items-center': verticalCenter }\"\n  >\n    <dt class=\"description-item__label\">{{ label }}</dt>\n    <dd class=\"description-item__content\">\n      <slot v-if=\"$slots.default\" />\n      <template v-else>\n        {{ content }}\n      </template>\n    </dd>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.description-item-wrapper {\n  @apply bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6;\n\n  .description-item__label {\n    @apply text-sm font-medium text-gray-900;\n  }\n\n  .description-item__content {\n    @apply mt-1 text-sm text-gray-900 sm:col-span-6 sm:mt-0 md:col-span-5 lg:col-span-3;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/description/index.ts",
    "content": "export { default as VDescription } from \"./Description.vue\";\nexport { default as VDescriptionItem } from \"./DescriptionItem.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/dialog/Dialog.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { Dialog, VDialog } from \".\";\nimport { VButton } from \"../button\";\n\nconst meta: Meta<typeof VDialog> = {\n  title: \"Dialog\",\n  component: VDialog,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VDialog, VButton },\n    height: 400,\n    setup() {\n      const showDialog = () => {\n        Dialog.success({\n          title: \"Hi\",\n          // @ts-ignore\n          type: args.type,\n        });\n      };\n\n      return {\n        args,\n        showDialog,\n      };\n    },\n    template: `\n      <div style=\"height: 400px\">\n        <VButton @click=\"showDialog\" >点击显示Dialog</VButton>\n      </div>\n    `,\n  }),\n  argTypes: {\n    type: {\n      control: { type: \"select\" },\n      options: [\"success\", \"info\", \"warning\", \"error\"],\n    },\n    confirmType: {\n      control: { type: \"select\" },\n      options: [\"default\", \"primary\", \"secondary\", \"danger\"],\n    },\n  },\n  args: {\n    title: \"Hello\",\n    visible: false,\n    description: \"Hello World\",\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VDialog>;\n\nexport const Default: Story = {\n  args: {\n    type: \"info\",\n  },\n};\n\nexport const Success: Story = {\n  args: {\n    type: \"success\",\n  },\n};\n\nexport const Warning: Story = {\n  args: {\n    type: \"warning\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/dialog/Dialog.vue",
    "content": "<script lang=\"ts\" setup>\nimport { markRaw, ref, type Component, type Raw } from \"vue\";\nimport type { DialogProps, DialogType } from \"@/components/dialog/types\";\nimport {\n  IconCheckboxCircle,\n  IconClose,\n  IconErrorWarning,\n  IconForbidLine,\n  IconInformation,\n} from \"../../icons/icons\";\nimport { VButton } from \"../button\";\nimport { VModal } from \"../modal\";\n\nconst props = withDefaults(defineProps<DialogProps>(), {\n  type: \"info\",\n  title: \"提示\",\n  description: \"\",\n  confirmText: \"确定\",\n  confirmType: \"primary\",\n  showCancel: true,\n  cancelText: \"取消\",\n  visible: false,\n  onConfirm: () => {\n    return;\n  },\n  onCancel: () => {\n    return;\n  },\n});\n\nconst emit = defineEmits<{\n  (event: \"update:visible\", visible: boolean): void;\n  (event: \"close\"): void;\n}>();\n\nconst icons: Record<DialogType, { icon: Raw<Component>; color: string }> = {\n  success: {\n    icon: markRaw(IconCheckboxCircle),\n    color: \"green\",\n  },\n  info: {\n    icon: markRaw(IconInformation),\n    color: \"blue\",\n  },\n  warning: {\n    icon: markRaw(IconErrorWarning),\n    color: \"orange\",\n  },\n  error: {\n    icon: markRaw(IconForbidLine),\n    color: \"red\",\n  },\n};\n\nconst loading = ref(false);\n\nconst handleCancel = () => {\n  if (props.onCancel) {\n    props.onCancel();\n  }\n  handleClose();\n};\n\nconst handleConfirm = async () => {\n  if (props.onConfirm) {\n    loading.value = true;\n\n    try {\n      await props.onConfirm();\n    } catch (error) {\n      console.error(\"Failed to execute onConfirm: \", error);\n    } finally {\n      loading.value = false;\n    }\n  }\n  handleClose();\n};\n\nconst handleClose = () => {\n  loading.value = false;\n  emit(\"update:visible\", false);\n  emit(\"close\");\n};\n</script>\n<template>\n  <VModal\n    :visible=\"visible\"\n    :width=\"450\"\n    :layer-closable=\"false\"\n    :data-unique-id=\"uniqueId\"\n    @close=\"handleCancel()\"\n  >\n    <div class=\"mb-2 flex items-start justify-between py-2\">\n      <div class=\"flex flex-row items-center gap-3\">\n        <component\n          :is=\"icons[type].icon\"\n          :class=\"`text-${icons[type].color}-500`\"\n          class=\"h-6 w-6 flex-none\"\n        ></component>\n        <div class=\"text-base font-bold text-gray-900\">{{ title }}</div>\n      </div>\n      <div>\n        <IconClose class=\"cursor-pointer\" @click=\"handleCancel\" />\n      </div>\n    </div>\n    <div class=\"flex items-center gap-4\">\n      <div class=\"flex flex-1 items-stretch\">\n        <div class=\"break-all text-sm text-gray-700\">{{ description }}</div>\n      </div>\n    </div>\n    <template #footer>\n      <div class=\"flex flex-row flex-wrap gap-3\">\n        <VButton :loading=\"loading\" :type=\"confirmType\" @click=\"handleConfirm\">\n          {{ confirmText }}\n        </VButton>\n        <VButton v-if=\"showCancel\" @click=\"handleCancel\">\n          {{ cancelText }}\n        </VButton>\n      </div>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/packages/components/src/components/dialog/__tests__/Dialog.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VDialog } from \"../index\";\n\ndescribe(\"Dialog\", () => {\n  it(\"should render\", () => {\n    expect(mount(VDialog)).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/dialog/dialog-manager.ts",
    "content": "import { createVNode, render, type Component } from \"vue\";\nimport DialogComponent from \"./Dialog.vue\";\nimport type { DialogProps } from \"./types\";\n\nexport type DialogApiProps = Omit<DialogProps, \"type\" | \"visible\">;\n\nexport type DialogApi = (props?: DialogApiProps) => void;\n\nexport interface DialogEntry {\n  (props: DialogProps): void;\n  info: DialogApi;\n  success: DialogApi;\n  error: DialogApi;\n  warning: DialogApi;\n}\n\nconst defaultProps: DialogProps = {\n  title: \"\",\n  visible: false,\n};\n\nconst DIALOG_CONTAINER_CLASS = \".dialog-container\";\nconst MODAL_WRAPPER_CLASS = \".modal-wrapper\";\n\nfunction getOrCreateContainer() {\n  let container = document.body.querySelector(DIALOG_CONTAINER_CLASS);\n  if (!container) {\n    container = document.createElement(\"div\");\n    container.className = \"dialog-container\";\n    document.body.appendChild(container);\n  }\n  return container;\n}\n\nconst dialog: DialogEntry = (userProps: DialogProps) => {\n  const props = {\n    ...defaultProps,\n    ...userProps,\n  };\n\n  const container = getOrCreateContainer();\n\n  if (\n    props.uniqueId &&\n    container.querySelector(`[data-unique-id=\"${props.uniqueId}\"]`)\n  ) {\n    return;\n  }\n\n  const { vnode, container: hostContainer } = createVNodeComponent(\n    DialogComponent,\n    props\n  );\n\n  if (hostContainer.firstElementChild) {\n    container.appendChild(hostContainer.firstElementChild);\n  }\n\n  if (vnode.component?.props) {\n    vnode.component.props.visible = true;\n  }\n\n  if (vnode?.props) {\n    vnode.props.onClose = () => {\n      const modals = container.querySelectorAll(MODAL_WRAPPER_CLASS);\n\n      if (modals.length > 1) {\n        hostContainer.firstElementChild?.remove();\n      } else {\n        container.remove();\n      }\n\n      render(null, hostContainer);\n    };\n  }\n};\n\nfunction createVNodeComponent(\n  component: Component,\n  props: Record<string, unknown>\n) {\n  const vnode = createVNode(component, props);\n  const container = document.createElement(\"div\");\n  render(vnode, container);\n  return { vnode, container };\n}\n\ndialog.success = (props?: DialogApiProps) =>\n  dialog({ ...props, type: \"success\" });\ndialog.info = (props?: DialogApiProps) => dialog({ ...props, type: \"info\" });\ndialog.warning = (props?: DialogApiProps) =>\n  dialog({ ...props, type: \"warning\" });\ndialog.error = (props?: DialogApiProps) => dialog({ ...props, type: \"error\" });\n\nexport { dialog as Dialog };\n"
  },
  {
    "path": "ui/packages/components/src/components/dialog/index.ts",
    "content": "export { Dialog } from \"./dialog-manager\";\nexport { default as VDialog } from \"./Dialog.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/dialog/types.ts",
    "content": "export type DialogType = \"success\" | \"info\" | \"warning\" | \"error\";\nexport const DialogProviderProvideKey = \"DIALOG_PROVIDER_PROVIDE_KEY\";\nimport type { ButtonType } from \"../button/types\";\n\nexport interface DialogProps {\n  type?: DialogType;\n  visible?: boolean;\n  title?: string;\n  description?: string;\n  confirmType?: ButtonType;\n  showCancel?: boolean;\n  confirmText?: string;\n  cancelText?: string;\n  uniqueId?: string;\n  onConfirm?: () => void;\n  onCancel?: () => void;\n}\n"
  },
  {
    "path": "ui/packages/components/src/components/dropdown/Draopdown.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VDropdown, VDropdownDivider, VDropdownItem } from \".\";\nimport { VButton } from \"../button\";\n\nconst meta: Meta<typeof VDropdown> = {\n  title: \"Dropdown\",\n  component: VDropdown,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VDropdown, VDropdownItem, VDropdownDivider, VButton },\n    height: 400,\n    setup() {\n      return {\n        args,\n      };\n    },\n    template: `\n    <div style=\"height: 300px\">\n      <VDropdown class=\"inline-flex\">\n        <VButton>Hello</VButton>\n        <template #popper>\n          <VDropdownItem>删除</VDropdownItem>\n          <VDropdownDivider></VDropdownDivider>\n          <VDropdownItem>删除</VDropdownItem>\n          <VDropdownItem>编辑</VDropdownItem>\n        </template>\n      </VDropdown>\n    </div>\n\n    `,\n  }),\n  argTypes: {},\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VDropdown>;\n\nexport const Default: Story = {\n  args: {},\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/dropdown/DropdownDivider.vue",
    "content": "<script lang=\"ts\" setup></script>\n\n<template>\n  <div class=\"my-1 h-[1px] w-full bg-gray-100\"></div>\n</template>\n"
  },
  {
    "path": "ui/packages/components/src/components/dropdown/DropdownItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { inject } from \"vue\";\nconst props = withDefaults(\n  defineProps<{\n    selected?: boolean;\n    disabled?: boolean;\n    type?: \"default\" | \"danger\";\n  }>(),\n  {\n    selected: false,\n    disabled: false,\n    type: \"default\",\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"click\", e: MouseEvent): void;\n}>();\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst dropdown = inject<{ parentPopper: any }>(\"__floating-vue__popper\");\n\nfunction onClick(e: MouseEvent) {\n  if (props.disabled) {\n    return;\n  }\n\n  dropdown?.parentPopper?.hide?.();\n\n  emit(\"click\", e);\n}\n</script>\n\n<template>\n  <div\n    class=\"dropdown-item-wrapper\"\n    :class=\"[\n      `dropdown-item-wrapper--${type}${selected ? '--selected' : ''}`,\n      { 'dropdown-item-wrapper--disabled': disabled },\n    ]\"\n    role=\"menuitem\"\n    tabindex=\"-1\"\n    @click=\"onClick\"\n  >\n    <div class=\"flex items-center gap-3\">\n      <slot name=\"prefix-icon\" />\n\n      <slot />\n    </div>\n\n    <slot name=\"suffix-icon\"></slot>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.dropdown-item-wrapper {\n  @apply flex w-full min-w-52 cursor-pointer items-center justify-between gap-1 rounded px-4 py-2 text-sm transition-colors;\n\n  &--default {\n    @apply text-gray-700 hover:bg-gray-100 hover:text-gray-900;\n\n    &--selected {\n      @apply bg-gray-100 text-gray-900;\n    }\n  }\n\n  &--danger {\n    @apply text-red-500 hover:bg-red-50 hover:text-red-700;\n\n    &--selected {\n      @apply bg-red-50 text-red-700;\n    }\n  }\n\n  &--disabled {\n    @apply cursor-not-allowed opacity-70;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/dropdown/index.ts",
    "content": "export {\n  Dropdown as VDropdown,\n  vClosePopper,\n  type Placement as VDropdownPlacement,\n} from \"floating-vue\";\nexport { default as VDropdownDivider } from \"./DropdownDivider.vue\";\nexport { default as VDropdownItem } from \"./DropdownItem.vue\";\nimport \"floating-vue/dist/style.css\";\nimport \"./style.scss\";\n"
  },
  {
    "path": "ui/packages/components/src/components/dropdown/style.scss",
    "content": ".v-popper--theme-dropdown {\n  .v-popper__inner {\n    padding: theme(\"spacing[1.5]\");\n  }\n}\n"
  },
  {
    "path": "ui/packages/components/src/components/empty/Empty.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VEmpty } from \".\";\nimport { VButton } from \"../button\";\nimport { VSpace } from \"../space\";\n\nconst meta: Meta<typeof VEmpty> = {\n  title: \"Empty\",\n  component: VEmpty,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VEmpty, VSpace, VButton },\n    height: 400,\n    setup() {\n      return {\n        args,\n      };\n    },\n    template: `\n      <VEmpty>\n        <template #actions>\n          ${args.actions}\n        </template>\n      </VEmpty>\n    `,\n  }),\n  argTypes: {},\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VEmpty>;\n\nexport const Default: Story = {\n  args: {\n    title: \"没有找到与搜索条件匹配的文章\",\n    message: \"没有找到与搜索条件匹配的文章，你可以清空搜索条件或者新建文章\",\n    actions: `\n      <VSpace>\n        <VButton>清空搜索</VButton>\n        <VButton type=\"primary\">新建文章</VButton>\n      </VSpace>\n    `,\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/empty/Empty.vue",
    "content": "<script lang=\"ts\" setup>\nimport Empty from \"./Empty.svg\";\n\ndefineProps<{\n  title?: string;\n  message?: string;\n  image?: string;\n}>();\n</script>\n<template>\n  <div class=\"empty-wrapper\">\n    <div class=\"empty-image h-32 w-32\">\n      <slot name=\"image\">\n        <img :src=\"image || Empty\" alt=\"Empty\" />\n      </slot>\n    </div>\n    <div class=\"empty-title\">{{ title }}</div>\n    <div class=\"empty-message\">\n      <slot name=\"message\">\n        {{ message }}\n      </slot>\n    </div>\n    <div class=\"empty-actions\">\n      <slot name=\"actions\"></slot>\n    </div>\n  </div>\n</template>\n<style lang=\"scss\">\n.empty-wrapper {\n  @apply my-10 flex flex-col items-center justify-center px-10;\n\n  .empty-title {\n    @apply text-center text-sm font-medium text-gray-900;\n  }\n\n  .empty-message {\n    @apply mt-1.5 text-center text-xs text-gray-500;\n  }\n\n  .empty-actions {\n    @apply mt-5 flex flex-row;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/empty/__tests__/Empty.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { h } from \"vue\";\nimport { VButton } from \"../../button\";\nimport { VEmpty } from \"../index\";\n\ndescribe(\"Empty\", () => {\n  it(\"should render\", () => {\n    expect(mount(VEmpty)).toBeDefined();\n  });\n\n  it(\"should match snapshot\", () => {\n    expect(\n      mount(VEmpty, {\n        props: {\n          title: \"Not found\",\n          message: \"No posts found\",\n          image: \"./Empty.svg\",\n        },\n        slots: {\n          actions: h(VButton, { type: \"primary\" }, \"New Post\"),\n        },\n      }).html()\n    ).toMatchSnapshot();\n  });\n\n  it(\"should work with title prop\", () => {\n    const wrapper = mount(VEmpty, { props: { title: \"Not found\" } });\n    expect(wrapper.find(\".empty-title\").text()).toEqual(\"Not found\");\n  });\n\n  it(\"should work with message prop\", () => {\n    const wrapper = mount(VEmpty, { props: { message: \"No posts found\" } });\n    expect(wrapper.find(\".empty-message\").text()).toEqual(\"No posts found\");\n  });\n\n  it(\"should work with message slot\", () => {\n    const wrapper = mount(VEmpty, {\n      props: { message: \"empty\" },\n      slots: { message: h(\"span\", h(\"storage\", \"No posts found\")) },\n    });\n\n    expect(wrapper.find(\".empty-message\")).not.toEqual(\"Empty\");\n    expect(wrapper.find(\".empty-message > span > storage\").text()).toEqual(\n      \"No posts found\"\n    );\n  });\n\n  it(\"should work with actions slot\", () => {\n    const wrapper = mount(VEmpty, {\n      slots: {\n        actions: h(VButton, { type: \"primary\" }, \"New Post\"),\n      },\n    });\n\n    expect(wrapper.findComponent(VButton)).toBeDefined();\n    expect(wrapper.findComponent(VButton).find(\".btn-content\").text()).toEqual(\n      \"New Post\"\n    );\n  });\n\n  it(\"should work with image prop\", async () => {\n    const wrapper = mount({\n      data() {\n        return {\n          image: \"\",\n        };\n      },\n      render() {\n        return h(VEmpty, {\n          image: this.image,\n        });\n      },\n    });\n\n    expect(\n      wrapper.find(\".empty-image > img\").attributes().src.endsWith(\"/Empty.svg\")\n    ).toBe(true);\n\n    await wrapper.setData({ image: \"./empty.png\" });\n\n    expect(wrapper.find(\".empty-image > img\").attributes().src).toEqual(\n      \"./empty.png\"\n    );\n  });\n\n  it(\"should work with image slot\", () => {\n    const wrapper = mount(VEmpty, {\n      slots: { image: h(\"img\", { src: \"./empty\", alt: \"Empty Status\" }) },\n    });\n\n    const attributes = wrapper.find(\".empty-image > img\").attributes();\n\n    expect(attributes.src).not.toEqual(\"/src/components/empty/Empty.svg\");\n\n    expect(attributes.src).toEqual(\"./empty\");\n    expect(attributes.alt).toEqual(\"Empty Status\");\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/empty/__tests__/__snapshots__/Empty.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Empty > should match snapshot 1`] = `\n\"<div class=\"empty-wrapper\">\n  <div class=\"empty-image h-32 w-32\"><img src=\"./Empty.svg\" alt=\"Empty\"></div>\n  <div class=\"empty-title\">Not found</div>\n  <div class=\"empty-message\">No posts found</div>\n  <div class=\"empty-actions\"><button class=\"btn-md btn-primary btn\" type=\"button\">\n      <!--v-if--><span class=\"btn-content\">New Post</span>\n    </button></div>\n</div>\"\n`;\n"
  },
  {
    "path": "ui/packages/components/src/components/empty/index.ts",
    "content": "export { default as VEmpty } from \"./Empty.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/entity/Entity.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport { IconMore } from \"../../icons/icons\";\nimport { VDropdown } from \"../dropdown\";\n\nconst props = withDefaults(\n  defineProps<{\n    isSelected?: boolean;\n  }>(),\n  {\n    isSelected: false,\n  }\n);\n\nconst classes = computed(() => {\n  const result = [];\n  if (props.isSelected) {\n    result.push(\"entity-selected\");\n  }\n  return result;\n});\n</script>\n\n<template>\n  <tr :class=\"classes\" v-bind=\"$attrs\" class=\"entity-wrapper group\">\n    <td class=\"entity-start-wrapper\">\n      <slot name=\"prepend\" />\n      <div v-show=\"isSelected\" class=\"entity-selected-indicator\"></div>\n      <div class=\"entity-start\">\n        <div v-if=\"$slots.checkbox\" class=\"entity-checkbox\">\n          <slot name=\"checkbox\" />\n        </div>\n        <slot name=\"start\" />\n      </div>\n    </td>\n    <td class=\"entity-end-wrapper\">\n      <div class=\"entity-end\">\n        <slot name=\"end\" />\n        <div v-if=\"$slots.dropdownItems\" class=\"entity-dropdown\">\n          <!-- @vue-ignore -->\n          <VDropdown :dispose-timeout=\"null\">\n            <div\n              class=\"entity-dropdown-trigger group-hover:bg-gray-200/60\"\n              :class=\"{ '!bg-gray-300/60': isSelected }\"\n              @click.stop\n            >\n              <IconMore />\n            </div>\n            <template #popper>\n              <slot name=\"dropdownItems\"></slot>\n            </template>\n          </VDropdown>\n        </div>\n      </div>\n    </td>\n  </tr>\n  <tr v-if=\"$slots.footer\">\n    <td colspan=\"2\">\n      <slot name=\"footer\" />\n    </td>\n  </tr>\n</template>\n<style lang=\"scss\">\n.entity-wrapper {\n  @apply relative w-full transition-all hover:bg-gray-50;\n\n  &.entity-selected {\n    @apply bg-gray-100;\n  }\n\n  .entity-selected-indicator {\n    @apply absolute inset-y-0 left-0 w-0.5 bg-primary;\n  }\n\n  .entity-body {\n    @apply relative flex w-full flex-row items-center;\n  }\n\n  .entity-checkbox {\n    @apply hidden sm:inline-flex;\n  }\n\n  .entity-start-wrapper,\n  .entity-end-wrapper {\n    @apply w-auto px-4 py-3 align-middle;\n  }\n\n  .entity-start {\n    @apply flex items-center gap-4;\n  }\n\n  .entity-end {\n    @apply flex items-center justify-end gap-6;\n  }\n\n  .entity-dropdown {\n    @apply inline-flex items-center;\n  }\n\n  .entity-dropdown-trigger {\n    @apply cursor-pointer rounded p-1 transition-all hover:text-blue-600;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/entity/EntityContainer.vue",
    "content": "<script setup lang=\"ts\"></script>\n\n<template>\n  <div class=\"w-full overflow-x-auto\">\n    <table class=\"w-full border-spacing-0\">\n      <tbody class=\"divide-y divide-gray-100\">\n        <slot></slot>\n      </tbody>\n    </table>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/components/src/components/entity/EntityField.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, type CSSProperties } from \"vue\";\nimport type { RouteLocationRaw } from \"vue-router\";\n\nconst props = withDefaults(\n  defineProps<{\n    title?: string;\n    description?: string;\n    route?: RouteLocationRaw;\n    width?: string | number;\n    maxWidth?: string | number;\n  }>(),\n  {\n    title: undefined,\n    description: undefined,\n    route: undefined,\n    width: undefined,\n    maxWidth: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"click\"): void;\n}>();\n\nconst wrapperStyles = computed(() => {\n  const styles: CSSProperties = {};\n  if (props.width) {\n    styles.width = getWidthStyleValue(props.width);\n  }\n  if (props.maxWidth) {\n    styles.maxWidth = getWidthStyleValue(props.maxWidth);\n  }\n  return styles;\n});\n\nfunction getWidthStyleValue(value: string | number) {\n  return typeof value === \"string\" ? value : `${value}px`;\n}\n</script>\n\n<template>\n  <div class=\"entity-field-wrapper\" :style=\"wrapperStyles\">\n    <div v-if=\"title || $slots.title\" class=\"entity-field-title-body\">\n      <slot name=\"title\">\n        <div class=\"entity-field-title\" @click=\"emit('click')\">\n          <RouterLink\n            v-if=\"route\"\n            class=\"hover:text-gray-600\"\n            :to=\"route\"\n            :title=\"title\"\n          >\n            {{ title }}\n          </RouterLink>\n          <span v-else :title=\"title\">\n            {{ title }}\n          </span>\n        </div>\n        <slot name=\"extra\" />\n      </slot>\n    </div>\n    <div\n      v-if=\"description || $slots.description\"\n      class=\"entity-field-description-body\"\n    >\n      <slot name=\"description\">\n        <span\n          v-if=\"description\"\n          class=\"entity-field-description\"\n          :title=\"description\"\n        >\n          {{ description }}\n        </span>\n      </slot>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.entity-field-wrapper {\n  @apply inline-flex max-w-xs flex-col gap-1;\n\n  .entity-field-title-body {\n    @apply inline-flex flex-row items-center whitespace-nowrap;\n\n    .entity-field-title {\n      @apply mr-2 truncate text-sm font-medium text-gray-900;\n    }\n  }\n\n  .entity-field-description-body {\n    @apply inline-flex items-center whitespace-nowrap;\n\n    .entity-field-description {\n      @apply truncate text-xs text-gray-500;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/entity/__tests__/Entity.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VEntity } from \"..\";\n\ndescribe(\"Entity\", () => {\n  it(\"should render\", () => {\n    expect(mount(VEntity)).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/entity/__tests__/EntityField.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VEntityField } from \"..\";\n\ndescribe(\"EntityField\", () => {\n  it(\"should render\", () => {\n    expect(mount(VEntityField)).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/entity/index.ts",
    "content": "export { default as VEntity } from \"./Entity.vue\";\nexport { default as VEntityContainer } from \"./EntityContainer.vue\";\nexport { default as VEntityField } from \"./EntityField.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/header/PageHeader.vue",
    "content": "<script lang=\"ts\" setup>\ndefineProps<{\n  title?: string;\n}>();\n</script>\n<template>\n  <div class=\"page-header\">\n    <h2 class=\"page-header__title\">\n      <slot name=\"icon\" />\n      <span class=\"page-header__title-text\">{{ title }}</span>\n    </h2>\n    <div class=\"page-header__actions\">\n      <slot name=\"actions\" />\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.page-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  min-height: 3.5rem;\n  background-color: theme(\"colors.white\");\n  padding-left: 1rem;\n  padding-right: 1rem;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  flex-wrap: wrap;\n  gap: theme(\"spacing.2\");\n\n  &__title {\n    display: flex;\n    align-items: center;\n    text-overflow: truncate;\n    font-size: 1.25rem;\n    font-weight: 700;\n    color: theme(\"colors.gray.800\");\n    gap: theme(\"spacing.2\");\n  }\n\n  &__title-text {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  &__actions {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    gap: theme(\"spacing.2\");\n    flex-wrap: wrap;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/header/index.ts",
    "content": "export { default as VPageHeader } from \"./PageHeader.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/loading/Loading.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VLoading } from \".\";\n\nconst meta: Meta<typeof VLoading> = {\n  title: \"Loading\",\n  component: VLoading,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VLoading },\n    setup() {\n      return { args };\n    },\n    template: `<VLoading v-bind=\"args\" />`,\n  }),\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VLoading>;\n\nexport const Default: Story = {\n  args: {},\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/loading/Loading.vue",
    "content": "<script lang=\"ts\" setup></script>\n\n<template>\n  <div class=\"flex items-center justify-center py-4\">\n    <svg\n      class=\"h-5 w-5 animate-spin\"\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <circle\n        class=\"opacity-25\"\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n        stroke=\"currentColor\"\n        stroke-width=\"4\"\n      ></circle>\n      <path\n        class=\"opacity-75\"\n        d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/components/src/components/loading/index.ts",
    "content": "export { default as VLoading } from \"./Loading.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/Menu.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport {\n  IconAddCircle,\n  IconBookRead,\n  IconDashboard,\n  IconFolder,\n  IconMessage,\n  IconPages,\n} from \"@/icons/icons\";\nimport { VMenu, VMenuItem, VMenuLabel } from \"./index\";\n\nconst meta: Meta<typeof VMenu> = {\n  title: \"Menu\",\n  component: VMenu,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: {\n      VMenu,\n      VMenuItem,\n      VMenuLabel,\n      IconBookRead,\n      IconDashboard,\n      IconMessage,\n      IconFolder,\n      IconPages,\n      IconAddCircle,\n    },\n    setup() {\n      return {\n        args,\n      };\n    },\n    template: `\n      <div class=\"w-1/3\">\n        <VMenu>\n          <VMenuItem active title=\"仪表盘\">\n            <template #icon>\n              <IconDashboard />\n            </template>\n          </VMenuItem>\n          <VMenuLabel>内容</VMenuLabel>\n          <VMenuItem title=\"文章\">\n            <template #icon>\n              <IconBookRead />\n            </template>\n            <VMenuItem title=\"新文章\">\n              <template #icon>\n                <IconBookRead />\n              </template>\n            </VMenuItem>\n          </VMenuItem>\n          <VMenuItem title=\"页面\">\n            <template #icon>\n              <IconPages />\n            </template>\n          </VMenuItem>\n          <VMenuItem title=\"评论\">\n            <template #icon>\n              <IconMessage />\n            </template>\n          </VMenuItem>\n          <VMenuItem title=\"附件\">\n            <template #icon>\n              <IconFolder />\n            </template>\n          </VMenuItem>\n        </VMenu>\n      </div>\n    `,\n  }),\n  argTypes: {},\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VMenu>;\n\nexport const Default: Story = {\n  args: {},\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/Menu.vue",
    "content": "<script lang=\"ts\" setup>\nimport { provide } from \"vue\";\n\nconst props = defineProps<{\n  openIds?: string[];\n}>();\n\nprovide<string[] | undefined>(\"openIds\", props.openIds);\n</script>\n\n<template>\n  <div class=\"menu-container w-full p-3\">\n    <ul>\n      <slot />\n    </ul>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/MenuItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, inject, ref, useSlots } from \"vue\";\nimport { IconArrowRight } from \"../../icons/icons\";\n\nconst props = withDefaults(\n  defineProps<{\n    id?: string;\n    title?: string;\n    active?: boolean;\n  }>(),\n  {\n    id: \"\",\n    title: \"\",\n    active: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"select\", id: string): void;\n}>();\n\nconst slots = useSlots();\n\nconst open = ref(false);\n\nconst openIds = inject<string[] | undefined>(\"openIds\");\n\nif (openIds?.includes(props.id)) {\n  open.value = true;\n}\n\nconst hasSubmenus = computed(() => {\n  return slots.default && slots.default().length > 0;\n});\n\nfunction handleClick() {\n  if (!open.value) {\n    handleExpand();\n  }\n  emit(\"select\", props.id);\n}\n\nfunction handleExpand() {\n  if (hasSubmenus.value) {\n    open.value = !open.value;\n  }\n}\n</script>\n\n<template>\n  <li\n    :class=\"{ 'has-submenus': hasSubmenus }\"\n    class=\"menu-item group\"\n    @click.stop=\"handleClick\"\n  >\n    <div :class=\"{ active }\" class=\"menu-item-title\">\n      <span v-if=\"$slots.icon\" class=\"menu-icon mr-3 self-center\">\n        <slot name=\"icon\" />\n      </span>\n      <span class=\"menu-title flex-1 self-center\">\n        {{ title }}\n      </span>\n      <span\n        v-if=\"$slots.default\"\n        :class=\"{ open }\"\n        class=\"menu-icon-collapse self-center transition-all\"\n        @click.stop=\"handleExpand\"\n      >\n        <IconArrowRight />\n      </span>\n    </div>\n\n    <Transition name=\"submenus-show\">\n      <ul v-show=\"$slots.default && open\" class=\"sub-menu-items transition-all\">\n        <slot />\n      </ul>\n    </Transition>\n  </li>\n</template>\n\n<style lang=\"scss\">\n.menu-item {\n  @apply cursor-pointer;\n}\n\n.menu-item-title {\n  @apply relative flex select-none rounded-base px-2 py-[0.4rem] text-base font-normal transition-all;\n\n  &:hover,\n  &.active {\n    @apply bg-gray-100 font-medium;\n  }\n\n  &.active::after {\n    @apply absolute;\n    top: calc(50% - 13px);\n    left: -8px;\n    width: 3px;\n    height: 26px;\n    content: \"\";\n    background: #242e41;\n    border-radius: 6px;\n  }\n}\n\n.menu-icon-collapse {\n  @apply rounded-full p-0.5 group-hover:bg-gray-200;\n\n  &.open {\n    @apply bg-gray-200;\n    transform: rotate(90deg);\n  }\n}\n\n.submenus-show-enter-active,\n.submenus-show-leave-active {\n  transition: all 0.1s ease;\n}\n\n.submenus-show-enter-from,\n.submenus-show-enter-to {\n  opacity: 0;\n}\n\n.sub-menu-items {\n  @apply my-1 pl-5;\n\n  .menu-item-title {\n    @apply p-1.5 text-sm;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/MenuLabel.vue",
    "content": "<template>\n  <li class=\"menu-label flex flex-col\">\n    <slot />\n  </li>\n</template>\n<style lang=\"scss\">\n.menu-label {\n  padding-top: 10px;\n  padding-bottom: 10px;\n  color: #847e7e;\n  font-size: 14px;\n  font-weight: 400;\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/__tests__/Menu.spec.tsx",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VMenu, VMenuItem } from \"../index\";\n\ndescribe(\"Menu\", () => {\n  it(\"should render\", () => {\n    expect(VMenu).toBeDefined();\n    expect(VMenuItem).toBeDefined();\n    expect(mount(VMenu).html()).toMatchSnapshot();\n    expect(mount(VMenuItem).html()).toMatchSnapshot();\n  });\n\n  // fixme: fix this test\n  it.skip(\"should work with sub menus\", async () => {\n    const wrapper = await mount({\n      setup() {\n        return () => (\n          <VMenu>\n            <VMenuItem id=\"1\" title=\"Menu Item 1\" />\n            <VMenuItem id=\"2\" title=\"Menu Item 2\" />\n            <VMenuItem id=\"3\" title=\"Menu Item 3\">\n              <VMenuItem key=\"4\" title=\"Menu Item 4\" />\n            </VMenuItem>\n          </VMenu>\n        );\n      },\n    });\n    expect(wrapper.html()).toMatchSnapshot();\n\n    // toggling sub menu\n    expect(\n      wrapper.find(\".has-submenus .sub-menu-items\").attributes().style\n    ).toBe(\"display: none;\");\n\n    await wrapper.find(\".has-submenus\").trigger(\"click\");\n\n    expect(\n      wrapper.find(\".has-submenus .sub-menu-items\").attributes().style\n    ).toBe(\"\"); // visible\n\n    await wrapper.find(\".has-submenus\").trigger(\"click\");\n    expect(\n      wrapper.find(\".has-submenus .sub-menu-items\").attributes().style\n    ).toBe(\"\"); // visible\n\n    await wrapper.find(\".has-submenus .menu-icon-collapse\").trigger(\"click\");\n\n    expect(\n      wrapper.find(\".has-submenus .sub-menu-items\").attributes().style\n    ).toBe(\"display: none;\");\n  });\n\n  it(\"should work with openIds prop\", function () {\n    const wrapper = mount({\n      setup() {\n        return () => (\n          <VMenu openIds={[\"3\"]}>\n            <VMenuItem id=\"1\" title=\"Menu Item 1\" />\n            <VMenuItem id=\"2\" title=\"Menu Item 2\" />\n            <VMenuItem id=\"3\" title=\"Menu Item 3\">\n              <VMenuItem key=\"4\" title=\"Menu Item 4\" />\n            </VMenuItem>\n          </VMenu>\n        );\n      },\n    });\n\n    expect(wrapper.html()).toMatchSnapshot();\n\n    expect(wrapper.find(\".sub-menu-items .menu-title\").text()).contain(\n      \"Menu Item 4\"\n    );\n  });\n\n  // fixme: fix this test\n  it.skip(\"should work with select emit\", async () => {\n    const wrapper = mount({\n      setup() {\n        return () => (\n          <VMenu openIds={[\"3\"]}>\n            <VMenuItem id=\"1\" title=\"Menu Item 1\" />\n            <VMenuItem id=\"2\" title=\"Menu Item 2\" />\n            <VMenuItem id=\"3\" title=\"Menu Item 3\">\n              <VMenuItem id=\"4\" title=\"Menu Item 4\" />\n            </VMenuItem>\n          </VMenu>\n        );\n      },\n    });\n\n    wrapper.findAllComponents(VMenuItem).forEach((item) => {\n      // has not sub menu\n      if (item.props().id === \"1\") {\n        item.trigger(\"click\");\n        expect(item.emitted().select[0]).toEqual([\"1\"]);\n      }\n      // has sub menu\n      if (item.props().id === \"3\") {\n        item.trigger(\"click\");\n        expect(item.emitted().select).toBeDefined();\n\n        expect(item.vm.open).toBe(true);\n\n        item.trigger(\"click\");\n        expect(item.vm.open).toBe(true);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/__tests__/MenuLabel.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VMenuLabel } from \"../index\";\n\ndescribe(\"MenuLabel\", () => {\n  it(\"should render\", () => {\n    expect(VMenuLabel).toBeDefined();\n    expect(\n      mount(VMenuLabel, { slots: { default: \"Hello Halo\" } }).html()\n    ).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/__tests__/__snapshots__/Menu.spec.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Menu > should render 1`] = `\n\"<div class=\"menu-container w-full p-3\">\n  <ul></ul>\n</div>\"\n`;\n\nexports[`Menu > should render 2`] = `\n\"<li class=\"menu-item group\">\n  <div class=\"menu-item-title\">\n    <!--v-if--><span class=\"menu-title flex-1 self-center\"></span>\n    <!--v-if-->\n  </div>\n  <ul class=\"sub-menu-items transition-all\" style=\"display: none;\"></ul>\n</li>\"\n`;\n\nexports[`Menu > should work with openIds prop 1`] = `\n\"<div class=\"menu-container w-full p-3\">\n  <ul>\n    <li class=\"menu-item group\">\n      <div class=\"menu-item-title\">\n        <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 1</span>\n        <!--v-if-->\n      </div>\n      <ul class=\"sub-menu-items transition-all\" style=\"display: none;\"></ul>\n    </li>\n    <li class=\"menu-item group\">\n      <div class=\"menu-item-title\">\n        <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 2</span>\n        <!--v-if-->\n      </div>\n      <ul class=\"sub-menu-items transition-all\" style=\"display: none;\"></ul>\n    </li>\n    <li class=\"has-submenus menu-item group\">\n      <div class=\"menu-item-title\">\n        <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 3</span><span class=\"open menu-icon-collapse self-center transition-all\"><svg viewBox=\"0 0 24 24\" width=\"1.2em\" height=\"1.2em\"><path fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"></path></svg></span>\n      </div>\n      <ul class=\"sub-menu-items transition-all\">\n        <li class=\"menu-item group\">\n          <div class=\"menu-item-title\">\n            <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 4</span>\n            <!--v-if-->\n          </div>\n          <ul class=\"sub-menu-items transition-all\" style=\"display: none;\"></ul>\n        </li>\n      </ul>\n    </li>\n  </ul>\n</div>\"\n`;\n\nexports[`Menu > should work with sub menus 1`] = `\n\"<div class=\"menu-container w-full p-3\">\n  <ul>\n    <li class=\"menu-item group\">\n      <div class=\"menu-item-title\">\n        <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 1</span>\n        <!--v-if-->\n      </div>\n      <ul class=\"sub-menu-items transition-all\" style=\"display: none;\"></ul>\n    </li>\n    <li class=\"menu-item group\">\n      <div class=\"menu-item-title\">\n        <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 2</span>\n        <!--v-if-->\n      </div>\n      <ul class=\"sub-menu-items transition-all\" style=\"display: none;\"></ul>\n    </li>\n    <li class=\"has-submenus menu-item group\">\n      <div class=\"menu-item-title\">\n        <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 3</span><span class=\"menu-icon-collapse self-center transition-all\"><svg viewBox=\"0 0 24 24\" width=\"1.2em\" height=\"1.2em\"><path fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"></path></svg></span>\n      </div>\n      <ul class=\"sub-menu-items transition-all\" style=\"display: none;\">\n        <li class=\"menu-item group\">\n          <div class=\"menu-item-title\">\n            <!--v-if--><span class=\"menu-title flex-1 self-center\">Menu Item 4</span>\n            <!--v-if-->\n          </div>\n          <ul class=\"sub-menu-items transition-all\" style=\"display: none;\"></ul>\n        </li>\n      </ul>\n    </li>\n  </ul>\n</div>\"\n`;\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/__tests__/__snapshots__/MenuLabel.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`MenuLabel > should render 1`] = `\"<li class=\"menu-label flex flex-col\">Hello Halo</li>\"`;\n"
  },
  {
    "path": "ui/packages/components/src/components/menu/index.ts",
    "content": "export { default as VMenu } from \"./Menu.vue\";\nexport { default as VMenuItem } from \"./MenuItem.vue\";\nexport { default as VMenuLabel } from \"./MenuLabel.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/modal/Modal.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { ref } from \"vue\";\nimport { IconArrowLeft, IconArrowRight } from \"@/icons/icons\";\nimport { VModal } from \".\";\nimport { VButton } from \"../button\";\nimport { VSpace } from \"../space\";\n\nconst meta: Meta<typeof VModal> = {\n  title: \"Modal\",\n  component: VModal,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VModal, VButton, VSpace, IconArrowLeft, IconArrowRight },\n    setup() {\n      const modal = ref();\n      return { args, modal };\n    },\n    template: `\n      <VButton type=\"secondary\" @click=\"args.visible = true\">打开</VButton>\n      <VModal\n        ref=\"modal\"\n        v-if=\"args.visible\"\n        :fullscreen=\"args.fullscreen\"\n        :title=\"args.title\"\n        :width=\"args.width\"\n        :mount-to-body=\"true\"\n        :layerClosable=\"true\"\n        @close=\"args.visible = false\"\n      >\n        <template #actions>\n          <span>\n            <IconArrowLeft role=\"button\" />\n          </span>\n\n          <span>\n            <IconArrowRight role=\"button\" />\n          </span>\n        </template>\n        <div class=\"flex flex-col\">\n          <img class=\"w-full\" src=\"https://ryanc.cc/avatar\" />\n          <img class=\"w-full\" src=\"https://ryanc.cc/avatar\" />\n          <img class=\"w-full\" src=\"https://www.halo.run/logo\" />\n        </div>\n\n        <template #footer>\n          <VSpace>\n            <VButton loading type=\"primary\" @click=\"args.visible = false\"\n              >确定\n            </VButton>\n            <VButton @click=\"modal.close()\">取消</VButton>\n          </VSpace>\n        </template>\n      </VModal>\n    `,\n  }),\n  args: {\n    visible: false,\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VModal>;\n\nexport const Default: Story = {\n  args: {},\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/modal/Modal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { UseOverlayScrollbarsParams } from \"overlayscrollbars-vue\";\nimport { useOverlayScrollbars } from \"overlayscrollbars-vue\";\nimport { computed, nextTick, onMounted, reactive, ref, watch } from \"vue\";\nimport { IconClose } from \"../../icons/icons\";\n\nconst props = withDefaults(\n  defineProps<{\n    visible?: boolean;\n    title?: string;\n    width?: number;\n    height?: string;\n    fullscreen?: boolean;\n    bodyClass?: string[];\n    mountToBody?: boolean;\n    centered?: boolean;\n    layerClosable?: boolean;\n  }>(),\n  {\n    visible: undefined,\n    title: undefined,\n    width: 500,\n    height: undefined,\n    fullscreen: false,\n    bodyClass: undefined,\n    mountToBody: false,\n    centered: true,\n    layerClosable: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:visible\", value: boolean): void;\n  (event: \"close\"): void;\n}>();\n\nconst internalVisible = ref<boolean | undefined>(false);\nconst rootVisible = ref(false);\nconst modelWrapper = ref<HTMLElement>();\n\nwatch(\n  () => props.visible,\n  () => {\n    internalVisible.value = props.visible;\n  }\n);\n\nonMounted(() => {\n  if (props.visible === undefined) {\n    internalVisible.value = true;\n  }\n});\n\nconst wrapperClasses = computed(() => {\n  return {\n    \"modal-wrapper-fullscreen\": props.fullscreen,\n    \"modal-wrapper-centered\": props.centered,\n  };\n});\n\nconst contentStyles = computed(() => {\n  return {\n    maxWidth: props.width + \"px\",\n    height: props.height,\n  };\n});\n\nfunction handleClose() {\n  internalVisible.value = false;\n  setTimeout(() => {\n    emit(\"update:visible\", false);\n    emit(\"close\");\n  }, 200);\n}\n\ndefineExpose({\n  close: handleClose,\n});\n\nconst focus = ref(false);\n\nfunction handleClickLayer() {\n  if (props.layerClosable) {\n    handleClose();\n    return;\n  }\n  focus.value = true;\n  setTimeout(() => {\n    focus.value = false;\n  }, 300);\n}\n\n// body scroll\nconst modalBody = ref(null);\nconst reactiveParams = reactive<UseOverlayScrollbarsParams>({\n  options: {\n    scrollbars: {\n      autoHide: \"scroll\",\n      autoHideDelay: 600,\n    },\n  },\n  defer: true,\n});\nconst [initialize, instance] = useOverlayScrollbars(reactiveParams);\nwatch(\n  () => internalVisible.value,\n  (value) => {\n    if (value) {\n      if (modalBody.value) initialize({ target: modalBody.value });\n      nextTick(() => {\n        modelWrapper.value?.focus();\n      });\n    } else {\n      instance()?.destroy();\n    }\n  }\n);\n</script>\n<template>\n  <Teleport :disabled=\"!mountToBody\" to=\"body\">\n    <div\n      v-show=\"rootVisible\"\n      ref=\"modelWrapper\"\n      :class=\"wrapperClasses\"\n      aria-modal=\"true\"\n      class=\"modal-wrapper\"\n      role=\"dialog\"\n      tabindex=\"0\"\n      v-bind=\"$attrs\"\n      @keyup.esc.stop=\"handleClose()\"\n    >\n      <transition\n        enter-active-class=\"ease-out duration-200\"\n        enter-from-class=\"opacity-0\"\n        enter-to-class=\"opacity-100\"\n        leave-active-class=\"ease-in duration-100\"\n        leave-from-class=\"opacity-100\"\n        leave-to-class=\"opacity-0\"\n        @before-enter=\"rootVisible = true\"\n        @after-leave=\"rootVisible = false\"\n      >\n        <div\n          v-show=\"internalVisible\"\n          class=\"modal-layer\"\n          @click.stop=\"handleClickLayer()\"\n        />\n      </transition>\n      <transition\n        enter-active-class=\"ease-out duration-200\"\n        enter-from-class=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"\n        enter-to-class=\"opacity-100 translate-y-0 sm:scale-100\"\n        leave-active-class=\"ease-in duration-100\"\n        leave-from-class=\"opacity-100 translate-y-0 sm:scale-100\"\n        leave-to-class=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"\n      >\n        <div\n          v-show=\"internalVisible\"\n          :style=\"contentStyles\"\n          class=\"modal-content transition-all duration-300\"\n          :class=\"{ 'modal-focus': focus }\"\n        >\n          <div v-if=\"$slots.header || title\" class=\"modal-header group\">\n            <slot name=\"header\">\n              <div class=\"modal-header-title\">{{ title }}</div>\n              <div v-if=\"$slots.center\" class=\"modal-header-center\">\n                <slot name=\"center\"></slot>\n              </div>\n              <div class=\"modal-header-actions\">\n                <slot name=\"actions\"></slot>\n                <span class=\"bg-gray-50\" @click=\"handleClose()\">\n                  <IconClose />\n                </span>\n              </div>\n            </slot>\n          </div>\n          <div ref=\"modalBody\" :class=\"bodyClass\" class=\"modal-body\">\n            <slot />\n          </div>\n          <div v-if=\"$slots.footer\" class=\"modal-footer\">\n            <slot name=\"footer\" />\n          </div>\n        </div>\n      </transition>\n    </div>\n  </Teleport>\n</template>\n\n<style lang=\"scss\">\n.modal-wrapper {\n  @apply fixed left-0 top-0 flex h-full w-full flex-row items-start justify-center py-10;\n  z-index: 2000;\n\n  .modal-layer {\n    @apply absolute left-0 top-0 h-full w-full flex-none bg-gray-500 bg-opacity-75 transition-opacity;\n  }\n\n  .modal-content {\n    @apply relative flex flex-col items-stretch rounded-base bg-white shadow-xl;\n    width: calc(100vw - 20px);\n    max-height: calc(100vh - 5rem);\n\n    &.modal-focus {\n      @apply scale-[1.02];\n    }\n\n    .modal-header {\n      @apply flex select-none items-center justify-between border-b;\n      padding: 10px 16px;\n\n      .modal-header-title {\n        @apply truncate text-base font-medium;\n      }\n\n      .modal-header-actions {\n        @apply flex flex-row gap-2;\n        span {\n          @apply inline-flex h-7 w-7 cursor-pointer select-none items-center justify-center rounded-full text-gray-600 hover:bg-gray-100 hover:text-gray-900 group-hover:hidden;\n        }\n      }\n    }\n\n    .modal-body {\n      @apply flex-1 overflow-x-hidden overflow-y-hidden;\n      word-wrap: break-word;\n      padding: 12px 16px;\n    }\n\n    .modal-footer {\n      @apply border-t;\n      padding: 12px 16px;\n    }\n  }\n\n  &.modal-wrapper-centered {\n    @apply items-center py-0;\n    .modal-content {\n      max-height: calc(100vh - 20px) !important;\n    }\n  }\n\n  &.modal-wrapper-fullscreen {\n    .modal-content {\n      width: 100vw !important;\n      max-width: 100vw !important;\n      height: 100vh !important;\n      max-height: 100vh !important;\n      border-radius: 0;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/modal/__tests__/Modal.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { VModal } from \"../index\";\n\ndescribe(\"Modal\", () => {\n  it(\"should render\", () => {\n    expect(VModal).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/modal/index.ts",
    "content": "export { default as VModal } from \"./Modal.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/pagination/Pagination.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VPagination } from \".\";\n\nconst meta: Meta<typeof VPagination> = {\n  title: \"Pagination\",\n  component: VPagination,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VPagination },\n    setup() {\n      return { args };\n    },\n    template: `\n      <VPagination        \n        v-model:page=\"args.page\"\n        v-model:size=\"args.size\"\n        :total=\"args.total\"\n      />\n    `,\n  }),\n  args: {\n    page: 1,\n    size: 10,\n    total: 100,\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VPagination>;\n\nexport const Default: Story = {\n  args: {},\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/pagination/Pagination.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport { IconArrowLeft, IconArrowRight } from \"../../icons/icons\";\n\nconst props = withDefaults(\n  defineProps<{\n    page?: number;\n    size?: number;\n    total?: number;\n    sizeOptions?: number[];\n    showTotal?: boolean;\n    pageLabel?: string;\n    sizeLabel?: string;\n    totalLabel?: string;\n  }>(),\n  {\n    page: 1,\n    size: 10,\n    total: 0,\n    sizeOptions: () => [10],\n    showTotal: true,\n    pageLabel: \"页\",\n    sizeLabel: \"条 / 页\",\n    totalLabel: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:page\", page: number): void;\n  (event: \"update:size\", size: number): void;\n  (event: \"change\", value: { page: number; size: number }): void;\n}>();\n\nconst totalPages = computed(() => Math.ceil(props.total / props.size));\n\nconst hasNext = computed(() => props.page < totalPages.value);\n\nconst hasPrevious = computed(() => props.page > 1);\n\nconst totalLabelText = computed(() => {\n  if (props.totalLabel) {\n    return props.totalLabel;\n  }\n  return `共 ${props.total} 项数据`;\n});\n\nconst onPageChange = (event: Event) => {\n  const target = event.target as HTMLSelectElement;\n  const page = Number(target.value);\n  emit(\"update:page\", page);\n  emit(\"change\", { page, size: props.size });\n};\n\nconst onSizeChange = (event: Event) => {\n  const target = event.target as HTMLSelectElement;\n  const size = Number(target.value);\n  emit(\"update:size\", size);\n\n  // reset page to 1\n  emit(\"update:page\", 1);\n  emit(\"change\", { page: 1, size });\n};\n\nconst previous = () => {\n  if (hasPrevious.value) {\n    const page = props.page - 1;\n    emit(\"update:page\", page);\n    emit(\"change\", { page: page, size: props.size });\n  }\n};\n\nconst next = () => {\n  if (hasNext.value) {\n    const page = props.page + 1;\n    emit(\"update:page\", page);\n    emit(\"change\", { page: page, size: props.size });\n  }\n};\n</script>\n<template>\n  <div class=\"pagination\">\n    <div v-if=\"showTotal\" class=\"pagination__total\">\n      {{ totalLabelText }}\n    </div>\n    <div class=\"pagination__controller\">\n      <nav aria-label=\"Pagination\" class=\"pagination__nav\">\n        <button\n          class=\"pagination__btn pagination__btn--prev\"\n          :disabled=\"!hasPrevious\"\n          @click=\"previous\"\n        >\n          <IconArrowLeft />\n        </button>\n        <button\n          class=\"pagination__btn pagination__btn--next\"\n          :disabled=\"!hasNext\"\n          @click=\"next\"\n        >\n          <IconArrowRight />\n        </button>\n      </nav>\n      <div class=\"pagination__select-wrap\">\n        <select\n          :value=\"page\"\n          :disabled=\"totalPages === 0\"\n          class=\"pagination__select\"\n          @change=\"onPageChange\"\n        >\n          <option v-if=\"totalPages === 0\" :value=\"1\">0 / 0</option>\n          <option v-for=\"i in totalPages || 1\" :key=\"i\" :value=\"i\">\n            {{ i }} / {{ totalPages }}\n          </option>\n        </select>\n        <span class=\"pagination__select-label\">\n          {{ pageLabel }}\n        </span>\n      </div>\n      <div class=\"pagination__select-wrap\">\n        <select :value=\"size\" class=\"pagination__select\" @change=\"onSizeChange\">\n          <option\n            v-for=\"(sizeOption, index) in sizeOptions\"\n            :key=\"index\"\n            :value=\"sizeOption\"\n          >\n            {{ sizeOption }}\n          </option>\n        </select>\n        <span class=\"pagination__select-label\">\n          {{ sizeLabel }}\n        </span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.pagination {\n  @apply flex flex-1 items-center gap-2 bg-white;\n\n  &__total {\n    @apply hidden text-sm text-gray-500 sm:block;\n  }\n\n  &__controller {\n    @apply flex flex-1 items-center justify-end gap-2;\n  }\n\n  &__nav {\n    @apply relative z-0 inline-flex -space-x-px rounded-base shadow-sm;\n  }\n\n  &__btn {\n    @apply relative inline-flex h-8 cursor-pointer items-center rounded-base border border-gray-300 bg-white px-2 py-1.5 text-sm font-medium text-gray-500 outline-none hover:bg-gray-50 disabled:cursor-not-allowed;\n\n    &--prev {\n      @apply rounded-r-none;\n    }\n\n    &--next {\n      @apply rounded-l-none;\n    }\n  }\n\n  &__select-wrap {\n    @apply inline-flex items-center gap-2;\n  }\n\n  &__select {\n    @apply h-8 rounded-base border border-solid border-gray-300 px-2 pr-10 text-sm text-gray-800 outline-none focus:border-primary;\n  }\n\n  &__select-label {\n    @apply text-sm text-gray-500;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/pagination/__tests__/Pagination.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VPagination } from \"../index\";\n\ndescribe(\"Pagination\", () => {\n  it(\"should be true\", () => {\n    expect(mount(VPagination)).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/pagination/index.ts",
    "content": "export { default as VPagination } from \"./Pagination.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/space/Space.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VSpace } from \".\";\nimport { VButton } from \"../button\";\n\nconst meta: Meta<typeof VSpace> = {\n  title: \"Space\",\n  component: VSpace,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VSpace, VButton },\n    setup() {\n      return { args };\n    },\n    template: `\n      <VSpace\n        v-bind=\"args\"\n      >\n        <div>Control：</div>\n        <VButton type=\"primary\">确定</VButton>\n        <VButton>取消</VButton>\n      </VSpace>\n    `,\n  }),\n  argTypes: {\n    align: {\n      control: { type: \"select\" },\n      options: [\"start\", \"end\", \"center\", \"stretch\"],\n    },\n    spacing: {\n      control: { type: \"select\" },\n      options: [\"xs\", \"sm\", \"md\", \"lg\"],\n    },\n    direction: {\n      control: { type: \"select\" },\n      options: [\"row\", \"column\"],\n    },\n  },\n  args: {\n    align: \"center\",\n    direction: \"row\",\n    spacing: \"xs\",\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VSpace>;\n\nexport const Default: Story = {\n  args: {},\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/space/Space.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport type { SpaceAlign, SpaceDirection, SpaceSpacing } from \"./types\";\nimport { SpaceSpacingSize } from \"./types\";\n\nconst props = withDefaults(\n  defineProps<{\n    spacing?: SpaceSpacing;\n    direction?: SpaceDirection;\n    align?: SpaceAlign;\n  }>(),\n  {\n    spacing: \"xs\",\n    direction: \"row\",\n    align: \"center\",\n  }\n);\n\nconst wrapperClasses = computed(() => {\n  const { direction, align } = props;\n  return [`space-direction-${direction}`, `space-align-${align}`];\n});\n</script>\n<template>\n  <div\n    :class=\"wrapperClasses\"\n    :style=\"`gap: ${SpaceSpacingSize[spacing]}px`\"\n    class=\"space-wrapper\"\n  >\n    <slot />\n  </div>\n</template>\n<style lang=\"scss\">\n.space-wrapper {\n  @apply box-border inline-flex;\n\n  &.space-direction-row {\n    @apply flex-row;\n  }\n\n  &.space-direction-column {\n    @apply flex-col;\n  }\n\n  &.space-align-center {\n    @apply items-center;\n  }\n\n  &.space-align-start {\n    @apply items-start;\n  }\n\n  &.space-align-end {\n    @apply items-end;\n  }\n\n  &.space-align-stretch {\n    @apply items-stretch;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/space/__tests__/Space.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VSpace } from \"../index\";\nimport { SpaceSpacingSize } from \"../types\";\n\ndescribe(\"Space\", () => {\n  it(\"should render\", function () {\n    expect(VSpace).toBeDefined();\n  });\n\n  it(\"should work with spacing prop\", function () {\n    Object.keys(SpaceSpacingSize).forEach((key: string) => {\n      const wrapper = mount(VSpace, {\n        propsData: {\n          spacing: key,\n        },\n      });\n      expect(wrapper.attributes()[\"style\"]).toContain(\n        `gap: ${SpaceSpacingSize[key]}px`\n      );\n      wrapper.unmount();\n    });\n  });\n\n  it(\"should work with direction prop\", function () {\n    [\"row\", \"column\"].forEach((direction: string) => {\n      const wrapper = mount(VSpace, {\n        propsData: {\n          direction: direction,\n        },\n      });\n      expect(wrapper.classes()).toContain(`space-direction-${direction}`);\n      wrapper.unmount();\n    });\n  });\n\n  it(\"should work with align prop\", function () {\n    [\"center\", \"start\", \"end\", \"stretch\"].forEach((align: string) => {\n      const wrapper = mount(VSpace, {\n        propsData: {\n          align: align,\n        },\n      });\n      expect(wrapper.classes()).toContain(`space-align-${align}`);\n      wrapper.unmount();\n    });\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/space/index.ts",
    "content": "export { default as VSpace } from \"./Space.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/space/types.ts",
    "content": "export type SpaceSpacing = \"xs\" | \"sm\" | \"md\" | \"lg\";\nexport type SpaceDirection = \"row\" | \"column\";\nexport type SpaceAlign = \"start\" | \"end\" | \"center\" | \"stretch\";\nexport const SpaceSpacingSize: Record<string, number> = {\n  xs: 10,\n  sm: 12,\n  md: 16,\n  lg: 20,\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/status/StatusDot.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VStatusDot } from \".\";\n\nconst meta: Meta<typeof VStatusDot> = {\n  title: \"StatusDot\",\n  component: VStatusDot,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VStatusDot },\n    setup() {\n      return {\n        args,\n      };\n    },\n    template: `\n        <VStatusDot v-bind=\"args\" />\n    `,\n  }),\n  argTypes: {\n    state: {\n      control: { type: \"select\" },\n      options: [\"default\", \"success\", \"warning\", \"error\"],\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VStatusDot>;\n\nexport const Default: Story = {\n  args: {\n    state: \"default\",\n    text: \"默认\",\n  },\n};\n\nexport const Success: Story = {\n  args: {\n    state: \"success\",\n    text: \"成功\",\n  },\n};\n\nexport const Warning: Story = {\n  args: {\n    state: \"warning\",\n    text: \"警告\",\n  },\n};\n\nexport const Error: Story = {\n  args: {\n    state: \"error\",\n    text: \"错误\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/status/StatusDot.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport type { StatusDotState } from \"./types\";\n\nconst props = withDefaults(\n  defineProps<{\n    state?: StatusDotState;\n    animate?: boolean;\n    text?: string;\n  }>(),\n  { state: \"success\", animate: false, text: undefined }\n);\n\nconst classes = computed(() => {\n  return [`status-dot-${props.state}`, { \"status-dot-animate\": props.animate }];\n});\n</script>\n\n<template>\n  <div class=\"status-dot-wrapper\" :class=\"classes\">\n    <div class=\"status-dot-body\">\n      <span class=\"status-dot-inner\"></span>\n    </div>\n    <slot v-if=\"$slots.text || text\" name=\"text\">\n      <span class=\"status-dot-text\">{{ text }}</span>\n    </slot>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.status-dot-wrapper {\n  @apply flex items-center gap-2;\n\n  .status-dot-body {\n    @apply inline-flex h-2 w-2 items-center justify-center rounded-full;\n  }\n\n  .status-dot-inner {\n    @apply inline-block h-1.5 w-1.5 rounded-full;\n  }\n\n  .status-dot-text {\n    @apply whitespace-nowrap text-xs text-gray-500;\n  }\n\n  &.status-dot-animate {\n    .status-dot-inner {\n      @apply animate-ping;\n    }\n  }\n\n  &.status-dot-default {\n    .status-dot-body {\n      @apply bg-gray-300;\n    }\n\n    .status-dot-inner {\n      @apply bg-gray-300;\n    }\n  }\n\n  &.status-dot-success {\n    .status-dot-body {\n      @apply bg-green-600;\n    }\n\n    .status-dot-inner {\n      @apply bg-green-600;\n    }\n  }\n\n  &.status-dot-warning {\n    .status-dot-body {\n      @apply bg-yellow-600;\n    }\n\n    .status-dot-inner {\n      @apply bg-yellow-600;\n    }\n  }\n\n  &.status-dot-error {\n    .status-dot-body {\n      @apply bg-red-600;\n    }\n\n    .status-dot-inner {\n      @apply bg-red-600;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/status/__tests__/StatusDot.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VStatusDot } from \"../index\";\n\ndescribe(\"StatusDot\", () => {\n  it(\"should render\", () => {\n    expect(mount(VStatusDot)).toBeDefined();\n  });\n\n  it(\"should match snapshot\", () => {\n    const wrapper = mount(VStatusDot);\n    expect(wrapper.html()).toMatchSnapshot();\n  });\n\n  it(\"should work with state prop\", () => {\n    [\"default\", \"success\", \"warning\", \"error\"].forEach((state) => {\n      const wrapper = mount(VStatusDot, { props: { state } });\n      expect(wrapper.classes()).toContain(`status-dot-${state}`);\n    });\n  });\n\n  it(\"should work with animate prop\", () => {\n    const wrapper = mount(VStatusDot, { props: { animate: true } });\n    expect(wrapper.classes()).toContain(\"status-dot-animate\");\n  });\n\n  it(\"should work with text prop\", () => {\n    const wrapper = mount(VStatusDot, { props: { text: \"text\" } });\n    expect(wrapper.find(\".status-dot-text\").text()).toBe(\"text\");\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/status/__tests__/__snapshots__/StatusDot.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`StatusDot > should match snapshot 1`] = `\n\"<div class=\"status-dot-wrapper status-dot-success\">\n  <div class=\"status-dot-body\"><span class=\"status-dot-inner\"></span></div>\n  <!--v-if-->\n</div>\"\n`;\n"
  },
  {
    "path": "ui/packages/components/src/components/status/index.ts",
    "content": "export { default as VStatusDot } from \"./StatusDot.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/status/types.ts",
    "content": "export type StatusDotState = \"default\" | \"success\" | \"warning\" | \"error\";\n"
  },
  {
    "path": "ui/packages/components/src/components/switch/Switch.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VSwitch } from \".\";\n\nconst meta: Meta<typeof VSwitch> = {\n  title: \"Switch\",\n  component: VSwitch,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VSwitch },\n    setup() {\n      return {\n        args,\n      };\n    },\n    template: `\n        <VSwitch v-bind=\"args\" v-model=\"args.modelValue\" />\n    `,\n  }),\n  argTypes: {},\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VSwitch>;\n\nexport const Default: Story = {\n  args: {},\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/switch/Switch.vue",
    "content": "<script lang=\"ts\" setup>\nconst props = withDefaults(\n  defineProps<{\n    modelValue?: boolean;\n    disabled?: boolean;\n    loading?: boolean;\n  }>(),\n  {\n    modelValue: false,\n    disabled: false,\n    loading: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:modelValue\", value: boolean): void;\n  (event: \"change\", value: boolean): void;\n}>();\n\nconst handleChange = () => {\n  if (props.disabled || props.loading) return;\n\n  emit(\"update:modelValue\", !props.modelValue);\n  emit(\"change\", !props.modelValue);\n};\n</script>\n<template>\n  <div class=\"switch-wrapper\">\n    <button\n      :class=\"{\n        'bg-gray-200': !modelValue,\n        '!bg-primary': modelValue,\n        'switch-disabled': disabled || loading,\n      }\"\n      aria-checked=\"false\"\n      class=\"switch-inner\"\n      role=\"switch\"\n      type=\"button\"\n      :disabled=\"disabled || loading\"\n      @click=\"handleChange\"\n    >\n      <span\n        :class=\"{\n          'translate-x-0': !modelValue,\n          'translate-x-5': modelValue,\n        }\"\n        aria-hidden=\"true\"\n        class=\"switch-indicator\"\n      >\n        <svg\n          v-if=\"loading\"\n          class=\"animate-spin\"\n          fill=\"none\"\n          viewBox=\"0 0 24 24\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <circle\n            class=\"opacity-0\"\n            cx=\"12\"\n            cy=\"12\"\n            r=\"10\"\n            stroke=\"currentColor\"\n            stroke-width=\"4\"\n          ></circle>\n          <path\n            class=\"opacity-30\"\n            d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n            fill=\"currentColor\"\n          ></path>\n        </svg>\n        <slot v-else name=\"icon\" />\n      </span>\n    </button>\n  </div>\n</template>\n<style lang=\"scss\">\n.switch-wrapper {\n  @apply box-border inline-flex;\n\n  .switch-inner {\n    @apply relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out;\n\n    &.switch-disabled {\n      @apply cursor-not-allowed opacity-60;\n    }\n\n    .switch-indicator {\n      @apply pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/switch/__tests__/Switch.spec.ts",
    "content": "import { mount } from \"@vue/test-utils\";\nimport { describe, expect, it } from \"vitest\";\nimport { VSwitch } from \"../index\";\n\ndescribe(\"Switch\", () => {\n  it(\"should render\", () => {\n    expect(mount(VSwitch)).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/switch/index.ts",
    "content": "export { default as VSwitch } from \"./Switch.vue\";\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/TabItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ComputedRef } from \"vue\";\nimport { computed, inject } from \"vue\";\n\nconst props = defineProps<{\n  id?: string;\n  label?: string;\n}>();\n\nconst activeId = inject<ComputedRef<string | number | undefined>>(\"activeId\");\n\nconst isActive = computed(() => {\n  return activeId?.value === props.id;\n});\n</script>\n<template>\n  <div v-if=\"isActive\" class=\"tabs-item-wrapper\">\n    <slot />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/Tabbar.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { ref } from \"vue\";\nimport { VTabbar } from \".\";\n\nconst meta: Meta<typeof VTabbar> = {\n  title: \"Tabbar\",\n  component: VTabbar,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VTabbar },\n    setup() {\n      const activeId = ref(\"general\");\n      const items = [\n        { label: \"基本设置\", id: \"general\" },\n        { label: \"文章设置\", id: \"post\" },\n        { label: \"SEO 设置\", id: \"seo\" },\n        { label: \"评论设置\", id: \"comment\" },\n        { label: \"主题路由设置\", id: \"theme-route\" },\n        { label: \"代码注入\", id: \"code-inject\" },\n      ];\n\n      return {\n        args,\n        activeId,\n        items,\n      };\n    },\n    template: `\n      <div class=\"p-3\">\n        <VTabbar \n          v-model:activeId=\"activeId\" \n          :items=\"items\" \n          :type=\"args.type\"\n          :direction=\"args.direction\"\n        />\n      </div>\n \n    `,\n  }),\n  argTypes: {\n    type: {\n      control: { type: \"select\" },\n      options: [\"default\", \"pills\", \"outline\"],\n    },\n    direction: {\n      control: { type: \"select\" },\n      options: [\"row\", \"column\"],\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VTabbar>;\n\nexport const Default: Story = {\n  args: {\n    type: \"default\",\n  },\n};\n\nexport const Pills: Story = {\n  args: {\n    type: \"pills\",\n  },\n};\n\nexport const Outline: Story = {\n  args: {\n    type: \"outline\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/Tabbar.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useElementSize, useThrottleFn } from \"@vueuse/core\";\nimport type { ComputedRef } from \"vue\";\nimport { computed, onMounted, onUnmounted, ref, watch } from \"vue\";\nimport { IconArrowLeft, IconArrowRight } from \"../../icons/icons\";\nimport type { TabsArrowShow, TabsDirection, TabsType } from \"./types\";\n\nconst props = withDefaults(\n  defineProps<{\n    activeId?: number | string;\n    items?: Array<Record<string, string>>;\n    type?: TabsType;\n    direction?: TabsDirection;\n    idKey?: string;\n    labelKey?: string;\n  }>(),\n  {\n    activeId: undefined,\n    items: undefined,\n    type: \"default\",\n    direction: \"row\",\n    idKey: \"id\",\n    labelKey: \"label\",\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:activeId\", value: number | string): void;\n  (event: \"change\", value: number | string): void;\n}>();\n\nconst classes = computed(() => {\n  return [`tabbar-${props.type}`, `tabbar-direction-${props.direction}`];\n});\n\nconst handleChange = (id: number | string, index: number) => {\n  handleClickTabItem(index);\n  emit(\"update:activeId\", id);\n  emit(\"change\", id);\n};\n\nconst tabbarItemsRef = ref<HTMLElement | undefined>();\nconst tabItemRefs = ref<HTMLElement[] | undefined>();\nconst itemWidthArr = ref<number[]>([]);\nconst indicatorRef = ref<HTMLElement | undefined>();\nconst arrowFlag = ref(false);\nconst { width: tabbarWidth } = useElementSize(tabbarItemsRef);\n\nconst arrowShow: ComputedRef<TabsArrowShow> = computed(() => {\n  const show: TabsArrowShow = { left: false, right: false };\n  if (!tabbarItemsRef.value) return show;\n  void arrowFlag.value;\n  void tabbarWidth.value;\n  const { scrollWidth, scrollLeft, clientWidth } = tabbarItemsRef.value;\n  if (scrollWidth > clientWidth) {\n    if (scrollLeft < scrollWidth - clientWidth) {\n      show.right = true;\n    }\n    if (scrollLeft > 20) {\n      show.left = true;\n    }\n  }\n  return show;\n});\n\nfunction handleHorizontalWheel(event: WheelEvent) {\n  if (!tabbarItemsRef.value) {\n    return;\n  }\n  const { scrollLeft, scrollWidth, clientWidth } = tabbarItemsRef.value;\n  const toLeft = event.deltaY < 0 && scrollLeft > 0;\n  const toRight = event.deltaY > 0 && scrollLeft < scrollWidth - clientWidth;\n\n  if (toLeft || toRight) {\n    event.preventDefault();\n    event.stopPropagation();\n    tabbarItemsRef.value.scrollBy({ left: event.deltaY });\n  }\n}\n\nfunction saveItemsWidth() {\n  if (!tabbarItemsRef.value || !tabItemRefs.value) return;\n  itemWidthArr.value = [];\n  for (const item of tabItemRefs.value) {\n    itemWidthArr.value.push(item.offsetWidth);\n  }\n  arrowFlag.value = !arrowFlag.value;\n}\n\nfunction handleClickTabItem(index: number) {\n  if (!tabbarItemsRef.value || !indicatorRef.value) return;\n  const { scrollWidth, clientWidth } = tabbarItemsRef.value;\n  if (scrollWidth <= clientWidth) return;\n  if (index === 0) {\n    tabbarItemsRef.value.scrollTo({ left: 0, behavior: \"smooth\" });\n    return;\n  }\n  if (index === itemWidthArr.value.length - 1) {\n    tabbarItemsRef.value.scrollTo({\n      left: scrollWidth - clientWidth,\n      behavior: \"smooth\",\n    });\n    return;\n  }\n}\n\nfunction handleClickArrow(prev: boolean) {\n  if (!tabbarItemsRef.value || !indicatorRef.value || !tabItemRefs.value)\n    return;\n  const { scrollWidth, scrollLeft, clientWidth } = tabbarItemsRef.value;\n  if (scrollWidth <= clientWidth) return;\n  if (!itemWidthArr.value[0]) {\n    itemWidthArr.value = [];\n    for (const item of tabItemRefs.value) {\n      itemWidthArr.value.push(item.offsetWidth);\n    }\n  }\n  let hiddenNum = 0;\n  let totalWith = 0;\n  let scrollByX = 0;\n  const lastItemWidth = itemWidthArr.value[itemWidthArr.value.length - 1];\n  if (prev) {\n    for (let i = 0; i < itemWidthArr.value.length; i++) {\n      const w = itemWidthArr.value[i];\n      totalWith += w;\n      if (totalWith >= scrollLeft) {\n        hiddenNum = i;\n        break;\n      }\n    }\n    if (hiddenNum === 0) {\n      scrollByX = -itemWidthArr.value[0];\n    } else {\n      scrollByX = -(\n        itemWidthArr.value[hiddenNum] -\n        totalWith +\n        scrollLeft +\n        itemWidthArr.value[hiddenNum - 1]\n      );\n    }\n  } else {\n    const overWidth = scrollWidth - scrollLeft - clientWidth;\n    for (let i = itemWidthArr.value.length - 1; i >= 0; i--) {\n      const w = itemWidthArr.value[i];\n      totalWith += w;\n      if (totalWith >= overWidth) {\n        hiddenNum = i;\n        break;\n      }\n    }\n\n    if (hiddenNum === itemWidthArr.value.length - 1) {\n      scrollByX =\n        lastItemWidth + itemWidthArr.value[itemWidthArr.value.length - 1];\n    } else {\n      scrollByX =\n        itemWidthArr.value[hiddenNum] -\n        (totalWith - overWidth) +\n        itemWidthArr.value[hiddenNum + 1];\n    }\n  }\n  tabbarItemsRef.value.scrollBy({\n    left: scrollByX,\n    behavior: \"smooth\",\n  });\n}\n\nconst handleScroll = useThrottleFn(\n  () => {\n    arrowFlag.value = !arrowFlag.value;\n  },\n  100,\n  true\n);\n\nwatch(() => tabItemRefs.value?.length, saveItemsWidth);\n\nonMounted(() => {\n  tabbarItemsRef.value?.addEventListener(\"wheel\", handleHorizontalWheel);\n  tabbarItemsRef.value?.addEventListener(\"scroll\", handleScroll);\n});\n\nonUnmounted(() => {\n  tabbarItemsRef.value?.removeEventListener(\"wheel\", handleHorizontalWheel);\n  tabbarItemsRef.value?.removeEventListener(\"scroll\", handleScroll);\n});\n</script>\n<template>\n  <div :class=\"classes\" class=\"tabbar-wrapper\">\n    <div\n      ref=\"indicatorRef\"\n      :class=\"['indicator', 'left', arrowShow.left ? 'visible' : 'invisible']\"\n    >\n      <div title=\"向前\" class=\"arrow-left\" @click=\"handleClickArrow(true)\">\n        <IconArrowLeft />\n      </div>\n    </div>\n    <div\n      :class=\"['indicator', 'right', arrowShow.right ? 'visible' : 'invisible']\"\n    >\n      <div title=\"向后\" class=\"arrow-right\" @click=\"handleClickArrow(false)\">\n        <IconArrowRight />\n      </div>\n    </div>\n    <div ref=\"tabbarItemsRef\" class=\"tabbar-items\">\n      <div\n        v-for=\"(item, index) in items\"\n        :key=\"index\"\n        ref=\"tabItemRefs\"\n        :class=\"{ 'tabbar-item-active': item[idKey] === activeId }\"\n        class=\"tabbar-item\"\n        @click=\"handleChange(item[idKey], index)\"\n      >\n        <div v-if=\"item.icon\" class=\"tabbar-item-icon\">\n          <component :is=\"item.icon\" />\n        </div>\n        <div v-if=\"item[labelKey]\" class=\"tabbar-item-label\">\n          {{ item[labelKey] }}\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n<style lang=\"scss\">\n.tabbar-wrapper {\n  @apply relative;\n  .indicator {\n    @apply pointer-events-none absolute top-0 z-10 flex h-full w-20 items-center from-transparent from-10% via-white/80 via-30% to-white to-70% pb-1.5 pt-1;\n\n    &.left {\n      @apply left-0 justify-start bg-gradient-to-l;\n    }\n    &.right {\n      @apply right-0 justify-end bg-gradient-to-r;\n    }\n    .arrow-left,\n    .arrow-right {\n      @apply pointer-events-auto flex h-9 w-10 cursor-pointer select-none items-center justify-center;\n      svg {\n        font-size: 1.5em;\n      }\n    }\n  }\n\n  .tabbar-items {\n    @apply flex flex-row items-center overflow-x-auto py-0.5;\n\n    &::-webkit-scrollbar-track-piece {\n      background-color: #f8f8f8;\n      -webkit-border-radius: 2em;\n      -moz-border-radius: 2em;\n      border-radius: 2em;\n    }\n\n    &::-webkit-scrollbar {\n      width: 4px;\n      height: 4px;\n    }\n\n    &::-webkit-scrollbar-thumb {\n      background-color: #ddd;\n      background-clip: padding-box;\n      -webkit-border-radius: 2em;\n      -moz-border-radius: 2em;\n      border-radius: 2em;\n    }\n\n    &::-webkit-scrollbar-thumb:hover {\n      background-color: #bbb;\n    }\n  }\n\n  .tabbar-item {\n    @apply inline-flex h-9 cursor-pointer justify-center gap-2 self-center whitespace-nowrap text-sm transition-all;\n\n    .tabbar-item-label,\n    .tabbar-item-icon {\n      @apply self-center;\n    }\n  }\n\n  &.tabbar-default {\n    border-bottom-width: 2px;\n    @apply border-b-gray-100;\n\n    .tabbar-items {\n      margin-bottom: -4px;\n      justify-content: flex-start;\n    }\n\n    .tabbar-item {\n      @apply border-b-gray-100 px-5 py-1;\n\n      border-bottom-width: 2px;\n\n      &.tabbar-item-active {\n        @apply border-b-secondary text-secondary;\n      }\n    }\n  }\n\n  &.tabbar-pills {\n    .tabbar-items {\n      @apply gap-1;\n      justify-content: flex-start;\n    }\n\n    .tabbar-item {\n      @apply rounded-base px-6 py-1 opacity-70;\n\n      &.tabbar-item-active {\n        @apply bg-gray-100 opacity-100;\n      }\n\n      &:hover {\n        @apply bg-gray-100;\n      }\n    }\n  }\n\n  &.tabbar-outline {\n    @apply rounded-base bg-gray-100 px-1 py-0.5;\n\n    .tabbar-items {\n      @apply justify-start gap-1;\n    }\n\n    .tabbar-item {\n      @apply rounded-sm px-6 py-1 opacity-70;\n\n      &.tabbar-item-active {\n        @apply bg-white opacity-100 shadow-sm;\n      }\n\n      &:hover {\n        @apply bg-white;\n      }\n    }\n  }\n\n  &.tabbar-direction-row {\n    .tabbar-items {\n      @apply flex-row;\n    }\n  }\n\n  &.tabbar-direction-column {\n    .tabbar-items {\n      @apply flex-col;\n    }\n\n    .tabbar-item {\n      width: 100%;\n    }\n\n    &.tabbar-default {\n      border-bottom-width: 0;\n      @apply border-b-0;\n      border-right-width: 2px;\n      @apply border-r-gray-100;\n\n      .tabbar-items {\n        margin-bottom: 0;\n        margin-right: -2px;\n      }\n\n      .tabbar-item {\n        border-bottom-width: 0;\n        border-right-width: 2px;\n\n        &.tabbar-item-active {\n          @apply border-r-secondary;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/Tabs.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VTabItem, VTabs } from \".\";\n\nconst meta: Meta<typeof VTabs> = {\n  title: \"Tabs\",\n  component: VTabs,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VTabs, VTabItem },\n    setup() {\n      return {\n        args,\n      };\n    },\n    template: `\n      <div class=\"p-3\">\n        <VTabs v-model:activeId=\"args.activeId\" type=\"pills\">\n          <VTabItem id=\"johnniang\" label=\"JohnNiang\">\n            This is JohnNiang's Item\n          </VTabItem>\n          <VTabItem id=\"ryanwang\" label=\"Ryan Wang\">\n            This is Ryan Wang's Item\n          </VTabItem>\n          <VTabItem id=\"guqing\" label=\"guqing\">\n            This is guqing's Item\n          </VTabItem>\n        </VTabs>\n      </div>\n    `,\n  }),\n  argTypes: {\n    type: {\n      control: { type: \"select\" },\n      options: [\"default\", \"pills\", \"outline\"],\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VTabs>;\n\nexport const Default: Story = {\n  args: {\n    activeId: \"ryanwang\",\n    type: \"default\",\n  },\n};\n\nexport const Pills: Story = {\n  args: {\n    activeId: \"ryanwang\",\n    type: \"pills\",\n  },\n};\n\nexport const Outline: Story = {\n  args: {\n    activeId: \"ryanwang\",\n    type: \"outline\",\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/Tabs.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ComputedRef } from \"vue\";\nimport { computed, provide, useSlots } from \"vue\";\nimport { VTabbar } from \"./index\";\nimport type { TabsDirection, TabsType } from \"./types\";\n\nconst props = withDefaults(\n  defineProps<{\n    activeId?: number | string;\n    type?: TabsType;\n    direction?: TabsDirection;\n    idKey?: string;\n    labelKey?: string;\n  }>(),\n  {\n    activeId: undefined,\n    type: \"default\",\n    direction: \"row\",\n    idKey: \"id\",\n    labelKey: \"label\",\n  }\n);\n\nprovide<ComputedRef<string | number | undefined>>(\n  \"activeId\",\n  computed(() => props.activeId)\n);\n\nconst emit = defineEmits<{\n  (event: \"update:activeId\", value: number | string): void;\n  (event: \"change\", value: number | string): void;\n}>();\n\nconst slots = useSlots();\n\nconst tabItems = computed(() => {\n  return slots\n    .default?.()\n    .map(({ props: slotProps }) => {\n      return {\n        id: slotProps?.[props.idKey],\n        label: slotProps?.[props.labelKey],\n      };\n    })\n    .filter((item) => item.id !== undefined && item.label !== undefined);\n});\n\nconst classes = computed(() => {\n  return [`tabs-direction-${props.direction}`];\n});\n\nconst handleChange = (id: string | number) => {\n  emit(\"update:activeId\", id);\n  emit(\"change\", id);\n};\n</script>\n<template>\n  <div :class=\"classes\" class=\"tabs-wrapper\">\n    <div class=\"tabs-bar-wrapper\">\n      <VTabbar\n        :active-id=\"activeId\"\n        :direction=\"direction\"\n        :items=\"tabItems\"\n        :type=\"type\"\n        @change=\"handleChange\"\n      />\n    </div>\n    <div class=\"tabs-items-wrapper\">\n      <slot />\n    </div>\n  </div>\n</template>\n<style lang=\"scss\">\n.tabs-wrapper {\n  @apply flex;\n\n  &.tabs-direction-row {\n    @apply flex-col;\n\n    .tabs-items-wrapper {\n      @apply mt-2;\n    }\n  }\n\n  &.tabs-direction-column {\n    .tabs-items-wrapper {\n      @apply ml-2;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/__tests__/TabItem.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { VTabItem } from \"../index\";\n\ndescribe(\"TabItem\", () => {\n  it(\"should render\", () => {\n    expect(VTabItem).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/__tests__/Tabbar.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { VTabbar } from \"../index\";\n\ndescribe(\"Tabbar\", () => {\n  it(\"should render\", () => {\n    expect(VTabbar).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/__tests__/Tabs.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { VTabs } from \"../index\";\n\ndescribe(\"Tabs\", () => {\n  it(\"should render\", () => {\n    expect(VTabs).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/index.ts",
    "content": "export { default as VTabbar } from \"./Tabbar.vue\";\nexport { default as VTabItem } from \"./TabItem.vue\";\nexport { default as VTabs } from \"./Tabs.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/tabs/types.ts",
    "content": "export type TabsType = \"default\" | \"pills\" | \"outline\";\nexport type TabsDirection = \"row\" | \"column\";\nexport type TabsArrowShow = {\n  left: boolean;\n  right: boolean;\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/tag/Tag.stories.ts",
    "content": "import type { Meta, StoryObj } from \"@storybook/vue3-vite\";\nimport { VTag } from \".\";\n\nconst meta: Meta<typeof VTag> = {\n  title: \"Tag\",\n  component: VTag,\n  tags: [\"autodocs\"],\n  render: (args) => ({\n    components: { VTag },\n    setup() {\n      return { args };\n    },\n    template: `<VTag v-bind=\"args\">${args.default}<template v-if=\"${\n      \"leftIcon\" in args\n    }\" #leftIcon>${args.leftIcon}</template></VTag>`,\n  }),\n  argTypes: {\n    default: {\n      control: { type: \"text\" },\n    },\n    theme: {\n      control: { type: \"select\" },\n      options: [\"default\", \"primary\", \"secondary\", \"danger\"],\n    },\n  },\n  args: {\n    default: \"Tag\",\n    theme: \"default\",\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof VTag>;\n\nexport const Default: Story = {\n  args: {\n    theme: \"default\",\n  },\n};\n\nexport const Primary: Story = {\n  args: {\n    theme: \"primary\",\n  },\n};\n\nexport const Secondary: Story = {\n  args: {\n    theme: \"secondary\",\n  },\n};\n\nexport const Danger: Story = {\n  args: {\n    theme: \"danger\",\n  },\n};\n\nexport const Icon: Story = {\n  args: {\n    leftIcon: `<IconSettings />`,\n  },\n};\n"
  },
  {
    "path": "ui/packages/components/src/components/tag/Tag.story.vue",
    "content": "<template>\n  <Story title=\"Tag\">\n    <Variant title=\"Theme\">\n      <template #default>\n        <VSpace>\n          <VTag theme=\"default\">Halo</VTag>\n          <VTag theme=\"primary\">Halo</VTag>\n          <VTag theme=\"secondary\">Halo</VTag>\n          <VTag theme=\"danger\">Halo</VTag>\n        </VSpace>\n      </template>\n    </Variant>\n    <Variant title=\"Icon\">\n      <template #default>\n        <VSpace>\n          <VTag rounded theme=\"default\">\n            <template #leftIcon>\n              <IconSettings />\n            </template>\n            Halo\n          </VTag>\n          <VTag rounded theme=\"primary\">\n            <template #leftIcon>\n              <IconSettings />\n            </template>\n            Halo\n          </VTag>\n          <VTag theme=\"secondary\">\n            <template #leftIcon>\n              <IconSettings />\n            </template>\n            Halo\n          </VTag>\n          <VTag theme=\"danger\">\n            <template #leftIcon>\n              <IconSettings />\n            </template>\n            Halo\n          </VTag>\n        </VSpace>\n      </template>\n    </Variant>\n  </Story>\n</template>\n<script lang=\"ts\" setup>\nimport { VSpace } from \"../../components/space\";\nimport { VTag } from \"../../components/tag\";\nimport { IconSettings } from \"../../icons/icons\";\n</script>\n"
  },
  {
    "path": "ui/packages/components/src/components/tag/Tag.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { CSSProperties } from \"vue\";\nimport { computed } from \"vue\";\nimport type { TagTheme } from \"./types\";\n\nconst props = withDefaults(\n  defineProps<{\n    theme?: TagTheme;\n    rounded?: boolean;\n    styles?: CSSProperties;\n  }>(),\n  {\n    theme: \"default\",\n    rounded: false,\n    styles: () => {\n      return {};\n    },\n  }\n);\n\nconst classes = computed(() => {\n  return [`tag-${props.theme}`, { \"tag-rounded\": props.rounded }];\n});\n</script>\n<template>\n  <div :class=\"classes\" :style=\"styles\" class=\"tag-wrapper\">\n    <div v-if=\"$slots.leftIcon\" class=\"tag-left-icon\">\n      <slot name=\"leftIcon\" />\n    </div>\n    <span class=\"tag-content\">\n      <slot />\n    </span>\n    <div v-if=\"$slots.rightIcon\" class=\"tag-right-icon\">\n      <slot name=\"rightIcon\" />\n    </div>\n  </div>\n</template>\n<style lang=\"scss\">\n.tag-wrapper {\n  @apply box-border inline-flex h-5 w-auto flex-shrink-0 cursor-pointer flex-wrap items-center justify-center rounded-base border border-solid px-1 text-center align-middle text-xs;\n\n  &.tag-default {\n    border: 1px solid #d9d9d9;\n  }\n\n  &.tag-primary {\n    @apply border-primary bg-primary text-white;\n  }\n\n  &.tag-secondary {\n    @apply border-secondary bg-secondary text-white;\n  }\n\n  &.tag-danger {\n    background: #d71d1d;\n    border: 1px solid #d71d1d;\n    @apply text-white;\n  }\n\n  &.tag-rounded {\n    @apply rounded-full;\n  }\n\n  .tag-content {\n    @apply px-1;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/tag/__tests__/Tag.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { VTag } from \"../index\";\n\ndescribe(\"Tag\", () => {\n  it(\"should render\", () => {\n    expect(VTag).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "ui/packages/components/src/components/tag/index.ts",
    "content": "export { default as VTag } from \"./Tag.vue\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/tag/types.ts",
    "content": "export type TagTheme = \"default\" | \"primary\" | \"secondary\" | \"danger\";\n"
  },
  {
    "path": "ui/packages/components/src/components/toast/Toast.story.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton } from \"../button\";\nimport { VSpace } from \"../space\";\nimport { Toast } from \"./toast-manager\";\nimport type { ToastType } from \"./types\";\n\nfunction handleShowToast(type: ToastType, content: string) {\n  Toast[type](content);\n}\n</script>\n<template>\n  <Story title=\"Toast\">\n    <VSpace>\n      <VButton type=\"primary\" @click=\"handleShowToast('success', 'Hello Halo')\">\n        成功\n      </VButton>\n      <VButton\n        type=\"secondary\"\n        @click=\"handleShowToast('info', '这是一个提示')\"\n      >\n        提示\n      </VButton>\n\n      <VButton\n        type=\"default\"\n        @click=\"handleShowToast('warning', '这是一个警告提示')\"\n      >\n        警告\n      </VButton>\n\n      <VButton\n        type=\"danger\"\n        @click=\"handleShowToast('error', '这是一个错误提示')\"\n      >\n        错误\n      </VButton>\n    </VSpace>\n  </Story>\n</template>\n"
  },
  {
    "path": "ui/packages/components/src/components/toast/Toast.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  markRaw,\n  onMounted,\n  ref,\n  watchEffect,\n  type Component,\n  type Raw,\n} from \"vue\";\nimport {\n  IconCheckboxCircle,\n  IconClose,\n  IconErrorWarning,\n  IconForbidLine,\n  IconInformation,\n} from \"@/icons/icons\";\nimport type { ToastType } from \"./types\";\n\nconst props = withDefaults(\n  defineProps<{\n    type?: ToastType;\n    content?: string;\n    duration?: number;\n    closable?: boolean;\n    frozenOnHover?: boolean;\n    count?: 0;\n    onClose?: () => void;\n  }>(),\n  {\n    type: \"success\",\n    content: \"\",\n    duration: 3000,\n    closable: true,\n    frozenOnHover: true,\n    count: 0,\n    onClose: undefined,\n  }\n);\n\nconst timer = ref();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst icons: Record<ToastType, { icon: Raw<Component>; color: string }> = {\n  success: {\n    icon: markRaw(IconCheckboxCircle),\n    color: \"text-green-500\",\n  },\n  info: {\n    icon: markRaw(IconInformation),\n    color: \"text-sky-500\",\n  },\n  warning: {\n    icon: markRaw(IconErrorWarning),\n    color: \"text-orange-500\",\n  },\n  error: {\n    icon: markRaw(IconForbidLine),\n    color: \"text-red-500\",\n  },\n};\n\nconst createTimer = () => {\n  if (props.duration < 0) return;\n  timer.value = setTimeout(() => {\n    close();\n  }, props.duration);\n};\n\nconst clearTimer = () => {\n  clearTimeout(timer.value);\n};\n\nconst close = () => {\n  emit(\"close\");\n};\n\nconst handleMouseEnter = () => {\n  if (!props.frozenOnHover) {\n    return;\n  }\n  clearTimer();\n};\n\nconst handleMouseLeave = () => {\n  if (!props.frozenOnHover) {\n    return;\n  }\n  createTimer();\n};\n\nwatchEffect(() => {\n  if (props.count > 0) {\n    clearTimer();\n    createTimer();\n  }\n});\n\nonMounted(createTimer);\n\ndefineExpose({ close });\n</script>\n\n<template>\n  <transition\n    appear\n    enter-active-class=\"transform ease-out duration-300 transition\"\n    enter-from-class=\"translate-x-0 -translate-y-2\"\n    enter-to-class=\"translate-y-0\"\n    leave-active-class=\"transition ease-in duration-100\"\n    leave-from-class=\"opacity-100\"\n    leave-to-class=\"opacity-0\"\n  >\n    <div\n      class=\"toast-wrapper\"\n      @mouseenter=\"handleMouseEnter\"\n      @mouseleave=\"handleMouseLeave\"\n    >\n      <div class=\"toast-body\">\n        <div class=\"toast-icon\">\n          <component :is=\"icons[type].icon\" :class=\"icons[type].color\" />\n        </div>\n        <div class=\"toast-content\">\n          <div class=\"toast-description\">\n            <slot>{{ content }}</slot>\n          </div>\n        </div>\n        <div v-if=\"closable\" class=\"toast-control\">\n          <IconClose class=\"\" @click=\"close\" />\n        </div>\n      </div>\n      <div v-if=\"count\" class=\"toast-count\">\n        <span>{{ count }}</span>\n      </div>\n    </div>\n  </transition>\n</template>\n<style lang=\"scss\">\n.toast-container {\n  @apply pointer-events-none fixed left-0 top-0 z-[9999] box-border flex w-full flex-col items-center justify-center gap-3 p-4 transition-all;\n\n  .toast-wrapper {\n    @apply pointer-events-auto relative inline-block max-w-xs;\n  }\n\n  .toast-body {\n    @apply flex cursor-pointer items-center gap-2 overflow-hidden break-all rounded bg-white px-2.5 py-2 shadow transition-all hover:shadow-md;\n  }\n\n  .toast-content {\n    @apply flex flex-col gap-1 text-sm;\n  }\n\n  .toast-description {\n    @apply text-gray-800;\n  }\n\n  .toast-control {\n    @apply cursor-pointer rounded-full p-0.5 text-gray-600 transition-all hover:bg-gray-100 hover:text-gray-900;\n  }\n\n  .toast-count {\n    @apply absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500;\n\n    span {\n      @apply text-[0.7rem] text-white;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/components/src/components/toast/index.ts",
    "content": "export { Toast } from \"./toast-manager\";\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/components/src/components/toast/toast-manager.ts",
    "content": "import { createVNode, render, type Component, type VNode } from \"vue\";\nimport ToastComponent from \"./Toast.vue\";\nimport type { ToastProps } from \"./types\";\n\nexport type ToastApiProps = Omit<ToastProps, \"type\" | \"content\">;\n\nexport interface ToastInstance {\n  id: string;\n  vnode: VNode;\n}\n\nexport type ToastApi = (\n  content: string,\n  props?: ToastApiProps\n) => ToastInstance;\n\nexport interface ToastEntry {\n  (props: ToastProps): ToastInstance;\n  info: ToastApi;\n  success: ToastApi;\n  error: ToastApi;\n  warning: ToastApi;\n}\n\nlet index = 0;\n\nconst instances: ToastInstance[] = [];\n\nconst defaultProps: ToastProps = {\n  frozenOnHover: true,\n  duration: 3000,\n  count: 0,\n};\n\nconst toast: ToastEntry = (userProps: ToastProps) => {\n  const id = \"toast-\" + index++;\n\n  const props = {\n    ...defaultProps,\n    ...userProps,\n    id,\n  };\n\n  let container = document.body.querySelector(\".toast-container\");\n  if (!container) {\n    container = document.createElement(\"div\");\n    container.className = \"toast-container\";\n    document.body.appendChild(container);\n  }\n\n  // Grouping toasts\n  if (instances.length > 0) {\n    const instance = instances.find((item) => {\n      const { vnode } = item;\n      if (vnode?.props) {\n        return (\n          vnode.props.content === props.content &&\n          vnode.props.type === props.type\n        );\n      }\n      return undefined;\n    });\n\n    if (instance?.vnode.component?.props) {\n      (instance.vnode.component.props.count as number) += 1;\n      index = instances.length - 1;\n      return instance;\n    }\n  }\n\n  const { vnode, container: hostContainer } = createVNodeComponent(\n    ToastComponent,\n    props\n  );\n\n  if (hostContainer.firstElementChild) {\n    container.appendChild(hostContainer.firstElementChild);\n  }\n\n  if (vnode?.props) {\n    // close emit\n    vnode.props.onClose = () => {\n      removeInstance(id);\n      render(null, hostContainer);\n    };\n  }\n\n  const instance = {\n    id,\n    vnode,\n    close: () => {\n      vnode?.component?.exposed?.close();\n    },\n  };\n\n  instances.push(instance);\n  return instance;\n};\n\nfunction createVNodeComponent(\n  component: Component,\n  props: Record<string, unknown>\n) {\n  const vnode = createVNode(component, props);\n  const container = document.createElement(\"div\");\n  render(vnode, container);\n  return { vnode, container };\n}\n\nfunction removeInstance(id: string) {\n  const index = instances.findIndex((instance) => instance.id === id);\n  if (index >= 0) {\n    instances.splice(index, 1);\n\n    if (instances.length === 0) {\n      const container = document.body.querySelector(\".toast-container\");\n      container?.remove();\n    }\n  }\n}\n\ntoast.success = (content: string, props?: ToastApiProps) =>\n  toast({ ...props, type: \"success\", content });\ntoast.info = (content: string, props?: ToastApiProps) =>\n  toast({ ...props, type: \"info\", content });\ntoast.warning = (content: string, props?: ToastApiProps) =>\n  toast({ ...props, type: \"warning\", content });\ntoast.error = (content: string, props?: ToastApiProps) =>\n  toast({ ...props, type: \"error\", content });\n\nexport { toast as Toast };\n"
  },
  {
    "path": "ui/packages/components/src/components/toast/types.ts",
    "content": "export type ToastType = \"success\" | \"info\" | \"warning\" | \"error\";\n\nexport interface ToastProps {\n  type?: ToastType;\n  content?: string;\n  duration?: number;\n  closable?: boolean;\n  frozenOnHover?: boolean;\n  count?: 0;\n  onClose?: () => void;\n}\n"
  },
  {
    "path": "ui/packages/components/src/components/tooltip/index.ts",
    "content": "export { Tooltip as VTooltipComponent, vTooltip } from \"floating-vue\";\nimport \"./style.css\";\n"
  },
  {
    "path": "ui/packages/components/src/components/tooltip/style.css",
    "content": ".v-popper--theme-tooltip {\n  pointer-events: none;\n}\n"
  },
  {
    "path": "ui/packages/components/src/components.ts",
    "content": "export * from \"./components/alert\";\nexport * from \"./components/avatar\";\nexport * from \"./components/button\";\nexport * from \"./components/card\";\nexport * from \"./components/description\";\nexport * from \"./components/dialog\";\nexport * from \"./components/dropdown\";\nexport * from \"./components/empty\";\nexport * from \"./components/entity\";\nexport * from \"./components/header\";\nexport * from \"./components/loading\";\nexport * from \"./components/menu\";\nexport * from \"./components/modal\";\nexport * from \"./components/pagination\";\nexport * from \"./components/space\";\nexport * from \"./components/status\";\nexport * from \"./components/switch\";\nexport * from \"./components/tabs\";\nexport * from \"./components/tag\";\nexport * from \"./components/toast\";\nexport * from \"./components/tooltip\";\n"
  },
  {
    "path": "ui/packages/components/src/icons/icons.ts",
    "content": "import IconAccountCircleLine from \"~icons/ri/account-circle-line\";\nimport IconAddCircle from \"~icons/ri/add-circle-line\";\nimport IconArrowDownCircleLine from \"~icons/ri/arrow-down-circle-line\";\nimport IconArrowDown from \"~icons/ri/arrow-down-s-fill\";\nimport IconArrowDownLine from \"~icons/ri/arrow-down-s-line\";\nimport IconArrowLeftRightLine from \"~icons/ri/arrow-left-right-line\";\nimport IconArrowLeft from \"~icons/ri/arrow-left-s-line\";\nimport IconArrowRight from \"~icons/ri/arrow-right-s-line\";\nimport IconArrowUpCircleLine from \"~icons/ri/arrow-up-circle-line\";\nimport IconArrowUpDownLine from \"~icons/ri/arrow-up-down-line\";\nimport IconArrowUpLine from \"~icons/ri/arrow-up-s-line\";\nimport IconBookRead from \"~icons/ri/book-read-line\";\nimport IconCalendar from \"~icons/ri/calendar-line\";\nimport IconCharacterRecognition from \"~icons/ri/character-recognition-line\";\nimport IconCheckboxFill from \"~icons/ri/checkbox-circle-fill\";\nimport IconCheckboxCircle from \"~icons/ri/checkbox-circle-line\";\nimport IconClipboardLine from \"~icons/ri/clipboard-line\";\nimport IconCloseCircle from \"~icons/ri/close-circle-line\";\nimport IconClose from \"~icons/ri/close-line\";\nimport IconCodeBoxLine from \"~icons/ri/code-box-line\";\nimport IconComputer from \"~icons/ri/computer-line\";\nimport IconDashboard from \"~icons/ri/dashboard-3-line\";\nimport IconDatabase2Line from \"~icons/ri/database-2-line\";\nimport IconDeleteBin from \"~icons/ri/delete-bin-2-line\";\nimport IconMotionLine from \"~icons/ri/emotion-line\";\nimport IconErrorWarning from \"~icons/ri/error-warning-line\";\nimport IconExchange from \"~icons/ri/exchange-line\";\nimport IconExternalLinkLine from \"~icons/ri/external-link-line\";\nimport IconEye from \"~icons/ri/eye-line\";\nimport IconEyeOff from \"~icons/ri/eye-off-line\";\nimport IconFolder from \"~icons/ri/folder-2-line\";\nimport IconForbidLine from \"~icons/ri/forbid-line\";\nimport IconGitBranch from \"~icons/ri/git-branch-line\";\nimport IconGitHub from \"~icons/ri/github-fill\";\nimport IconGrid from \"~icons/ri/grid-line\";\nimport IconHistoryLine from \"~icons/ri/history-line\";\nimport IconImageAddLine from \"~icons/ri/image-add-line\";\nimport IconInformation from \"~icons/ri/information-line\";\nimport IconLink from \"~icons/ri/link\";\nimport IconListSettings from \"~icons/ri/list-settings-line\";\nimport IconList from \"~icons/ri/list-unordered\";\nimport IconLockPasswordLine from \"~icons/ri/lock-password-line\";\nimport IconLogoutCircleRLine from \"~icons/ri/logout-circle-r-line\";\nimport IconMagic from \"~icons/ri/magic-line\";\nimport IconMessage from \"~icons/ri/message-3-line\";\nimport IconMore from \"~icons/ri/more-line\";\nimport IconNotificationBadgeLine from \"~icons/ri/notification-badge-line\";\nimport IconPages from \"~icons/ri/pages-line\";\nimport IconPalette from \"~icons/ri/palette-line\";\nimport IconRiPencilFill from \"~icons/ri/pencil-fill\";\nimport IconPlug from \"~icons/ri/plug-2-line\";\nimport IconRefreshLine from \"~icons/ri/refresh-line\";\nimport IconReplyLine from \"~icons/ri/reply-line\";\nimport IconRocketLine from \"~icons/ri/rocket-line\";\nimport IconSave from \"~icons/ri/save-line\";\nimport IconSearch from \"~icons/ri/search-2-line\";\nimport IconSendPlaneFill from \"~icons/ri/send-plane-fill\";\nimport IconServerLine from \"~icons/ri/server-line\";\nimport IconSettings3Line from \"~icons/ri/settings-3-line\";\nimport IconSettings from \"~icons/ri/settings-4-line\";\nimport IconShieldUser from \"~icons/ri/shield-user-line\";\nimport IconPhone from \"~icons/ri/smartphone-line\";\nimport IconStopCircle from \"~icons/ri/stop-circle-line\";\nimport IconTablet from \"~icons/ri/tablet-line\";\nimport IconTeam from \"~icons/ri/team-fill\";\nimport IconTerminalBoxLine from \"~icons/ri/terminal-box-line\";\nimport IconThumbUpLine from \"~icons/ri/thumb-up-line\";\nimport IconTimerLine from \"~icons/ri/timer-line\";\nimport IconToolsFill from \"~icons/ri/tools-fill\";\nimport IconRiUpload2Fill from \"~icons/ri/upload-2-fill\";\nimport IconUpload from \"~icons/ri/upload-cloud-2-line\";\nimport IconUserFollow from \"~icons/ri/user-follow-line\";\nimport IconUserLine from \"~icons/ri/user-line\";\nimport IconUserSettings from \"~icons/ri/user-settings-line\";\nimport IconWindowLine from \"~icons/ri/window-line\";\nimport IconZoomInLine from \"~icons/ri/zoom-in-line\";\nimport IconZoomOutLine from \"~icons/ri/zoom-out-line\";\n\nexport {\n  IconAccountCircleLine,\n  IconAddCircle,\n  IconArrowDown,\n  IconArrowDownCircleLine,\n  IconArrowDownLine,\n  IconArrowLeft,\n  IconArrowLeftRightLine,\n  IconArrowRight,\n  IconArrowUpCircleLine,\n  IconArrowUpDownLine,\n  IconArrowUpLine,\n  IconBookRead,\n  IconCalendar,\n  IconCharacterRecognition,\n  IconCheckboxCircle,\n  IconCheckboxFill,\n  IconClipboardLine,\n  IconClose,\n  IconCloseCircle,\n  IconCodeBoxLine,\n  IconComputer,\n  IconDashboard,\n  IconDatabase2Line,\n  IconDeleteBin,\n  IconErrorWarning,\n  IconExchange,\n  IconExternalLinkLine,\n  IconEye,\n  IconEyeOff,\n  IconFolder,\n  IconForbidLine,\n  IconGitBranch,\n  IconGitHub,\n  IconGrid,\n  IconHistoryLine,\n  IconImageAddLine,\n  IconInformation,\n  IconLink,\n  IconList,\n  IconListSettings,\n  IconLockPasswordLine,\n  IconLogoutCircleRLine,\n  IconMagic,\n  IconMessage,\n  IconMore,\n  IconMotionLine,\n  IconNotificationBadgeLine,\n  IconPages,\n  IconPalette,\n  IconPhone,\n  IconPlug,\n  IconRefreshLine,\n  IconReplyLine,\n  IconRiPencilFill,\n  IconRiUpload2Fill,\n  IconRocketLine,\n  IconSave,\n  IconSearch,\n  IconSendPlaneFill,\n  IconServerLine,\n  IconSettings,\n  IconSettings3Line,\n  IconShieldUser,\n  IconStopCircle,\n  IconTablet,\n  IconTeam,\n  IconTerminalBoxLine,\n  IconThumbUpLine,\n  IconTimerLine,\n  IconToolsFill,\n  IconUpload,\n  IconUserFollow,\n  IconUserLine,\n  IconUserSettings,\n  IconWindowLine,\n  IconZoomInLine,\n  IconZoomOutLine,\n};\n"
  },
  {
    "path": "ui/packages/components/src/index.ts",
    "content": "import \"overlayscrollbars/overlayscrollbars.css\";\nimport \"./styles/tailwind.css\";\n\nexport * from \"./components\";\nexport * from \"./icons/icons\";\n"
  },
  {
    "path": "ui/packages/components/src/stories/Introduction.mdx",
    "content": "import { Meta } from \"@storybook/addon-docs/blocks\";\nimport Code from \"./assets/code-brackets.svg\";\nimport Colors from \"./assets/colors.svg\";\nimport Comments from \"./assets/comments.svg\";\nimport Direction from \"./assets/direction.svg\";\nimport Flow from \"./assets/flow.svg\";\nimport Plugin from \"./assets/plugin.svg\";\nimport Repo from \"./assets/repo.svg\";\nimport StackAlt from \"./assets/stackalt.svg\";\n\n<Meta title=\"Introduction\" />\n\n<style>\n  {`\n    .subheading {\n      --mediumdark: '#999999';\n      font-weight: 700;\n      font-size: 13px;\n      color: #999;\n      letter-spacing: 6px;\n      line-height: 24px;\n      text-transform: uppercase;\n      margin-bottom: 12px;\n      margin-top: 40px;\n    }\n\n    .link-list {\n      display: grid;\n      grid-template-columns: 1fr;\n      grid-template-rows: 1fr 1fr;\n      row-gap: 10px;\n    }\n\n    @media (min-width: 620px) {\n      .link-list {\n        row-gap: 20px;\n        column-gap: 20px;\n        grid-template-columns: 1fr 1fr;\n      }\n    }\n\n    @media all and (-ms-high-contrast:none) {\n    .link-list {\n        display: -ms-grid;\n        -ms-grid-columns: 1fr 1fr;\n        -ms-grid-rows: 1fr 1fr;\n      }\n    }\n\n    .link-item {\n      display: block;\n      padding: 20px;\n      border: 1px solid #00000010;\n      border-radius: 5px;\n      transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out;\n      color: #333333;\n      display: flex;\n      align-items: flex-start;\n    }\n\n    .link-item:hover {\n      border-color: #1EA7FD50;\n      transform: translate3d(0, -3px, 0);\n      box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;\n    }\n\n    .link-item:active {\n      border-color: #1EA7FD;\n      transform: translate3d(0, 0, 0);\n    }\n\n    .link-item strong {\n      font-weight: 700;\n      display: block;\n      margin-bottom: 2px;\n    }\n\n    .link-item img {\n      height: 40px;\n      width: 40px;\n      margin-right: 15px;\n      flex: none;\n    }\n\n    .link-item span,\n    .link-item p {\n      margin: 0;\n      font-size: 14px;\n      line-height: 20px;\n    }\n\n    .tip {\n      display: inline-block;\n      border-radius: 1em;\n      font-size: 11px;\n      line-height: 12px;\n      font-weight: 700;\n      background: #E7FDD8;\n      color: #66BF3C;\n      padding: 4px 12px;\n      margin-right: 10px;\n      vertical-align: top;\n    }\n\n    .tip-wrapper {\n      font-size: 13px;\n      line-height: 20px;\n      margin-top: 40px;\n      margin-bottom: 40px;\n    }\n\n    .tip-wrapper code {\n      font-size: 12px;\n      display: inline-block;\n    }\n  `}\n</style>\n\n# Welcome to Storybook\n\nStorybook helps you build UI components in isolation from your app's business logic, data, and context.\nThat makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.\n\nBrowse example stories now by navigating to them in the sidebar.\nView their code in the `stories` directory to learn how they work.\nWe recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.\n\n<div className=\"subheading\">Configure</div>\n\n<div className=\"link-list\">\n  <a\n    className=\"link-item\"\n    href=\"https://storybook.js.org/docs/react/addons/addon-types\"\n    target=\"_blank\"\n  >\n    <img src={Plugin} alt=\"plugin\" />\n    <span>\n      <strong>Presets for popular tools</strong>\n      Easy setup for TypeScript, SCSS and more.\n    </span>\n  </a>\n  <a\n    className=\"link-item\"\n    href=\"https://storybook.js.org/docs/react/configure/webpack\"\n    target=\"_blank\"\n  >\n    <img src={StackAlt} alt=\"Build\" />\n    <span>\n      <strong>Build configuration</strong>\n      How to customize webpack and Babel\n    </span>\n  </a>\n  <a\n    className=\"link-item\"\n    href=\"https://storybook.js.org/docs/react/configure/styling-and-css\"\n    target=\"_blank\"\n  >\n    <img src={Colors} alt=\"colors\" />\n    <span>\n      <strong>Styling</strong>\n      How to load and configure CSS libraries\n    </span>\n  </a>\n  <a\n    className=\"link-item\"\n    href=\"https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack\"\n    target=\"_blank\"\n  >\n    <img src={Flow} alt=\"flow\" />\n    <span>\n      <strong>Data</strong>\n      Providers and mocking for data libraries\n    </span>\n  </a>\n</div>\n\n<div className=\"subheading\">Learn</div>\n\n<div className=\"link-list\">\n  <a className=\"link-item\" href=\"https://storybook.js.org/docs\" target=\"_blank\">\n    <img src={Repo} alt=\"repo\" />\n    <span>\n      <strong>Storybook documentation</strong>\n      Configure, customize, and extend\n    </span>\n  </a>\n  <a\n    className=\"link-item\"\n    href=\"https://storybook.js.org/tutorials/\"\n    target=\"_blank\"\n  >\n    <img src={Direction} alt=\"direction\" />\n    <span>\n      <strong>In-depth guides</strong>\n      Best practices from leading teams\n    </span>\n  </a>\n  <a\n    className=\"link-item\"\n    href=\"https://github.com/storybookjs/storybook\"\n    target=\"_blank\"\n  >\n    <img src={Code} alt=\"code\" />\n    <span>\n      <strong>GitHub project</strong>\n      View the source and add issues\n    </span>\n  </a>\n  <a className=\"link-item\" href=\"https://discord.gg/storybook\" target=\"_blank\">\n    <img src={Comments} alt=\"comments\" />\n    <span>\n      <strong>Discord chat</strong>\n      Chat with maintainers and the community\n    </span>\n  </a>\n</div>\n\n<div className=\"tip-wrapper\">\n  <span className=\"tip\">Tip</span>Edit the Markdown in{\" \"}\n  <code>stories/Introduction.stories.mdx</code>\n</div>\n"
  },
  {
    "path": "ui/packages/components/src/styles/tailwind.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "ui/packages/components/tailwind.config.ts",
    "content": "import tailwindConfig from \"../../tailwind.config\";\n\nexport default tailwindConfig;\n"
  },
  {
    "path": "ui/packages/components/tsconfig.app.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"exclude\": [\"src/**/__tests__/*\", \"node_modules\", \"src/**/*.stories.ts\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"types\": [\"unplugin-icons/types/vue\"]\n  }\n}\n"
  },
  {
    "path": "ui/packages/components/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.vitest.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "ui/packages/components/tsconfig.node.json",
    "content": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"include\": [\n    \"vite.config.*\",\n    \"vitest.config.*\",\n    \"tailwind.config.ts\",\n    \"../../tailwind.config.ts\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "ui/packages/components/tsconfig.vitest.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"exclude\": [],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"lib\": [],\n    \"types\": [\"node\", \"jsdom\"]\n  }\n}\n"
  },
  {
    "path": "ui/packages/components/vite.config.ts",
    "content": "import path from \"node:path\";\nimport { fileURLToPath, URL } from \"node:url\";\nimport Vue from \"@vitejs/plugin-vue\";\nimport VueJsx from \"@vitejs/plugin-vue-jsx\";\nimport Icons from \"unplugin-icons/vite\";\nimport { defineConfig, type Plugin } from \"vite\";\nimport Dts from \"vite-plugin-dts\";\nimport { configDefaults } from \"vitest/config\";\n\nexport default defineConfig({\n  plugins: [\n    Vue(),\n    VueJsx(),\n    Icons({ compiler: \"vue3\" }),\n    Dts({\n      tsconfigPath: \"./tsconfig.app.json\",\n      entryRoot: \"./src\",\n      outDir: \"./dist\",\n      insertTypesEntry: true,\n    }) as Plugin,\n  ],\n  define: {\n    \"process.env.NODE_ENV\": '\"production\"',\n  },\n  resolve: {\n    alias: {\n      \"@\": fileURLToPath(new URL(\"./src\", import.meta.url)),\n    },\n  },\n  build: {\n    outDir: path.resolve(__dirname, \"dist\"),\n    lib: {\n      entry: path.resolve(__dirname, \"src/index.ts\"),\n      name: \"HaloComponents\",\n      formats: [\"es\", \"iife\"],\n      fileName: (format) => `index.${format}.js`,\n      cssFileName: \"style\",\n    },\n    rollupOptions: {\n      external: [\n        \"vue\",\n        \"vue-router\",\n        \"@vueuse/core\",\n        \"@vueuse/components\",\n        \"@vueuse/router\",\n      ],\n      output: {\n        globals: {\n          vue: \"Vue\",\n          \"vue-router\": \"VueRouter\",\n          \"@vueuse/core\": \"VueUse\",\n          \"@vueuse/components\": \"VueUse\",\n          \"@vueuse/router\": \"VueUse\",\n        },\n        exports: \"named\",\n      },\n    },\n    sourcemap: true,\n  },\n  test: {\n    environment: \"jsdom\",\n    exclude: [...configDefaults.exclude],\n    root: fileURLToPath(new URL(\"./\", import.meta.url)),\n  },\n});\n"
  },
  {
    "path": "ui/packages/console-shared/README.md",
    "content": "Please use `@halo-dev/ui-shared`, this package is only for compatibility.\n"
  },
  {
    "path": "ui/packages/console-shared/index.js",
    "content": "var HaloConsoleShared = (function (n) {\n  \"use strict\";\n  function e(i) {\n    return i;\n  }\n  return ((n.definePlugin = e), n);\n})({});\n"
  },
  {
    "path": "ui/packages/console-shared/package.json",
    "content": "{\n  \"name\": \"@halo-dev/console-shared\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"main\": \"index.js\"\n}\n"
  },
  {
    "path": "ui/packages/editor/README.md",
    "content": "# @halo-dev/richtext-editor\n\nThe default editor for Halo, built with [Tiptap](https://tiptap.dev/) + [ProseMirror](https://prosemirror.net/).\n\n> ⚠️ This package is not suitable for general editor use cases, as it is tightly coupled with Halo and uses some of Halo's built-in tools and shared dependencies.\n> However, you can use it in Halo plugins just like you would use it in Halo itself.\n\n## Installation\n\nIn Halo plugins:\n\n```bash\npnpm install @halo-dev/richtext-editor\n```\n\n## Usage\n\n```vue\n<script setup lang=\"ts\">\nimport {\n  ExtensionsKit,\n  RichTextEditor,\n  VueEditor,\n} from \"@halo-dev/richtext-editor\";\nimport { onMounted, ref, shallowRef } from \"vue\";\n\nconst editor = shallowRef<VueEditor>();\nconst content = ref(\"Hello World\");\n\nonMounted(() => {\n  editor.value = new VueEditor({\n    content: content.value,\n    extensions: [ExtensionsKit],\n    parseOptions: {\n      preserveWhitespace: true,\n    },\n    onUpdate: () => {\n      content.value = editor.value?.getHTML() || \"\";\n    },\n  });\n});\n</script>\n<template>\n  <RichTextEditor v-if=\"editor\" :editor=\"editor\" />\n</template>\n```\n\n## Requirements\n\n- Vue 3.5.x or higher\n- Halo plugin environment\n\n## Links\n\n- [Halo](https://github.com/halo-dev/halo)\n- [Documentation](https://docs.halo.run)\n- [Report Issues](https://github.com/halo-dev/halo/issues)\n\n## License\n\nGPL-3.0\n"
  },
  {
    "path": "ui/packages/editor/docs/extension.md",
    "content": "# 扩展说明\n\n本文档介绍如何对编辑器的功能进行扩展，包括但不限于扩展工具栏、悬浮工具栏、Slash Command、拖拽功能等。各扩展区域参考下图：\n\n![编辑器扩展说明](extension.png)\n\n目前支持的所有扩展类型 [ExtensionOptions](../packages/editor/src/types/index.ts) 如下所示：\n\n```ts\nexport interface ExtensionOptions {\n  // 顶部工具栏扩展\n  getToolbarItems?: ({\n    editor,\n  }: {\n    editor: Editor;\n  }) => ToolbarItemType | ToolbarItemType[];\n\n  // Slash Command 扩展\n  getCommandMenuItems?: () => CommandMenuItemType | CommandMenuItemType[];\n\n  // 悬浮菜单扩展\n  getBubbleMenu?: ({ editor }: { editor: Editor }) => NodeBubbleMenuType;\n\n  // 工具箱扩展\n  getToolboxItems?: ({\n    editor,\n  }: {\n    editor: Editor;\n  }) => ToolboxItemType | ToolboxItemType[];\n\n  // 拖拽菜单扩展\n  getDraggableMenuItems?: ({\n    editor,\n  }: {\n    editor: Editor;\n  }) => DragButtonType | DragButtonType[];\n}\n```\n\n> 对于 Tiptap 本身的扩展方式可以参考 <https://tiptap.dev/api/introduction>\n\n## 1. 顶部工具栏扩展\n\n编辑器顶部功能区域内容的扩展，通常用于增加用户常用操作，例如文本加粗、变更颜色等。\n\n在 <https://github.com/halo-sigs/richtext-editor/pull/16> 中，我们实现了对顶部工具栏的扩展，如果需要添加额外的功能，只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getToolbarItems` 函数即可，如：\n\n```ts\n{\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return []\n      },\n    };\n  },\n}\n```\n\n其中 `getToolbarItems` 即为对顶部工具栏的扩展。其返回类型为：\n\n```ts\n// 顶部工具栏扩展\ngetToolbarItems?: ({\n  editor,\n}: {\n  editor: Editor;\n}) => ToolbarItemType | ToolbarItemType[];\n\n// 工具栏\nexport interface ToolbarItemType {\n  priority: number;\n  component: Component;\n  props: {\n    editor: Editor;\n    isActive: boolean;\n    disabled?: boolean;\n    icon?: Component;\n    title?: string;\n    action?: () => void;\n  };\n  children?: ToolbarItemType[];\n}\n```\n\n如下为 [`Bold`](../packages/editor/src/extensions/bold/index.ts) 扩展中对于 `getToolbarItems` 的扩展示例：\n\n```ts\naddOptions() {\n  return {\n    ...this.parent?.(),\n    getToolbarItems({ editor }: { editor: Editor }) {\n      return {\n        priority: 40,\n        component: markRaw(ToolbarItem),\n        props: {\n          editor,\n          isActive: editor.isActive(\"bold\"),\n          icon: markRaw(MdiFormatBold),\n          title: i18n.global.t(\"editor.common.bold\"),\n          action: () => editor.chain().focus().toggleBold().run(),\n        },\n      };\n    },\n  };\n},\n```\n\n## 2. 工具箱扩展\n\n编辑器工具箱区域的扩展，可用于增加编辑器附属操作，例如插入表格，插入第三方组件等功能。\n\n在 <https://github.com/halo-sigs/richtext-editor/pull/27> 中，我们实现了对编辑器工具箱区域的扩展，如果需要添加额外的功能，只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getToolboxItems` 函数即可，如：\n\n```ts\n{\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return []\n      },\n    };\n  },\n}\n```\n\n其中 `getToolboxItems` 即为对工具箱的扩展。其返回类型为：\n\n```ts\n// 工具箱扩展\ngetToolboxItems?: ({\n  editor,\n}: {\n  editor: Editor;\n}) => ToolboxItemType | ToolboxItemType[];\n\nexport interface ToolboxItemType {\n  priority: number;\n  component: Component;\n  props: {\n    editor: Editor;\n    icon?: Component;\n    title?: string;\n    description?: string;\n    action?: () => void;\n  };\n}\n```\n\n如下为 [`Table`](../packages/editor/src/extensions/table/index.ts) 扩展中对于 `getToolboxItems` 工具箱的扩展示例：\n\n```ts\naddOptions() {\n  return {\n    ...this.parent?.(),\n    getToolboxItems({ editor }: { editor: Editor }) {\n      return {\n        priority: 15,\n        component: markRaw(ToolboxItem),\n        props: {\n          editor,\n          icon: markRaw(MdiTablePlus),\n          title: i18n.global.t(\"editor.menus.table.add\"),\n          action: () =>\n            editor\n              .chain()\n              .focus()\n              .insertTable({ rows: 3, cols: 3, withHeaderRow: true })\n              .run(),\n        },\n      };\n    },\n  }\n}\n```\n\n## 3. Slash Command 扩展\n\nSlash Command （斜杠命令）的扩展，可用于在当前行快捷执行功能操作，例如转换当前行为标题、在当前行添加代码块等功能。\n\n在 <https://github.com/halo-sigs/richtext-editor/pull/16> 中，我们实现了对 Slash Command 指令的扩展，如果需要添加额外的功能，只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getCommandMenuItems` 函数即可，如：\n\n```ts\n{\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getCommandMenuItems() {\n        return []\n      },\n    };\n  },\n}\n```\n\n其中 `getCommandMenuItems` 即为对工具箱的扩展。其返回类型为：\n\n```ts\n// Slash Command 扩展\ngetCommandMenuItems?: () => CommandMenuItemType | CommandMenuItemType[];\n\nexport interface CommandMenuItemType {\n  priority: number;\n  icon: Component;\n  title: string;\n  keywords: string[];\n  command: ({ editor, range }: { editor: Editor; range: Range }) => void;\n}\n```\n\n如下为 [`Table`](../packages/editor/src/extensions/table/index.ts) 扩展中对于 `getCommandMenuItems` 的扩展示例：\n\n```ts\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getCommandMenuItems() {\n        return {\n          priority: 120,\n          icon: markRaw(MdiTable),\n          title: \"editor.extensions.commands_menu.table\",\n          keywords: [\"table\", \"biaoge\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .insertTable({ rows: 3, cols: 3, withHeaderRow: true })\n              .run();\n          },\n        };\n      },\n    }\n  }\n```\n\n## 4. 悬浮菜单扩展\n\n编辑器悬浮菜单的扩展。可用于支持目标元素组件的功能扩展及操作简化。例如 `Table` 扩展中的添加下一列、添加上一列等操作。\n\n在 <https://github.com/halo-sigs/richtext-editor/pull/38> 中，我们重构了对编辑器悬浮区域的扩展，如果需要对某个块进行支持，只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getBubbleMenu` 函数即可，如：\n\n```ts\n{\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getBubbleMenu({ editor }: { editor: Editor }) {\n        return []\n      },\n    };\n  },\n}\n```\n\n其中 `getBubbleMenu` 即为对悬浮菜单的扩展。其返回类型为：\n\n```ts\n// 悬浮菜单扩展\ngetBubbleMenu?: ({ editor }: { editor: Editor }) => NodeBubbleMenuType;\n\ninterface BubbleMenuProps {\n  pluginKey?: string;                                             // 悬浮菜单插件 Key，建议命名方式 xxxBubbleMenu\n  editor?: Editor;\n  shouldShow: (props: {                                           // 悬浮菜单显示的条件\n    editor: Editor;\n    state: EditorState;\n    node?: HTMLElement;\n    view?: EditorView;\n    oldState?: EditorState;\n    from?: number;\n    to?: number;\n  }) => boolean;\n  tippyOptions?: Record<string, unknown>;                          // 可自由定制悬浮菜单所用的 tippy 组件的选项\n  getRenderContainer?: (node: HTMLElement) => HTMLElement;         // 悬浮菜单所基准的 DOM\n  defaultAnimation?: boolean;                                      // 是否启用默认动画。默认为 true\n}\n\n// 悬浮菜单\nexport interface NodeBubbleMenuType extends BubbleMenuProps {\n  component?: Component;                                           // 不使用默认的样式，与 items 二选一\n  items?: BubbleItemType[];                                       // 悬浮菜单子项，使用默认的形式进行，与 items 二选一\n  extendsKey?: string | PluginKey;                                 // 用于扩展已有悬浮菜单的 key，如果未提供，则会被视为一个新的悬浮菜单\n}\n\n// 悬浮菜单子项\nexport interface BubbleItemType {\n  priority: number;                                                // 优先级，数字越小优先级越大，越靠前\n  component?: Component;                                           // 完全自定义子项样式\n  key?: string;                                                    // 子项的唯一标识，通常用于扩展悬浮菜单时仅保留唯一的子项。\n  props?: {                                                        // 子项属性，可选。同时支持传入自定义属性\n    isActive?: ({ editor }: { editor: Editor }) => boolean;         // 当前功能是否已经处于活动状态\n    visible?: ({ editor }: { editor: Editor }) => boolean;         // 是否显示当前子项\n    icon?: Component;                                              // 图标\n    iconStyle?: string;                                            // 图标自定义样式\n    title?: string;                                                // 标题\n    action?: ({ editor }: { editor: Editor }) => Component | void; // 点击子项后的操作，如果返回 Component，则会将其包含在下拉框中。\n  } & Record<string, unknown>;\n}\n```\n\n如下为 [`Table`](../packages/editor/src/extensions/table/index.ts) 扩展中对于 `getBubbleMenu` 悬浮菜单的部分扩展示例：\n\n```ts\naddOptions() {\n  return {\n    ...this.parent?.(),\n      getBubbleMenu({ editor }) {\n        return {\n          pluginKey: \"tableBubbleMenu\",\n          shouldShow: ({ state }: { state: EditorState }): boolean => {\n            return isActive(state, Table.name);\n          },\n          getRenderContainer(node) {\n            let container = node;\n            if (container.nodeName === \"#text\") {\n              container = node.parentElement as HTMLElement;\n            }\n            while (\n              container &&\n              container.classList &&\n              !container.classList.contains(\"tableWrapper\")\n            ) {\n              container = container.parentElement as HTMLElement;\n            }\n            return container;\n          },\n          tippyOptions: {\n            offset: [26, 0],\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                icon: markRaw(MdiTableColumnPlusBefore),\n                title: i18n.global.t(\"editor.menus.table.add_column_before\"),\n                action: () => editor.chain().focus().addColumnBefore().run(),\n              },\n            },\n          ]\n        }\n      }\n  }\n}\n```\n\n## 5. 拖拽菜单扩展\n\n拖拽菜单扩展主要用于拖拽的菜单功能扩展，例如转换为、复制、剪切、删除等操作。\n\n在 <https://github.com/halo-dev/halo/pull/7861> 中，我们重构了对编辑器拖拽区域的扩展，并且支持了对拖拽菜单的扩展。如果需要对拖拽菜单进行扩展，只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getDraggableMenuItems` 函数即可，如：\n\n```ts\n{\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getDraggableMenuItems({ editor }: { editor: Editor }) {\n        return []\n      },\n    };\n  },\n}\n```\n\n同时，为了支持不同扩展对同一菜单项的扩展，我们提供了 `extendsKey` 属性，用于指定扩展目标菜单项的唯一标识。只需将 `extendsKey` 设置为已有的菜单项的 `key`，即可扩展该菜单项。可扩展已有菜单项的 `visible`、`isActive`、`disabled`、`action` 方法以及 `children.items` 属性，如：\n\n```ts\n{\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getDraggableMenuItems({ editor }: { editor: Editor }) {\n        return {\n          extendsKey: CONVERT_TO_KEY,\n          // 当任意扩展目标菜单项的 visible 方法返回 false 时，当前菜单项不会显示。返回 true 则会继续执行后续的扩展实现。\n          visible: ({ editor }) => {\n            if (isActive(editor.state, \"table\")) {\n              return false;\n            }\n            return true;\n          },\n        };\n      },\n    };\n  },\n};\n```\n\n拖拽菜单最多支持两级菜单嵌套， 如果想扩展已有的一级菜单，为其二级菜单增加内容，则需要同时设置 `extendsKey` 和 `children.items` 属性。如：\n\n```ts\n{\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getDraggableMenuItems({ editor }: { editor: Editor }) {\n        return {\n          extendsKey: CONVERT_TO_KEY,\n          children: {\n            items: [\n              {\n                priority: 10,\n                icon: markRaw(MdiFormatParagraph),\n                title: i18n.global.t(\"editor.common.heading.paragraph\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setParagraph().run(),\n              },\n            ],\n          },\n        }\n      },\n    };\n  },\n}\n```\n\n默认情况下，将会追加 `items`，若想覆盖，则需要设置子菜单的 `key` 属性，将会覆盖原有的子菜单项。\n\n下面为 `getDraggableMenuItems` 的返回类型：\n\n```ts\n\n// 拖拽菜单扩展\ngetDraggableMenuItems?: ({\n    editor,\n  }: {\n    editor: Editor;\n  }) => DragButtonType | DragButtonType[];\n\n// 拖拽菜单项目属性\nexport interface DragButtonItemProps {\n  extendsKey?: string;                                    // 扩展目标菜单项的唯一标识，如果提供了该属性，则视为扩展目标菜单项。\n  key?: string;                                           // 唯一标识，如果同级菜单项设置了同样的 key，则会被合并为一个菜单项。\n  priority?: number;                                      // 优先级，数字越小优先级越大，越靠前\n  title?: string | (() => string);                        // 标题\n  icon?: Component;                                       // 图标\n  action?: ({                                             // 点击菜单后的操作，如果返回 Component，则会将其包含在子菜单中。\n                                                          // 可以通过调用 close 方法可以在操作完成后关闭拖拽菜单，或者当返回为 true 或 undefined 时，会自动关闭拖拽菜单，如果返回 false，则不会关闭拖拽菜单。\n                                                          // 多个扩展实现时，则按照顺序执行，并在返回非 undefined 值时停止执行。\n    editor,\n    node,\n    pos,\n    close,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n    close: () => void;\n  }) => Component | boolean | void | Promise<Component | boolean | void>;\n  iconStyle?: string;                                       // 图标自定义样式\n  class?: string;                                           // 自定义样式\n  visible?: ({                                              // 是否显示当前菜单项，默认为 true，多个扩展实现时，以 AND 逻辑判断，即所有扩展返回 true 时，当前菜单项才会显示。\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => boolean;\n  isActive?: ({                                             // 当前菜单项是否处于活动状态，默认为 false，多个扩展实现时，以 OR 逻辑判断，即只要有一个扩展返回 true，则当前菜单项处于活动状态。\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => boolean;\n  disabled?: ({                                                // 是否禁用当前菜单项，默认为 false，多个扩展实现时，以 OR 逻辑判断，即只要有一个扩展返回 true，则当前菜单项会被禁用。\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => boolean;\n  keyboard?: string;                                            // 快捷键，遵循 https://tiptap.dev/docs/editor/core-concepts/keyboard-shortcuts\n  component?: Component;                                        // 自定义组件，如果提供了该属性，则不会显示默认的菜单项，而是会显示自定义组件，并且将所有 props 传递给自定义组件。\n  [key: string]: any;                                           // 其他自定义属性，将会传递给自定义组件。\n}\n\n// 一级菜单项\nexport interface DragButtonType extends DragButtonItemProps {\n  children?: {                                                    // 子菜单项，如果提供了该属性，则视为扩展目标菜单项的二级菜单。\n    component?: Component;                                        // 自定义组件，如果提供了该属性，则不会显示默认的子菜单项，而是会显示自定义组件，并且将所有 props 传递给自定义组件。\n    items?: DragButtonItemProps[];                                // 子菜单项列表，如果提供了该属性，则视为扩展目标菜单项的二级菜单。\n  };\n}\n```\n"
  },
  {
    "path": "ui/packages/editor/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"unplugin-icons/types/vue\" />\n"
  },
  {
    "path": "ui/packages/editor/package.json",
    "content": "{\n  \"name\": \"@halo-dev/richtext-editor\",\n  \"version\": \"2.23.0\",\n  \"description\": \"Default editor for Halo\",\n  \"homepage\": \"https://github.com/halo-dev/halo/tree/main/ui/packages/editor#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/halo-dev/halo/issues\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"@halo-dev\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/halo-dev/halo.git\",\n    \"directory\": \"ui/packages/editor\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"./dist/index.iife.js\",\n  \"module\": \"./dist/index.es.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"unpkg\": \"./dist/index.iife.js\",\n  \"jsdelivr\": \"./dist/index.iife.js\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.es.js\"\n    },\n    \"./dist/style.css\": \"./dist/style.css\"\n  },\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"dev\": \"vite build --watch --mode development\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"typecheck\": \"vue-tsc --noEmit -p tsconfig.app.json --composite false\"\n  },\n  \"dependencies\": {\n    \"@floating-ui/dom\": \"^1.7.4\",\n    \"@halo-dev/api-client\": \"workspace:*\",\n    \"@halo-dev/components\": \"workspace:*\",\n    \"@halo-dev/ui-shared\": \"workspace:*\",\n    \"@tiptap/core\": \"^3.17.1\",\n    \"@tiptap/extension-blockquote\": \"^3.17.1\",\n    \"@tiptap/extension-bold\": \"^3.17.1\",\n    \"@tiptap/extension-code\": \"^3.17.1\",\n    \"@tiptap/extension-code-block\": \"^3.17.1\",\n    \"@tiptap/extension-color\": \"^3.17.1\",\n    \"@tiptap/extension-details\": \"^3.17.1\",\n    \"@tiptap/extension-document\": \"^3.17.1\",\n    \"@tiptap/extension-drag-handle\": \"^3.17.1\",\n    \"@tiptap/extension-drag-handle-vue-3\": \"^3.17.1\",\n    \"@tiptap/extension-hard-break\": \"^3.17.1\",\n    \"@tiptap/extension-heading\": \"^3.17.1\",\n    \"@tiptap/extension-highlight\": \"^3.17.1\",\n    \"@tiptap/extension-horizontal-rule\": \"^3.17.1\",\n    \"@tiptap/extension-image\": \"^3.17.1\",\n    \"@tiptap/extension-italic\": \"^3.17.1\",\n    \"@tiptap/extension-link\": \"^3.17.1\",\n    \"@tiptap/extension-list\": \"^3.17.1\",\n    \"@tiptap/extension-paragraph\": \"^3.17.1\",\n    \"@tiptap/extension-strike\": \"^3.17.1\",\n    \"@tiptap/extension-subscript\": \"^3.17.1\",\n    \"@tiptap/extension-superscript\": \"^3.17.1\",\n    \"@tiptap/extension-table\": \"^3.17.1\",\n    \"@tiptap/extension-text\": \"^3.17.1\",\n    \"@tiptap/extension-text-align\": \"^3.17.1\",\n    \"@tiptap/extension-text-style\": \"^3.17.1\",\n    \"@tiptap/extension-underline\": \"^3.17.1\",\n    \"@tiptap/extensions\": \"^3.17.1\",\n    \"@tiptap/pm\": \"^3.17.1\",\n    \"@tiptap/suggestion\": \"^3.17.1\",\n    \"@tiptap/vue-3\": \"^3.17.1\",\n    \"github-markdown-css\": \"^5.2.0\",\n    \"linkifyjs\": \"^4.1.3\",\n    \"scroll-into-view-if-needed\": \"^3.1.0\"\n  },\n  \"devDependencies\": {\n    \"@types/linkifyjs\": \"^2.1.7\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"^3.5.x\"\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "ui/packages/editor/src/components/Editor.vue",
    "content": "<script lang=\"ts\" setup>\nimport { watch, type CSSProperties, type PropType } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport { EditorContent, VueEditor } from \"@/tiptap\";\nimport EditorBubbleMenu from \"./bubble/EditorBubbleMenu.vue\";\nimport EditorDragHandle from \"./drag/EditorDragHandle.vue\";\nimport EditorHeader from \"./EditorHeader.vue\";\n\nconst props = defineProps({\n  editor: {\n    type: Object as PropType<VueEditor>,\n    required: true,\n  },\n  contentStyles: {\n    type: Object as PropType<CSSProperties>,\n    required: false,\n    default: () => ({}),\n  },\n  locale: {\n    type: String as PropType<\"zh-CN\" | \"en\" | \"zh\" | \"en-US\">,\n    required: false,\n    default: \"zh-CN\",\n  },\n});\n\nwatch(\n  () => props.locale,\n  () => {\n    i18n.global.locale.value = props.locale;\n  },\n  {\n    immediate: true,\n  }\n);\n</script>\n<template>\n  <div v-if=\"editor\" class=\"halo-rich-text-editor\">\n    <editor-bubble-menu :editor=\"editor\" />\n    <editor-drag-handle :editor=\"editor\" />\n    <editor-header :editor=\"editor\" />\n    <div class=\"editor-entry\">\n      <div class=\"editor-main\">\n        <div v-if=\"$slots.content\" class=\"editor-main-extra\">\n          <slot name=\"content\" />\n        </div>\n\n        <editor-content\n          :editor=\"editor\"\n          :style=\"contentStyles\"\n          class=\"editor-main-content markdown-body\"\n        />\n      </div>\n      <div v-if=\"$slots.extra\" class=\"editor-entry-extra\">\n        <slot name=\"extra\"></slot>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/EditorHeader.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport MingcuteAddCircleFill from \"~icons/mingcute/add-circle-fill\";\nimport { type AnyExtension, VueEditor } from \"@/tiptap\";\nimport type { ToolbarItemType, ToolboxItemType } from \"@/types\";\n\nconst props = defineProps({\n  editor: {\n    type: VueEditor,\n    required: true,\n  },\n});\n\nfunction getToolbarItemsFromExtensions() {\n  const extensionManager = props.editor?.extensionManager;\n  return extensionManager.extensions\n    .reduce((acc: ToolbarItemType[], extension: AnyExtension) => {\n      const { getToolbarItems } = extension.options;\n\n      if (!getToolbarItems) {\n        return acc;\n      }\n\n      const items = getToolbarItems({\n        editor: props.editor,\n      });\n\n      if (Array.isArray(items)) {\n        return [...acc, ...items];\n      }\n\n      return [...acc, items];\n    }, [])\n    .sort((a, b) => a.priority - b.priority);\n}\n\nfunction getToolboxItemsFromExtensions() {\n  const extensionManager = props.editor?.extensionManager;\n  return extensionManager.extensions\n    .reduce((acc: ToolboxItemType[], extension: AnyExtension) => {\n      const { getToolboxItems } = extension.options;\n\n      if (!getToolboxItems) {\n        return acc;\n      }\n\n      const items = getToolboxItems({\n        editor: props.editor,\n      });\n\n      if (Array.isArray(items)) {\n        return [...acc, ...items];\n      }\n\n      return [...acc, items];\n    }, [])\n    .sort((a, b) => a.priority - b.priority);\n}\n</script>\n<template>\n  <div\n    class=\"editor-header space-x-1 overflow-auto border-b bg-white px-1 py-1 text-center shadow-sm\"\n  >\n    <div class=\"inline-flex h-full items-center gap-1\">\n      <VDropdown :triggers=\"['click']\" :popper-triggers=\"['click']\">\n        <template #default=\"{ shown }\">\n          <button\n            class=\"inline-flex size-8 items-center justify-center rounded-md p-1 transition-colors hover:bg-gray-100 active:!bg-gray-200\"\n            :class=\"{ 'bg-gray-200': shown }\"\n            tabindex=\"-1\"\n          >\n            <MingcuteAddCircleFill class=\"text-primary\" />\n          </button>\n        </template>\n        <template #popper>\n          <div class=\"relative max-h-96 w-56 overflow-hidden overflow-y-auto\">\n            <component\n              :is=\"toolboxItem.component\"\n              v-for=\"(toolboxItem, index) in getToolboxItemsFromExtensions()\"\n              v-bind=\"toolboxItem.props\"\n              :key=\"index\"\n              tabindex=\"-1\"\n            />\n          </div>\n        </template>\n      </VDropdown>\n      <div class=\"mx-1 h-5 w-[1px] bg-gray-100\"></div>\n      <div\n        v-for=\"(item, index) in getToolbarItemsFromExtensions()\"\n        :key=\"index\"\n      >\n        <component\n          :is=\"item.component\"\n          v-if=\"!item.children?.length\"\n          v-bind=\"item.props\"\n          tabindex=\"-1\"\n        />\n        <template v-else>\n          <VDropdown\n            class=\"inline-flex\"\n            tabindex=\"-1\"\n            :triggers=\"['click']\"\n            :popper-triggers=\"['click']\"\n          >\n            <template #default=\"{ shown }\">\n              <component\n                :is=\"item.component\"\n                v-bind=\"item.props\"\n                :children=\"item.children\"\n                tabindex=\"-1\"\n                :class=\"{ 'bg-gray-200': shown }\"\n              />\n            </template>\n            <template #popper>\n              <div\n                class=\"relative max-h-96 w-56 overflow-hidden overflow-y-auto\"\n              >\n                <component\n                  v-bind=\"child.props\"\n                  :is=\"child.component\"\n                  v-for=\"(child, childIndex) in item.children\"\n                  :key=\"childIndex\"\n                  tabindex=\"-1\"\n                />\n              </div>\n            </template>\n          </VDropdown>\n        </template>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/base/DropdownItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport MingcuteCheckCircleLine from \"~icons/mingcute/check-circle-line\";\n\nconst props = withDefaults(\n  defineProps<{\n    disabled?: boolean;\n    isActive?: boolean;\n  }>(),\n  {\n    disabled: false,\n    isActive: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"click\", e: MouseEvent): void;\n}>();\n\nfunction handleClick(e: MouseEvent) {\n  if (props.disabled) {\n    return;\n  }\n  emit(\"click\", e);\n}\n</script>\n<template>\n  <div\n    role=\"menuitem\"\n    tabindex=\"-1\"\n    :class=\"[\n      { 'cursor-not-allowed opacity-70': disabled },\n      { 'hover:bg-gray-100': !disabled },\n      { 'px-3': !$slots.icon },\n      { 'px-1.5': !!$slots.icon },\n    ]\"\n    class=\"group my-1.5 flex min-h-9 cursor-pointer flex-row items-center gap-3 rounded py-1 transition-colors first:mt-0 last:mb-0\"\n    @click=\"handleClick\"\n  >\n    <div\n      v-if=\"$slots.icon\"\n      class=\"size-7 flex-none rounded bg-gray-100 p-1.5 [&>svg]:size-full\"\n      :class=\"{\n        'group-hover:bg-white': !disabled,\n      }\"\n    >\n      <slot name=\"icon\" />\n    </div>\n\n    <div\n      class=\"min-w-0 flex-1 shrink text-sm text-gray-600\"\n      :class=\"[\n        { '!font-medium !text-gray-900': isActive },\n        { 'group-hover:font-medium group-hover:text-gray-900': !disabled },\n      ]\"\n    >\n      <slot />\n    </div>\n\n    <MingcuteCheckCircleLine\n      v-if=\"isActive\"\n      class=\"size-4 flex-none text-gray-900\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/base/Input.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, watchEffect } from \"vue\";\nimport MingcuteInformationLine from \"~icons/mingcute/information-line\";\n\nconst modelValue = defineModel<string | number | undefined>({\n  default: \"\",\n});\n\nconst props = withDefaults(\n  defineProps<{\n    type?: string;\n    label?: string;\n    help?: boolean;\n    placeholder?: string;\n    tooltip?: string;\n    autoFocus?: boolean;\n  }>(),\n  {\n    type: \"text\",\n    label: undefined,\n    help: undefined,\n    placeholder: undefined,\n    tooltip: undefined,\n    autoFocus: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"focus\"): void;\n}>();\n\nconst inputRef = ref<HTMLInputElement>();\n\nwatchEffect(() => {\n  if (inputRef.value) {\n    if (props.autoFocus) {\n      setTimeout(() => {\n        inputRef.value?.focus();\n      }, 100);\n    }\n  }\n});\n</script>\n\n<template>\n  <div class=\"group relative h-11 w-full\" :class=\"{ 'mt-2': !!label }\">\n    <label\n      v-if=\"label\"\n      class=\"absolute -top-2 left-2 inline-block rounded-md bg-white px-1 text-xs text-gray-500 group-focus-within:text-gray-900\"\n    >\n      {{ label }}\n    </label>\n    <input\n      ref=\"inputRef\"\n      v-model.lazy.trim=\"modelValue\"\n      :type=\"type\"\n      class=\"block size-full rounded-md bg-white px-3 text-sm text-gray-900 ring-1 ring-gray-100 transition-all placeholder:text-gray-400 focus:!ring-1 focus:!ring-primary\"\n      :placeholder=\"placeholder\"\n      @focus=\"emit('focus')\"\n    />\n    <MingcuteInformationLine\n      v-if=\"tooltip\"\n      v-tooltip=\"tooltip\"\n      class=\"absolute right-2 top-1/2 size-4 -translate-y-1/2 text-gray-500 transition-colors hover:text-primary\"\n    />\n    <span v-if=\"help\" class=\"line-clamp-1 text-xs text-gray-500\">\n      {{ help }}\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/base/index.ts",
    "content": "export { default as DropdownItem } from \"./DropdownItem.vue\";\nexport { default as BlockActionInput, default as Input } from \"./Input.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/components/block/BlockActionButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport { vTooltip } from \"@halo-dev/components\";\n\nwithDefaults(\n  defineProps<{\n    tooltip?: string;\n    selected?: boolean;\n    disabled?: boolean;\n    isActive?: boolean;\n    visible?: boolean;\n  }>(),\n  {\n    tooltip: undefined,\n    selected: false,\n    disabled: false,\n    isActive: false,\n    visible: true,\n  }\n);\n</script>\n\n<template>\n  <button\n    v-if=\"visible\"\n    v-tooltip=\"tooltip\"\n    type=\"button\"\n    class=\"editor-block__actions-button\"\n    :class=\"{\n      'editor-block__actions-button--selected': selected,\n      'editor-block__actions-button--active': isActive,\n    }\"\n    :disabled=\"disabled\"\n  >\n    <slot name=\"icon\" />\n  </button>\n</template>\n\n<style lang=\"scss\">\n.editor-block__actions-button {\n  @apply inline-flex size-8 items-center justify-center rounded-md text-lg text-gray-600 hover:bg-gray-100 active:!bg-gray-200;\n\n  &--selected {\n    @apply bg-gray-200;\n  }\n\n  &--active {\n    @apply bg-gray-200;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/packages/editor/src/components/block/BlockActionHorizontalSeparator.vue",
    "content": "<template>\n  <div class=\"editor-block__actions-horizontal-separator\"></div>\n</template>\n\n<style>\n.editor-block__actions-horizontal-separator {\n  @apply my-1.5 w-full bg-slate-200;\n  height: 1px;\n}\n</style>\n"
  },
  {
    "path": "ui/packages/editor/src/components/block/BlockActionSeparator.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport type { BubbleItemComponentProps } from \"@/types\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst visible = computed(() => {\n  if (!props.visible) {\n    return true;\n  }\n  return props.editor && props.visible?.({ editor: props.editor });\n});\n</script>\n\n<template>\n  <div v-if=\"visible\" class=\"editor-block__actions-separator\"></div>\n</template>\n\n<style>\n.editor-block__actions-separator {\n  @apply mx-1.5 h-5 bg-slate-200;\n  width: 1px;\n}\n</style>\n"
  },
  {
    "path": "ui/packages/editor/src/components/block/index.ts",
    "content": "export { default as BlockActionButton } from \"./BlockActionButton.vue\";\nexport { default as BlockActionHorizontalSeparator } from \"./BlockActionHorizontalSeparator.vue\";\nexport { default as BlockActionSeparator } from \"./BlockActionSeparator.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/components/bubble/BubbleButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport MingcuteDownSmallFill from \"~icons/mingcute/down-small-fill\";\n\nconst props = withDefaults(\n  defineProps<{\n    title?: string;\n    text?: string;\n    isActive?: boolean;\n    showMoreIndicator?: boolean;\n  }>(),\n  {\n    showMoreIndicator: false,\n    isActive: false,\n    title: undefined,\n    text: undefined,\n  }\n);\n\nconst onlyIcon = computed(() => {\n  return !props.text && !props.showMoreIndicator;\n});\n</script>\n<template>\n  <button\n    v-tooltip=\"{\n      content: title,\n      distance: 8,\n      delay: {\n        show: 0,\n      },\n    }\"\n    :class=\"[\n      { 'bg-gray-200 !text-black': isActive },\n      { 'size-8': onlyIcon },\n      { 'h-8 gap-1 px-1': !onlyIcon },\n    ]\"\n    :title=\"title\"\n    class=\"text inline-flex items-center justify-center rounded-md text-gray-600 hover:bg-gray-100 active:!bg-gray-200 [&>svg]:size-5\"\n  >\n    <slot name=\"icon\" />\n    <span v-if=\"text\">{{ text }}</span>\n    <MingcuteDownSmallFill v-if=\"showMoreIndicator\" />\n  </button>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/bubble/BubbleItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { ref, type Component } from \"vue\";\nimport type { Editor } from \"@/tiptap\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport BubbleButton from \"./BubbleButton.vue\";\n\nconst props = withDefaults(defineProps<BubbleItemComponentProps>(), {\n  isActive: () => false,\n  visible: () => true,\n});\n\nconst componentRef = ref<Component | void>();\nconst handleBubbleItemClick = (editor: Editor) => {\n  if (!props.action) {\n    return;\n  }\n  const callback = props.action?.({ editor });\n  if (typeof callback === \"object\") {\n    componentRef.value = callback;\n  }\n};\n</script>\n\n<template>\n  <VDropdown\n    v-if=\"visible({ editor })\"\n    class=\"inline-flex\"\n    :triggers=\"[]\"\n    :auto-hide=\"true\"\n    :shown=\"!!componentRef\"\n    :distance=\"10\"\n    @hide=\"componentRef = undefined\"\n  >\n    <BubbleButton\n      :title=\"title\"\n      :is-active=\"isActive({ editor })\"\n      @click=\"handleBubbleItemClick(editor)\"\n    >\n      <template #icon>\n        <component :is=\"icon\" :style=\"iconStyle\" class=\"size-5\" />\n      </template>\n    </BubbleButton>\n    <template #popper>\n      <KeepAlive>\n        <component :is=\"componentRef\" v-bind=\"props\"></component>\n      </KeepAlive>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/bubble/EditorBubbleMenu.vue",
    "content": "<script lang=\"ts\" setup>\nimport { BubbleMenu } from \"@tiptap/vue-3/menus\";\nimport { type PropType } from \"vue\";\nimport {\n  Editor,\n  EditorState,\n  EditorView,\n  PluginKey,\n  VueEditor,\n} from \"@/tiptap\";\nimport type { BubbleItemType, NodeBubbleMenuType } from \"@/types\";\nimport BubbleItem from \"./BubbleItem.vue\";\n\nconst props = defineProps({\n  editor: {\n    type: Object as PropType<VueEditor>,\n    required: true,\n  },\n});\n\nconst getBubbleMenuFromExtensions = (): NodeBubbleMenuType[] => {\n  const extensionManager = props.editor?.extensionManager;\n  const extendsBubbleMap: Map<string | PluginKey, NodeBubbleMenuType[]> =\n    new Map();\n  const bubbleMenus: NodeBubbleMenuType[] = [];\n  for (const extension of extensionManager.extensions) {\n    const { getBubbleMenu } = extension.options;\n\n    if (!getBubbleMenu) {\n      continue;\n    }\n\n    const nodeBubbleMenu = getBubbleMenu({\n      editor: props.editor,\n    }) as NodeBubbleMenuType;\n\n    if (nodeBubbleMenu.extendsKey) {\n      if (!extendsBubbleMap.has(nodeBubbleMenu.extendsKey)) {\n        extendsBubbleMap.set(nodeBubbleMenu.extendsKey, []);\n      }\n      extendsBubbleMap.get(nodeBubbleMenu.extendsKey)?.push(nodeBubbleMenu);\n      continue;\n    }\n\n    bubbleMenus.push(nodeBubbleMenu);\n  }\n\n  return bubbleMenus.map<NodeBubbleMenuType>((bubbleMenu) => {\n    if (!bubbleMenu.pluginKey) {\n      bubbleMenu.items = sortBubbleMenuItems(bubbleMenu.items);\n      return bubbleMenu;\n    }\n\n    if (!extendsBubbleMap.has(bubbleMenu.pluginKey)) {\n      bubbleMenu.items = sortBubbleMenuItems(bubbleMenu.items);\n      return bubbleMenu;\n    }\n\n    const extendsBubbleMenus = extendsBubbleMap.get(bubbleMenu.pluginKey) ?? [];\n    return mergeBubbleMenu(bubbleMenu, extendsBubbleMenus);\n  });\n};\n\n/**\n * Merge bubble menu\n *\n * If the item has a key, it will be overwritten if it exists in the extendsBubbleMenus.\n * If the item does not have a key, it will be appended to the end of the items.\n * If the extendsBubbleMenus has a key, but it is not found in the items, it will be appended to the end of the items.\n *\n * For shouldShow: all shouldShow functions from the original and extended bubble menus will be merged.\n * The merged bubble menu will only be shown if all shouldShow functions return true.\n * If a shouldShow is not defined, it defaults to true.\n *\n * @param bubbleMenu - The bubble menu to merge.\n * @param extendsBubbleMenus - The extends bubble menus to merge.\n * @returns The merged bubble menu.\n */\nconst mergeBubbleMenu = (\n  bubbleMenu: NodeBubbleMenuType,\n  extendsBubbleMenus: NodeBubbleMenuType[]\n): NodeBubbleMenuType => {\n  const items = bubbleMenu.items ?? [];\n  const extendsItems =\n    extendsBubbleMenus\n      .map((extendsBubbleMenu) => extendsBubbleMenu.items)\n      .filter((item) => item !== undefined)\n      .flat() ?? [];\n\n  const keyedItems = new Map<string, BubbleItemType>();\n  const nonKeyedItems: BubbleItemType[] = [];\n\n  items.forEach((item) => {\n    if (item.key) {\n      keyedItems.set(item.key, item);\n    } else {\n      nonKeyedItems.push(item);\n    }\n  });\n\n  extendsItems.forEach((item) => {\n    if (item.key) {\n      keyedItems.set(item.key, item);\n    } else {\n      nonKeyedItems.push(item);\n    }\n  });\n\n  const mergedItems = [...Array.from(keyedItems.values()), ...nonKeyedItems];\n\n  const shouldShowFunctions = [\n    bubbleMenu.shouldShow,\n    ...extendsBubbleMenus.map((menu) => menu.shouldShow),\n  ].filter((fn) => fn !== undefined);\n\n  const mergedShouldShow =\n    shouldShowFunctions.length > 0\n      ? (\n          props: Parameters<NonNullable<NodeBubbleMenuType[\"shouldShow\"]>>[0]\n        ) => {\n          return shouldShowFunctions.every((fn) => (fn ? fn(props) : true));\n        }\n      : undefined;\n\n  return {\n    ...bubbleMenu,\n    items: sortBubbleMenuItems(mergedItems),\n    shouldShow: mergedShouldShow,\n  };\n};\n\nconst sortBubbleMenuItems = (items: BubbleItemType[] | undefined) => {\n  return items?.sort((a, b) => a.priority - b.priority);\n};\n\nconst shouldShow = (\n  props: {\n    editor: Editor;\n    element: HTMLElement;\n    view: EditorView;\n    state: EditorState;\n    oldState?: EditorState;\n    from: number;\n    to: number;\n  },\n  bubbleMenu: NodeBubbleMenuType\n) => {\n  if (!props.editor.isEditable) {\n    return false;\n  }\n  return bubbleMenu.shouldShow?.(props);\n};\n</script>\n<template>\n  <bubble-menu\n    v-for=\"(bubbleMenu, index) in getBubbleMenuFromExtensions()\"\n    :key=\"index\"\n    :plugin-key=\"bubbleMenu?.pluginKey\"\n    :should-show=\"(prop) => shouldShow(prop, bubbleMenu) ?? false\"\n    :editor=\"editor\"\n    :options=\"{\n      ...(bubbleMenu.options as any),\n    }\"\n    :update-delay=\"0\"\n    :get-referenced-virtual-element=\"bubbleMenu.getReferencedVirtualElement\"\n  >\n    <div\n      class=\"bubble-menu flex items-center space-x-1 rounded-md border bg-white p-1 shadow\"\n    >\n      <template v-if=\"bubbleMenu.items\">\n        <template\n          v-for=\"(item, itemIndex) in bubbleMenu.items\"\n          :key=\"itemIndex\"\n        >\n          <template v-if=\"item.component\">\n            <component\n              :is=\"item.component\"\n              v-bind=\"item.props\"\n              :editor=\"editor\"\n            />\n          </template>\n          <bubble-item v-else :editor=\"editor\" v-bind=\"item.props\" />\n        </template>\n      </template>\n      <template v-else-if=\"bubbleMenu.component\">\n        <component :is=\"bubbleMenu?.component\" :editor=\"editor\" />\n      </template>\n    </div>\n  </bubble-menu>\n</template>\n<style scoped>\n.bubble-menu {\n  max-width: calc(100vw - 30px);\n  overflow-x: auto;\n}\n</style>\n"
  },
  {
    "path": "ui/packages/editor/src/components/bubble/index.ts",
    "content": "export { default as BubbleButton } from \"./BubbleButton.vue\";\nexport { default as BubbleItem } from \"./BubbleItem.vue\";\nexport { default as EditorBubbleMenu } from \"./EditorBubbleMenu.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/components/common/ColorPickerDropdown.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Payload } from \"@ckpack/vue-color\";\nimport { Sketch } from \"@ckpack/vue-color\";\nimport { VDropdown } from \"@halo-dev/components\";\nimport tailwindcssColors from \"tailwindcss/colors\";\nimport MingcutePaletteLine from \"~icons/mingcute/palette-line\";\nimport MingcuteRightLine from \"~icons/mingcute/right-line\";\nimport { i18n } from \"@/locales\";\n\ninterface Color {\n  color: string;\n  name: string;\n}\n\nwithDefaults(\n  defineProps<{\n    modelValue?: string;\n  }>(),\n  {\n    modelValue: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (emit: \"update:modelValue\", value?: string): void;\n}>();\n\nfunction getColors(): Color[] {\n  const result: Color[] = [];\n\n  const colors: { [key: string]: { [key: string]: string } } = Object.keys(\n    tailwindcssColors\n  ).reduce((acc, key) => {\n    if (\n      [\n        \"gray\",\n        \"red\",\n        \"orange\",\n        \"yellow\",\n        \"green\",\n        \"blue\",\n        \"purple\",\n        \"pink\",\n      ].includes(key)\n    ) {\n      // @ts-ignore\n      acc[key] = tailwindcssColors[key];\n    }\n    return acc;\n  }, {});\n\n  for (const color in colors) {\n    const colorShades = colors[color];\n    const colorShadesArr = Object.entries(colorShades);\n\n    const sortedShades = colorShadesArr\n      .filter(([shade]) => parseInt(shade) >= 100 && parseInt(shade) <= 900)\n      .sort((a, b) => parseInt(b[0]) - parseInt(a[0]));\n\n    const formattedShades = sortedShades.map(([shade, value]) => ({\n      color: value,\n      name: `${color} ${shade}`,\n    }));\n\n    result.push(...formattedShades);\n  }\n\n  return result;\n}\n\nfunction handleSetColor(color: string) {\n  emit(\"update:modelValue\", color);\n}\n\nfunction onColorChange(color: Payload) {\n  handleSetColor(color.hex);\n}\n</script>\n\n<template>\n  <VDropdown class=\"inline-flex items-center\">\n    <slot />\n    <template #popper>\n      <slot name=\"prefix\" />\n      <div class=\"grid grid-cols-9 gap-1.5 p-2 pt-1\">\n        <div\n          v-for=\"item in getColors()\"\n          :key=\"item.color\"\n          :style=\"{ backgroundColor: item.color }\"\n          class=\"size-5 cursor-pointer rounded-sm ring-gray-300 ring-offset-1 hover:ring-1\"\n          :title=\"item.name\"\n          @click=\"handleSetColor(item.color)\"\n        ></div>\n      </div>\n\n      <VDropdown popper-class=\"[&_.v-popper\\_\\_inner]:!p-0\" placement=\"right\">\n        <div class=\"p-1\">\n          <div\n            class=\"flex cursor-pointer items-center justify-between rounded p-1 text-xs text-gray-600 hover:bg-gray-100 hover:text-gray-900\"\n          >\n            <div class=\"inline-flex items-center gap-2\">\n              <MingcutePaletteLine />\n              <span>\n                {{ i18n.global.t(\"editor.components.color_picker.more_color\") }}\n              </span>\n            </div>\n            <div>\n              <MingcuteRightLine />\n            </div>\n          </div>\n        </div>\n        <template #popper>\n          <Sketch model-value=\"#000\" @update:model-value=\"onColorChange\" />\n        </template>\n      </VDropdown>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/drag/EditorDragButtonItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed, ref, type Component } from \"vue\";\nimport MaterialSymbolsArrowForwardIosRounded from \"~icons/material-symbols/arrow-forward-ios-rounded\";\nimport type { PMNode, VueEditor } from \"@/tiptap\";\nimport type { DragButtonType } from \"@/types\";\nimport { formatShortcut } from \"@/utils\";\nimport EditorDragHandleMenu from \"./EditorDragMenu.vue\";\n\nconst props = defineProps<\n  DragButtonType & { editor: VueEditor; node: PMNode | null; pos: number }\n>();\n\nconst emit = defineEmits<{\n  (e: \"close\"): () => void;\n}>();\n\nconst showChildren = ref(false);\n\nconst componentRef = ref<Component | void>();\nconst handleDragButtonItemClick = async () => {\n  const visible =\n    props.visible?.({\n      editor: props.editor,\n      node: props.node,\n      pos: props.pos,\n    }) ?? true;\n  if (!visible) {\n    return;\n  }\n  const disabled =\n    props.disabled?.({\n      editor: props.editor,\n      node: props.node,\n      pos: props.pos,\n    }) ?? false;\n  if (disabled) {\n    return;\n  }\n  if (props.action) {\n    const callback = props.action?.({\n      editor: props.editor,\n      node: props.node,\n      pos: props.pos,\n      close: handleCloseDropdown,\n    });\n\n    const resolvedCallback =\n      callback instanceof Promise ? await callback : callback;\n\n    if (typeof resolvedCallback === \"object\" && resolvedCallback !== null) {\n      componentRef.value = resolvedCallback;\n    }\n\n    if (resolvedCallback === undefined || resolvedCallback === true) {\n      handleCloseDropdown();\n    }\n  } else {\n    showChildren.value = !showChildren.value;\n  }\n};\n\nconst handleCloseDropdown = () => {\n  showChildren.value = false;\n  emit(\"close\");\n};\n\nconst isChildrenComponent = computed(() => {\n  return (props.children?.items?.length || 0) > 0;\n});\n\nconst displayTitle = computed(() => {\n  return typeof props.title === \"function\"\n    ? props.title({ editor: props.editor, node: props.node, pos: props.pos })\n    : props.title;\n});\n\nconst triggerClick = () => {\n  handleDragButtonItemClick();\n};\n\ndefineExpose({\n  triggerClick,\n});\n</script>\n\n<template>\n  <VDropdown\n    v-if=\"\n      props.visible?.({\n        editor: props.editor,\n        node: props.node,\n        pos: props.pos,\n      }) ?? true\n    \"\n    class=\"inline-flex\"\n    :triggers=\"[]\"\n    :placement=\"'right'\"\n    :shown=\"showChildren && isChildrenComponent\"\n    :distance=\"8\"\n    @hide=\"showChildren = false\"\n  >\n    <component :is=\"props.component\" v-if=\"props.component\" v-bind=\"props\" />\n    <template v-else>\n      <div\n        :class=\"[\n          'flex w-full rounded px-2 py-2 text-sm text-gray-600 transition-colors',\n          {\n            'bg-gray-200 !text-black': props.isActive?.({\n              editor: props.editor,\n              node: props.node,\n              pos: props.pos,\n            }),\n          },\n          {\n            'cursor-pointer hover:bg-gray-100 hover:text-gray-900 active:!bg-gray-200':\n              !props.isActive?.({\n                editor: props.editor,\n                node: props.node,\n                pos: props.pos,\n              }),\n          },\n          props.class,\n        ]\"\n        @click=\"handleDragButtonItemClick()\"\n      >\n        <button :title=\"displayTitle\" class=\"flex-grow\">\n          <div class=\"flex items-center gap-2\">\n            <component\n              :is=\"props.icon\"\n              :style=\"props.iconStyle\"\n              class=\"size-4\"\n            />\n            <span class=\"flex flex-grow justify-start px-0.5\">\n              {{ displayTitle }}\n            </span>\n          </div>\n        </button>\n        <div v-if=\"isChildrenComponent\" class=\"flex items-center gap-2\">\n          <MaterialSymbolsArrowForwardIosRounded class=\"size-3\" />\n        </div>\n        <div\n          v-else-if=\"props.keyboard\"\n          class=\"flex items-center justify-center rounded-md border border-gray-200 bg-white p-0.5 px-1 font-sans text-xs font-bold text-gray-500\"\n        >\n          {{ formatShortcut(props.keyboard) }}\n        </div>\n      </div>\n    </template>\n\n    <template #popper>\n      <div class=\"relative overflow-hidden overflow-y-auto\">\n        <KeepAlive v-if=\"componentRef\">\n          <component\n            :is=\"componentRef\"\n            v-bind=\"props\"\n            :items=\"props.children?.items ?? []\"\n            @close=\"handleCloseDropdown\"\n          ></component>\n        </KeepAlive>\n        <EditorDragHandleMenu\n          v-else\n          :class=\"'!min-w-full'\"\n          :editor=\"props.editor\"\n          :node=\"props.node\"\n          :pos=\"props.pos\"\n          :items=\"props.children?.items ?? []\"\n          @close=\"handleCloseDropdown\"\n        />\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/drag/EditorDragHandle.vue",
    "content": "<script lang=\"ts\" setup>\nimport { offset } from \"@floating-ui/dom\";\nimport { VDropdown, vTooltip } from \"@halo-dev/components\";\nimport { DragHandle } from \"@tiptap/extension-drag-handle-vue-3\";\nimport { sortBy } from \"es-toolkit\";\nimport { computed, ref, shallowRef, type PropType } from \"vue\";\nimport MaterialSymbolsAddRounded from \"~icons/material-symbols/add-rounded\";\nimport MaterialSymbolsDragIndicator from \"~icons/material-symbols/drag-indicator\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  NodeSelection,\n  PMNode,\n  TextSelection,\n  VueEditor,\n} from \"@/tiptap\";\nimport type {\n  DragButtonItemProps,\n  DragButtonType,\n  ExtensionOptions,\n} from \"@/types\";\nimport { isBlockEmpty } from \"@/utils\";\nimport defaultDragItems from \"./default-drag\";\nimport EditorDragMenu from \"./EditorDragMenu.vue\";\n\nconst { editor } = defineProps({\n  editor: {\n    type: Object as PropType<VueEditor>,\n    required: true,\n  },\n});\n\nconst currentNode = shallowRef<PMNode | null>(null);\nconst currentPos = shallowRef<number>(0);\nconst showMenu = ref(false);\n\nconst isEmptyNode = computed(() => {\n  if (!currentNode.value) {\n    return false;\n  }\n  return isBlockEmpty(currentNode.value);\n});\n\nconst handleInsertBlock = () => {\n  if (isEmptyNode.value) {\n    editor.chain().insertContent(\"/\").focus().run();\n  } else {\n    const insertPos = currentPos.value + (currentNode.value?.nodeSize ?? 0);\n    editor.commands.insertContentAt(\n      insertPos,\n      [{ type: \"paragraph\", content: [{ type: \"text\", text: \"/\" }] }],\n      {\n        updateSelection: true,\n      }\n    );\n    editor.commands.focus(insertPos + 2, {\n      scrollIntoView: true,\n    });\n  }\n};\n\nconst handleNodeChange = ({\n  node,\n  pos,\n}: {\n  node: PMNode | null;\n  pos: number;\n}) => {\n  currentNode.value = node;\n  currentPos.value = pos;\n};\n\nconst handleMenuShow = () => {\n  const { tr } = editor.state;\n  tr.setMeta(\"lockDragHandle\", true);\n  if (currentPos.value !== undefined) {\n    const $pos = tr.doc.resolve(currentPos.value);\n    tr.setSelection(new NodeSelection($pos));\n  }\n  editor.view.dispatch(tr);\n};\n\nconst handleMenuHide = () => {\n  const { tr } = editor.state;\n  tr.setMeta(\"lockDragHandle\", false);\n  const { selection } = editor.state;\n  if (!selection.empty) {\n    const $pos = tr.doc.resolve(selection.to);\n    tr.setSelection(TextSelection.create(tr.doc, $pos.pos));\n  }\n  editor.view.dispatch(tr);\n};\n\nconst dragMenuItems = computed(() => {\n  const extensionManager = editor?.extensionManager;\n  const rootDragButtonItems: DragButtonType[] = [...defaultDragItems];\n  const extendsDragButtonItems: DragButtonType[] = [];\n  for (const extension of extensionManager.extensions) {\n    const { getDraggableMenuItems } = extension.options as ExtensionOptions;\n    if (!getDraggableMenuItems) {\n      continue;\n    }\n\n    const dragButtonItems = getDraggableMenuItems({ editor }) as\n      | DragButtonType\n      | DragButtonType[];\n\n    for (const item of Array.isArray(dragButtonItems)\n      ? dragButtonItems\n      : [dragButtonItems]) {\n      if (item.extendsKey && item.extendsKey.trim() !== \"\") {\n        extendsDragButtonItems.push(item);\n        continue;\n      }\n      rootDragButtonItems.push(item);\n    }\n  }\n\n  const mergedDragButtonItems = mergeDragButtonItems(\n    rootDragButtonItems,\n    extendsDragButtonItems\n  );\n\n  return sortDragButtonItems(mergedDragButtonItems);\n});\n\n/**\n * Merges root and extension drag button items into a unified array.\n *\n * Extension items can extend root items by matching their `extendsKey` property to a root item's `key`.\n * When merging, the following rules apply:\n * 1. Extension items with matching `extendsKey` are merged into their parent root item's children\n * 2. If an extension child has a `key` matching an existing child, it replaces that child\n * 3. If an extension child has a unique `key`, it's appended to the parent's children\n * 4. Extension children without a `key` are always appended\n *\n * @param rootDragButtonItems - Base drag button items from default configuration\n * @param extendsDragButtonItems - Extension drag button items from plugins\n * @returns Merged and deduplicated drag button items\n */\nconst mergeDragButtonItems = (\n  rootDragButtonItems: DragButtonType[],\n  extendsDragButtonItems: DragButtonType[]\n): DragButtonType[] => {\n  const mergedDragButtonItems: DragButtonType[] = [];\n  const extendsDragButtonItemsMap: Map<string, DragButtonItemProps[]> =\n    new Map();\n  for (const extendsDragButton of extendsDragButtonItems) {\n    if (extendsDragButton.extendsKey) {\n      if (!extendsDragButtonItemsMap.has(extendsDragButton.extendsKey)) {\n        extendsDragButtonItemsMap.set(extendsDragButton.extendsKey, []);\n      }\n      const originalItems: DragButtonItemProps[] =\n        extendsDragButtonItemsMap.get(extendsDragButton.extendsKey) ?? [];\n      extendsDragButtonItemsMap.set(extendsDragButton.extendsKey, [\n        ...originalItems,\n        extendsDragButton,\n      ]);\n    }\n  }\n\n  for (const rootDragButtonItem of rootDragButtonItems) {\n    const rootKey = rootDragButtonItem.key;\n    if (rootKey) {\n      const extendsDragButtonItems =\n        extendsDragButtonItemsMap.get(rootKey) ?? [];\n      for (const extendsDragButton of extendsDragButtonItems) {\n        mergeRootDragButtonItemsProps(rootDragButtonItem, extendsDragButton);\n      }\n    }\n\n    mergedDragButtonItems.push(rootDragButtonItem);\n  }\n\n  return mergedDragButtonItems;\n};\n\n/**\n * Merges properties from an extension drag button into a root drag button item.\n *\n * Property merge behavior:\n * - `visible`: AND logic - item visible only if all functions return true (default: true)\n * - `isActive`: OR logic - item active if any function returns true (default: false)\n * - `disabled`: OR logic - item disabled if any function returns true (default: false)\n * - `action`: Extension action executes first, falls back to root action if undefined\n * - `children`: Recursively merged with deduplication by `key`\n *\n * @param rootDragButtonItem - Root drag button item to be extended\n * @param extendsDragButton - Extension drag button providing additional behavior\n */\nconst mergeRootDragButtonItemsProps = (\n  rootDragButtonItem: DragButtonType,\n  extendsDragButton: DragButtonType\n) => {\n  mergeRootDragButtonVisibleProps(rootDragButtonItem, extendsDragButton);\n  mergeRootDragButtonIsActiveProps(rootDragButtonItem, extendsDragButton);\n  mergeRootDragButtonDisabledProps(rootDragButtonItem, extendsDragButton);\n  mergeRootDragButtonActionProps(rootDragButtonItem, extendsDragButton);\n  mergeRootDragButtonItemsChildrenProps(rootDragButtonItem, extendsDragButton);\n  return rootDragButtonItem;\n};\n\n/**\n * Recursively merges children items from extension into root drag button.\n * Performs deduplication by `key` and recursively merges nested extensions.\n *\n * @param rootDragButtonItem - Root drag button item to modify\n * @param extendsDragButtonItem - Extension providing additional children\n */\nconst mergeRootDragButtonItemsChildrenProps = (\n  rootDragButtonItem: DragButtonType,\n  extendsDragButtonItem: DragButtonType\n) => {\n  const extendsChildrenItems = extendsDragButtonItem.children?.items ?? [];\n  if (extendsChildrenItems.length === 0) {\n    return;\n  }\n\n  const items = [\n    ...(rootDragButtonItem.children?.items ?? []),\n    ...extendsChildrenItems,\n  ].filter((item, index, self) => {\n    if (item.key) {\n      return self.findIndex((t) => t.key === item.key) === index;\n    }\n    return true;\n  });\n\n  const originalItems = items.filter((item) => {\n    return item.extendsKey === undefined;\n  });\n  const extendsItems = items.filter((item) => {\n    return item.extendsKey && item.extendsKey.trim() !== \"\";\n  });\n\n  const mergedItems = mergeDragButtonItems(originalItems, extendsItems);\n\n  rootDragButtonItem.children = {\n    ...rootDragButtonItem.children,\n    items: sortDragButtonItems(mergedItems),\n  };\n};\n\n/**\n * Merges `disabled` property handlers with OR logic.\n * The merged item is disabled if either extension or root handler returns true.\n *\n * @param rootDragButtonItem - Root drag button item to modify\n * @param extendsDragButton - Extension providing additional disabled conditions\n */\nconst mergeRootDragButtonDisabledProps = (\n  rootDragButtonItem: DragButtonType,\n  extendsDragButton: DragButtonType\n) => {\n  const { disabled: extendsDisabled } = extendsDragButton;\n  const { disabled: rootDisabled } = rootDragButtonItem;\n  rootDragButtonItem.disabled = ({\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => {\n    if (extendsDisabled) {\n      const extendsDisabledResult = extendsDisabled({ editor, node, pos });\n      if (extendsDisabledResult === true) {\n        return true;\n      }\n    }\n    if (rootDisabled) {\n      return rootDisabled({ editor, node, pos });\n    }\n    return false;\n  };\n};\n\n/**\n * Merges `isActive` property handlers with OR logic.\n * The merged item is active if either extension or root handler returns true.\n *\n * @param rootDragButtonItem - Root drag button item to modify\n * @param extendsDragButton - Extension providing additional active state conditions\n */\nconst mergeRootDragButtonIsActiveProps = (\n  rootDragButtonItem: DragButtonType,\n  extendsDragButton: DragButtonType\n) => {\n  const { isActive: extendsIsActive } = extendsDragButton;\n  const { isActive: rootIsActive } = rootDragButtonItem;\n  rootDragButtonItem.isActive = ({\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => {\n    if (extendsIsActive) {\n      const extendsIsActiveResult = extendsIsActive({ editor, node, pos });\n      if (extendsIsActiveResult === true) {\n        return true;\n      }\n    }\n    if (rootIsActive) {\n      return rootIsActive({ editor, node, pos });\n    }\n    return false;\n  };\n};\n\n/**\n * Merges `action` property handlers with fallback behavior.\n * Extension action executes first; if it returns undefined, falls back to root action.\n *\n * @param rootDragButtonItem - Root drag button item to modify\n * @param extendsDragButton - Extension providing action override\n */\nconst mergeRootDragButtonActionProps = (\n  rootDragButtonItem: DragButtonType,\n  extendsDragButton: DragButtonType\n) => {\n  const { action: extendsAction } = extendsDragButton;\n  const { action: rootAction } = rootDragButtonItem;\n  if (!extendsAction && !rootAction) {\n    return;\n  }\n  rootDragButtonItem.action = async ({\n    editor,\n    node,\n    pos,\n    close,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n    close: () => void;\n  }) => {\n    if (extendsAction) {\n      const extendsActionResult = await extendsAction({\n        editor,\n        node,\n        pos,\n        close,\n      });\n\n      if (extendsActionResult !== undefined) {\n        return extendsActionResult;\n      }\n    }\n    if (rootAction) {\n      return rootAction({ editor, node, pos, close });\n    }\n    return undefined;\n  };\n};\n\n/**\n * Merges `visible` property handlers with AND logic.\n * The merged item is visible only when both extension and root handlers return true.\n *\n * @param rootDragButtonItem - Root drag button item to modify\n * @param extendsDragButton - Extension providing additional visibility conditions\n */\nconst mergeRootDragButtonVisibleProps = (\n  rootDragButtonItem: DragButtonType,\n  extendsDragButton: DragButtonType\n) => {\n  const { visible: extendsVisible } = extendsDragButton;\n  const { visible: rootVisible } = rootDragButtonItem;\n  rootDragButtonItem.visible = ({\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => {\n    if (extendsVisible) {\n      const extendsVisibleResult = extendsVisible({ editor, node, pos });\n      if (extendsVisibleResult === false) {\n        return false;\n      }\n    }\n    if (rootVisible) {\n      return rootVisible({ editor, node, pos });\n    }\n    return true;\n  };\n};\n\n/**\n * Sorts drag button items by priority in ascending order.\n *\n * @param items - Drag button items to sort\n * @returns Sorted items by priority (lower priority values appear first)\n */\nconst sortDragButtonItems = (items: DragButtonType[]): DragButtonType[] => {\n  return sortBy(items, [\"priority\"]);\n};\n</script>\n\n<template>\n  <!-- @vue-ignore-->\n  <DragHandle\n    :editor=\"editor\"\n    :compute-position-config=\"{\n      middleware: [offset(5)],\n    }\"\n    @node-change=\"handleNodeChange\"\n  >\n    <div class=\"flex items-center justify-center gap-0.5\">\n      <button\n        v-tooltip=\"{\n          content: i18n.global.t('editor.drag.button.insert_block'),\n          distance: 8,\n          delay: {\n            show: 500,\n          },\n        }\"\n        type=\"button\"\n        class=\"flex size-6 rounded-md p-0.5 hover:bg-gray-100\"\n        @click=\"handleInsertBlock\"\n      >\n        <MaterialSymbolsAddRounded class=\"size-5\" />\n      </button>\n\n      <VDropdown\n        :triggers=\"['click']\"\n        :shown=\"showMenu\"\n        :distance=\"8\"\n        placement=\"right\"\n        @show=\"handleMenuShow\"\n        @hide=\"handleMenuHide\"\n      >\n        <button\n          v-tooltip=\"{\n            content: i18n.global.t('editor.drag.button.drag_handle'),\n            distance: 8,\n            delay: {\n              show: 500,\n            },\n          }\"\n          type=\"button\"\n          class=\"flex rounded-md p-0.5 hover:bg-gray-100\"\n          @click=\"showMenu = true\"\n        >\n          <MaterialSymbolsDragIndicator class=\"size-5\" />\n        </button>\n\n        <template #popper>\n          <EditorDragMenu\n            :editor=\"editor\"\n            :node=\"currentNode\"\n            :pos=\"currentPos\"\n            :items=\"dragMenuItems\"\n            @close=\"showMenu = false\"\n          />\n        </template>\n      </VDropdown>\n    </div>\n  </DragHandle>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/drag/EditorDragMenu.vue",
    "content": "<script lang=\"ts\" setup>\nimport { onMounted, onUnmounted, type PropType, ref } from \"vue\";\nimport type { PMNode, VueEditor } from \"@/tiptap\";\nimport type { DragButtonType } from \"@/types\";\nimport { matchShortcut } from \"@/utils/keyboard\";\nimport EditorDragButtonItem from \"./EditorDragButtonItem.vue\";\n\nconst props = defineProps({\n  editor: {\n    type: Object as PropType<VueEditor>,\n    required: true,\n  },\n  node: {\n    type: Object as PropType<PMNode | null>,\n    required: true,\n  },\n  pos: {\n    type: Number,\n    required: true,\n  },\n  items: {\n    type: Array as PropType<DragButtonType[]>,\n    required: true,\n  },\n});\n\nconst emit = defineEmits<{\n  (e: \"close\"): void;\n}>();\n\nconst itemRefs = ref<Map<string, InstanceType<typeof EditorDragButtonItem>>>(\n  new Map()\n);\n\nconst handleKeyDown = (event: KeyboardEvent) => {\n  for (const item of props.items) {\n    if (item.keyboard && matchShortcut(event, item.keyboard)) {\n      event.preventDefault();\n      event.stopPropagation();\n\n      const key = item.key || \"\";\n      const itemRef = itemRefs.value.get(key);\n      if (itemRef) {\n        itemRef.triggerClick();\n      }\n      return;\n    }\n  }\n};\n\nonMounted(() => {\n  window.addEventListener(\"keydown\", handleKeyDown);\n});\n\nonUnmounted(() => {\n  window.removeEventListener(\"keydown\", handleKeyDown);\n});\n\nconst setItemRef = (key: string, ref: unknown) => {\n  if (ref && typeof ref === \"object\" && \"triggerClick\" in ref) {\n    itemRefs.value.set(key, ref as InstanceType<typeof EditorDragButtonItem>);\n  }\n  if (!ref) {\n    itemRefs.value.delete(key);\n  }\n};\n</script>\n\n<template>\n  <div class=\"flex min-w-60 flex-col gap-0.5 bg-white\">\n    <EditorDragButtonItem\n      v-for=\"(item, index) in items\"\n      :key=\"item.key || String(index)\"\n      :ref=\"(el) => setItemRef(item.key || String(index), el)\"\n      :editor=\"editor\"\n      :node=\"node\"\n      :pos=\"pos\"\n      v-bind=\"item\"\n      @close=\"emit('close')\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/drag/default-drag.ts",
    "content": "import { isActive } from \"@tiptap/core\";\nimport { markRaw } from \"vue\";\nimport MdiContentCut from \"~icons/mdi/content-cut\";\nimport MingcuteCopyLine from \"~icons/mingcute/copy-line\";\nimport MingcuteRefresh2Line from \"~icons/mingcute/refresh-2-line\";\nimport { i18n } from \"@/locales\";\nimport type { DragButtonType } from \"@/types\";\nimport { copySelectionToClipboard, deleteNode } from \"@/utils\";\nimport BlockActionHorizontalSeparator from \"../block/BlockActionHorizontalSeparator.vue\";\nimport MingcuteDelete2Line from \"../icon/MingcuteDelete2Line.vue\";\n\nexport const CONVERT_TO_KEY = \"convert-to\";\nexport const DELETE_KEY = \"delete\";\nexport const COPY_KEY = \"copy\";\nexport const CUT_KEY = \"cut\";\nexport const INSERT_BELOW_KEY = \"insert-below\";\n\nconst defaultDragItems: DragButtonType[] = [\n  {\n    priority: 10,\n    title: () => i18n.global.t(\"editor.drag.menu.convert_to\"),\n    key: CONVERT_TO_KEY,\n    icon: markRaw(MingcuteRefresh2Line),\n    children: {\n      items: [\n        // Internal sub-nodes are implemented by various extensions, such as the Header extension\n      ],\n    },\n  },\n  {\n    priority: 700,\n    component: markRaw(BlockActionHorizontalSeparator),\n    visible: ({ editor }) => {\n      if (isActive(editor.state, \"table\")) {\n        return false;\n      }\n      return true;\n    },\n  },\n  {\n    priority: 710,\n    title: () => i18n.global.t(\"editor.drag.menu.copy\"),\n    key: COPY_KEY,\n    icon: markRaw(MingcuteCopyLine),\n    keyboard: \"Mod-C\",\n    action: async ({ editor, pos, node }) => {\n      if (node) {\n        const nodeSize = node.nodeSize;\n        const from = pos;\n        const to = pos + nodeSize;\n        editor.commands.setTextSelection({ from, to });\n\n        await copySelectionToClipboard(editor);\n      }\n    },\n  },\n  {\n    priority: 720,\n    title: () => i18n.global.t(\"editor.drag.menu.cut\"),\n    key: CUT_KEY,\n    icon: markRaw(MdiContentCut),\n    keyboard: \"Mod-X\",\n    action: async ({ editor, pos, node }) => {\n      if (node) {\n        const nodeSize = node.nodeSize;\n        const from = pos;\n        const to = pos + nodeSize;\n        editor.commands.setTextSelection({ from, to });\n        const success = await copySelectionToClipboard(editor);\n        if (success) {\n          editor.chain().focus().deleteRange({ from, to }).run();\n        }\n      }\n    },\n  },\n  {\n    priority: 800,\n    component: markRaw(BlockActionHorizontalSeparator),\n  },\n  {\n    priority: 900,\n    title: () => i18n.global.t(\"editor.drag.menu.delete\"),\n    key: DELETE_KEY,\n    icon: markRaw(MingcuteDelete2Line),\n    keyboard: \"Delete\",\n    action: ({ editor, node }) => {\n      if (node) {\n        deleteNode(node?.type.name, editor);\n      }\n    },\n  },\n];\n\nexport default defaultDragItems;\n"
  },
  {
    "path": "ui/packages/editor/src/components/drag/index.ts",
    "content": "export {\n  CONVERT_TO_KEY,\n  COPY_KEY,\n  CUT_KEY,\n  DELETE_KEY,\n  default as defaultDragItems,\n} from \"./default-drag\";\nexport { default as EditorDragButtonItem } from \"./EditorDragButtonItem.vue\";\nexport { default as EditorDragHandle } from \"./EditorDragHandle.vue\";\nexport { default as EditorDragMenu } from \"./EditorDragMenu.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/components/icon/MingcuteDelete2Line.vue",
    "content": "<script lang=\"ts\" setup>\nimport MingcuteDelete2Line from \"~icons/mingcute/delete-2-line\";\n</script>\n\n<template>\n  <MingcuteDelete2Line class=\"text-red-600\" />\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/index.ts",
    "content": "// block\nexport * from \"./block\";\n\n// toolbar\nexport * from \"./toolbar\";\n\n// drag\nexport * from \"./drag\";\n\n// toolbox\nexport * from \"./toolbox\";\n\n// upload\nexport * from \"./upload\";\n\n// bubble\nexport * from \"./bubble\";\n\n// base\nexport * from \"./base\";\n\nexport { default as RichTextEditor } from \"./Editor.vue\";\nexport { default as EditorHeader } from \"./EditorHeader.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/components/toolbar/ToolbarItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { vTooltip } from \"@halo-dev/components\";\nimport MingcuteDownSmallFill from \"~icons/mingcute/down-small-fill\";\nimport type { ToolbarItemComponentProps } from \"@/types\";\n\ndefineProps<ToolbarItemComponentProps>();\n</script>\n\n<template>\n  <button\n    v-tooltip=\"title\"\n    :class=\"[\n      { 'bg-gray-200/70': isActive },\n      { 'cursor-not-allowed opacity-70': disabled },\n      { 'hover:bg-gray-100': !disabled },\n      { 'h-8 w-auto px-1.5': !!children?.length },\n      { 'size-8': !children?.length },\n    ]\"\n    class=\"inline-flex items-center justify-center rounded-md p-1 transition-colors active:!bg-gray-200\"\n    :disabled=\"disabled\"\n    tabindex=\"-1\"\n    @click=\"action\"\n  >\n    <component :is=\"icon\" />\n    <MingcuteDownSmallFill v-if=\"children?.length\" />\n  </button>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/toolbar/ToolbarSubItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ToolbarItemComponentProps } from \"@/types\";\nimport DropdownItem from \"../base/DropdownItem.vue\";\n\nconst props = defineProps<ToolbarItemComponentProps>();\n\nconst action = () => {\n  if (props.disabled) return;\n  props.action?.();\n};\n</script>\n\n<template>\n  <DropdownItem :disabled=\"disabled\" :is-active=\"isActive\" @click=\"action\">\n    <template v-if=\"icon\" #icon>\n      <component :is=\"icon\" />\n    </template>\n    {{ title }}\n  </DropdownItem>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/toolbar/index.ts",
    "content": "export { default as ToolbarItem } from \"./ToolbarItem.vue\";\nexport { default as ToolbarSubItem } from \"./ToolbarSubItem.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/components/toolbox/ToolboxItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ToolboxItemComponentProps } from \"@/types\";\n\ndefineProps<ToolboxItemComponentProps>();\n</script>\n\n<template>\n  <div\n    role=\"menuitem\"\n    tabindex=\"-1\"\n    class=\"group my-1.5 flex cursor-pointer flex-row items-center gap-3 rounded px-1.5 py-1 transition-colors first:mt-0 last:mb-0 hover:bg-gray-100\"\n    @click=\"action?.()\"\n  >\n    <div\n      class=\"size-7 flex-none rounded bg-gray-100 p-1.5 group-hover:bg-white\"\n    >\n      <component :is=\"icon\" class=\"size-full\" />\n    </div>\n\n    <div class=\"flex min-w-0 flex-1 shrink flex-col gap-0.5\">\n      <span\n        class=\"line-clamp-1 text-sm text-gray-600 group-hover:font-medium group-hover:text-gray-900\"\n        :title=\"title\"\n      >\n        {{ title }}\n      </span>\n      <span\n        v-if=\"description\"\n        class=\"line-clamp-2 text-xs text-gray-500\"\n        :title=\"description\"\n      >\n        {{ description }}\n      </span>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/toolbox/index.ts",
    "content": "export { default as ToolboxItem } from \"./ToolboxItem.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/components/upload/EditorLinkObtain.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport { VButton, VDropdown, VSpace } from \"@halo-dev/components\";\nimport {\n  utils,\n  type AttachmentLike,\n  type AttachmentSimple,\n} from \"@halo-dev/ui-shared\";\nimport { useFileDialog } from \"@vueuse/core\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { onUnmounted, ref, watch } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport { uploadFile } from \"@/utils/upload\";\nimport Input from \"../base/Input.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    editor: Editor;\n    accept?: string;\n    uploadedFile?: File;\n    uploadToAttachmentFile: (\n      file: File,\n      options?: AxiosRequestConfig\n    ) => Promise<Attachment>;\n  }>(),\n  {\n    accept: \"*\",\n    uploadedFile: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"setExternalLink\", attachment?: AttachmentSimple): void;\n  (event: \"onUploadReady\", file: File): void;\n  (event: \"onUploadProgress\", progress: number): void;\n  (event: \"onUploadFinish\"): void;\n  (event: \"onUploadError\", error: Error): void;\n  (event: \"onUploadAbort\"): void;\n}>();\n\nconst externalLink = ref(\"\");\n\nconst handleEnterSetExternalLink = () => {\n  if (!externalLink.value) {\n    return;\n  }\n  emit(\"setExternalLink\", {\n    url: externalLink.value,\n  });\n};\n\nconst { open, reset, onChange } = useFileDialog({\n  accept: props.accept,\n  multiple: false,\n});\n\nconst controller = ref<AbortController>();\nconst originalFile = ref<File>();\nconst uploadState = ref<\"init\" | \"uploading\" | \"error\">(\"init\");\nconst uploadProgress = ref<number | undefined>(undefined);\n\n/**\n *\n * Upload files to the attachment library.\n *\n * @param file attachments that need to be uploaded to the attachment library\n */\nconst handleUploadFile = (file: File) => {\n  controller.value = new AbortController();\n  originalFile.value = file;\n  uploadState.value = \"uploading\";\n  uploadProgress.value = undefined;\n  emit(\"onUploadReady\", file);\n  uploadFile(file, props.uploadToAttachmentFile, {\n    controller: controller.value,\n    onUploadProgress: (progress) => {\n      uploadProgress.value = progress;\n      emit(\"onUploadProgress\", progress);\n    },\n\n    onFinish: (attachment?: Attachment) => {\n      if (attachment) {\n        emit(\"setExternalLink\", {\n          url: attachment.status?.permalink,\n        } as AttachmentSimple);\n      }\n      handleResetUpload();\n      emit(\"onUploadFinish\");\n    },\n\n    onError: (error: Error) => {\n      if (error.name !== \"CanceledError\") {\n        uploadState.value = \"error\";\n      }\n      emit(\"onUploadError\", error);\n    },\n  });\n};\n\nconst handleUploadAbort = () => {\n  emit(\"onUploadAbort\");\n  handleResetUpload();\n};\n\nconst handleUploadRetry = () => {\n  if (!controller.value) {\n    return;\n  }\n  controller.value.abort();\n  if (!originalFile.value) {\n    return;\n  }\n  handleUploadFile(originalFile.value);\n};\n\nconst handleResetUpload = () => {\n  uploadState.value = \"init\";\n  controller.value?.abort();\n  controller.value = undefined;\n  originalFile.value = undefined;\n  uploadProgress.value = undefined;\n  reset();\n};\n\nonChange((files) => {\n  if (!files) {\n    return;\n  }\n  if (files.length > 0) {\n    handleUploadFile(files[0]);\n  }\n});\n\nwatch(\n  () => props.uploadedFile,\n  async (file) => {\n    if (!file) {\n      return;\n    }\n    handleUploadFile(file);\n  },\n  {\n    immediate: true,\n  }\n);\n\nonUnmounted(() => {\n  handleUploadAbort();\n});\n\ndefineExpose({\n  abort: handleUploadAbort,\n  retry: handleUploadRetry,\n  reset: handleResetUpload,\n});\n\n// Attachment Selector Modal\nconst attachmentSelectorModalVisible = ref(false);\n\nfunction onAttachmentSelect(attachments: AttachmentLike[]) {\n  if (!attachments.length) {\n    return;\n  }\n  const attachment = attachments[0];\n  const attachmentSimple = utils.attachment.convertToSimple(attachment);\n  emit(\"setExternalLink\", attachmentSimple);\n  attachmentSelectorModalVisible.value = false;\n}\n</script>\n<template>\n  <AttachmentSelectorModal\n    v-if=\"attachmentSelectorModalVisible\"\n    :accepts=\"[props.accept]\"\n    :min=\"1\"\n    :max=\"1\"\n    @select=\"onAttachmentSelect\"\n    @close=\"attachmentSelectorModalVisible = false\"\n  />\n  <div class=\"flex h-64 w-full items-center justify-center\">\n    <slot\n      v-if=\"$slots.uploading && uploadState === 'uploading'\"\n      name=\"uploading\"\n      :progress=\"uploadProgress\"\n    ></slot>\n    <slot\n      v-else-if=\"$slots.error && uploadState === 'error'\"\n      name=\"error\"\n    ></slot>\n    <div\n      v-else\n      class=\"flex size-full cursor-pointer flex-col items-center justify-center border-2 border-dashed border-gray-300 bg-gray-50\"\n    >\n      <div\n        class=\"flex flex-col items-center justify-center space-y-7 pb-6 pt-5\"\n      >\n        <div\n          v-if=\"$slots.icon\"\n          class=\"flex h-14 w-14 items-center justify-center rounded-full bg-primary/20\"\n        >\n          <slot name=\"icon\"></slot>\n        </div>\n        <VSpace>\n          <VButton\n            v-if=\"utils.permission.has(['uc:attachments:manage'])\"\n            @click=\"open()\"\n          >\n            {{ i18n.global.t(\"editor.common.button.upload\") }}\n          </VButton>\n\n          <VButton\n            v-if=\"\n              utils.permission.has([\n                'system:attachments:view',\n                'uc:attachments:manage',\n              ])\n            \"\n            @click=\"attachmentSelectorModalVisible = true\"\n          >\n            {{ i18n.global.t(\"editor.extensions.upload.attachment.title\") }}\n          </VButton>\n\n          <VDropdown>\n            <VButton>\n              {{ i18n.global.t(\"editor.extensions.upload.permalink.title\") }}\n            </VButton>\n            <template #popper>\n              <div class=\"w-80\">\n                <Input\n                  v-model=\"externalLink\"\n                  auto-focus\n                  :placeholder=\"\n                    i18n.global.t(\n                      'editor.extensions.upload.permalink.placeholder'\n                    )\n                  \"\n                  @keydown.enter=\"handleEnterSetExternalLink\"\n                />\n              </div>\n            </template>\n          </VDropdown>\n        </VSpace>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/upload/ResourceReplaceButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VDropdown, VDropdownItem } from \"@halo-dev/components\";\nimport {\n  utils,\n  type AttachmentLike,\n  type AttachmentSimple,\n} from \"@halo-dev/ui-shared\";\nimport { useFileDialog } from \"@vueuse/core\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { ref } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport Input from \"../base/Input.vue\";\n\nconst props = defineProps<{\n  originalLink?: string;\n  accept?: string;\n  upload: (file: File, options?: AxiosRequestConfig) => Promise<Attachment>;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"change\", attachment?: AttachmentSimple): void;\n}>();\n\n// Attachment Selector Modal\nconst attachmentSelectorModalVisible = ref(false);\n\nfunction onAttachmentSelect(attachments: AttachmentLike[]) {\n  if (!attachments.length) {\n    return;\n  }\n  const attachment = attachments[0];\n  const attachmentSimple = utils.attachment.convertToSimple(attachment);\n  emit(\"change\", attachmentSimple);\n  attachmentSelectorModalVisible.value = false;\n}\n\n// Upload\nconst { onChange: onInputChange, open } = useFileDialog({\n  accept: props.accept,\n  multiple: false,\n});\n\nonInputChange((files) => {\n  const file = files?.[0];\n  if (!file) {\n    return;\n  }\n  props\n    .upload?.(file)\n    .then((attachment) => {\n      emit(\"change\", utils.attachment.convertToSimple(attachment));\n    })\n    .catch((e: Error) => {\n      Toast.error(\n        `${i18n.global.t(\"editor.extensions.upload.error\")} - ${e.message}`\n      );\n    });\n});\n\nfunction onExternalLinkChange(value?: string | number) {\n  if (value && typeof value === \"string\") {\n    emit(\"change\", utils.attachment.convertToSimple(value));\n  }\n}\n</script>\n<template>\n  <VDropdown class=\"inline-flex\">\n    <VButton size=\"sm\" type=\"secondary\">\n      {{ i18n.global.t(\"editor.extensions.upload.operations.replace.button\") }}\n    </VButton>\n    <template #popper>\n      <VDropdownItem\n        v-if=\"utils.permission.has(['uc:attachments:manage'])\"\n        @click=\"open()\"\n      >\n        {{ i18n.global.t(\"editor.common.button.upload\") }}\n      </VDropdownItem>\n      <VDropdownItem\n        v-if=\"\n          utils.permission.has([\n            'system:attachments:view',\n            'uc:attachments:manage',\n          ])\n        \"\n        @click=\"attachmentSelectorModalVisible = true\"\n      >\n        {{ i18n.global.t(\"editor.extensions.upload.attachment.title\") }}\n      </VDropdownItem>\n      <VDropdown>\n        <VDropdownItem>\n          {{ i18n.global.t(\"editor.extensions.upload.permalink.title\") }}\n        </VDropdownItem>\n        <template #popper>\n          <div class=\"w-80\">\n            <Input\n              auto-focus\n              :model-value=\"originalLink\"\n              :placeholder=\"\n                i18n.global.t('editor.extensions.upload.permalink.placeholder')\n              \"\n              @update:model-value=\"onExternalLinkChange\"\n            />\n          </div>\n        </template>\n      </VDropdown>\n    </template>\n  </VDropdown>\n\n  <AttachmentSelectorModal\n    v-if=\"attachmentSelectorModalVisible\"\n    :accepts=\"[accept].filter(Boolean)\"\n    :min=\"1\"\n    :max=\"1\"\n    @select=\"onAttachmentSelect\"\n    @close=\"attachmentSelectorModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/components/upload/index.ts",
    "content": "export { default as EditorLinkObtain } from \"./EditorLinkObtain.vue\";\nexport { default as ResourceReplaceButton } from \"./ResourceReplaceButton.vue\";\n"
  },
  {
    "path": "ui/packages/editor/src/composables/use-attachment.ts",
    "content": "import { ucApiClient } from \"@halo-dev/api-client\";\nimport { Toast } from \"@halo-dev/components\";\nimport { stores, type AttachmentSimple } from \"@halo-dev/ui-shared\";\nimport { computed, ref, type Ref } from \"vue\";\nimport { i18n } from \"@/locales\";\n\nexport function useExternalAssetsTransfer(\n  src: Ref<string | undefined>,\n  callback: (attachment: AttachmentSimple) => void\n) {\n  const { globalInfo } = stores.globalInfo();\n\n  const isExternalAsset = computed(() => {\n    if (src.value?.startsWith(\"/\")) {\n      return false;\n    }\n\n    if (!globalInfo?.externalUrl) {\n      return false;\n    }\n\n    return !src.value?.startsWith(globalInfo?.externalUrl);\n  });\n\n  const transferring = ref(false);\n\n  async function handleTransfer() {\n    if (!src.value) {\n      return;\n    }\n\n    transferring.value = true;\n\n    const { data } = await ucApiClient.storage.attachment.uploadAttachmentForUc(\n      {\n        url: src.value,\n      }\n    );\n\n    callback({\n      url: data.status?.permalink || \"\",\n      alt: data.spec.displayName,\n    });\n\n    Toast.success(i18n.global.t(\"editor.common.toast.save_success\"));\n\n    transferring.value = false;\n  }\n\n  return {\n    isExternalAsset,\n    transferring,\n    handleTransfer,\n  };\n}\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/align/index.ts",
    "content": "import { markRaw } from \"vue\";\nimport MingcuteAlignCenterLine from \"~icons/mingcute/align-center-line\";\nimport MingcuteAlignJustifyLine from \"~icons/mingcute/align-justify-line\";\nimport MingcuteAlignLeftLine from \"~icons/mingcute/align-left-line\";\nimport MingcuteAlignRightLine from \"~icons/mingcute/align-right-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport ToolbarSubItem from \"@/components/toolbar/ToolbarSubItem.vue\";\nimport { i18n } from \"@/locales\";\nimport { Extension, type Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nconst inlineIconComponent = {\n  left: MingcuteAlignLeftLine,\n  center: MingcuteAlignCenterLine,\n  right: MingcuteAlignRightLine,\n  justify: MingcuteAlignJustifyLine,\n};\n\nconst blockIconComponent = {\n  start: MingcuteAlignLeftLine,\n  center: MingcuteAlignCenterLine,\n  end: MingcuteAlignRightLine,\n};\n\nconst getIcon = (editor: Editor) => {\n  let icon = MingcuteAlignLeftLine;\n  Object.entries(inlineIconComponent).forEach(([key, value]) => {\n    if (editor.isActive({ textAlign: key })) {\n      icon = value;\n      return;\n    }\n  });\n  Object.entries(blockIconComponent).forEach(([key, value]) => {\n    if (editor.isActive({ alignItems: key })) {\n      icon = value;\n      return;\n    }\n  });\n  return icon;\n};\n\n/**\n * The extension for the align attribute. Including inline and block nodes.\n */\nexport const ExtensionAlign = Extension.create<ExtensionOptions>({\n  name: \"align\",\n\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 180,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: false,\n            icon: markRaw(getIcon(editor)),\n            title: i18n.global.t(\"editor.common.align_method\"),\n          },\n          children: [\n            {\n              priority: 0,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive:\n                  editor.isActive({ textAlign: \"left\" }) ||\n                  editor.isActive({ alignItems: \"start\" }),\n                icon: markRaw(MingcuteAlignLeftLine),\n                title: i18n.global.t(\"editor.common.align_left\"),\n                action: () => {\n                  console.log(\n                    editor.isActive({ textAlign: \"left\" }),\n                    editor.isActive({ alignItems: \"start\" })\n                  );\n                  return editor\n                    .chain()\n                    .focus()\n                    .setTextAlign(\"left\")\n                    .setBlockPosition(\"start\")\n                    .run();\n                },\n              },\n            },\n            {\n              priority: 10,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive:\n                  editor.isActive({ textAlign: \"center\" }) ||\n                  editor.isActive({ alignItems: \"center\" }),\n                icon: markRaw(MingcuteAlignCenterLine),\n                title: i18n.global.t(\"editor.common.align_center\"),\n                action: () =>\n                  editor\n                    .chain()\n                    .focus()\n                    .setTextAlign(\"center\")\n                    .setBlockPosition(\"center\")\n                    .run(),\n              },\n            },\n            {\n              priority: 20,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive:\n                  editor.isActive({ textAlign: \"right\" }) ||\n                  editor.isActive({ alignItems: \"end\" }),\n                icon: markRaw(MingcuteAlignRightLine),\n                title: i18n.global.t(\"editor.common.align_right\"),\n                action: () =>\n                  editor\n                    .chain()\n                    .focus()\n                    .setTextAlign(\"right\")\n                    .setBlockPosition(\"end\")\n                    .run(),\n              },\n            },\n            {\n              priority: 30,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive({ textAlign: \"justify\" }),\n                icon: markRaw(MingcuteAlignJustifyLine),\n                title: i18n.global.t(\"editor.common.align_justify\"),\n                action: () =>\n                  editor.chain().focus().setTextAlign(\"justify\").run(),\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/audio/AudioView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton } from \"@halo-dev/components\";\nimport { utils, type AttachmentSimple } from \"@halo-dev/ui-shared\";\nimport { computed, ref } from \"vue\";\nimport MingcuteMusic2Line from \"~icons/mingcute/music-2-line\";\nimport { EditorLinkObtain } from \"@/components\";\nimport { ResourceReplaceButton } from \"@/components/upload\";\nimport { useExternalAssetsTransfer } from \"@/composables/use-attachment\";\nimport { i18n } from \"@/locales\";\nimport {\n  findParentNodeClosestToPos,\n  NodeViewWrapper,\n  type NodeViewProps,\n} from \"@/tiptap\";\nimport { ExtensionFigure } from \"../figure\";\n\nconst props = defineProps<NodeViewProps>();\n\nconst src = computed({\n  get: () => {\n    return props.node?.attrs.src;\n  },\n  set: (src: string) => {\n    props.updateAttributes({ src: src });\n  },\n});\n\nconst autoplay = computed(() => {\n  return props.node.attrs.autoplay;\n});\n\nconst loop = computed(() => {\n  return props.node.attrs.loop;\n});\n\nconst initialization = computed(() => {\n  return !src.value;\n});\n\n// Get the align items of the audio from the figure parent\nconst alignItems = computed(() => {\n  const pos = props.getPos();\n  if (!pos) {\n    return \"start\";\n  }\n  const $pos = props.editor.state.doc.resolve(pos);\n  const figureParent = findParentNodeClosestToPos(\n    $pos,\n    (node) => node.type.name === ExtensionFigure.name\n  );\n\n  if (figureParent) {\n    return figureParent.node.attrs.alignItems;\n  }\n\n  return \"start\";\n});\n\nconst editorLinkObtain = ref();\n\nconst handleSetExternalLink = (attachment?: AttachmentSimple) => {\n  if (!attachment) return;\n  props.updateAttributes({\n    src: attachment.url,\n  });\n};\n\nconst resetUpload = () => {\n  const { file } = props.node.attrs;\n  if (file) {\n    props.updateAttributes({\n      width: undefined,\n      height: undefined,\n      file: undefined,\n    });\n  }\n};\n\nconst handleUploadRetry = () => {\n  editorLinkObtain.value?.reset();\n};\n\nconst handleUploadAbort = () => {\n  editorLinkObtain.value?.abort();\n};\n\nconst { isExternalAsset, transferring, handleTransfer } =\n  useExternalAssetsTransfer(src, handleSetExternalLink);\n\nconst isPercentageWidth = computed(() => {\n  return props.node?.attrs.width?.includes(\"%\");\n});\n</script>\n\n<template>\n  <node-view-wrapper\n    as=\"div\"\n    class=\"flex w-full\"\n    :class=\"{\n      [`justify-${alignItems}`]: true,\n    }\"\n  >\n    <div\n      class=\"relative inline-block h-full max-w-full overflow-hidden rounded-md text-center transition-all\"\n      :class=\"{\n        'rounded ring-2': selected,\n      }\"\n      :style=\"{\n        width: initialization ? '100%' : node.attrs.width,\n        height: initialization ? '100%' : node.attrs.height,\n      }\"\n    >\n      <div v-if=\"src\" class=\"group relative\">\n        <audio\n          class=\"m-0 rounded-md\"\n          controls\n          :src=\"src\"\n          :autoplay=\"autoplay\"\n          :loop=\"loop\"\n          preload=\"metadata\"\n          :style=\"{\n            width: isPercentageWidth ? '100%' : node.attrs.width,\n            height: isPercentageWidth ? '100%' : node.attrs.height,\n          }\"\n        ></audio>\n        <div\n          v-if=\"src\"\n          class=\"absolute left-0 top-0 hidden w-full cursor-pointer justify-end gap-2 rounded-md bg-gradient-to-b from-gray-300 to-transparent p-2 ease-in-out group-hover:flex\"\n        >\n          <VButton\n            v-if=\"\n              utils.permission.has([\n                'uc:attachments:manage',\n                'system:attachments:manage',\n              ]) && isExternalAsset\n            \"\n            v-tooltip=\"\n              i18n.global.t(\n                'editor.extensions.upload.operations.transfer.tooltip'\n              )\n            \"\n            :loading=\"transferring\"\n            size=\"sm\"\n            ghost\n            @click=\"handleTransfer\"\n          >\n            {{\n              i18n.global.t(\n                \"editor.extensions.upload.operations.transfer.button\"\n              )\n            }}\n          </VButton>\n          <ResourceReplaceButton\n            accept=\"audio/*\"\n            :original-link=\"src\"\n            :upload=\"extension.options.uploadAudio\"\n            @change=\"handleSetExternalLink\"\n          />\n        </div>\n      </div>\n      <div v-show=\"!src\" class=\"relative w-full\">\n        <EditorLinkObtain\n          ref=\"editorLinkObtain\"\n          class=\"!h-40 w-full\"\n          :accept=\"'audio/*'\"\n          :editor=\"editor\"\n          :upload-to-attachment-file=\"extension.options.uploadAudio\"\n          :uploaded-file=\"node?.attrs.file\"\n          @set-external-link=\"handleSetExternalLink\"\n          @on-upload-finish=\"resetUpload\"\n          @on-upload-abort=\"resetUpload\"\n        >\n          <template #icon>\n            <MingcuteMusic2Line class=\"text-xl text-primary\" />\n          </template>\n          <template #uploading=\"{ progress }\">\n            <div class=\"absolute top-0 size-full bg-black bg-opacity-20\">\n              <div class=\"absolute top-[50%] w-full space-y-2 text-white\">\n                <div class=\"px-10\">\n                  <div\n                    class=\"relative h-4 w-full overflow-hidden rounded-full bg-gray-200\"\n                  >\n                    <div\n                      class=\"h-full bg-primary\"\n                      :style=\"{\n                        width: `${progress || 0}%`,\n                      }\"\n                    ></div>\n                    <div\n                      class=\"absolute left-[50%] top-0 -translate-x-[50%] text-xs leading-4 text-white\"\n                    >\n                      {{\n                        progress\n                          ? `${progress}%`\n                          : `${i18n.global.t(\"editor.extensions.upload.loading\")}...`\n                      }}\n                    </div>\n                  </div>\n                </div>\n\n                <div\n                  class=\"inline-block cursor-pointer text-sm hover:opacity-70\"\n                  @click=\"handleUploadAbort\"\n                >\n                  {{ i18n.global.t(\"editor.common.button.cancel\") }}\n                </div>\n              </div>\n            </div>\n          </template>\n          <template #error>\n            <div class=\"absolute top-0 size-full bg-black bg-opacity-20\">\n              <div class=\"absolute top-[50%] w-full space-y-2 text-white\">\n                <div class=\"px-10\">\n                  <div\n                    class=\"relative h-4 w-full overflow-hidden rounded-full bg-gray-200\"\n                  >\n                    <div class=\"size-full bg-red-600\"></div>\n                    <div\n                      class=\"absolute left-[50%] top-0 -translate-x-[50%] text-xs leading-4 text-white\"\n                    >\n                      {{ i18n.global.t(\"editor.extensions.upload.error\") }}\n                    </div>\n                  </div>\n                </div>\n                <div\n                  class=\"inline-block cursor-pointer text-sm hover:opacity-70\"\n                  @click=\"handleUploadRetry\"\n                >\n                  {{ i18n.global.t(\"editor.extensions.upload.click_retry\") }}\n                </div>\n              </div>\n            </div>\n          </template>\n        </EditorLinkObtain>\n      </div>\n    </div>\n  </node-view-wrapper>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/audio/BubbleItemAudioLink.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport Input from \"@/components/base/Input.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionAudio } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst src = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionAudio.name)?.src;\n  },\n  set: (src: string) => {\n    props.editor\n      .chain()\n      .updateAttributes(ExtensionAudio.name, { src: src })\n      .setNodeSelection(props.editor.state.selection.from)\n      .focus()\n      .run();\n  },\n});\n</script>\n\n<template>\n  <div class=\"w-80\">\n    <Input\n      v-model=\"src\"\n      auto-focus\n      :placeholder=\"i18n.global.t('editor.common.placeholder.link_input')\"\n      :label=\"i18n.global.t('editor.extensions.audio.src_input_label')\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/audio/BubbleItemAudioPosition.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { findParentNode } from \"@tiptap/core\";\nimport { computed } from \"vue\";\nimport MingcuteAlignCenterLine from \"~icons/mingcute/align-center-line\";\nimport MingcuteAlignLeftLine from \"~icons/mingcute/align-left-line\";\nimport MingcuteAlignRightLine from \"~icons/mingcute/align-right-line\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionAudio } from \"..\";\n\nconst props = withDefaults(defineProps<BubbleItemComponentProps>(), {\n  visible: () => true,\n});\n\nconst positionOptions = [\n  {\n    text: i18n.global.t(\"editor.common.align_left\"),\n    value: \"start\",\n    icon: MingcuteAlignLeftLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_center\"),\n    value: \"center\",\n    icon: MingcuteAlignCenterLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_right\"),\n    value: \"end\",\n    icon: MingcuteAlignRightLine,\n  },\n];\n\nconst currentPosition = computed(() => {\n  const positionAttribute = props.editor.getAttributes(\n    ExtensionAudio.name\n  ).position;\n\n  const positionOption = positionOptions.find(\n    (option) => option.value === positionAttribute\n  );\n\n  if (!positionOption) {\n    return positionOptions[0];\n  }\n\n  return positionOption;\n});\n\nconst isActive = (blockPosition: string) => {\n  const audioParent = findParentNode(\n    (node) => node.type.name === ExtensionAudio.name\n  )(props.editor.state.selection);\n  if (!audioParent) {\n    return false;\n  }\n  return audioParent.node.attrs.alignItems === blockPosition;\n};\n\nconst handleSetPosition = (position: string) => {\n  return props.editor.chain().focus().setBlockPosition(position).run();\n};\n</script>\n<template>\n  <VDropdown\n    v-if=\"visible({ editor })\"\n    class=\"inline-flex\"\n    :auto-hide=\"true\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.common.align_method')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <component :is=\"currentPosition.icon\" />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <div class=\"relative max-h-72 w-56 overflow-hidden overflow-y-auto\">\n        <DropdownItem\n          v-for=\"option in positionOptions\"\n          :key=\"option.value\"\n          v-close-popper\n          :is-active=\"isActive(option.value)\"\n          @click=\"handleSetPosition(option.value)\"\n        >\n          <template #icon>\n            <component :is=\"option.icon\" />\n          </template>\n          {{ option.text }}\n        </DropdownItem>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/audio/index.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { isEmpty } from \"es-toolkit/compat\";\nimport { markRaw } from \"vue\";\nimport MdiMotionPlay from \"~icons/mdi/motion-play\";\nimport MdiMotionPlayOutline from \"~icons/mdi/motion-play-outline\";\nimport MdiPlayCircle from \"~icons/mdi/play-circle\";\nimport MdiPlayCircleOutline from \"~icons/mdi/play-circle-outline\";\nimport MingcuteLinkLine from \"~icons/mingcute/link-line\";\nimport MingcuteMusic2Line from \"~icons/mingcute/music-2-line\";\nimport MingcuteShare3Line from \"~icons/mingcute/share-3-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport ToolboxItem from \"@/components/toolbox/ToolboxItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  findParentNode,\n  isActive,\n  mergeAttributes,\n  Node,\n  nodeInputRule,\n  PluginKey,\n  VueNodeViewRenderer,\n  type Range,\n} from \"@/tiptap\";\nimport type { EditorState } from \"@/tiptap/pm\";\nimport type { ExtensionOptions, NodeBubbleMenuType } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\nimport AudioView from \"./AudioView.vue\";\nimport BubbleItemAudioLink from \"./BubbleItemAudioLink.vue\";\nimport BubbleItemAudioPosition from \"./BubbleItemAudioPosition.vue\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    audio: {\n      setAudio: (options: { src: string }) => ReturnType;\n    };\n  }\n}\n\nexport const AUDIO_BUBBLE_MENU_KEY = new PluginKey(\"audioBubbleMenu\");\n\nexport interface ExtensionAudioOptions extends ExtensionOptions {\n  uploadAudio?: (\n    file: File,\n    options?: AxiosRequestConfig\n  ) => Promise<Attachment>;\n}\n\nexport const ExtensionAudio = Node.create<ExtensionAudioOptions>({\n  name: \"audio\",\n  fakeSelection: true,\n\n  inline: false,\n\n  group: \"block\",\n\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      src: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"src\");\n        },\n      },\n      autoplay: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"autoplay\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            autoplay: attributes.autoplay,\n          };\n        },\n      },\n      controls: {\n        default: true,\n        parseHTML: (element) => {\n          return element.getAttribute(\"controls\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            controls: attributes.controls,\n          };\n        },\n      },\n      loop: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"loop\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            loop: attributes.loop,\n          };\n        },\n      },\n      file: {\n        default: null,\n        renderHTML() {\n          return {};\n        },\n        parseHTML() {\n          return null;\n        },\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"audio\",\n      },\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\"audio\", mergeAttributes(HTMLAttributes)];\n  },\n\n  addCommands() {\n    return {\n      setAudio:\n        (options) =>\n        ({ commands }) => {\n          return commands.insertContent({\n            type: this.name,\n            attrs: options,\n          });\n        },\n    };\n  },\n\n  addInputRules() {\n    return [\n      nodeInputRule({\n        find: /^\\$audio\\$$/,\n        type: this.type,\n        getAttributes: () => {\n          return { width: \"100%\" };\n        },\n      }),\n    ];\n  },\n\n  addNodeView() {\n    return VueNodeViewRenderer(AudioView);\n  },\n\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      uploadAudio: undefined,\n      getCommandMenuItems() {\n        return {\n          priority: 110,\n          icon: markRaw(MingcuteMusic2Line),\n          title: \"editor.extensions.commands_menu.audio\",\n          keywords: [\"audio\", \"yinpin\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .insertContent([\n                {\n                  type: \"figure\",\n                  attrs: { contentType: \"audio\" },\n                  content: [{ type: \"audio\", attrs: { src: \"\" } }],\n                },\n                { type: \"paragraph\", content: \"\" },\n              ])\n              .run();\n          },\n        };\n      },\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 30,\n          component: markRaw(ToolboxItem),\n          props: {\n            editor,\n            icon: markRaw(MingcuteMusic2Line),\n            title: i18n.global.t(\"editor.extensions.commands_menu.audio\"),\n            action: () => {\n              editor\n                .chain()\n                .focus()\n                .insertContent([\n                  {\n                    type: \"figure\",\n                    attrs: { contentType: \"audio\" },\n                    content: [{ type: \"audio\", attrs: { src: \"\" } }],\n                  },\n                ])\n                .run();\n            },\n          },\n        };\n      },\n      getBubbleMenu({ editor }: { editor: Editor }): NodeBubbleMenuType {\n        return {\n          pluginKey: AUDIO_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }) => {\n            return isActive(state, ExtensionAudio.name);\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionAudio.name).src\n                  );\n                },\n                isActive: () => {\n                  return editor.getAttributes(ExtensionAudio.name).autoplay;\n                },\n                icon: markRaw(\n                  editor.getAttributes(ExtensionAudio.name).autoplay\n                    ? MdiPlayCircle\n                    : MdiPlayCircleOutline\n                ),\n                action: () => {\n                  editor\n                    .chain()\n                    .updateAttributes(ExtensionAudio.name, {\n                      autoplay: editor.getAttributes(ExtensionAudio.name)\n                        .autoplay\n                        ? null\n                        : true,\n                    })\n                    .setNodeSelection(editor.state.selection.from)\n                    .focus()\n                    .run();\n                },\n                title: editor.getAttributes(ExtensionAudio.name).autoplay\n                  ? i18n.global.t(\"editor.extensions.audio.disable_autoplay\")\n                  : i18n.global.t(\"editor.extensions.audio.enable_autoplay\"),\n              },\n            },\n            {\n              priority: 20,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionAudio.name).src\n                  );\n                },\n                isActive: () => {\n                  return editor.getAttributes(ExtensionAudio.name).loop;\n                },\n                icon: markRaw(\n                  editor.getAttributes(ExtensionAudio.name).loop\n                    ? MdiMotionPlay\n                    : MdiMotionPlayOutline\n                ),\n                action: () => {\n                  editor\n                    .chain()\n                    .updateAttributes(ExtensionAudio.name, {\n                      loop: editor.getAttributes(ExtensionAudio.name).loop\n                        ? null\n                        : true,\n                    })\n                    .setNodeSelection(editor.state.selection.from)\n                    .focus()\n                    .run();\n                },\n                title: editor.getAttributes(ExtensionAudio.name).loop\n                  ? i18n.global.t(\"editor.extensions.audio.disable_loop\")\n                  : i18n.global.t(\"editor.extensions.audio.enable_loop\"),\n              },\n            },\n            {\n              priority: 30,\n              component: markRaw(BlockActionSeparator),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionAudio.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 40,\n              component: markRaw(BubbleItemAudioPosition),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionAudio.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 50,\n              component: markRaw(BlockActionSeparator),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionAudio.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 60,\n              props: {\n                icon: markRaw(MingcuteLinkLine),\n                title: i18n.global.t(\"editor.common.button.edit_link\"),\n                action: () => {\n                  return markRaw(BubbleItemAudioLink);\n                },\n              },\n            },\n            {\n              priority: 70,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionAudio.name).src\n                  );\n                },\n                icon: markRaw(MingcuteShare3Line),\n                title: i18n.global.t(\"editor.common.tooltip.open_link\"),\n                action: () => {\n                  window.open(\n                    editor.getAttributes(ExtensionAudio.name).src,\n                    \"_blank\"\n                  );\n                },\n              },\n            },\n            {\n              priority: 80,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 90,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }) => {\n                  const figureParent = findParentNode(\n                    (node) => node.type.name === \"figure\"\n                  )(editor.state.selection);\n                  deleteNode(\n                    figureParent ? \"figure\" : ExtensionAudio.name,\n                    editor\n                  );\n                },\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/block-position/index.ts",
    "content": "import { Extension } from \"@tiptap/core\";\n\nexport interface ExtensionBlockPositionOptions {\n  /**\n   * The types where the block position attribute can be applied.\n   * @default []\n   * @example ['figure']\n   */\n  types: string[];\n\n  /**\n   * The positions which are allowed.\n   * @default ['start', 'center', 'end']\n   * @example ['start', 'center']\n   */\n  positions: string[];\n\n  /**\n   * The default position.\n   * @default null\n   * @example 'start'\n   */\n  defaultPosition: string | null;\n}\n\ndeclare module \"@tiptap/core\" {\n  interface Commands<ReturnType> {\n    blockPosition: {\n      /**\n       * Set the block position attribute\n       * @param position The position (\"start\", \"center\", \"end\")\n       * @example editor.commands.setBlockPosition('start')\n       */\n      setBlockPosition: (position: string) => ReturnType;\n      /**\n       * Unset the block position attribute\n       * @example editor.commands.unsetBlockPosition()\n       */\n      unsetBlockPosition: () => ReturnType;\n      /**\n       * Toggle the block position attribute\n       * @param alignment The alignment\n       * @example editor.commands.toggleBlockPosition('end')\n       */\n      toggleBlockPosition: (position: string) => ReturnType;\n    };\n  }\n}\n\n/**\n * This extension allows to set the block position.\n */\nexport const ExtensionBlockPosition =\n  Extension.create<ExtensionBlockPositionOptions>({\n    name: \"blockPosition\",\n\n    addOptions() {\n      return {\n        types: [],\n        positions: [\"start\", \"center\", \"end\"],\n        defaultPosition: null,\n      };\n    },\n\n    addGlobalAttributes() {\n      return [\n        {\n          types: this.options.types,\n          attributes: {\n            alignItems: {\n              default: this.options.defaultPosition,\n              parseHTML: (element) => {\n                const position = element.style.alignItems;\n\n                return this.options.positions.includes(position)\n                  ? position\n                  : this.options.defaultPosition;\n              },\n              renderHTML: (attributes) => {\n                if (!attributes.alignItems) {\n                  return {};\n                }\n\n                return { style: `align-items: ${attributes.alignItems}` };\n              },\n            },\n          },\n        },\n      ];\n    },\n\n    addCommands() {\n      return {\n        setBlockPosition:\n          (position: string) =>\n          ({ commands }) => {\n            if (!this.options.positions.includes(position)) {\n              return false;\n            }\n\n            return this.options.types\n              .map((type) =>\n                commands.updateAttributes(type, { alignItems: position })\n              )\n              .some((response) => response);\n          },\n\n        unsetBlockPosition:\n          () =>\n          ({ commands }) => {\n            return this.options.types\n              .map((type) => commands.resetAttributes(type, \"alignItems\"))\n              .some((response) => response);\n          },\n\n        toggleBlockPosition:\n          (position) =>\n          ({ editor, commands }) => {\n            if (!this.options.positions.includes(position)) {\n              return false;\n            }\n\n            if (editor.isActive({ alignItems: position })) {\n              return commands.unsetBlockPosition();\n            }\n            return commands.setBlockPosition(position);\n          },\n      };\n    },\n\n    addKeyboardShortcuts() {\n      return {\n        \"Mod-Shift-l\": () => this.editor.commands.setBlockPosition(\"left\"),\n        \"Mod-Shift-e\": () => this.editor.commands.setBlockPosition(\"center\"),\n        \"Mod-Shift-r\": () => this.editor.commands.setBlockPosition(\"right\"),\n      };\n    },\n  }).configure({\n    types: [\"figure\"],\n    positions: [\"start\", \"center\", \"end\"],\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/blockquote/index.ts",
    "content": "import type { BlockquoteOptions } from \"@tiptap/extension-blockquote\";\nimport TiptapBlockquote from \"@tiptap/extension-blockquote\";\nimport { markRaw } from \"vue\";\nimport MingcuteBlockquoteLine from \"~icons/mingcute/blockquote-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionBlockquoteOptions = Partial<BlockquoteOptions> &\n  ExtensionOptions;\n\nexport const ExtensionBlockquote =\n  TiptapBlockquote.extend<ExtensionBlockquoteOptions>({\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 90,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: editor.isActive(TiptapBlockquote.name),\n              icon: markRaw(MingcuteBlockquoteLine),\n              title: i18n.global.t(\"editor.common.quote\"),\n              action: () => {\n                editor.commands.toggleBlockquote();\n              },\n            },\n          };\n        },\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/bold/index.ts",
    "content": "import TiptapBold, { type BoldOptions } from \"@tiptap/extension-bold\";\nimport { markRaw } from \"vue\";\nimport MingcuteBoldLine from \"~icons/mingcute/bold-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionBoldOptions = Partial<BoldOptions> & ExtensionOptions;\n\nexport const ExtensionBold = TiptapBold.extend<ExtensionBoldOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 40,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: editor.isActive(TiptapBold.name),\n            icon: markRaw(MingcuteBoldLine),\n            title: i18n.global.t(\"editor.common.bold\"),\n            action: () => {\n              editor.chain().focus().toggleBold().run();\n            },\n          },\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/bullet-list/index.ts",
    "content": "import {\n  ListItem,\n  BulletList as TiptapBulletList,\n  type BulletListOptions,\n} from \"@tiptap/extension-list\";\nimport { markRaw } from \"vue\";\nimport MingcuteListCheckLine from \"~icons/mingcute/list-check-line\";\nimport type { Editor, Range } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionBulletListOptions = Partial<BulletListOptions> &\n  ExtensionOptions;\n\nexport const ExtensionBulletList = TiptapBulletList.extend<ExtensionOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getCommandMenuItems() {\n        return {\n          priority: 130,\n          icon: markRaw(MingcuteListCheckLine),\n          title: \"editor.common.bullet_list\",\n          keywords: [\"bulletlist\", \"wuxuliebiao\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor.chain().focus().deleteRange(range).toggleBulletList().run();\n          },\n        };\n      },\n    };\n  },\n  addExtensions() {\n    return [ListItem];\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/character-count/index.ts",
    "content": "export { CharacterCount as ExtensionCharacterCount } from \"@tiptap/extensions\";\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/clear-format/index.ts",
    "content": "import { markRaw } from \"vue\";\nimport MingcuteEraserLine from \"~icons/mingcute/eraser-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport { Extension } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionClearFormatOptions = ExtensionOptions;\n\nexport const ExtensionClearFormat =\n  Extension.create<ExtensionClearFormatOptions>({\n    name: \"clearFormat\",\n\n    addOptions() {\n      return {\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 23,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: false,\n              icon: markRaw(MingcuteEraserLine),\n              title: i18n.global.t(\"editor.common.clear_format\"),\n              action: () => editor.chain().focus().unsetAllMarks().run(),\n            },\n          };\n        },\n      };\n    },\n\n    addKeyboardShortcuts() {\n      return {\n        \"Mod-\\\\\": () => this.editor.chain().focus().unsetAllMarks().run(),\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/code/index.ts",
    "content": "import type { CodeOptions } from \"@tiptap/extension-code\";\nimport TiptapCode from \"@tiptap/extension-code\";\nimport { markRaw } from \"vue\";\nimport MingcuteCodeLine from \"~icons/mingcute/code-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionCodeOptions = Partial<CodeOptions> & ExtensionOptions;\n\nexport const ExtensionCode = TiptapCode.extend<ExtensionCodeOptions>({\n  exitable: true,\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 100,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: editor.isActive(TiptapCode.name),\n            icon: markRaw(MingcuteCodeLine),\n            title: i18n.global.t(\"editor.common.code\"),\n            action: () => editor.chain().focus().toggleCode().run(),\n          },\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/code-block/CodeBlockSelect.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed, ref, watch } from \"vue\";\nimport MingcuteDownLine from \"~icons/mingcute/down-line\";\n\nexport interface Option {\n  label: string;\n  value: string;\n}\nconst props = defineProps<{\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  container?: any;\n  containerClass?: string;\n  options: Option[];\n  filterSort?: (options: Option[], query: string) => number;\n}>();\n\nconst value = defineModel<string>({\n  default: \"\",\n});\n\nconst emit = defineEmits<{\n  (event: \"select\"): void;\n}>();\n\nconst isFocus = ref(false);\nconst inputValue = ref<string>(\"\");\nconst selectedOption = ref<Option | null>(null);\nconst inputRef = ref<HTMLInputElement | null>(null);\n\nconst displayLabel = computed(() => {\n  if (selectedOption.value) {\n    return selectedOption.value.label;\n  }\n  return value.value;\n});\n\nconst filterOptions = computed(() => {\n  if (!inputValue.value) {\n    return props.options;\n  }\n  return props.options.filter((option) =>\n    option.value\n      .toLocaleLowerCase()\n      .includes(inputValue.value.toLocaleLowerCase())\n  );\n});\n\nconst handleInputFocus = () => {\n  isFocus.value = true;\n  setTimeout(() => {\n    handleScrollIntoView();\n  }, 50);\n};\n\nconst handleInputBlur = () => {\n  isFocus.value = false;\n  if (inputValue.value) {\n    value.value = inputValue.value;\n    inputValue.value = \"\";\n  }\n};\n\nconst handleSelectOption = (option: Option) => {\n  selectedOption.value = option;\n  value.value = option.value;\n  inputValue.value = \"\";\n  inputRef.value?.blur();\n  emit(\"select\");\n};\n\nconst selectedIndex = ref(-1);\n\nconst handleOptionKeydown = (event: KeyboardEvent) => {\n  const key = event.key;\n  if (key === \"ArrowUp\") {\n    selectedIndex.value =\n      (selectedIndex.value - 1 + filterOptions.value.length) %\n      filterOptions.value.length;\n    return true;\n  }\n\n  if (key === \"ArrowDown\") {\n    selectedIndex.value =\n      (selectedIndex.value + 1) % filterOptions.value.length;\n    return true;\n  }\n\n  if (key === \"Enter\") {\n    if (selectedIndex.value === -1) {\n      return true;\n    }\n    handleSelectOption(filterOptions.value[selectedIndex.value]);\n    return true;\n  }\n};\n\nwatch(\n  value,\n  (newValue) => {\n    if (newValue) {\n      selectedOption.value =\n        props.options.find((option) => option.value === newValue) || null;\n      selectedIndex.value = props.options.findIndex(\n        (option) => option.value === newValue\n      );\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nwatch(\n  selectedIndex,\n  () => {\n    setTimeout(() => {\n      handleScrollIntoView();\n    });\n  },\n  {\n    immediate: true,\n  }\n);\n\nconst handleScrollIntoView = () => {\n  if (selectedIndex.value === -1) {\n    return;\n  }\n  const optionElement = document.querySelector(\n    `.select > div:nth-child(${selectedIndex.value + 1})`\n  );\n  if (optionElement) {\n    optionElement.scrollIntoView({\n      behavior: \"instant\",\n      block: \"nearest\",\n      inline: \"nearest\",\n    });\n  }\n};\n</script>\n<template>\n  <VDropdown\n    :triggers=\"[]\"\n    :shown=\"isFocus\"\n    :auto-hide=\"false\"\n    :distance=\"0\"\n    auto-size\n    :container=\"container || 'body'\"\n  >\n    <div class=\"relative inline-block w-full\" @keydown=\"handleOptionKeydown\">\n      <div class=\"h-8\">\n        <div\n          class=\"select-input box-border grid size-full cursor-pointer items-center rounded-md px-3 text-sm\"\n          :class=\"{\n            'bg-white': isFocus,\n            'border-[1px]': isFocus,\n          }\"\n        >\n          <span class=\"absolute bottom-0 top-0\">\n            <input\n              ref=\"inputRef\"\n              v-model=\"inputValue\"\n              class=\"m-0 h-full cursor-auto appearance-none border-none bg-transparent p-0 pe-0 ps-0 outline-none\"\n              :placeholder=\"isFocus ? displayLabel : ''\"\n              @focus=\"handleInputFocus\"\n              @blur=\"handleInputBlur\"\n            />\n          </span>\n          <span v-show=\"!isFocus\" class=\"text-ellipsis text-sm\">\n            {{ displayLabel }}\n          </span>\n          <span class=\"justify-self-end\" @click=\"inputRef?.focus()\">\n            <MingcuteDownLine />\n          </span>\n        </div>\n      </div>\n    </div>\n\n    <template #popper>\n      <div class=\"bg-white\">\n        <div class=\"select max-h-64 cursor-pointer p-1\">\n          <template v-if=\"filterOptions && filterOptions.length > 0\">\n            <div\n              v-for=\"(option, index) in filterOptions\"\n              :key=\"option.value\"\n              :index=\"index\"\n              class=\"flex h-8 w-full items-center rounded-md px-3 py-1 text-base hover:bg-zinc-100\"\n              :class=\"{\n                'bg-zinc-200': option.value === value,\n                'bg-zinc-100': selectedIndex === index,\n              }\"\n              @mousedown=\"handleSelectOption(option)\"\n            >\n              <span class=\"flex-1 text-ellipsis text-sm\">\n                {{ option.label }}\n              </span>\n            </div>\n          </template>\n          <template v-else>\n            <div\n              class=\"flex h-8 w-full items-center rounded-md px-3 py-1 text-base\"\n            >\n              <span class=\"flex-1 text-ellipsis text-sm\">No options</span>\n            </div>\n          </template>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n<style lang=\"scss\" scoped>\n.select-input {\n  grid-template-columns: 1fr auto;\n}\n</style>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useTimeout } from \"@vueuse/core\";\nimport { computed } from \"vue\";\nimport MingcuteCheckCircleLine from \"~icons/mingcute/check-circle-line\";\nimport MingcuteCopyLine from \"~icons/mingcute/copy-line\";\nimport MingcuteRightSmallFill from \"~icons/mingcute/right-small-fill\";\nimport { i18n } from \"@/locales\";\nimport {\n  NodeViewContent,\n  NodeViewWrapper,\n  type NodeViewProps,\n} from \"@/tiptap/vue-3\";\nimport CodeBlockSelect from \"./CodeBlockSelect.vue\";\n\nconst props = defineProps<NodeViewProps>();\n\nconst languageOptions = computed(() => {\n  let languages: Array<{\n    label: string;\n    value: string;\n  }> = [];\n  const lang = props.extension.options.languages;\n  if (typeof lang === \"function\") {\n    languages = lang(props.editor.state);\n  } else {\n    languages = lang;\n  }\n  languages = languages || [];\n  const languageValues = languages.map((language) => language.value);\n  if (languageValues.indexOf(\"auto\") === -1) {\n    languages.unshift({\n      label: \"Auto\",\n      value: \"auto\",\n    });\n  }\n  return languages;\n});\n\nconst selectedLanguage = computed({\n  get: () => {\n    return props.node?.attrs.language || \"auto\";\n  },\n  set: (language: string) => {\n    props.updateAttributes({ language: language });\n  },\n});\n\nconst themeOptions = computed(() => {\n  let themes:\n    | Array<{\n        label: string;\n        value: string;\n      }>\n    | undefined = [];\n  const theme = props.extension.options.themes;\n  if (typeof theme === \"function\") {\n    themes = theme(props.editor.state);\n  } else {\n    themes = theme;\n  }\n\n  if (!themes) {\n    return undefined;\n  }\n  return themes;\n});\n\nconst selectedTheme = computed({\n  get: () => {\n    return props.node?.attrs.theme || themeOptions.value?.[0].value;\n  },\n  set: (theme: string) => {\n    props.updateAttributes({ theme: theme });\n  },\n});\n\nconst collapsed = computed<boolean>({\n  get: () => {\n    return props.node.attrs.collapsed || false;\n  },\n  set: (collapsed: boolean) => {\n    props.updateAttributes({ collapsed: collapsed });\n  },\n});\n\nconst { ready, start } = useTimeout(2000, { controls: true, immediate: false });\n\nconst handleCopyCode = () => {\n  if (!ready.value) return;\n  const code = props.node.textContent;\n  navigator.clipboard.writeText(code).then(() => {\n    start();\n  });\n};\n</script>\n<template>\n  <node-view-wrapper\n    as=\"div\"\n    class=\"code-node mt-3 overflow-hidden rounded border-[1px]\"\n  >\n    <div\n      contenteditable=\"false\"\n      class=\"flex items-center justify-between border-b-[1px] border-b-gray-100 bg-neutral-100 py-1\"\n    >\n      <div\n        class=\"flex flex-1 items-center pl-3\"\n        @click.self=\"collapsed ? (collapsed = false) : null\"\n      >\n        <div class=\"flex items-center pr-3\">\n          <button\n            type=\"button\"\n            class=\"flex size-8 cursor-pointer items-center justify-center rounded transition-colors hover:bg-gray-200 active:!bg-gray-300\"\n            @click.stop=\"collapsed = !collapsed\"\n          >\n            <MingcuteRightSmallFill\n              class=\"size-6 transition-all\"\n              :class=\"{ 'rotate-90': !collapsed }\"\n            />\n          </button>\n        </div>\n        <CodeBlockSelect\n          v-model=\"selectedLanguage\"\n          class=\"w-48\"\n          :container=\"editor.options.element\"\n          :options=\"languageOptions\"\n        >\n        </CodeBlockSelect>\n        <CodeBlockSelect\n          v-if=\"themeOptions && themeOptions.length > 0\"\n          v-model=\"selectedTheme\"\n          :container=\"editor.options.element\"\n          class=\"w-48\"\n          :options=\"themeOptions\"\n          @select=\"editor.commands.focus()\"\n        >\n        </CodeBlockSelect>\n      </div>\n      <div class=\"flex items-center pr-3\">\n        <button\n          v-tooltip=\"\n            ready\n              ? i18n.global.t('editor.common.codeblock.copy_code')\n              : i18n.global.t('editor.common.codeblock.copy_code_success')\n          \"\n          type=\"button\"\n          class=\"flex size-8 cursor-pointer items-center justify-center rounded\"\n          :class=\"{ 'hover:bg-gray-200 active:!bg-gray-300': ready }\"\n          @click=\"handleCopyCode\"\n        >\n          <MingcuteCheckCircleLine\n            v-if=\"!ready\"\n            class=\"size-4 text-green-500\"\n          />\n          <MingcuteCopyLine v-else class=\"size-4 text-gray-500\" />\n        </button>\n      </div>\n    </div>\n    <pre v-show=\"!collapsed\"><node-view-content as=\"code\" class=\"hljs\" /></pre>\n  </node-view-wrapper>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/code-block/code-block.ts",
    "content": "import TiptapCodeBlock from \"@tiptap/extension-code-block\";\nimport { markRaw } from \"vue\";\nimport MingcuteBracesLine from \"~icons/mingcute/braces-line\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport ToolboxItem from \"@/components/toolbox/ToolboxItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  VueNodeViewRenderer,\n  findParentNode,\n  isActive,\n  isNodeActive,\n  posToDOMRect,\n  type CommandProps,\n  type Range,\n} from \"@/tiptap\";\nimport {\n  EditorState,\n  Plugin,\n  PluginKey,\n  TextSelection,\n  type Transaction,\n} from \"@/tiptap/pm\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\nimport CodeBlockViewRenderer from \"./CodeBlockViewRenderer.vue\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    codeIndent: {\n      codeIndent: () => ReturnType;\n      codeOutdent: () => ReturnType;\n    };\n  }\n}\n\ntype IndentType = \"indent\" | \"outdent\";\n\nconst updateIndent = (tr: Transaction, type: IndentType): Transaction => {\n  const { doc, selection } = tr;\n  if (!doc || !selection) return tr;\n  if (!(selection instanceof TextSelection)) {\n    return tr;\n  }\n  const { from, to } = selection;\n  doc.nodesBetween(from, to, (_node, pos) => {\n    if (from - to == 0 && type === \"indent\") {\n      tr.insertText(\"  \", from, to);\n      return false;\n    }\n\n    const precedeContent = doc.textBetween(pos + 1, from, \"\\n\");\n    const precedeLineBreakPos = precedeContent.lastIndexOf(\"\\n\");\n    const startBetWeenIndex =\n      precedeLineBreakPos === -1 ? pos + 1 : pos + precedeLineBreakPos + 1;\n    const text = doc.textBetween(startBetWeenIndex, to, \"\\n\");\n    if (type === \"indent\") {\n      let replacedStr = text.replace(/\\n/g, \"\\n  \");\n      if (startBetWeenIndex === pos + 1) {\n        replacedStr = \"  \" + replacedStr;\n      }\n      tr.insertText(replacedStr, startBetWeenIndex, to);\n    } else {\n      let replacedStr = text.replace(/\\n {2}/g, \"\\n\");\n      if (startBetWeenIndex === pos + 1) {\n        const firstNewLineIndex = replacedStr.indexOf(\"  \");\n        if (firstNewLineIndex == 0) {\n          replacedStr = replacedStr.replace(\"  \", \"\");\n        }\n      }\n      tr.insertText(replacedStr, startBetWeenIndex, to);\n    }\n    return false;\n  });\n\n  return tr;\n};\n\ninterface Option {\n  label: string;\n  value: string;\n}\n\nexport interface CodeBlockOptions {\n  /**\n   * Define whether the node should be exited on triple enter.\n   * @default true\n   */\n  exitOnTripleEnter: boolean;\n  /**\n   * Define whether the node should be exited on arrow down if there is no node after it.\n   * @default true\n   */\n  exitOnArrowDown: boolean;\n  /**\n   * Custom HTML attributes that should be added to the rendered HTML tag.\n   * @default {}\n   * @example { class: 'foo' }\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  HTMLAttributes: Record<string, any>;\n\n  /**\n   * The default language for code block\n   * @default null\n   */\n  defaultLanguage: string | null | undefined;\n\n  /**\n   * The default theme for code block\n   * @default null\n   */\n  defaultTheme: string | null | undefined;\n}\n\nexport interface ExtensionCodeBlockOptions extends CodeBlockOptions {\n  /**\n   * Used for language list\n   *\n   * @default []\n   */\n  languages:\n    | Array<Option>\n    | ((state: EditorState) => Array<{\n        label: string;\n        value: string;\n      }>);\n\n  /**\n   * Used for theme list\n   *\n   * @default []\n   */\n  themes?:\n    | Array<{\n        label: string;\n        value: string;\n      }>\n    | ((state: EditorState) => Array<{\n        label: string;\n        value: string;\n      }>);\n}\n\nexport const CODE_BLOCK_BUBBLE_MENU_KEY = new PluginKey(\"codeBlockBubbleMenu\");\n\nexport const ExtensionCodeBlock = TiptapCodeBlock.extend<\n  ExtensionOptions & Partial<ExtensionCodeBlockOptions>\n>({\n  allowGapCursor: true,\n  // It needs to have a higher priority than range-selection (100),\n  // otherwise the Mod-a shortcut key will be overridden.\n  // But it should be lower than paragraph (1000) to avoid Backspace issues.\n  priority: 101,\n\n  fakeSelection: true,\n\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      collapsed: {\n        default: false,\n        parseHTML: (element) => !!element.getAttribute(\"collapsed\"),\n        renderHTML: (attributes) => {\n          if (attributes.collapsed) {\n            return {\n              collapsed: attributes.collapsed,\n            };\n          }\n          return {};\n        },\n      },\n      theme: {\n        default: this.options.defaultTheme,\n        parseHTML: (element) => element.getAttribute(\"theme\") || null,\n        renderHTML: (attributes) => {\n          if (attributes.theme) {\n            return {\n              theme: attributes.theme,\n            };\n          }\n          return {};\n        },\n      },\n    };\n  },\n\n  addCommands() {\n    return {\n      ...this.parent?.(),\n      codeIndent:\n        () =>\n        ({ tr, state, dispatch }: CommandProps) => {\n          const { selection } = state;\n          tr = tr.setSelection(selection);\n          tr = updateIndent(tr, \"indent\");\n          if (tr.docChanged && dispatch) {\n            dispatch(tr);\n            return true;\n          }\n          return false;\n        },\n      codeOutdent:\n        () =>\n        ({ tr, state, dispatch }: CommandProps) => {\n          const { selection } = state;\n          tr = tr.setSelection(selection);\n          tr = updateIndent(tr, \"outdent\");\n          if (tr.docChanged && dispatch) {\n            dispatch(tr);\n            return true;\n          }\n          return false;\n        },\n    };\n  },\n  addKeyboardShortcuts() {\n    return {\n      Backspace: ({ editor }) => {\n        if (!isNodeActive(editor.state, this.name)) {\n          return false;\n        }\n\n        const { selection } = editor.state;\n        // Clear the selected content and adapt to the all-select shortcut key operation.\n        if (!selection.empty) {\n          editor\n            .chain()\n            .focus()\n            .deleteSelection()\n            .setTextSelection(selection.$from.pos)\n            .run();\n          return true;\n        }\n\n        const { $anchor } = selection;\n        const isAtStart = $anchor.parentOffset === 0;\n        // If the cursor is at the beginning of the code block or the code block is empty, it is not deleted.\n        if (isAtStart || !$anchor.parent.textContent.length) {\n          return true;\n        }\n\n        return false;\n      },\n      Tab: () => {\n        if (this.editor.isActive(TiptapCodeBlock.name)) {\n          return this.editor.chain().focus().codeIndent().run();\n        }\n        return false;\n      },\n      \"Shift-Tab\": () => {\n        if (this.editor.isActive(TiptapCodeBlock.name)) {\n          return this.editor.chain().focus().codeOutdent().run();\n        }\n        return false;\n      },\n      \"Mod-a\": () => {\n        if (this.editor.isActive(TiptapCodeBlock.name)) {\n          const { tr, selection } = this.editor.state;\n          const codeBlack = findParentNode(\n            (node) => node.type.name === TiptapCodeBlock.name\n          )(selection);\n          if (!codeBlack) {\n            return false;\n          }\n          const head = codeBlack.start;\n          const anchor = codeBlack.start + codeBlack.node.nodeSize - 1;\n          const $head = tr.doc.resolve(head);\n          const $anchor = tr.doc.resolve(anchor);\n          this.editor.view.dispatch(\n            tr.setSelection(new TextSelection($head, $anchor))\n          );\n          return true;\n        }\n        return false;\n      },\n    };\n  },\n  addNodeView() {\n    return VueNodeViewRenderer(CodeBlockViewRenderer);\n  },\n\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      languages: [],\n      themes: [],\n      defaultLanguage: null,\n      defaultTheme: null,\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 160,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: editor.isActive(TiptapCodeBlock.name),\n            icon: markRaw(MingcuteBracesLine),\n            title: i18n.global.t(\"editor.common.codeblock.title\"),\n            action: () => editor.chain().focus().toggleCodeBlock().run(),\n          },\n        };\n      },\n      getCommandMenuItems() {\n        return {\n          priority: 80,\n          icon: markRaw(MingcuteBracesLine),\n          title: \"editor.common.codeblock.title\",\n          keywords: [\"codeblock\", \"daimakuai\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor.chain().focus().deleteRange(range).setCodeBlock().run();\n          },\n        };\n      },\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return [\n          {\n            priority: 60,\n            component: markRaw(ToolboxItem),\n            props: {\n              editor,\n              icon: markRaw(MingcuteBracesLine),\n              title: i18n.global.t(\"editor.common.codeblock.title\"),\n              action: () => {\n                editor.chain().focus().setCodeBlock().run();\n              },\n            },\n          },\n        ];\n      },\n      getBubbleMenu() {\n        return {\n          pluginKey: CODE_BLOCK_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }): boolean => {\n            return isActive(state, TiptapCodeBlock.name);\n          },\n          options: {\n            placement: \"top-start\",\n          },\n          getReferencedVirtualElement() {\n            const editor = this.editor;\n            if (!editor) {\n              return null;\n            }\n            const parentNode = findParentNode(\n              (node) => node.type.name === ExtensionCodeBlock.name\n            )(editor.state.selection);\n            if (parentNode) {\n              const domRect = posToDOMRect(\n                editor.view,\n                parentNode.pos,\n                parentNode.pos + parentNode.node.nodeSize\n              );\n              return {\n                getBoundingClientRect: () => domRect,\n                getClientRects: () => [domRect],\n              };\n            }\n            return null;\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  deleteNode(TiptapCodeBlock.name, editor),\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n\n  addProseMirrorPlugins() {\n    return [\n      // Solve the paste problem. Because the upstream has not been\n      // able to deal with this problem for a long time, it is\n      // handled manually locally.\n      // see: https://github.com/ueberdosis/tiptap/pull/3606\n      new Plugin({\n        key: new PluginKey(\"codeBlockVSCodeHandlerFixPaste\"),\n        props: {\n          handlePaste: (view, event) => {\n            if (!event.clipboardData) {\n              return false;\n            }\n            // don’t create a new code block within code blocks\n            if (this.editor.isActive(this.type.name)) {\n              return false;\n            }\n\n            const text = event.clipboardData.getData(\"text/plain\");\n            const vscode = event.clipboardData.getData(\"vscode-editor-data\");\n            const vscodeData = vscode ? JSON.parse(vscode) : undefined;\n            const language = vscodeData?.mode;\n\n            if (!text || !language) {\n              return false;\n            }\n\n            const { tr, schema } = view.state;\n\n            // add text to code block\n            // strip carriage return chars from text pasted as code\n            // see: https://github.com/ProseMirror/prosemirror-view/commit/a50a6bcceb4ce52ac8fcc6162488d8875613aacd\n            const contentTextNode = schema.text(text.replace(/\\r\\n?/g, \"\\n\"));\n\n            // create an empty code block\n            tr.replaceSelectionWith(\n              this.type.create({ language }, contentTextNode)\n            );\n\n            const { selection } = tr;\n            // Whether the current position is code block, if not, move forward to code block.\n            let codeBlockPos = Math.max(0, selection.from - 1);\n            while (\n              codeBlockPos > 0 &&\n              tr.doc.resolve(codeBlockPos).parent.type.name !== this.type.name\n            ) {\n              codeBlockPos--;\n            }\n            // put cursor inside the newly created code block\n            tr.setSelection(TextSelection.near(tr.doc.resolve(codeBlockPos)));\n\n            // store meta information\n            // this is useful for other plugins that depends on the paste event\n            // like the paste rule plugin\n            tr.setMeta(\"paste\", true);\n\n            view.dispatch(tr);\n\n            return true;\n          },\n        },\n      }),\n      ...(this.parent?.() || []),\n    ];\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/code-block/index.ts",
    "content": "export * from \"./code-block\";\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/color/ColorBubbleItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { BubbleItem } from \"@/components\";\nimport ColorPickerDropdown from \"@/components/common/ColorPickerDropdown.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nfunction handleSetColor(color?: string) {\n  if (!color) {\n    return;\n  }\n  props.editor?.chain().focus().setColor(color).run();\n}\n\nfunction handleUnsetColor() {\n  props.editor?.chain().focus().unsetColor().run();\n}\n</script>\n\n<template>\n  <ColorPickerDropdown @update:model-value=\"handleSetColor\">\n    <BubbleItem v-bind=\"props\" :editor=\"editor\" />\n    <template #prefix>\n      <div class=\"p-1\">\n        <div\n          class=\"flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100\"\n          @click=\"handleUnsetColor\"\n        >\n          <div\n            class=\"size-5 cursor-pointer rounded-sm bg-black ring-gray-300 ring-offset-1 hover:ring-1\"\n          ></div>\n          <span class=\"text-xs text-gray-600\">\n            {{ i18n.global.t(\"editor.common.button.restore_default\") }}\n          </span>\n        </div>\n      </div>\n    </template>\n  </ColorPickerDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/color/ColorToolbarItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ToolbarItem } from \"@/components\";\nimport ColorPickerDropdown from \"@/components/common/ColorPickerDropdown.vue\";\nimport { i18n } from \"@/locales\";\nimport type { ToolbarItemComponentProps } from \"@/types\";\n\nconst props = defineProps<ToolbarItemComponentProps>();\n\nfunction handleSetColor(color?: string) {\n  if (!color) {\n    return;\n  }\n  props.editor?.chain().focus().setColor(color).run();\n}\n\nfunction handleUnsetColor() {\n  props.editor?.chain().focus().unsetColor().run();\n}\n</script>\n\n<template>\n  <ColorPickerDropdown @update:model-value=\"handleSetColor\">\n    <ToolbarItem v-bind=\"props\" />\n    <template #prefix>\n      <div class=\"p-1\">\n        <div\n          class=\"flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100\"\n          @click=\"handleUnsetColor\"\n        >\n          <div\n            class=\"size-5 cursor-pointer rounded-sm bg-black ring-gray-300 ring-offset-1 hover:ring-1\"\n          ></div>\n          <span class=\"text-xs text-gray-600\">\n            {{ i18n.global.t(\"editor.common.button.restore_default\") }}\n          </span>\n        </div>\n      </div>\n    </template>\n  </ColorPickerDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/color/index.ts",
    "content": "import type { ColorOptions } from \"@tiptap/extension-color\";\nimport TiptapColor from \"@tiptap/extension-color\";\nimport { markRaw } from \"vue\";\nimport MingcuteTextColorLine from \"~icons/mingcute/text-color-line\";\nimport { ExtensionTextStyle } from \"@/extensions/text-style\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport ColorToolbarItem from \"./ColorToolbarItem.vue\";\n\nexport type ExtensionColorOptions = Partial<ColorOptions> & ExtensionOptions;\n\nexport const ExtensionColor = TiptapColor.extend<ExtensionColorOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 81,\n          component: markRaw(ColorToolbarItem),\n          props: {\n            editor,\n            isActive: false,\n            icon: markRaw(MingcuteTextColorLine),\n            title: i18n.global.t(\"editor.common.color\"),\n          },\n        };\n      },\n    };\n  },\n  addExtensions() {\n    return [ExtensionTextStyle];\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/columns/column.ts",
    "content": "import { mergeAttributes, Node } from \"@/tiptap/vue-3\";\n\nexport type ExtensionColumnOptions = {\n  HTMLAttributes: {\n    class: string;\n  };\n};\n\nexport const ExtensionColumn = Node.create<ExtensionColumnOptions>({\n  name: \"column\",\n  content: \"block+\",\n  isolating: true,\n  fakeSelection: true,\n\n  addOptions() {\n    return {\n      HTMLAttributes: {\n        class: \"column\",\n      },\n    };\n  },\n\n  addAttributes() {\n    return {\n      index: {\n        default: 0,\n        parseHTML: (element) => element.getAttribute(\"index\"),\n      },\n      style: {\n        default: \"min-width: 0;flex: 1 1;box-sizing: border-box;\",\n        parseHTML: (element) => element.getAttribute(\"style\"),\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"div[class=column]\",\n      },\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\n      \"div\",\n      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n      0,\n    ];\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/columns/columns.ts",
    "content": "import { markRaw } from \"vue\";\nimport MingcuteColumns2Line from \"~icons/mingcute/columns-2-line\";\nimport RiDeleteColumn from \"~icons/ri/delete-column\";\nimport RiInsertColumnLeft from \"~icons/ri/insert-column-left\";\nimport RiInsertColumnRight from \"~icons/ri/insert-column-right\";\nimport { BlockActionSeparator, ToolboxItem } from \"@/components\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  findParentNode,\n  isActive,\n  mergeAttributes,\n  Node,\n  posToDOMRect,\n  type Range,\n} from \"@/tiptap\";\nimport type { NodeType, Schema } from \"@/tiptap/pm\";\nimport {\n  EditorState,\n  PluginKey,\n  Node as PMNode,\n  TextSelection,\n} from \"@/tiptap/pm\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\nimport { ExtensionColumn } from \"./column\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    columns: {\n      insertColumns: (attrs?: { cols: number }) => ReturnType;\n      addColBefore: () => ReturnType;\n      addColAfter: () => ReturnType;\n      deleteCol: () => ReturnType;\n    };\n  }\n}\n\nexport const COLUMNS_BUBBLE_MENU_KEY = new PluginKey(\"columnsBubbleMenu\");\n\nconst createColumns = (schema: Schema, colsCount: number) => {\n  const types = getColumnsNodeTypes(schema);\n  const cols = [];\n\n  for (let index = 0; index < colsCount; index += 1) {\n    const col = types.column.createAndFill({ index });\n\n    if (col) {\n      cols.push(col);\n    }\n  }\n  return types.columns.createChecked({ cols: colsCount }, cols);\n};\n\nconst getColumnsNodeTypes = (\n  schema: Schema\n): {\n  columns: NodeType;\n  column: NodeType;\n} => {\n  if (schema.cached.columnsNodeTypes) {\n    return schema.cached.columnsNodeTypes;\n  }\n\n  const roles = {\n    columns: schema.nodes[\"columns\"],\n    column: schema.nodes[\"column\"],\n  };\n\n  schema.cached.columnsNodeTypes = roles;\n\n  return roles;\n};\n\ntype ColOperateType = \"addBefore\" | \"addAfter\" | \"delete\";\nconst addOrDeleteCol = (\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  dispatch: any,\n  state: EditorState,\n  type: ColOperateType\n) => {\n  const maybeColumns = findParentNode(\n    (node) => node.type.name === ExtensionColumns.name\n  )(state.selection);\n  const maybeColumn = findParentNode(\n    (node) => node.type.name === ExtensionColumn.name\n  )(state.selection);\n  if (dispatch && maybeColumns && maybeColumn) {\n    const cols = maybeColumns.node;\n    const colIndex = maybeColumn.node.attrs.index;\n    const colsJSON = cols.toJSON();\n\n    let nextIndex = colIndex;\n\n    if (type === \"delete\") {\n      nextIndex = colIndex - 1;\n      colsJSON.content.splice(colIndex, 1);\n    } else {\n      nextIndex = type === \"addBefore\" ? colIndex : colIndex + 1;\n      colsJSON.content.splice(nextIndex, 0, {\n        type: \"column\",\n        attrs: {\n          index: colIndex,\n        },\n        content: [\n          {\n            type: \"paragraph\",\n          },\n        ],\n      });\n    }\n\n    colsJSON.attrs.cols = colsJSON.content.length;\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    colsJSON.content.forEach((colJSON: any, index: number) => {\n      colJSON.attrs.index = index;\n    });\n\n    const nextCols = PMNode.fromJSON(state.schema, colsJSON);\n\n    let nextSelectPos = maybeColumns.pos;\n    nextCols.content.forEach((col, _pos, index) => {\n      if (index < nextIndex) {\n        nextSelectPos += col.nodeSize;\n      }\n    });\n\n    const tr = state.tr.setTime(Date.now());\n\n    tr.replaceWith(\n      maybeColumns.pos,\n      maybeColumns.pos + maybeColumns.node.nodeSize,\n      nextCols\n    ).setSelection(TextSelection.near(tr.doc.resolve(nextSelectPos)));\n\n    dispatch(tr);\n  }\n  return true;\n};\n\ntype GotoColType = \"before\" | \"after\";\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst gotoCol = (state: EditorState, dispatch: any, type: GotoColType) => {\n  const maybeColumns = findParentNode(\n    (node) => node.type.name === ExtensionColumns.name\n  )(state.selection);\n  const maybeColumn = findParentNode(\n    (node) => node.type.name === ExtensionColumn.name\n  )(state.selection);\n\n  if (dispatch && maybeColumns && maybeColumn) {\n    const cols = maybeColumns.node;\n    const colIndex = maybeColumn.node.attrs.index;\n\n    let nextIndex = 0;\n\n    if (type === \"before\") {\n      nextIndex = (colIndex - 1 + cols.attrs.cols) % cols.attrs.cols;\n    } else {\n      nextIndex = (colIndex + 1) % cols.attrs.cols;\n    }\n\n    let nextSelectPos = maybeColumns.pos;\n    cols.content.forEach((col, _pos, index) => {\n      if (index < nextIndex) {\n        nextSelectPos += col.nodeSize;\n      }\n    });\n\n    const tr = state.tr.setTime(Date.now());\n\n    tr.setSelection(TextSelection.near(tr.doc.resolve(nextSelectPos)));\n    dispatch(tr);\n    return true;\n  }\n\n  return false;\n};\n\nexport interface ExtensionColumnsOptions extends ExtensionOptions {\n  HTMLAttributes: {\n    class: string;\n  };\n}\n\nexport const ExtensionColumns = Node.create<ExtensionColumnsOptions>({\n  name: \"columns\",\n  group: \"block\",\n  priority: 10,\n  defining: true,\n  isolating: true,\n  allowGapCursor: true,\n  content: \"column{1,}\",\n  fakeSelection: false,\n\n  addOptions() {\n    return {\n      HTMLAttributes: {\n        class: \"columns\",\n      },\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return [\n          {\n            priority: 70,\n            component: markRaw(ToolboxItem),\n            props: {\n              editor,\n              icon: markRaw(MingcuteColumns2Line),\n              title: i18n.global.t(\"editor.extensions.commands_menu.columns\"),\n              action: () => {\n                editor\n                  .chain()\n                  .focus()\n                  .insertColumns({\n                    cols: 2,\n                  })\n                  .run();\n              },\n            },\n          },\n        ];\n      },\n      getCommandMenuItems() {\n        return {\n          priority: 70,\n          icon: markRaw(MingcuteColumns2Line),\n          title: \"editor.extensions.commands_menu.columns\",\n          keywords: [\"fenlan\", \"columns\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .insertColumns({\n                cols: 2,\n              })\n              .run();\n          },\n        };\n      },\n      getBubbleMenu() {\n        return {\n          pluginKey: COLUMNS_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }): boolean => {\n            return isActive(state, ExtensionColumns.name);\n          },\n          options: {\n            placement: \"bottom-start\",\n          },\n          getReferencedVirtualElement() {\n            const editor = this.editor;\n            if (!editor) {\n              return null;\n            }\n            const parentNode = findParentNode(\n              (node) => node.type.name === ExtensionColumn.name\n            )(editor.state.selection);\n            if (parentNode) {\n              const domRect = posToDOMRect(\n                editor.view,\n                parentNode.pos,\n                parentNode.pos + parentNode.node.nodeSize\n              );\n              return {\n                getBoundingClientRect: () => domRect,\n                getClientRects: () => [domRect],\n              };\n            }\n            return null;\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                icon: markRaw(RiInsertColumnLeft),\n                title: i18n.global.t(\n                  \"editor.extensions.columns.add_column_before\"\n                ),\n                action: ({ editor }: { editor: Editor }) => {\n                  editor.chain().focus().addColBefore().run();\n                },\n              },\n            },\n            {\n              priority: 20,\n              props: {\n                icon: markRaw(RiInsertColumnRight),\n                title: i18n.global.t(\n                  \"editor.extensions.columns.add_column_after\"\n                ),\n                action: ({ editor }: { editor: Editor }) => {\n                  editor.chain().focus().addColAfter().run();\n                },\n              },\n            },\n            {\n              priority: 30,\n              props: {\n                icon: markRaw(RiDeleteColumn),\n                title: i18n.global.t(\"editor.extensions.columns.delete_column\"),\n                action: ({ editor }: { editor: Editor }) => {\n                  editor.chain().focus().deleteCol().run();\n                },\n              },\n            },\n            {\n              priority: 40,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 50,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }: { editor: Editor }) => {\n                  deleteNode(ExtensionColumns.name, editor);\n                },\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n\n  addAttributes() {\n    return {\n      cols: {\n        default: 2,\n        parseHTML: (element) => element.getAttribute(\"cols\"),\n      },\n      style: {\n        default: \"display: flex;width: 100%;gap: 1em;\",\n        parseHTML: (element) => element.getAttribute(\"style\"),\n      },\n    };\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\n      \"div\",\n      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n      0,\n    ];\n  },\n\n  addCommands() {\n    return {\n      insertColumns:\n        (attrs) =>\n        ({ tr, dispatch, editor }) => {\n          const node = createColumns(editor.schema, (attrs && attrs.cols) || 3);\n\n          if (dispatch) {\n            const offset = tr.selection.anchor + 1;\n\n            tr.replaceSelectionWith(node)\n              .scrollIntoView()\n              .setSelection(TextSelection.near(tr.doc.resolve(offset)));\n          }\n\n          return true;\n        },\n      addColBefore:\n        () =>\n        ({ dispatch, state }) => {\n          return addOrDeleteCol(dispatch, state, \"addBefore\");\n        },\n      addColAfter:\n        () =>\n        ({ dispatch, state }) => {\n          return addOrDeleteCol(dispatch, state, \"addAfter\");\n        },\n      deleteCol:\n        () =>\n        ({ dispatch, state }) => {\n          return addOrDeleteCol(dispatch, state, \"delete\");\n        },\n    };\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      \"Mod-Alt-G\": () => this.editor.commands.insertColumns(),\n      Tab: () => {\n        return gotoCol(this.editor.state, this.editor.view.dispatch, \"after\");\n      },\n      \"Shift-Tab\": () => {\n        return gotoCol(this.editor.state, this.editor.view.dispatch, \"before\");\n      },\n    };\n  },\n  addExtensions() {\n    return [ExtensionColumn];\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/columns/index.ts",
    "content": "export * from \"./column\";\nexport * from \"./columns\";\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/commands-menu/CommandsView.vue",
    "content": "<script lang=\"ts\" setup>\nimport scrollIntoView from \"scroll-into-view-if-needed\";\nimport { ref, watch, type PropType } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport type { CommandMenuItemType } from \"@/types\";\n\nconst props = defineProps({\n  items: {\n    type: Array as PropType<CommandMenuItemType[]>,\n    required: true,\n  },\n\n  command: {\n    type: Function as PropType<(item: CommandMenuItemType) => void>,\n    required: true,\n  },\n});\n\nconst selectedIndex = ref(0);\n\nwatch(\n  () => props.items,\n  () => {\n    selectedIndex.value = 0;\n  }\n);\n\nfunction onKeyDown({ event }: { event: KeyboardEvent }) {\n  if (event.key === \"ArrowUp\" || (event.key === \"k\" && event.ctrlKey)) {\n    handleKeyUp();\n    return true;\n  }\n\n  if (event.key === \"ArrowDown\" || (event.key === \"j\" && event.ctrlKey)) {\n    handleKeyDown();\n    return true;\n  }\n\n  if (event.key === \"Enter\") {\n    handleKeyEnter();\n    return true;\n  }\n\n  return false;\n}\n\nfunction handleKeyUp() {\n  selectedIndex.value =\n    (selectedIndex.value + props.items.length - 1) % props.items.length;\n}\n\nfunction handleKeyDown() {\n  selectedIndex.value = (selectedIndex.value + 1) % props.items.length;\n}\n\nfunction handleKeyEnter() {\n  handleSelectItem(selectedIndex.value);\n}\n\nfunction handleSelectItem(index: number) {\n  const item = props.items[index];\n\n  if (item) {\n    props.command(item);\n  }\n}\n\nwatch(\n  () => selectedIndex.value,\n  () => {\n    const selected = document.getElementById(\n      `command-item-${selectedIndex.value}`\n    );\n\n    if (selected) {\n      scrollIntoView(selected, { behavior: \"smooth\", scrollMode: \"if-needed\" });\n    }\n  }\n);\n\ndefineExpose({\n  onKeyDown,\n});\n</script>\n<template>\n  <div\n    class=\"relative flex max-h-72 w-60 flex-col gap-1 overflow-y-auto overflow-x-hidden rounded-lg border bg-white p-1.5 shadow-md\"\n  >\n    <template v-if=\"items.length\">\n      <button\n        v-for=\"(item, index) in items\"\n        :id=\"`command-item-${index}`\"\n        :key=\"index\"\n        type=\"button\"\n        :class=\"{ 'bg-gray-100': index === selectedIndex }\"\n        class=\"group flex w-full items-center gap-3 rounded p-1.5 transition-colors hover:bg-gray-100\"\n        @click=\"handleSelectItem(index)\"\n      >\n        <div\n          class=\"size-6 flex-none rounded bg-gray-100 p-1 group-hover:bg-white\"\n          :class=\"{ '!bg-white': index === selectedIndex }\"\n        >\n          <component :is=\"item.icon\" class=\"size-full\" />\n        </div>\n        <div\n          class=\"min-w-0 flex-1 shrink text-left text-sm text-gray-600 group-hover:font-medium group-hover:text-gray-900\"\n          :class=\"{ 'font-medium text-gray-900': index === selectedIndex }\"\n        >\n          {{ i18n.global.t(item.title) }}\n        </div>\n      </button>\n    </template>\n    <div\n      v-else\n      class=\"flex items-center justify-center p-1 text-sm text-gray-600\"\n    >\n      <span>\n        {{ i18n.global.t(\"editor.extensions.commands_menu.no_results\") }}\n      </span>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/commands-menu/commands.ts",
    "content": "import { computePosition, flip, shift } from \"@floating-ui/dom\";\nimport Suggestion, { type SuggestionOptions } from \"@tiptap/suggestion\";\nimport {\n  Extension,\n  PluginKey,\n  posToDOMRect,\n  VueRenderer,\n  type AnyExtension,\n  type Editor,\n  type Range,\n} from \"@/tiptap\";\nimport type { CommandMenuItemType } from \"@/types\";\nimport CommandsView from \"./CommandsView.vue\";\n\nexport const ExtensionCommandsMenu = Extension.create({\n  name: \"commands-menu\",\n\n  addProseMirrorPlugins() {\n    const commandMenuItems = getToolbarItemsFromExtensions(this.editor);\n\n    const suggestionPlugin: SuggestionOptions = {\n      editor: this.editor,\n      command: ({\n        editor,\n        range,\n        props,\n      }: {\n        editor: Editor;\n        range: Range;\n        props: CommandMenuItemType;\n      }) => {\n        props.command({ editor, range });\n      },\n      items: ({ query }: { query: string }) => {\n        return commandMenuItems.filter((item) =>\n          [...item.keywords, item.title].some((keyword) =>\n            keyword.includes(query)\n          )\n        );\n      },\n      render: () => {\n        let component: VueRenderer | null = null;\n        return {\n          onStart: (props) => {\n            component = new VueRenderer(CommandsView, {\n              props,\n              editor: props.editor,\n            });\n            if (!props.clientRect) {\n              return;\n            }\n            if (!component.element) {\n              return;\n            }\n            if (!(component.element instanceof HTMLElement)) {\n              return;\n            }\n            component.element.style.position = \"absolute\";\n\n            document.body.appendChild(component.element);\n\n            updatePosition(props.editor, component.element);\n          },\n\n          onUpdate(props) {\n            if (!component) {\n              return;\n            }\n            if (!component.element) {\n              return;\n            }\n            if (!(component.element instanceof HTMLElement)) {\n              return;\n            }\n            component.updateProps(props);\n\n            if (!props.clientRect) {\n              return;\n            }\n\n            updatePosition(props.editor, component.element);\n          },\n\n          onKeyDown(props) {\n            if (!component) {\n              return false;\n            }\n            if (props.event.key === \"Escape\") {\n              if (!component.element) {\n                return false;\n              }\n              component.destroy();\n              component.element.remove();\n              return true;\n            }\n\n            return component.ref?.onKeyDown(props);\n          },\n\n          onExit() {\n            if (!component) {\n              return;\n            }\n            if (!component.element) {\n              return;\n            }\n            component.destroy();\n            component.element.remove();\n          },\n        };\n      },\n    };\n\n    return [\n      Suggestion({\n        pluginKey: new PluginKey(\"commands-menu-english\"),\n        char: \"/\",\n        ...suggestionPlugin,\n      }),\n      Suggestion({\n        pluginKey: new PluginKey(\"commands-menu-chinese\"),\n        char: \"、\",\n        ...suggestionPlugin,\n      }),\n    ];\n  },\n});\n\nconst updatePosition = (editor: Editor, element: HTMLElement) => {\n  const virtualElement = {\n    getBoundingClientRect: () =>\n      posToDOMRect(\n        editor.view,\n        editor.state.selection.from,\n        editor.state.selection.to\n      ),\n  };\n\n  computePosition(virtualElement, element, {\n    placement: \"bottom-start\",\n    strategy: \"absolute\",\n    middleware: [shift(), flip()],\n  }).then(({ x, y, strategy }) => {\n    element.style.position = strategy;\n    element.style.left = `${x}px`;\n    element.style.top = `${y}px`;\n    element.style.zIndex = \"1000\";\n  });\n};\n\nconst getToolbarItemsFromExtensions = (editor: Editor) => {\n  const extensionManager = editor?.extensionManager;\n  return extensionManager.extensions\n    .reduce((acc: CommandMenuItemType[], extension: AnyExtension) => {\n      const { getCommandMenuItems } = extension.options;\n\n      if (!getCommandMenuItems) {\n        return acc;\n      }\n\n      const items = getCommandMenuItems();\n\n      if (Array.isArray(items)) {\n        return [...acc, ...items];\n      }\n\n      return [...acc, items];\n    }, [])\n    .sort((a, b) => a.priority - b.priority);\n};\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/commands-menu/index.ts",
    "content": "export * from \"./commands\";\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/details/index.ts",
    "content": "import TiptapDetails, {\n  DetailsContent,\n  DetailsSummary,\n  type DetailsOptions,\n} from \"@tiptap/extension-details\";\nimport { markRaw } from \"vue\";\nimport MingcuteFoldVerticalLine from \"~icons/mingcute/fold-vertical-line\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  EditorState,\n  findParentNode,\n  isActive,\n  PluginKey,\n  posToDOMRect,\n  type Editor,\n  type Range,\n} from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\n\nexport const DETAILS_BUBBLE_MENU_KEY = new PluginKey(\"detailsBubbleMenu\");\n\nexport type ExtensionDetailsOptions = Partial<DetailsOptions> &\n  ExtensionOptions;\n\nexport const ExtensionDetails = TiptapDetails.extend<ExtensionDetailsOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      HTMLAttributes: {\n        class: \"details\",\n      },\n      getCommandMenuItems() {\n        return {\n          priority: 160,\n          icon: markRaw(MingcuteFoldVerticalLine),\n          title: \"editor.extensions.details.command_item\",\n          keywords: [\"details\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .setDetails()\n              .updateAttributes(TiptapDetails.name, { open: true })\n              .run();\n          },\n        };\n      },\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 95,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: editor.isActive(TiptapDetails.name),\n            icon: markRaw(MingcuteFoldVerticalLine),\n            title: i18n.global.t(\"editor.extensions.details.command_item\"),\n            action: () => {\n              if (editor.isActive(TiptapDetails.name)) {\n                editor.chain().focus().unsetDetails().run();\n              } else {\n                editor\n                  .chain()\n                  .focus()\n                  .setDetails()\n                  .updateAttributes(TiptapDetails.name, { open: true })\n                  .run();\n              }\n            },\n          },\n        };\n      },\n      getBubbleMenu() {\n        return {\n          pluginKey: DETAILS_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }): boolean => {\n            return isActive(state, ExtensionDetails.name);\n          },\n          options: {\n            placement: \"top-start\",\n          },\n          getReferencedVirtualElement() {\n            const editor = this.editor;\n            if (!editor) {\n              return null;\n            }\n            const parentNode = findParentNode(\n              (node) => node.type.name === ExtensionDetails.name\n            )(editor.state.selection);\n            if (parentNode) {\n              const domRect = posToDOMRect(\n                editor.view,\n                parentNode.pos,\n                parentNode.pos + parentNode.node.nodeSize\n              );\n              return {\n                getBoundingClientRect: () => domRect,\n                getClientRects: () => [domRect],\n              };\n            }\n            return null;\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }: { editor: Editor }): boolean =>\n                  deleteNode(ExtensionDetails.name, editor),\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n  addExtensions() {\n    return [DetailsSummary, DetailsContent];\n  },\n}).configure({\n  persist: true,\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/document/index.ts",
    "content": "import { Document as ExtensionDocument } from \"@tiptap/extension-document\";\n\nexport { ExtensionDocument };\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/drop-cursor/index.ts",
    "content": "import { Dropcursor } from \"@tiptap/extensions\";\n\nexport const ExtensionDropcursor = Dropcursor.configure({\n  width: 2,\n  class: \"dropcursor\",\n  color: \"skyblue\",\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/extensions-kit.ts",
    "content": "import type { HardBreakOptions } from \"@tiptap/extension-hard-break\";\nimport type { HorizontalRuleOptions } from \"@tiptap/extension-horizontal-rule\";\nimport type {\n  CharacterCountOptions,\n  DropcursorOptions,\n  PlaceholderOptions,\n} from \"@tiptap/extensions\";\nimport { Extension, type Extensions } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { filterDuplicateExtensions } from \"../utils\";\nimport { ExtensionAlign } from \"./align\";\nimport type { ExtensionAudioOptions } from \"./audio\";\nimport { ExtensionAudio } from \"./audio\";\nimport type { ExtensionBlockPositionOptions } from \"./block-position\";\nimport { ExtensionBlockPosition } from \"./block-position\";\nimport {\n  ExtensionBlockquote,\n  type ExtensionBlockquoteOptions,\n} from \"./blockquote\";\nimport { ExtensionBold, type ExtensionBoldOptions } from \"./bold\";\nimport {\n  ExtensionBulletList,\n  type ExtensionBulletListOptions,\n} from \"./bullet-list\";\nimport { ExtensionCharacterCount } from \"./character-count\";\nimport {\n  ExtensionClearFormat,\n  type ExtensionClearFormatOptions,\n} from \"./clear-format\";\nimport { ExtensionCode, type ExtensionCodeOptions } from \"./code\";\nimport {\n  ExtensionCodeBlock,\n  type ExtensionCodeBlockOptions,\n} from \"./code-block\";\nimport { ExtensionColor, type ExtensionColorOptions } from \"./color\";\nimport { ExtensionColumns, type ExtensionColumnsOptions } from \"./columns\";\nimport { ExtensionCommandsMenu } from \"./commands-menu\";\nimport { ExtensionDetails, type ExtensionDetailsOptions } from \"./details\";\nimport { ExtensionDocument } from \"./document\";\nimport { ExtensionDropcursor } from \"./drop-cursor\";\nimport { ExtensionFigure, type ExtensionFigureOptions } from \"./figure\";\nimport { ExtensionFontSize, type ExtensionFontSizeOptions } from \"./font-size\";\nimport { ExtensionFormatBrush } from \"./format-brush\";\nimport { ExtensionGallery, type ExtensionGalleryOptions } from \"./gallery\";\nimport { ExtensionGapCursor } from \"./gap-cursor\";\nimport { ExtensionHardBreak } from \"./hard-break\";\nimport { ExtensionHeading, type ExtensionHeadingOptions } from \"./heading\";\nimport {\n  ExtensionHighlight,\n  type ExtensionHighlightOptions,\n} from \"./highlight\";\nimport { ExtensionHistory } from \"./history\";\nimport { ExtensionHorizontalRule } from \"./horizontal-rule\";\nimport { ExtensionIframe } from \"./iframe\";\nimport { ExtensionImage, type ExtensionImageOptions } from \"./image\";\nimport { ExtensionIndent, type ExtensionIndentOptions } from \"./indent\";\nimport { ExtensionItalic, type ExtensionItalicOptions } from \"./italic\";\nimport { ExtensionLink, type ExtensionLinkOptions } from \"./link\";\nimport { ExtensionListExtra } from \"./list-extra\";\nimport {\n  ExtensionListKeymap,\n  type ExtensionListKeymapOptions,\n} from \"./list-keymap\";\nimport {\n  ExtensionNodeSelected,\n  type ExtensionNodeSelectedOptions,\n} from \"./node-selected\";\nimport {\n  ExtensionOrderedList,\n  type ExtensionOrderedListOptions,\n} from \"./ordered-list\";\nimport {\n  ExtensionParagraph,\n  type ExtensionParagraphOptions,\n} from \"./paragraph\";\nimport { ExtensionPlaceholder } from \"./placeholder\";\nimport { ExtensionRangeSelection } from \"./range-selection\";\nimport { ExtensionSearchAndReplace } from \"./search-and-replace\";\nimport { ExtensionSmartScroll, type SmartScrollOptions } from \"./smart-scroll\";\nimport { ExtensionStrike, type ExtensionStrikeOptions } from \"./strike\";\nimport {\n  ExtensionSubscript,\n  type ExtensionSubscriptOptions,\n} from \"./subscript\";\nimport {\n  ExtensionSuperscript,\n  type ExtensionSuperscriptOptions,\n} from \"./superscript\";\nimport { ExtensionTable, type ExtensionTableOptions } from \"./table\";\nimport { ExtensionTaskList, type ExtensionTaskListOptions } from \"./task-list\";\nimport { ExtensionText, type ExtensionTextOptions } from \"./text\";\nimport {\n  ExtensionTextAlign,\n  type ExtensionTextAlignOptions,\n} from \"./text-align\";\nimport {\n  ExtensionTextStyle,\n  type ExtensionTextStyleOptions,\n} from \"./text-style\";\nimport { ExtensionTrailingNode } from \"./trailing-node\";\nimport {\n  ExtensionUnderline,\n  type ExtensionUnderlineOptions,\n} from \"./underline\";\nimport { ExtensionUpload } from \"./upload\";\nimport { ExtensionVideo, type ExtensionVideoOptions } from \"./video\";\n\nexport interface ExtensionsKitOptions {\n  audio: Partial<ExtensionAudioOptions> | false;\n  blockquote: Partial<ExtensionBlockquoteOptions> | false;\n  bold: Partial<ExtensionBoldOptions> | false;\n  bulletList: Partial<ExtensionBulletListOptions> | false;\n  characterCount: Partial<CharacterCountOptions> | false;\n  clearFormat: Partial<ExtensionClearFormatOptions> | false;\n  code: Partial<ExtensionCodeOptions> | false;\n  codeBlock: Partial<ExtensionOptions & ExtensionCodeBlockOptions> | false;\n  color: Partial<ExtensionColorOptions> | false;\n  columns: Partial<ExtensionColumnsOptions> | false;\n  commandsMenu?: false;\n  details: Partial<ExtensionDetailsOptions> | false;\n  document?: boolean;\n  dropCursor: Partial<DropcursorOptions> | false;\n  figure: Partial<ExtensionFigureOptions> | false;\n  fontSize: Partial<ExtensionFontSizeOptions> | false;\n  formatBrush: Partial<ExtensionOptions> | false;\n  gallery: Partial<ExtensionGalleryOptions> | false;\n  gapCursor?: boolean;\n  hardBreak: Partial<HardBreakOptions> | false;\n  heading: Partial<ExtensionHeadingOptions> | false;\n  highlight: Partial<ExtensionHighlightOptions> | false;\n  history: Partial<ExtensionOptions> | false;\n  horizontalRule: Partial<HorizontalRuleOptions> | false;\n  iframe: Partial<ExtensionOptions> | false;\n  image: Partial<ExtensionImageOptions> | false;\n  indent: Partial<ExtensionIndentOptions> | false;\n  italic: Partial<ExtensionItalicOptions> | false;\n  link: Partial<ExtensionLinkOptions> | false;\n  listKeymap: Partial<ExtensionListKeymapOptions> | false;\n  nodeSelected: Partial<ExtensionNodeSelectedOptions> | false;\n  orderedList: Partial<ExtensionOrderedListOptions> | false;\n  paragraph: Partial<ExtensionParagraphOptions> | false;\n  placeholder: Partial<PlaceholderOptions> | false;\n  rangeSelection?: boolean;\n  searchAndReplace?: boolean;\n  smartScroll: Partial<SmartScrollOptions> | false;\n  strike: Partial<ExtensionStrikeOptions> | false;\n  subscript: Partial<ExtensionSubscriptOptions> | false;\n  superscript: Partial<ExtensionSuperscriptOptions> | false;\n  table: Partial<ExtensionTableOptions> | false;\n  taskList: Partial<ExtensionTaskListOptions> | false;\n  text: Partial<ExtensionTextOptions> | false;\n  textAlign: Partial<ExtensionTextAlignOptions> | false;\n  textStyle: Partial<ExtensionTextStyleOptions> | false;\n  trailingNode?: boolean;\n  underline: Partial<ExtensionUnderlineOptions> | false;\n  upload?: boolean;\n  video: Partial<ExtensionVideoOptions> | false;\n  listExtra: Partial<ExtensionOptions> | false;\n  blockPosition: Partial<ExtensionBlockPositionOptions> | false;\n  align: Partial<ExtensionOptions> | false;\n  customExtensions?: Extensions;\n}\n\nexport const ExtensionsKit = Extension.create<ExtensionsKitOptions>({\n  name: \"halo-extensions-kit\",\n  addExtensions() {\n    const internalExtensions: Extensions = [];\n\n    if (this.options.audio !== false) {\n      internalExtensions.push(ExtensionAudio.configure(this.options.audio));\n    }\n\n    if (this.options.blockquote !== false) {\n      internalExtensions.push(\n        ExtensionBlockquote.configure(this.options.blockquote)\n      );\n    }\n\n    if (this.options.bold !== false) {\n      internalExtensions.push(ExtensionBold.configure(this.options.bold));\n    }\n\n    if (this.options.bulletList !== false) {\n      internalExtensions.push(\n        ExtensionBulletList.configure(this.options.bulletList)\n      );\n    }\n\n    if (this.options.characterCount !== false) {\n      internalExtensions.push(\n        ExtensionCharacterCount.configure(this.options.characterCount)\n      );\n    }\n\n    if (this.options.clearFormat !== false) {\n      internalExtensions.push(\n        ExtensionClearFormat.configure(this.options.clearFormat)\n      );\n    }\n\n    if (this.options.code !== false) {\n      internalExtensions.push(ExtensionCode.configure(this.options.code));\n    }\n\n    if (this.options.codeBlock !== false) {\n      internalExtensions.push(\n        ExtensionCodeBlock.configure(this.options.codeBlock)\n      );\n    }\n\n    if (this.options.color !== false) {\n      internalExtensions.push(ExtensionColor.configure(this.options.color));\n    }\n\n    if (this.options.columns !== false) {\n      internalExtensions.push(ExtensionColumns.configure(this.options.columns));\n    }\n\n    if (this.options.commandsMenu !== false) {\n      internalExtensions.push(ExtensionCommandsMenu);\n    }\n\n    if (this.options.details !== false) {\n      internalExtensions.push(ExtensionDetails.configure(this.options.details));\n    }\n\n    if (this.options.document !== false) {\n      internalExtensions.push(ExtensionDocument);\n    }\n\n    if (this.options.dropCursor !== false) {\n      internalExtensions.push(\n        ExtensionDropcursor.configure(this.options.dropCursor)\n      );\n    }\n\n    if (this.options.figure !== false) {\n      internalExtensions.push(ExtensionFigure.configure(this.options.figure));\n    }\n\n    if (this.options.fontSize !== false) {\n      internalExtensions.push(\n        ExtensionFontSize.configure(this.options.fontSize)\n      );\n    }\n\n    if (this.options.formatBrush !== false) {\n      internalExtensions.push(\n        ExtensionFormatBrush.configure(this.options.formatBrush)\n      );\n    }\n\n    if (this.options.gallery !== false) {\n      internalExtensions.push(ExtensionGallery.configure(this.options.gallery));\n    }\n\n    if (this.options.gapCursor !== false) {\n      internalExtensions.push(ExtensionGapCursor);\n    }\n\n    if (this.options.hardBreak !== false) {\n      internalExtensions.push(\n        ExtensionHardBreak.configure(this.options.hardBreak)\n      );\n    }\n\n    if (this.options.heading !== false) {\n      internalExtensions.push(ExtensionHeading.configure(this.options.heading));\n    }\n\n    if (this.options.highlight !== false) {\n      internalExtensions.push(\n        ExtensionHighlight.configure(this.options.highlight)\n      );\n    }\n\n    if (this.options.history !== false) {\n      internalExtensions.push(ExtensionHistory.configure(this.options.history));\n    }\n\n    if (this.options.horizontalRule !== false) {\n      internalExtensions.push(\n        ExtensionHorizontalRule.configure(this.options.horizontalRule)\n      );\n    }\n\n    if (this.options.iframe !== false) {\n      internalExtensions.push(ExtensionIframe.configure(this.options.iframe));\n    }\n\n    if (this.options.image !== false) {\n      internalExtensions.push(ExtensionImage.configure(this.options.image));\n    }\n\n    if (this.options.indent !== false) {\n      internalExtensions.push(ExtensionIndent.configure(this.options.indent));\n    }\n\n    if (this.options.italic !== false) {\n      internalExtensions.push(ExtensionItalic.configure(this.options.italic));\n    }\n\n    if (this.options.link !== false) {\n      internalExtensions.push(ExtensionLink.configure(this.options.link));\n    }\n\n    if (this.options.listKeymap !== false) {\n      internalExtensions.push(\n        ExtensionListKeymap.configure(this.options.listKeymap)\n      );\n    }\n\n    if (this.options.nodeSelected !== false) {\n      internalExtensions.push(\n        ExtensionNodeSelected.configure(this.options.nodeSelected)\n      );\n    }\n\n    if (this.options.orderedList !== false) {\n      internalExtensions.push(\n        ExtensionOrderedList.configure(this.options.orderedList)\n      );\n    }\n\n    if (this.options.paragraph !== false) {\n      internalExtensions.push(\n        ExtensionParagraph.configure(this.options.paragraph)\n      );\n    }\n\n    if (this.options.placeholder !== false) {\n      internalExtensions.push(\n        ExtensionPlaceholder.configure(this.options.placeholder)\n      );\n    }\n\n    if (this.options.rangeSelection !== false) {\n      internalExtensions.push(ExtensionRangeSelection);\n    }\n\n    if (this.options.searchAndReplace !== false) {\n      internalExtensions.push(ExtensionSearchAndReplace);\n    }\n\n    if (this.options.smartScroll !== false) {\n      internalExtensions.push(\n        ExtensionSmartScroll.configure(this.options.smartScroll)\n      );\n    }\n\n    if (this.options.strike !== false) {\n      internalExtensions.push(ExtensionStrike.configure(this.options.strike));\n    }\n\n    if (this.options.subscript !== false) {\n      internalExtensions.push(\n        ExtensionSubscript.configure(this.options.subscript)\n      );\n    }\n\n    if (this.options.superscript !== false) {\n      internalExtensions.push(\n        ExtensionSuperscript.configure(this.options.superscript)\n      );\n    }\n\n    if (this.options.table !== false) {\n      internalExtensions.push(ExtensionTable.configure(this.options.table));\n    }\n\n    if (this.options.taskList !== false) {\n      internalExtensions.push(\n        ExtensionTaskList.configure(this.options.taskList)\n      );\n    }\n\n    if (this.options.text !== false) {\n      internalExtensions.push(ExtensionText.configure(this.options.text));\n    }\n\n    if (this.options.textAlign !== false) {\n      internalExtensions.push(\n        ExtensionTextAlign.configure(this.options.textAlign)\n      );\n    }\n\n    if (this.options.textStyle !== false) {\n      internalExtensions.push(\n        ExtensionTextStyle.configure(this.options.textStyle)\n      );\n    }\n\n    if (this.options.trailingNode !== false) {\n      internalExtensions.push(ExtensionTrailingNode);\n    }\n\n    if (this.options.underline !== false) {\n      internalExtensions.push(\n        ExtensionUnderline.configure(this.options.underline)\n      );\n    }\n\n    if (this.options.upload !== false) {\n      internalExtensions.push(ExtensionUpload);\n    }\n\n    if (this.options.video !== false) {\n      internalExtensions.push(ExtensionVideo.configure(this.options.video));\n    }\n\n    if (this.options.listExtra !== false) {\n      internalExtensions.push(\n        ExtensionListExtra.configure(this.options.listExtra)\n      );\n    }\n\n    if (this.options.blockPosition !== false) {\n      internalExtensions.push(\n        ExtensionBlockPosition.configure(this.options.blockPosition)\n      );\n    }\n\n    if (this.options.align !== false) {\n      internalExtensions.push(ExtensionAlign);\n    }\n\n    const extensions =\n      filterDuplicateExtensions([\n        ...internalExtensions,\n        ...(this.options.customExtensions || []),\n      ]) || [];\n\n    return extensions;\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/figure/FigureCaptionView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, ref, watch } from \"vue\";\nimport type { NodeViewProps } from \"@/tiptap/vue-3\";\nimport { NodeViewContent, NodeViewWrapper } from \"@/tiptap/vue-3\";\n\nconst props = defineProps<NodeViewProps>();\n\nconst width = computed(() => props.node?.attrs.width);\nconst placeholder = computed(() => props.node?.attrs[\"data-placeholder\"]);\nconst isEmpty = ref(props.node?.textContent.trim().length === 0);\n\nwatch(\n  () => props.node?.textContent,\n  (newContent) => {\n    isEmpty.value = newContent?.trim().length === 0;\n  }\n);\n</script>\n\n<template>\n  <node-view-wrapper\n    as=\"figcaption\"\n    :data-empty=\"isEmpty ? 'true' : undefined\"\n    :data-placeholder=\"placeholder\"\n    :style=\"{\n      width: width || '',\n      maxWidth: width ? '100%' : '',\n      textAlign: width ? 'center' : '',\n    }\"\n  >\n    <node-view-content />\n  </node-view-wrapper>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/figure/figure-caption.ts",
    "content": "import { i18n } from \"@/locales\";\nimport {\n  isActive,\n  mergeAttributes,\n  Node,\n  Plugin,\n  PluginKey,\n  TextSelection,\n  VueNodeViewRenderer,\n} from \"@/tiptap\";\nimport FigureCaptionView from \"./FigureCaptionView.vue\";\n\nexport const ExtensionFigureCaption = Node.create({\n  name: \"figureCaption\",\n  content: \"text*\",\n  inline: false,\n  group: \"block\",\n\n  addAttributes() {\n    return {\n      \"data-placeholder\": {\n        default: null,\n        parseHTML: (element) => element.getAttribute(\"data-placeholder\"),\n        renderHTML: (attributes) => {\n          return {\n            \"data-placeholder\":\n              attributes.dataPlaceholder ||\n              i18n.global.t(\n                \"editor.extensions.figure_caption.empty_placeholder\"\n              ),\n          };\n        },\n      },\n      width: {\n        default: null,\n        parseHTML: (element) => {\n          const style = element.getAttribute(\"style\");\n          if (style) {\n            const match = style.match(/width:\\s*([^;]+)/);\n            if (match) {\n              return match[1].trim();\n            }\n          }\n          return null;\n        },\n        renderHTML: (attributes) => {\n          if (!attributes.width) return {};\n          return {\n            style: `width: ${attributes.width}; max-width: 100%; text-align: center;`,\n          };\n        },\n      },\n    };\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      \"Mod-a\": ({ editor }) => {\n        const { state } = editor;\n        const { selection } = state;\n        const { $from } = selection;\n        if (isActive(state, this.name)) {\n          const start = $from.start($from.depth);\n          const end = $from.end($from.depth);\n          editor.commands.setTextSelection({ from: start, to: end });\n          return true;\n        }\n        return false;\n      },\n      Enter: ({ editor }) => {\n        const { state } = editor;\n        const { selection } = state;\n        const { $from } = selection;\n\n        let inCaption = false;\n        let figureDepth = -1;\n        for (let depth = $from.depth; depth > 0; depth--) {\n          const node = $from.node(depth);\n          if (node.type.name === this.name) {\n            inCaption = true;\n            figureDepth = depth - 1;\n            break;\n          }\n        }\n\n        if (!inCaption) {\n          return false;\n        }\n\n        const figureNode = $from.node(figureDepth);\n        const figurePos = $from.before(figureDepth);\n        const afterFigurePos = figurePos + figureNode.nodeSize;\n\n        editor\n          .chain()\n          .command(({ tr }) => {\n            const paragraph = tr.doc.type.schema.nodes.paragraph.create();\n            tr.insert(afterFigurePos, paragraph);\n            tr.setSelection(\n              TextSelection.near(tr.doc.resolve(afterFigurePos + 1))\n            );\n            return true;\n          })\n          .run();\n\n        return true;\n      },\n    };\n  },\n\n  addProseMirrorPlugins() {\n    const pluginKey = new PluginKey<{ previousCaptionPos: number | null }>(\n      \"figureCaptionAutoDelete\"\n    );\n\n    return [\n      new Plugin({\n        key: pluginKey,\n        state: {\n          init() {\n            return { previousCaptionPos: null as number | null };\n          },\n          apply(tr) {\n            const { selection } = tr;\n            const { $from } = selection;\n            let currentCaptionPos: number | null = null;\n            for (let depth = $from.depth; depth > 0; depth--) {\n              const node = $from.node(depth);\n              if (node.type.name === ExtensionFigureCaption.name) {\n                currentCaptionPos = $from.before(depth);\n                break;\n              }\n            }\n\n            return { previousCaptionPos: currentCaptionPos };\n          },\n        },\n        appendTransaction(transactions, oldState, newState) {\n          const pluginState = pluginKey.getState(oldState);\n          if (!pluginState) {\n            return null;\n          }\n\n          const { previousCaptionPos } = pluginState;\n          const { selection } = newState;\n          const { $from } = selection;\n\n          let currentInCaption = false;\n          for (let depth = $from.depth; depth > 0; depth--) {\n            const node = $from.node(depth);\n            if (node.type.name === ExtensionFigureCaption.name) {\n              currentInCaption = true;\n              break;\n            }\n          }\n\n          if (currentInCaption) {\n            return null;\n          }\n\n          if (\n            previousCaptionPos !== null &&\n            transactions.some((tr) => tr.docChanged || tr.selectionSet)\n          ) {\n            const captionNode = oldState.doc.nodeAt(previousCaptionPos);\n\n            if (\n              captionNode &&\n              captionNode.type.name === ExtensionFigureCaption.name\n            ) {\n              const isEmpty =\n                captionNode.textContent.trim().length === 0 ||\n                captionNode.childCount === 0;\n\n              if (isEmpty) {\n                const tr = newState.tr;\n                tr.delete(\n                  previousCaptionPos,\n                  previousCaptionPos + captionNode.nodeSize\n                );\n                return tr;\n              }\n            }\n          }\n\n          return null;\n        },\n      }),\n    ];\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"figcaption\",\n      },\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\n      \"figcaption\",\n      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n      0,\n    ];\n  },\n\n  addNodeView() {\n    return VueNodeViewRenderer(FigureCaptionView);\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/figure/index.ts",
    "content": "import {\n  findChildren,\n  mergeAttributes,\n  Node,\n  Plugin,\n  PluginKey,\n  TextSelection,\n  type CommandProps,\n} from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { ExtensionParagraph } from \"../paragraph\";\nimport { RangeSelection } from \"../range-selection\";\nimport { ExtensionFigureCaption } from \"./figure-caption\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    figure: {\n      setFigure: (attrs?: Record<string, unknown>) => ReturnType;\n      unsetFigure: () => ReturnType;\n      updateFigureContainerWidth: (width?: string) => ReturnType;\n    };\n  }\n}\n\nexport interface ExtensionFigureOptions extends ExtensionOptions {\n  HTMLAttributes: Record<string, unknown>;\n}\n\nexport const ExtensionFigure = Node.create<ExtensionFigureOptions>({\n  name: \"figure\",\n  group: \"block\",\n  content: \"(image|video|audio)? figureCaption?\",\n  isolating: true,\n  // Priority must be higher than paragraph (1000) and code-block to ensure\n  // the Backspace shortcut handles figure selection correctly.\n\n  addOptions() {\n    return {\n      HTMLAttributes: {},\n      getToolbarItems() {\n        return [];\n      },\n    };\n  },\n\n  addAttributes() {\n    return {\n      contentType: {\n        default: null,\n        parseHTML: (element) => element.getAttribute(\"data-content-type\"),\n        renderHTML: (attributes) => {\n          return {\n            \"data-content-type\": attributes.contentType,\n          };\n        },\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"figure\",\n      },\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\n      \"figure\",\n      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {\n        style: `display: flex; flex-direction: column;`,\n      }),\n      0,\n    ];\n  },\n\n  addExtensions() {\n    return [ExtensionFigureCaption];\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      Enter: ({ editor }) => {\n        const { state } = editor;\n        const { selection } = state;\n        const { $from } = selection;\n\n        let inFigure = false;\n        let figureDepth = -1;\n        for (let depth = $from.depth; depth > 0; depth--) {\n          const node = $from.node(depth);\n          if (node.type.name === this.name) {\n            inFigure = true;\n            figureDepth = depth;\n            break;\n          }\n        }\n\n        if (!inFigure) {\n          return false;\n        }\n\n        for (let depth = $from.depth; depth > 0; depth--) {\n          const node = $from.node(depth);\n          if (node.type.name === ExtensionFigureCaption.name) {\n            return false;\n          }\n        }\n\n        const figureNode = $from.node(figureDepth);\n        const figurePos = $from.before(figureDepth);\n        const afterFigurePos = figurePos + figureNode.nodeSize;\n\n        editor\n          .chain()\n          .command(({ tr }) => {\n            const paragraph = tr.doc.type.schema.nodes.paragraph.create();\n            tr.insert(afterFigurePos, paragraph);\n            tr.setSelection(\n              TextSelection.near(tr.doc.resolve(afterFigurePos + 1))\n            );\n            return true;\n          })\n          .run();\n\n        return true;\n      },\n      Backspace: ({ editor }) => {\n        const { state } = editor;\n        const { selection, doc } = state;\n        const { $from, empty } = selection;\n        if (!empty || $from.parentOffset !== 0) {\n          return false;\n        }\n\n        if ($from.parent.type.name !== ExtensionParagraph.name) {\n          return false;\n        }\n\n        const beforePos = $from.before($from.depth);\n        if (beforePos <= 0) {\n          return false;\n        }\n\n        const beforeResolve = doc.resolve(beforePos - 1);\n        const nodeBefore = beforeResolve.nodeBefore;\n\n        if (\n          !nodeBefore ||\n          nodeBefore.type.name !== ExtensionFigureCaption.name\n        ) {\n          return false;\n        }\n        let depth = beforeResolve.depth;\n        while (depth > 0) {\n          const node = beforeResolve.node(depth);\n          if (node.type.name === this.name) {\n            const figurePos = beforeResolve.before(depth);\n            const rangeSelection = RangeSelection.create(\n              doc,\n              figurePos,\n              figurePos + node.nodeSize\n            );\n            const tr = state.tr.setSelection(rangeSelection);\n            editor.view.dispatch(tr);\n            return true;\n          }\n          depth--;\n        }\n        return false;\n      },\n    };\n  },\n\n  addProseMirrorPlugins() {\n    return [\n      new Plugin({\n        key: new PluginKey(\"figureAutoDelete\"),\n        appendTransaction: (transactions, _oldState, newState) => {\n          const docChanged = transactions.some((tr) => tr.docChanged);\n          if (!docChanged) {\n            return null;\n          }\n          const tr = newState.tr;\n          const nodesToDelete: { pos: number; size: number }[] = [];\n\n          newState.doc.descendants((node, pos) => {\n            if (node.type.name !== this.name) {\n              return;\n            }\n\n            let hasValidContent = false;\n\n            node.forEach((child) => {\n              if (\n                child.type.name !== ExtensionParagraph.name ||\n                child.childCount > 0 ||\n                child.textContent.trim().length > 0\n              ) {\n                hasValidContent = true;\n              }\n            });\n\n            if (!hasValidContent) {\n              nodesToDelete.push({\n                pos: pos,\n                size: node.nodeSize,\n              });\n            }\n          });\n\n          nodesToDelete\n            .sort((a, b) => b.pos - a.pos)\n            .forEach(({ pos, size }) => {\n              tr.delete(pos, pos + size);\n            });\n\n          return nodesToDelete.length > 0 ? tr : null;\n        },\n      }),\n    ];\n  },\n\n  addCommands() {\n    return {\n      setFigure:\n        (attrs?: Record<string, unknown>) =>\n        ({ commands }: CommandProps) => {\n          return commands.wrapIn(this.name, attrs);\n        },\n      unsetFigure:\n        () =>\n        ({ commands }: CommandProps) => {\n          return commands.lift(this.name);\n        },\n      updateFigureContainerWidth:\n        (width?: string) =>\n        ({ state, dispatch }: CommandProps) => {\n          const { selection } = state;\n          const { $from } = selection;\n\n          let figureDepth = -1;\n          for (let d = $from.depth; d > 0; d--) {\n            if ($from.node(d).type.name === this.name) {\n              figureDepth = d;\n              break;\n            }\n          }\n\n          if (figureDepth === -1) {\n            return false;\n          }\n\n          const figureNode = $from.node(figureDepth);\n          const figureCaptionNodes = findChildren(\n            figureNode,\n            (node) => node.type.name === ExtensionFigureCaption.name\n          );\n\n          if (figureCaptionNodes.length === 0) {\n            return false;\n          }\n\n          const figureCaptionNode = figureCaptionNodes[0];\n          const figurePos = $from.start(figureDepth);\n          const captionPos = figurePos + figureCaptionNode.pos;\n\n          const tr = state.tr.setNodeMarkup(captionPos, undefined, {\n            width: width,\n          });\n          dispatch?.(tr);\n          return true;\n        },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/font-size/index.ts",
    "content": "import {\n  FontSize as TiptapFontSize,\n  type FontSizeOptions,\n} from \"@tiptap/extension-text-style\";\nimport { markRaw } from \"vue\";\nimport MingcuteFontSizeLine from \"~icons/mingcute/font-size-line\";\nimport { ToolbarItem, ToolbarSubItem } from \"@/components\";\nimport { i18n } from \"@/locales\";\nimport { type Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionFontSizeOptions = Partial<FontSizeOptions> &\n  ExtensionOptions;\n\nexport const ExtensionFontSize =\n  TiptapFontSize.extend<ExtensionFontSizeOptions>({\n    name: \"fontSize\",\n\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        types: [\"textStyle\"],\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 31,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: false,\n              icon: markRaw(MingcuteFontSizeLine),\n              title: i18n.global.t(\"editor.extensions.font_size.title\"),\n            },\n            children: [\n              {\n                priority: 0,\n                component: markRaw(ToolbarSubItem),\n                props: {\n                  editor,\n                  isActive: false,\n                  title: i18n.global.t(\"editor.common.text.default\"),\n                  action: () => editor.chain().focus().unsetFontSize().run(),\n                },\n              },\n              ...[8, 10, 12, 14, 16, 18, 20, 24, 30, 36, 48, 60, 72].map(\n                (size) => {\n                  return {\n                    priority: size,\n                    component: markRaw(ToolbarSubItem),\n                    props: {\n                      editor,\n                      isActive: false,\n                      title: `${size} px`,\n                      action: () => {\n                        return editor\n                          .chain()\n                          .focus()\n                          .setFontSize(`${size}px`)\n                          .run();\n                      },\n                    },\n                  };\n                }\n              ),\n            ],\n          };\n        },\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/format-brush/index.ts",
    "content": "import { markRaw } from \"vue\";\nimport MingcuteBrush3Line from \"~icons/mingcute/brush-3-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport { Editor, Extension, Plugin, PluginKey } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { getMarksByFirstTextNode, setMarks } from \"./util\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    formatBrush: {\n      copyFormatBrush: () => ReturnType;\n      pasteFormatBrush: () => ReturnType;\n    };\n  }\n}\n\nexport interface ExtensionFormatBrushStore {\n  formatBrush: boolean;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  formatBrushMarks: any[];\n}\n\nexport const ExtensionFormatBrush = Extension.create<\n  ExtensionOptions,\n  ExtensionFormatBrushStore\n>({\n  name: \"formatBrush\",\n\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        const formatBrush =\n          editor.view.dom.classList.contains(\"format-brush-mode\");\n        return {\n          priority: 25,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: formatBrush,\n            icon: markRaw(MingcuteBrush3Line),\n            title: formatBrush\n              ? i18n.global.t(\n                  \"editor.extensions.format_brush.toolbar_item.cancel\"\n                )\n              : i18n.global.t(\n                  \"editor.extensions.format_brush.toolbar_item.title\"\n                ),\n            action: () => {\n              if (formatBrush) {\n                editor.commands.pasteFormatBrush();\n              } else {\n                editor.commands.copyFormatBrush();\n              }\n            },\n          },\n        };\n      },\n    };\n  },\n\n  addCommands() {\n    return {\n      copyFormatBrush:\n        () =>\n        ({ state }) => {\n          const markRange = getMarksByFirstTextNode(state);\n          this.storage.formatBrushMarks = markRange;\n          this.storage.formatBrush = true;\n          this.editor.view.dom.classList.add(\"format-brush-mode\");\n          return true;\n        },\n      pasteFormatBrush: () => () => {\n        this.storage.formatBrushMarks = [];\n        this.storage.formatBrush = false;\n        this.editor.view.dom.classList.remove(\"format-brush-mode\");\n        return true;\n      },\n    };\n  },\n\n  addStorage() {\n    return {\n      formatBrush: false,\n      formatBrushMarks: [],\n    };\n  },\n\n  addProseMirrorPlugins() {\n    const storage = this.storage;\n    const editor = this.editor;\n    return [\n      new Plugin({\n        key: new PluginKey(\"formatBrushPlugin\"),\n        props: {\n          handleDOMEvents: {\n            mouseup(view) {\n              if (!storage.formatBrush) {\n                return;\n              }\n              editor\n                .chain()\n                .command(({ tr }) => {\n                  setMarks(view.state, storage.formatBrushMarks, tr);\n                  return true;\n                })\n                .pasteFormatBrush()\n                .run();\n            },\n          },\n        },\n      }),\n    ];\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      \"Shift-Mod-c\": () => {\n        this.editor.commands.copyFormatBrush();\n        return true;\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/format-brush/util.ts",
    "content": "import type { EditorState, MarkRange, Transaction } from \"@/tiptap\";\nimport { AddMarkStep, CellSelection, RemoveMarkStep } from \"@/tiptap/pm\";\n\n/**\n * get its marks through the first text node in the selector\n *\n * @param state editor state\n * @returns the marks of the current first text node\n */\nexport const getMarksByFirstTextNode = (state: EditorState): MarkRange[] => {\n  const marks: MarkRange[] = [];\n  const { doc, selection } = state;\n  const { from, to, empty } = selection;\n  if (empty) {\n    return marks;\n  }\n\n  let flag = false;\n  doc.nodesBetween(from, to, (node, pos) => {\n    if (!node || node?.nodeSize === undefined) {\n      return;\n    }\n\n    if (node.isText && !flag) {\n      flag = true;\n      marks.push(\n        ...node.marks.map((mark) => ({\n          from: pos,\n          to: pos + node.nodeSize,\n          mark,\n        }))\n      );\n      return false;\n    }\n  });\n  return marks;\n};\n\n/**\n *\n * Set marks for the text in the currently selected content. This method will first remove all marks\n * from the currently selected text, and then add marks again.\n *\n * For CellSelection, it is necessary to iterate through ranges and set marks for each range.\n *\n * @param state editor state\n * @param marks the marks to be set\n * @param transaction transaction\n *\n * @returns transaction\n *\n * */\nexport const setMarks = (\n  state: EditorState,\n  marks: MarkRange[],\n  transaction?: Transaction\n): Transaction => {\n  const { selection } = state;\n  const tr = transaction || state.tr;\n  const { from, to } = selection;\n\n  // When selection is CellSelection, iterate through ranges\n  if (selection instanceof CellSelection) {\n    selection.ranges.forEach((cellRange) => {\n      const range = {\n        from: cellRange.$from.pos,\n        to: cellRange.$to.pos,\n      };\n      setMarksByRange(tr, state, range, marks);\n    });\n  } else {\n    setMarksByRange(\n      tr,\n      state,\n      {\n        from,\n        to,\n      },\n      marks\n    );\n  }\n\n  return tr;\n};\n\nexport const setMarksByRange = (\n  tr: Transaction,\n  state: EditorState,\n  range: {\n    from: number;\n    to: number;\n  },\n  marks: MarkRange[]\n) => {\n  const { from, to } = range;\n  state.doc.nodesBetween(from, to, (node, pos) => {\n    if (!node || node?.nodeSize === undefined) {\n      return;\n    }\n\n    if (node.isText) {\n      // the range of the current text node\n      const range = {\n        from: Math.max(pos, from),\n        to: Math.min(pos + node.nodeSize, to),\n      };\n      // remove all marks of the current text node\n      node.marks.forEach((mark) => {\n        tr.step(new RemoveMarkStep(range.from, range.to, mark));\n      });\n      // add all marks of the current text node\n      marks.forEach((mark) => {\n        tr.step(new AddMarkStep(range.from, range.to, mark.mark));\n      });\n    }\n\n    return true;\n  });\n};\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/BubbleItemAddImage.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { utils, type AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { ref } from \"vue\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport type { ExtensionGalleryImageItem } from \".\";\nimport {\n  getCurrentGalleryImages,\n  updateGalleryImages,\n  useUploadGalleryImage,\n} from \"./useGalleryImages\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst emit = defineEmits([\"close\"]);\n\nconst dropdownShown = ref(false);\nconst { openFileDialog } = useUploadGalleryImage(props.editor);\n\nconst handleUploadClick = () => {\n  dropdownShown.value = false;\n  openFileDialog();\n};\n\n// Attachment Selector Modal\nconst attachmentSelectorModalVisible = ref(false);\n\nconst handleOpenAttachmentSelector = () => {\n  dropdownShown.value = false;\n  attachmentSelectorModalVisible.value = true;\n};\n\nfunction onAttachmentSelect(attachments: AttachmentLike[]) {\n  const currentImages = getCurrentGalleryImages(props.editor);\n  const newImages = attachments\n    .map((attachment) => {\n      const url = utils.attachment.getUrl(attachment);\n      if (!url) {\n        return;\n      }\n      return {\n        src: url,\n        aspectRatio: 0,\n      };\n    })\n    .filter(Boolean) as ExtensionGalleryImageItem[];\n  updateGalleryImages(props.editor, [...currentImages, ...newImages]);\n  emit(\"close\");\n}\n</script>\n\n<template>\n  <VDropdown v-model:shown=\"dropdownShown\" :triggers=\"['click']\" :distance=\"10\">\n    <BubbleButton :title=\"title\" :is-active=\"dropdownShown\">\n      <template #icon>\n        <component :is=\"icon\" :style=\"iconStyle\" />\n      </template>\n    </BubbleButton>\n    <template #popper>\n      <DropdownItem\n        v-if=\"\n          utils.permission.has([\n            'uc:attachments:manage',\n            'system:attachments:manage',\n          ])\n        \"\n        class=\"!min-w-36\"\n        @click=\"handleUploadClick\"\n      >\n        {{ i18n.global.t(\"editor.common.button.upload\") }}\n      </DropdownItem>\n      <DropdownItem\n        v-if=\"\n          utils.permission.has([\n            'system:attachments:view',\n            'uc:attachments:manage',\n          ])\n        \"\n        class=\"!min-w-36\"\n        @click=\"handleOpenAttachmentSelector\"\n      >\n        {{ i18n.global.t(\"editor.extensions.upload.attachment.title\") }}\n      </DropdownItem>\n    </template>\n  </VDropdown>\n  <AttachmentSelectorModal\n    v-if=\"attachmentSelectorModalVisible\"\n    :accepts=\"['image/*']\"\n    @select=\"onAttachmentSelect\"\n    @close=\"attachmentSelectorModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/BubbleItemGap.vue",
    "content": "<script setup lang=\"ts\">\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport MingcuteDistributeSpacingHorizontalLine from \"~icons/mingcute/distribute-spacing-horizontal-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport Input from \"@/components/base/Input.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionGallery } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst gap = computed(() => {\n  return props.editor.getAttributes(ExtensionGallery.name).gap;\n});\n\nfunction onGapChange(value: string | number | undefined) {\n  props.editor\n    .chain()\n    .updateAttributes(ExtensionGallery.name, { gap: value })\n    .run();\n}\n</script>\n\n<template>\n  <VDropdown\n    ref=\"dropdownRef\"\n    class=\"inline-flex\"\n    :triggers=\"['click']\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.extensions.gallery.gap')\"\n      :text=\"`${gap}px`\"\n      show-more-indicator\n    >\n      <template #icon>\n        <MingcuteDistributeSpacingHorizontalLine />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <div class=\"w-80\">\n        <Input\n          auto-focus\n          :label=\"i18n.global.t('editor.extensions.gallery.gap')\"\n          type=\"number\"\n          :model-value=\"gap\"\n          @update:model-value=\"onGapChange\"\n        />\n      </div>\n    </template>\n  </VDropdown>\n\n  <BlockActionSeparator :editor=\"editor\" />\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/BubbleItemGroupSize.vue",
    "content": "<script setup lang=\"ts\">\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed, ref } from \"vue\";\nimport MingcuteDotGridLine from \"~icons/mingcute/dot-grid-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionGallery } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst dropdownRef = ref();\n\nconst groupSize = computed(() => {\n  return props.editor.getAttributes(ExtensionGallery.name).groupSize || 3;\n});\n\nfunction handleSetGroupSize(size: number) {\n  const currentImages =\n    props.editor.getAttributes(ExtensionGallery.name).images || [];\n\n  props.editor\n    .chain()\n    .updateAttributes(ExtensionGallery.name, {\n      images: currentImages,\n      groupSize: size,\n    })\n    .setNodeSelection(props.editor.state.selection.from)\n    .focus()\n    .run();\n\n  dropdownRef.value?.hide();\n}\n</script>\n\n<template>\n  <VDropdown\n    ref=\"dropdownRef\"\n    class=\"inline-flex\"\n    :triggers=\"['click']\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.extensions.gallery.group_size')\"\n      :text=\"\n        i18n.global.t('editor.extensions.gallery.group_size_label', {\n          count: groupSize,\n        })\n      \"\n      show-more-indicator\n    >\n      <template #icon>\n        <MingcuteDotGridLine />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <DropdownItem\n        v-for=\"i in 10\"\n        :key=\"i\"\n        class=\"!min-w-36\"\n        :is-active=\"i === groupSize\"\n        @click=\"handleSetGroupSize(i)\"\n      >\n        {{ i }}\n      </DropdownItem>\n    </template>\n  </VDropdown>\n\n  <BlockActionSeparator :editor=\"editor\" />\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/BubbleItemLayout.vue",
    "content": "<script setup lang=\"ts\">\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed, ref } from \"vue\";\nimport MingcuteLayout10Line from \"~icons/mingcute/layout-10-line\";\nimport MingcuteLayoutGridLine from \"~icons/mingcute/layout-grid-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionGallery } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst dropdownRef = ref();\n\nconst layout = computed(() => {\n  return props.editor.getAttributes(ExtensionGallery.name).layout || \"auto\";\n});\n\nconst options = [\n  {\n    label: i18n.global.t(\"editor.extensions.gallery.layout.auto\"),\n    value: \"auto\",\n    icon: MingcuteLayout10Line,\n  },\n  {\n    label: i18n.global.t(\"editor.extensions.gallery.layout.square\"),\n    value: \"square\",\n    icon: MingcuteLayoutGridLine,\n  },\n];\n\nconst currentLayout = computed(() => {\n  return options.find((option) => option.value === layout.value) || options[0];\n});\n\nfunction handleSetLayout(layout: string) {\n  props.editor\n    .chain()\n    .updateAttributes(ExtensionGallery.name, { layout })\n    .run();\n  dropdownRef.value?.hide();\n}\n</script>\n\n<template>\n  <VDropdown\n    ref=\"dropdownRef\"\n    class=\"inline-flex\"\n    :triggers=\"['click']\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.extensions.gallery.layout.title')\"\n      :text=\"currentLayout.label\"\n      show-more-indicator\n    >\n      <template #icon>\n        <component :is=\"currentLayout.icon\" />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <DropdownItem\n        v-for=\"option in options\"\n        :key=\"option.value\"\n        class=\"!min-w-36\"\n        :is-active=\"option.value === layout\"\n        @click=\"handleSetLayout(option.value)\"\n      >\n        <template #icon>\n          <component :is=\"option.icon\" />\n        </template>\n        {{ option.label }}\n      </DropdownItem>\n    </template>\n  </VDropdown>\n\n  <BlockActionSeparator :editor=\"editor\" />\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/GalleryView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton, VSpace } from \"@halo-dev/components\";\nimport { utils, type AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { computed, ref } from \"vue\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport { i18n } from \"@/locales\";\nimport { NodeViewWrapper, type NodeViewProps } from \"@/tiptap\";\nimport type { ExtensionGalleryImageItem } from \"./index\";\nimport { useUploadGalleryImage } from \"./useGalleryImages\";\n\nconst props = defineProps<NodeViewProps>();\n\nconst images = computed({\n  get: () => {\n    return props.node?.attrs.images || [];\n  },\n  set: (images: ExtensionGalleryImageItem[]) => {\n    props.updateAttributes({\n      images: images,\n    });\n  },\n});\n\nconst { openFileDialog } = useUploadGalleryImage(props.editor);\n\nfunction handleSetFocus() {\n  props.editor.commands.setNodeSelection(props.getPos() || 0);\n}\n\nfunction removeImage(index: number) {\n  const newImages = [...images.value];\n  newImages.splice(index, 1);\n  images.value = newImages;\n}\n\nfunction handleImageLoad(event: Event, index: number) {\n  const img = event.target as HTMLImageElement;\n  if (img.naturalWidth && img.naturalHeight) {\n    const ratio = img.naturalWidth / img.naturalHeight;\n    const newImages = [...images.value];\n    newImages[index] = {\n      src: img.src,\n      aspectRatio: ratio,\n    };\n    images.value = [...newImages];\n  }\n}\n\nconst groupSize = computed<number>(() => {\n  return props.node?.attrs.groupSize || props.extension.options?.groupSize || 3;\n});\n\nconst layout = computed<string>(() => {\n  return props.node?.attrs.layout || \"auto\";\n});\n\nconst gap = computed<number>(() => {\n  return props.node?.attrs.gap;\n});\n\nconst groups = computed<ExtensionGalleryImageItem[][]>(() => {\n  return images.value.reduce(\n    (\n      acc: ExtensionGalleryImageItem[][],\n      image: ExtensionGalleryImageItem,\n      index: number\n    ) => {\n      const groupIndex = Math.floor(index / groupSize.value);\n      acc[groupIndex] = acc[groupIndex] || [];\n      acc[groupIndex].push(image);\n      return acc;\n    },\n    []\n  );\n});\n\nconst draggedIndex = ref<number | null>(null);\nconst dragOverIndex = ref<number | null>(null);\n\nfunction handleDragStart(index: number, event: DragEvent) {\n  draggedIndex.value = index;\n  if (event.dataTransfer) {\n    event.dataTransfer.effectAllowed = \"move\";\n    event.dataTransfer.setData(\"text/html\", String(index));\n  }\n  (event.target as HTMLElement).classList.add(\"opacity-50\");\n}\n\nfunction handleDragEnd(event: DragEvent) {\n  (event.target as HTMLElement).classList.remove(\"opacity-50\");\n  draggedIndex.value = null;\n  dragOverIndex.value = null;\n}\n\nfunction handleDragOver(event: DragEvent) {\n  event.preventDefault();\n  if (event.dataTransfer) {\n    event.dataTransfer.dropEffect = \"move\";\n  }\n}\n\nfunction handleDragEnter(index: number, event: DragEvent) {\n  event.preventDefault();\n  dragOverIndex.value = index;\n  const target = event.currentTarget as HTMLElement;\n  target.classList.add(\"ring-2\", \"ring-blue-500\");\n}\n\nfunction handleDragLeave(event: DragEvent) {\n  const target = event.currentTarget as HTMLElement;\n  target.classList.remove(\"ring-2\", \"ring-blue-500\");\n}\n\nfunction handleDrop(targetIndex: number, event: DragEvent) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  const target = event.currentTarget as HTMLElement;\n  target.classList.remove(\"ring-2\", \"ring-blue-500\");\n\n  if (draggedIndex.value === null || draggedIndex.value === targetIndex) {\n    return;\n  }\n\n  const newImages = [...images.value];\n  const [movedImage] = newImages.splice(draggedIndex.value, 1);\n  newImages.splice(targetIndex, 0, movedImage);\n  images.value = newImages;\n\n  draggedIndex.value = null;\n  dragOverIndex.value = null;\n}\n\n// Attachment Selector Modal\nconst attachmentSelectorModalVisible = ref(false);\n\nfunction onAttachmentSelect(attachments: AttachmentLike[]) {\n  const newImages = attachments\n    .map((attachment) => {\n      const url = utils.attachment.getUrl(attachment);\n      if (!url) {\n        return;\n      }\n      return {\n        src: url,\n        aspectRatio: 0,\n      };\n    })\n    .filter(Boolean) as ExtensionGalleryImageItem[];\n  images.value = [...images.value, ...newImages];\n}\n</script>\n\n<template>\n  <node-view-wrapper\n    as=\"div\"\n    class=\"mt-2 p-0.5\"\n    :class=\"{\n      'rounded ring-2': selected,\n    }\"\n    @click=\"handleSetFocus\"\n  >\n    <div\n      v-if=\"images.length === 0\"\n      class=\"relative flex h-full items-center justify-center rounded-md border border-gray-200 bg-gray-50 before:pb-[62.5%]\"\n    >\n      <VSpace>\n        <VButton\n          v-if=\"\n            utils.permission.has([\n              'uc:attachments:manage',\n              'system:attachments:manage',\n            ])\n          \"\n          @click=\"openFileDialog()\"\n        >\n          {{ i18n.global.t(\"editor.common.button.upload\") }}\n        </VButton>\n\n        <VButton\n          v-if=\"\n            utils.permission.has(\n              ['system:attachments:view', 'uc:attachments:manage'],\n              true\n            )\n          \"\n          @click=\"attachmentSelectorModalVisible = true\"\n        >\n          {{ i18n.global.t(\"editor.extensions.upload.attachment.title\") }}\n        </VButton>\n      </VSpace>\n    </div>\n    <div v-else class=\"relative grid\" :style=\"{ gap: `${gap}px` }\">\n      <div\n        v-for=\"(group, groupIndex) in groups\"\n        :key=\"groupIndex\"\n        class=\"flex flex-row justify-center\"\n        :style=\"{ gap: `${gap}px` }\"\n      >\n        <div\n          v-for=\"(image, imgIndex) in group\"\n          :key=\"groupIndex * groupSize + imgIndex\"\n          draggable=\"true\"\n          class=\"group/image relative cursor-grab transition-all active:cursor-grabbing\"\n          :class=\"{\n            'aspect-1': layout === 'square',\n          }\"\n          :style=\"{\n            flex: `${layout === 'square' ? '1' : image.aspectRatio} 1 0%`,\n          }\"\n          @dragstart=\"\n            handleDragStart(groupIndex * groupSize + imgIndex, $event)\n          \"\n          @dragend=\"handleDragEnd($event)\"\n          @dragover=\"handleDragOver($event)\"\n          @dragenter=\"\n            handleDragEnter(groupIndex * groupSize + imgIndex, $event)\n          \"\n          @dragleave=\"handleDragLeave($event)\"\n          @drop=\"handleDrop(groupIndex * groupSize + imgIndex, $event)\"\n        >\n          <img\n            :src=\"image.src\"\n            :alt=\"`Gallery image ${groupIndex * groupSize + imgIndex + 1}`\"\n            class=\"pointer-events-none block size-full object-cover\"\n            @load=\"handleImageLoad($event, groupIndex * groupSize + imgIndex)\"\n          />\n          <div\n            class=\"pointer-events-none invisible absolute inset-0 bg-gradient-to-t from-black/0 via-black/5 to-black/30 p-1 opacity-0 transition-all group-hover/image:visible group-hover/image:opacity-100\"\n          >\n            <div class=\"flex flex-row-reverse\">\n              <button\n                v-tooltip=\"\n                  i18n.global.t(\n                    'editor.extensions.upload.operations.remove.button'\n                  )\n                \"\n                aria-label=\"Delete\"\n                class=\"text-grey-900 group pointer-events-auto relative flex size-8 cursor-pointer items-center justify-center rounded-md bg-white/90 transition-all hover:bg-white hover:text-black active:!bg-white/80\"\n                type=\"button\"\n                @click.stop=\"removeImage(groupIndex * groupSize + imgIndex)\"\n              >\n                <MingcuteDelete2Line class=\"size-4\" />\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <AttachmentSelectorModal\n      v-if=\"attachmentSelectorModalVisible\"\n      :accepts=\"['image/*']\"\n      @select=\"onAttachmentSelect\"\n      @close=\"attachmentSelectorModalVisible = false\"\n    />\n  </node-view-wrapper>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/gallery-bubble.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { markRaw } from \"vue\";\nimport MdiImagePlus from \"~icons/mdi/image-plus\";\nimport { i18n } from \"@/locales\";\nimport { Extension } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { GALLERY_BUBBLE_MENU_KEY } from \".\";\nimport BubbleItemAddImage from \"./BubbleItemAddImage.vue\";\n\nexport type ExtensionGalleryBubbleOptions = ExtensionOptions & {\n  uploadImage?: (\n    file: File,\n    options?: AxiosRequestConfig\n  ) => Promise<Attachment>;\n};\n\nexport const ExtensionGalleryBubble =\n  Extension.create<ExtensionGalleryBubbleOptions>({\n    name: \"gallery-bubble\",\n    addOptions() {\n      const { parent } = this;\n      return {\n        ...parent?.(),\n        uploadImage: undefined,\n        getBubbleMenu: () => {\n          return {\n            extendsKey: GALLERY_BUBBLE_MENU_KEY,\n            items: [\n              {\n                priority: 10,\n                component: markRaw(BubbleItemAddImage),\n                key: \"add-image\",\n                props: {\n                  icon: markRaw(MdiImagePlus),\n                  title: i18n.global.t(\"editor.extensions.gallery.add_image\"),\n                },\n              },\n            ],\n          };\n        },\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/index.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { markRaw } from \"vue\";\nimport MdiImagePlus from \"~icons/mdi/image-plus\";\nimport MingcutePhotoAlbumLine from \"~icons/mingcute/photo-album-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport ToolboxItem from \"@/components/toolbox/ToolboxItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  isActive,\n  Node,\n  PluginKey,\n  VueNodeViewRenderer,\n  type Editor,\n  type Range,\n} from \"@/tiptap\";\nimport type { EditorState } from \"@/tiptap/pm\";\nimport type { ExtensionOptions, NodeBubbleMenuType } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\nimport BubbleItemAddImage from \"./BubbleItemAddImage.vue\";\nimport BubbleItemGap from \"./BubbleItemGap.vue\";\nimport BubbleItemGroupSize from \"./BubbleItemGroupSize.vue\";\nimport BubbleItemLayout from \"./BubbleItemLayout.vue\";\nimport { ExtensionGalleryBubble } from \"./gallery-bubble\";\nimport GalleryView from \"./GalleryView.vue\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    gallery: {\n      setGallery: (options?: { images?: string[] }) => ReturnType;\n    };\n  }\n}\n\nexport type ExtensionGalleryImageItem = {\n  src: string;\n  aspectRatio: number;\n};\n\nexport const GALLERY_BUBBLE_MENU_KEY = new PluginKey(\"galleryBubbleMenu\");\n\nexport type ExtensionGalleryOptions = ExtensionOptions & {\n  groupSize?: number;\n  gap?: number;\n  allowBase64: boolean;\n  HTMLAttributes: Record<string, unknown>;\n  uploadImage?: (\n    file: File,\n    options?: AxiosRequestConfig\n  ) => Promise<Attachment>;\n};\n\nexport const ExtensionGallery = Node.create<\n  ExtensionGalleryOptions,\n  {\n    images: ExtensionGalleryImageItem[];\n  }\n>({\n  name: \"gallery\",\n\n  group: \"block\",\n\n  atom: true,\n\n  draggable: true,\n\n  fakeSelection: true,\n\n  allowGapCursor: true,\n\n  addAttributes() {\n    return {\n      images: {\n        default: [],\n        parseHTML: (element) => {\n          return Array.from(element.querySelectorAll(\"img\")).map((img) => {\n            return {\n              src: img.getAttribute(\"src\") || \"\",\n              aspectRatio: Number(img.getAttribute(\"data-aspect-ratio\")) || 0,\n            };\n          });\n        },\n      },\n      groupSize: {\n        default: 3,\n        parseHTML: (element) => {\n          return Number(element.getAttribute(\"data-group-size\")) || 3;\n        },\n      },\n      layout: {\n        default: \"auto\",\n        parseHTML: (element) => {\n          return element.getAttribute(\"data-layout\") || \"auto\";\n        },\n      },\n      gap: {\n        default: 8,\n        parseHTML: (element) => {\n          const gap = Number(element.getAttribute(\"data-gap\"));\n          if (isNaN(gap) || gap < 0) {\n            return 0;\n          }\n          return gap;\n        },\n      },\n      file: {\n        default: null,\n        renderHTML() {\n          return {};\n        },\n        parseHTML() {\n          return null;\n        },\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: 'div[data-type=\"gallery\"]',\n      },\n    ];\n  },\n\n  renderHTML({ node }) {\n    const images: ExtensionGalleryImageItem[] = node.attrs.images || [];\n    const groupSize = node.attrs.groupSize || this.options?.groupSize || 3;\n    const layout = node.attrs.layout || \"auto\";\n    const gap = node.attrs.gap || this.options?.gap || 0;\n    const imageGroups: ExtensionGalleryImageItem[][] = images.reduce(\n      (\n        acc: ExtensionGalleryImageItem[][],\n        image: ExtensionGalleryImageItem,\n        index: number\n      ) => {\n        const groupIndex = Math.floor(index / groupSize);\n        acc[groupIndex] = acc[groupIndex] || [];\n        acc[groupIndex].push(image);\n        return acc;\n      },\n      []\n    );\n    const imageGroupElements = imageGroups.map(\n      (items: ExtensionGalleryImageItem[]) => [\n        \"div\",\n        {\n          \"data-type\": \"gallery-group\",\n          style: `display: flex; flex-direction: row; justify-content: center; gap: ${gap}px;`,\n        },\n        ...items.map((image: ExtensionGalleryImageItem) => {\n          return [\n            \"div\",\n            {\n              style: `flex: ${layout === \"square\" ? \"1\" : image.aspectRatio} 1 0%;${layout === \"square\" ? \"aspect-ratio: 1/1;\" : \"\"}`,\n              \"data-aspect-ratio\": image.aspectRatio.toString(),\n            },\n            [\n              \"img\",\n              {\n                src: image.src,\n                \"data-type\": \"gallery-image\",\n                style:\n                  \"width: 100%; height: 100%; margin: 0; object-fit: cover;\",\n              },\n            ],\n          ];\n        }),\n      ]\n    );\n\n    return [\n      \"div\",\n      {\n        \"data-type\": \"gallery\",\n        \"data-group-size\": groupSize.toString(),\n        \"data-layout\": layout,\n        \"data-gap\": gap?.toString(),\n      },\n      [\n        \"div\",\n        { style: `display: grid; gap: ${gap}px;` },\n        ...imageGroupElements,\n      ],\n    ];\n  },\n\n  addCommands() {\n    return {\n      setGallery:\n        (options) =>\n        ({ commands }) => {\n          return commands.insertContent({\n            type: this.name,\n            attrs: {\n              images: options?.images || [],\n            },\n          });\n        },\n    };\n  },\n\n  addNodeView() {\n    return VueNodeViewRenderer(GalleryView);\n  },\n\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      allowBase64: false,\n      HTMLAttributes: {},\n      uploadImage: undefined,\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 15,\n          component: markRaw(ToolboxItem),\n          props: {\n            editor,\n            icon: markRaw(MingcutePhotoAlbumLine),\n            title: i18n.global.t(\"editor.extensions.gallery.title\"),\n            action: () => {\n              editor.chain().focus().setGallery({ images: [] }).run();\n            },\n          },\n        };\n      },\n      getCommandMenuItems() {\n        return {\n          priority: 96,\n          icon: markRaw(MingcutePhotoAlbumLine),\n          title: \"editor.extensions.commands_menu.gallery\",\n          keywords: [\"gallery\", \"hualang\", \"tupian\", \"images\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .setGallery({ images: [] })\n              .run();\n          },\n        };\n      },\n      getBubbleMenu(): NodeBubbleMenuType {\n        return {\n          pluginKey: GALLERY_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }): boolean => {\n            return isActive(state, ExtensionGallery.name);\n          },\n          options: {\n            placement: \"top-start\",\n          },\n          items: [\n            {\n              priority: 10,\n              component: markRaw(BubbleItemAddImage),\n              key: \"add-image\",\n              props: {\n                icon: markRaw(MdiImagePlus),\n                title: i18n.global.t(\"editor.extensions.gallery.add_image\"),\n              },\n            },\n            {\n              priority: 20,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 30,\n              component: markRaw(BubbleItemGroupSize),\n            },\n            {\n              priority: 40,\n              component: markRaw(BubbleItemLayout),\n            },\n            {\n              priority: 50,\n              component: markRaw(BubbleItemGap),\n            },\n            {\n              priority: 60,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }) => {\n                  deleteNode(ExtensionGallery.name, editor);\n                },\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n\n  addExtensions() {\n    return [\n      ...(this.parent?.() || []),\n      ExtensionGalleryBubble.configure({\n        uploadImage: this.options.uploadImage,\n      }),\n    ];\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gallery/useGalleryImages.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport { useFileDialog } from \"@vueuse/core\";\nimport { computed, ref } from \"vue\";\nimport type { Editor } from \"@/tiptap\";\nimport { uploadFile } from \"../../utils/upload\";\nimport { ExtensionGallery, type ExtensionGalleryImageItem } from \"./index\";\n\nexport function getCurrentGalleryImages(\n  editor: Editor\n): ExtensionGalleryImageItem[] {\n  return editor.getAttributes(ExtensionGallery.name).images || [];\n}\n\nexport function updateGalleryImages(\n  editor: Editor,\n  images: ExtensionGalleryImageItem[],\n  focus = true\n) {\n  const chain = editor\n    .chain()\n    .updateAttributes(ExtensionGallery.name, { images })\n    .setNodeSelection(editor.state.selection.from);\n\n  if (focus) {\n    chain.focus();\n  }\n\n  chain.run();\n}\n\nexport function useUploadGalleryImage(editor: Editor) {\n  const controller = ref<AbortController>();\n\n  const { open: openFileDialog, onChange } = useFileDialog({\n    accept: \"image/*\",\n    multiple: true,\n    reset: true,\n  });\n\n  onChange((files) => {\n    if (!files) {\n      return;\n    }\n    if (files.length > 0) {\n      handleFiles(Array.from(files));\n    }\n  });\n\n  const handleFiles = (files: File[]) => {\n    for (const file of files) {\n      handleUploadFile(file);\n    }\n  };\n\n  const uploadImage = computed(() => {\n    return editor.extensionManager.extensions.find(\n      (extension) => extension.name === ExtensionGallery.name\n    )?.options.uploadImage;\n  });\n\n  /**\n   *\n   * Upload files to the attachment library.\n   *\n   * @param file attachments that need to be uploaded to the attachment library\n   */\n  const handleUploadFile = (file: File) => {\n    if (!uploadImage.value) {\n      return;\n    }\n    controller.value = new AbortController();\n    uploadFile(file, uploadImage.value, {\n      controller: controller.value,\n      onUploadProgress: () => {},\n\n      onFinish: (attachment?: Attachment) => {\n        if (attachment) {\n          editor.commands.updateAttributes(ExtensionGallery.name, {\n            images: [\n              ...getCurrentGalleryImages(editor),\n              {\n                src: attachment.status?.permalink,\n                aspectRatio: 0,\n              },\n            ],\n          });\n        }\n      },\n\n      onError: () => {},\n    });\n  };\n\n  return {\n    openFileDialog,\n  };\n}\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gap-cursor/gap-cursor-selection.ts",
    "content": "import type { PMNode } from \"@/tiptap\";\nimport {\n  NodeSelection,\n  ResolvedPos,\n  Selection,\n  Slice,\n  type Mappable,\n} from \"@/tiptap/pm\";\n\nclass GapCursorSelection extends Selection {\n  private start: boolean | null = false;\n\n  constructor($pos: ResolvedPos) {\n    super($pos, $pos);\n    this.start = isNodeStart($pos);\n  }\n\n  map(doc: PMNode, mapping: Mappable): Selection {\n    const $pos = doc.resolve(mapping.map(this.head));\n    return GapCursorSelection.valid($pos)\n      ? new GapCursorSelection($pos)\n      : Selection.near($pos);\n  }\n\n  content() {\n    return Slice.empty;\n  }\n\n  eq(other: Selection): boolean {\n    return other instanceof GapCursorSelection && other.head == this.head;\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  toJSON(): any {\n    return { type: \"node-gap-cursor\", pos: this.head };\n  }\n\n  get isStart() {\n    return this.start;\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  static fromJSON(doc: PMNode, json: any): GapCursorSelection {\n    if (typeof json.pos != \"number\") {\n      throw new RangeError(\"Invalid input for GapCursorSelection.fromJSON\");\n    }\n    return new GapCursorSelection(doc.resolve(json.pos));\n  }\n\n  getBookmark() {\n    return new GapBookmark(this.anchor);\n  }\n\n  /**\n   * Validates if a GapCursor can be placed at the given position\n   *\n   * This function checks whether a GapCursor can be placed at the specified position in the document.\n   * It ensures that the position is not within a text block, and that the node at the position allows a GapCursor.\n   *\n   * @param {ResolvedPos} $pos - The resolved position in the document to validate.\n   * @returns {boolean} - Returns true if a GapCursor can be placed at the given position, false otherwise.\n   */\n  static valid($pos: ResolvedPos) {\n    if ($pos.depth < 1) {\n      return false;\n    }\n    // Get the node at the current position\n    const nodeOffset = $pos.doc.childBefore($pos.pos);\n    const root = nodeOffset.node;\n    if (!root) {\n      return false;\n    }\n    const parent = $pos.parent;\n    if (parent.isTextblock || (!closedBefore($pos) && !closedAfter($pos))) {\n      return false;\n    }\n    // Check if the node allows a GapCursor\n    const override = root.type.spec.allowGapCursor;\n    if (!override) {\n      return false;\n    }\n    return !root.type.inlineContent;\n  }\n\n  static findGapCursorFrom($pos: ResolvedPos, dir: number, mustMove = false) {\n    let keepSearching = true;\n    while (keepSearching) {\n      if (!mustMove && GapCursorSelection.valid($pos)) {\n        return $pos;\n      }\n      let pos = $pos.pos;\n      let next: PMNode | null = null;\n\n      // Scan up from this position\n      for (let d = $pos.depth; d >= 0; d--) {\n        const parent = $pos.node(d);\n        const index = dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1;\n\n        if (dir > 0 ? index < parent.childCount : index >= 0) {\n          next = parent.child(index);\n          break;\n        }\n\n        if (d == 0) {\n          return null;\n        }\n\n        pos += dir;\n        const $cur = $pos.doc.resolve(pos);\n        if (GapCursorSelection.valid($cur)) {\n          return $cur;\n        }\n      }\n\n      // And then down into the next node\n      while (next) {\n        const inside = dir > 0 ? next.firstChild : next.lastChild;\n        if (!inside) {\n          if (\n            next.isAtom &&\n            !next.isText &&\n            !NodeSelection.isSelectable(next)\n          ) {\n            $pos = $pos.doc.resolve(pos + next.nodeSize * dir);\n            mustMove = false;\n            break;\n          }\n          keepSearching = false;\n          break;\n        }\n        next = inside;\n        pos += dir;\n        const $cur = $pos.doc.resolve(pos);\n        if (GapCursorSelection.valid($cur)) {\n          return $cur;\n        }\n      }\n\n      if (!next) {\n        keepSearching = false;\n      }\n    }\n\n    return null;\n  }\n}\n\nGapCursorSelection.prototype.visible = false;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n(GapCursorSelection as any).findFrom = GapCursorSelection.findGapCursorFrom;\n\nSelection.jsonID(\"node-gap-cursor\", GapCursorSelection);\n\nclass GapBookmark {\n  constructor(readonly pos: number) {}\n\n  map(mapping: Mappable) {\n    return new GapBookmark(mapping.map(this.pos));\n  }\n\n  resolve(doc: PMNode) {\n    const $pos = doc.resolve(this.pos);\n    return GapCursorSelection.valid($pos)\n      ? new GapCursorSelection($pos)\n      : Selection.near($pos);\n  }\n}\n\n/**\n * Checks if the position before the given resolved position is closed\n *\n * This function traverses up the document tree from the given resolved position and checks if the position\n * immediately before it is closed. A position is considered closed if the previous node is closed or\n * if the parent node is isolating.\n *\n * @param {ResolvedPos} $pos - The resolved position in the document to check.\n * @returns {boolean} - Returns true if the position before the given position is closed, false otherwise.\n */\nexport function closedBefore($pos: ResolvedPos) {\n  for (let d = $pos.depth; d >= 0; d--) {\n    const index = $pos.index(d);\n    const parent = $pos.node(d);\n\n    if (index === 0) {\n      if (parent.type.spec.isolating) {\n        return true;\n      }\n      continue;\n    }\n\n    if (isNodeClosed(parent.child(index - 1), false)) {\n      return true;\n    }\n  }\n  return true;\n}\n\nexport function closedAfter($pos: ResolvedPos) {\n  for (let d = $pos.depth; d >= 0; d--) {\n    const index = $pos.indexAfter(d);\n    const parent = $pos.node(d);\n\n    if (index === parent.childCount) {\n      if (parent.type.spec.isolating) {\n        return true;\n      }\n      continue;\n    }\n\n    if (isNodeClosed(parent.child(index), true)) {\n      return true;\n    }\n  }\n  return true;\n}\n\nfunction isNodeClosed(node: PMNode, isAfter: boolean): boolean {\n  while (node) {\n    if (\n      (node.childCount === 0 && !node.inlineContent) ||\n      node.isAtom ||\n      node.type.spec.isolating\n    ) {\n      return true;\n    }\n    if (node.inlineContent) {\n      return false;\n    }\n    node = (isAfter ? node.firstChild : node.lastChild) as PMNode;\n  }\n  return false;\n}\n\nexport function isNodeStart($pos: ResolvedPos) {\n  if ($pos.depth < 1) {\n    return null;\n  }\n  const startPos = $pos.start(1);\n  const endPos = $pos.end(1);\n  return $pos.pos < startPos + (endPos - startPos) / 2;\n}\n\nexport default GapCursorSelection;\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/gap-cursor/index.ts",
    "content": "import type {\n  Dispatch,\n  EditorState,\n  EditorView,\n  ResolvedPos,\n  Transaction,\n} from \"@/tiptap\";\nimport {\n  Extension,\n  callOrReturn,\n  getExtensionField,\n  isActive,\n  type ParentConfig,\n} from \"@/tiptap/core\";\nimport {\n  AllSelection,\n  Decoration,\n  DecorationSet,\n  Plugin,\n  PluginKey,\n  TextSelection,\n  keydownHandler,\n  type Command,\n} from \"@/tiptap/pm\";\nimport { deleteNodeByPos } from \"@/utils\";\nimport { isEmpty } from \"@/utils/is-node-empty\";\nimport GapCursorSelection from \"./gap-cursor-selection\";\n\ndeclare module \"@tiptap/core\" {\n  interface NodeConfig<Options, Storage> {\n    allowGapCursor?:\n      | boolean\n      | null\n      | ((this: {\n          name: string;\n          options: Options;\n          storage: Storage;\n          parent: ParentConfig<NodeConfig<Options>>[\"allowGapCursor\"];\n        }) => boolean | null);\n  }\n}\n\n/**\n * Adds GapCursor to top-level nodes\n *\n * When the top-level nodes (nodes with a depth of 1 relative to the doc) have the {@link NodeConfig#allowGapCursor} attribute set to true,\n * a GapCursor can be inserted before and after these nodes.\n *\n * This extension provides the ability to navigate between these nodes using the arrow keys.\n *\n * Note that some nodes and shortcuts may conflict with GapCursor due to their own behaviors, such as:\n *  - CodeBlock nodes\n *  - Backspace on an empty line\n *  - Tab key\n */\nexport const ExtensionGapCursor = Extension.create({\n  priority: 900,\n  name: \"gapCursor\",\n\n  addProseMirrorPlugins() {\n    return [\n      new Plugin({\n        key: new PluginKey(\"custom-gap-cursor\"),\n        props: {\n          decorations: drawGapCursor,\n\n          // If a GapCursor can be created at the current position, use GapCursor instead of other selection types\n          createSelectionBetween(_view, $anchor, $head) {\n            // TODO: When clicking outside of a node, it will first generate a GapCursorSelection,\n            // and then after handleClick returns false, it will turn into a TextSelection.\n            // The reason for this issue is that createSelectionBetween is triggered first, at which point\n            // GapCursorSelection.valid($head) validation fails, and then handleClick is triggered, at which point validation succeeds.\n            return $anchor.pos == $head.pos && GapCursorSelection.valid($head)\n              ? new GapCursorSelection($head)\n              : null;\n          },\n\n          handleClick(view, pos, event) {\n            if (!view || !view.editable) {\n              return false;\n            }\n            const clickPos = view.posAtCoords({\n              left: event.clientX,\n              top: event.clientY,\n            });\n            // Skip if the click position is inside a node.\n            if (clickPos && clickPos.inside > -1) {\n              return false;\n            }\n            const $pos = view.state.doc.resolve(pos);\n            if (!GapCursorSelection.valid($pos)) {\n              return false;\n            }\n            view.dispatch(\n              view.state.tr.setSelection(new GapCursorSelection($pos))\n            );\n            return true;\n          },\n\n          handleKeyDown: keydownHandler({\n            ArrowLeft: arrow(\"horiz\", -1),\n            ArrowRight: arrow(\"horiz\", 1),\n            ArrowUp: arrow(\"vert\", -1),\n            ArrowDown: arrow(\"vert\", 1),\n            Enter: (state, dispatch) => {\n              const tr = createParagraphNearByGapCursor(state, false);\n              if (tr && dispatch) {\n                dispatch(tr);\n                return true;\n              }\n              return false;\n            },\n            Backspace: (state, dispatch) => {\n              const { selection, tr } = state;\n              if (\n                selection instanceof TextSelection &&\n                isActive(state, \"paragraph\") &&\n                isEmpty(selection.$from.parent) &&\n                selection.empty\n              ) {\n                const { $from } = selection;\n                const beforePos = $from.before($from.depth);\n                if (beforePos === 0) {\n                  return true;\n                }\n                deleteNodeByPos($from)(tr);\n                if (dispatch) {\n                  const $found = arrowGapCursor(-1, \"left\", state)(tr);\n                  if ($found) {\n                    dispatch(tr);\n                    return true;\n                  }\n                }\n                return false;\n              }\n\n              if (!(selection instanceof GapCursorSelection) || !dispatch) {\n                return false;\n              }\n\n              const { isStart, $from } = selection;\n              const nodeOffset = state.doc.childBefore($from.pos);\n\n              if (!nodeOffset || nodeOffset.index === undefined) {\n                return false;\n              }\n\n              const index = nodeOffset.index;\n              const pos = state.doc.resolve(0).posAtIndex(index);\n\n              if (isStart) {\n                return handleBackspaceAtStart(pos, state, dispatch);\n              } else if (\n                nodeOffset.node &&\n                deleteNodeByPos(state.doc.resolve(pos))(tr)\n              ) {\n                dispatch(tr);\n                return true;\n              }\n\n              return false;\n            },\n            Tab: (state, dispatch) => {\n              const tr = createParagraphNearByGapCursor(state);\n              if (tr && dispatch) {\n                dispatch(tr);\n                return true;\n              }\n              return false;\n            },\n          }),\n\n          handleTextInput(view) {\n            const { state, dispatch } = view;\n            const tr = createParagraphNearByGapCursor(state);\n            if (tr && dispatch) {\n              dispatch(tr);\n            }\n            return false;\n          },\n\n          handleDOMEvents: {\n            /**\n             * Solve the issue of inserting text during composition input events, e.g., Chinese input\n             */\n            beforeinput: (view, event) => {\n              const { state, dispatch } = view;\n              if (\n                event.inputType != \"insertCompositionText\" ||\n                !(state.selection instanceof GapCursorSelection)\n              ) {\n                return false;\n              }\n              // TODO: After creating a new node, due to the change in selection, the content of the composition input\n              // will be inserted into the newly created node, causing the first character to be created directly.\n              const tr = createParagraphNearByGapCursor(state);\n              if (tr && dispatch) {\n                dispatch(tr);\n              }\n              return false;\n            },\n          },\n        },\n      }),\n    ];\n  },\n\n  extendNodeSchema(extension) {\n    const context = {\n      name: extension.name,\n      options: extension.options,\n      storage: extension.storage,\n    };\n\n    return {\n      allowGapCursor:\n        callOrReturn(getExtensionField(extension, \"allowGapCursor\", context)) ??\n        null,\n    };\n  },\n});\n\nfunction handleBackspaceAtStart(\n  pos: number,\n  state: EditorState,\n  dispatch: Dispatch\n) {\n  const { tr } = state;\n  if (pos == 0) {\n    return false;\n  }\n\n  const $beforePos = state.doc.resolve(pos - 1);\n  const parent = $beforePos.parent;\n\n  if (parent.inlineContent || parent.isTextblock) {\n    return handleInlineContent($beforePos, state, dispatch);\n  }\n\n  if (GapCursorSelection.valid($beforePos) && dispatch) {\n    dispatch(tr.setSelection(new GapCursorSelection($beforePos)));\n    return true;\n  }\n\n  if (deleteNodeByPos($beforePos)(tr) && dispatch) {\n    dispatch(tr);\n    return true;\n  }\n  return false;\n}\n\nfunction handleInlineContent(\n  $beforePos: ResolvedPos,\n  state: EditorState,\n  dispatch: Dispatch\n) {\n  if ($beforePos.parentOffset == 0 && $beforePos.pos > 1 && dispatch) {\n    dispatch(state.tr.delete($beforePos.pos - 1, $beforePos.pos));\n    return true;\n  }\n\n  if (dispatch) {\n    dispatch(\n      state.tr.setSelection(TextSelection.create(state.doc, $beforePos.pos))\n    );\n  }\n  return true;\n}\n\n/**\n * Handles arrow key navigation for GapCursor\n *\n * This function determines the direction (vertical or horizontal) and\n * the movement (positive or negative) based on the axis and direction parameters.\n *\n * @param {(\"vert\" | \"horiz\")} axis - The axis of movement, either vertical (\"vert\") or horizontal (\"horiz\").\n * @param {number} dir - The direction of movement, positive (1) or negative (-1).\n */\nfunction arrow(axis: \"vert\" | \"horiz\", dir: number): Command {\n  const dirStr =\n    axis == \"vert\" ? (dir > 0 ? \"down\" : \"up\") : dir > 0 ? \"right\" : \"left\";\n  return (state, dispatch, view) => {\n    const { tr } = state;\n    if (arrowGapCursor(dir, dirStr, state, view)(tr) && dispatch) {\n      dispatch(tr);\n      return true;\n    }\n    return false;\n  };\n}\n\nconst arrowGapCursor = (\n  dir: number,\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  dirStr: any,\n  state: EditorState,\n  view?: EditorView\n) => {\n  return (tr: Transaction) => {\n    const sel = state.selection;\n    let $start = dir > 0 ? sel.$to : sel.$from;\n    let mustMove = sel.empty;\n    if (sel instanceof TextSelection) {\n      // Do nothing if the next node is not at the end of the document or is at the root node.\n      if ($start.depth == 0) {\n        return;\n      }\n      if (view && !view.endOfTextblock(dirStr)) {\n        return;\n      }\n      mustMove = false;\n      $start = state.doc.resolve(dir > 0 ? $start.after() : $start.before());\n      // If inside a node, check if it has reached the boundary of the node\n      if ($start.depth > 0) {\n        const pos = $start.pos;\n        const start = $start.start(1) + 1;\n        const end = $start.end(1) - 1;\n        if (pos != start && pos != end) {\n          return;\n        }\n      }\n    }\n\n    if (sel instanceof GapCursorSelection) {\n      return;\n    }\n\n    const $found = GapCursorSelection.findGapCursorFrom($start, dir, mustMove);\n    if (!$found) {\n      return;\n    }\n    tr.setSelection(new GapCursorSelection($found));\n    return $found;\n  };\n};\n\n/**\n * Creates a new paragraph near the current GapCursor\n *\n * If the current selection is a GapCursorSelection, this function creates a new paragraph node\n * either before or after the current node depending on the selection direction.\n *\n * @param {EditorState} state - The current editor state.\n * @param {boolean} [changeSelection=true] - Whether to change the selection to the new paragraph.\n * @returns {Transaction|undefined} - The updated transaction or undefined if no changes are made.\n */\nfunction createParagraphNearByGapCursor(\n  state: EditorState,\n  changeSelection = true\n) {\n  const { tr } = state;\n  if (!(state.selection instanceof GapCursorSelection)) {\n    return;\n  }\n\n  const { isStart, $from } = state.selection;\n  // Create a new paragraph node before the current node\n  if (state.selection instanceof AllSelection || $from.parent.inlineContent) {\n    return;\n  }\n\n  const docPos = state.doc.resolve(0);\n  const nodeOffset = state.doc.childBefore($from.pos);\n  const index = isStart ? nodeOffset.index : nodeOffset.index + 1;\n  const pos = docPos.posAtIndex(index);\n  tr.insert(pos, state.schema.nodes.paragraph.create());\n  if (changeSelection || !isStart) {\n    tr.setSelection(TextSelection.create(tr.doc, pos + 1));\n    tr.scrollIntoView();\n  }\n\n  return tr;\n}\n\n/**\n * Draws a visual representation of the GapCursor\n *\n * If the current selection is a GapCursorSelection, this function creates a decoration set\n * to visually indicate the presence of a GapCursor at the appropriate position.\n *\n * @param {EditorState} state - The current editor state.\n * @returns {DecorationSet|null} - The decoration set for the GapCursor or null if not applicable.\n */\nfunction drawGapCursor(state: EditorState) {\n  if (!(state.selection instanceof GapCursorSelection)) {\n    return null;\n  }\n\n  const $head = state.selection.$head;\n  if ($head.depth < 1) {\n    return null;\n  }\n  const node = $head.node(1);\n  const pos = $head.start(1) - 1;\n  const isStart = state.selection.isStart;\n  return DecorationSet.create(state.doc, [\n    Decoration.node(pos, pos + node.nodeSize, {\n      key: \"node-gap-cursor\",\n      class: `card-gap-cursor ${isStart ? \"start\" : \"end\"}-card-gap-cursor`,\n    }),\n  ]);\n}\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/hard-break/index.ts",
    "content": "export { HardBreak as ExtensionHardBreak } from \"@tiptap/extension-hard-break\";\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/heading/index.ts",
    "content": "import TiptapHeading, { type HeadingOptions } from \"@tiptap/extension-heading\";\nimport { markRaw } from \"vue\";\nimport LucideHeading1 from \"~icons/lucide/heading-1\";\nimport LucideHeading2 from \"~icons/lucide/heading-2\";\nimport LucideHeading3 from \"~icons/lucide/heading-3\";\nimport LucideHeading4 from \"~icons/lucide/heading-4\";\nimport LucideHeading5 from \"~icons/lucide/heading-5\";\nimport LucideHeading6 from \"~icons/lucide/heading-6\";\nimport MingcuteParagraphLine from \"~icons/mingcute/paragraph-line\";\nimport { CONVERT_TO_KEY } from \"@/components/drag/default-drag\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport ToolbarSubItem from \"@/components/toolbar/ToolbarSubItem.vue\";\nimport { ExtensionParagraph } from \"@/extensions/paragraph\";\nimport { i18n } from \"@/locales\";\nimport {\n  AttrStep,\n  mergeAttributes,\n  Plugin,\n  PluginKey,\n  type Editor,\n  type Range,\n} from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { generateAnchorId } from \"@/utils\";\n\nexport type ExtensionHeadingOptions = ExtensionOptions & HeadingOptions;\n\nexport const ExtensionHeading = TiptapHeading.extend<ExtensionHeadingOptions>({\n  renderHTML({ node, HTMLAttributes }) {\n    const hasLevel = this.options.levels.includes(node.attrs.level);\n    const level = hasLevel ? node.attrs.level : this.options.levels[0];\n    return [\n      `h${level}`,\n      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n      0,\n    ];\n  },\n\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      id: {\n        default: null,\n      },\n    };\n  },\n\n  addOptions() {\n    return {\n      ...this.parent!(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 30,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive:\n              editor.isActive(ExtensionParagraph.name) ||\n              editor.isActive(TiptapHeading.name),\n            icon: markRaw(getIcon(editor)),\n            title: i18n.global.t(\"editor.common.heading.title\"),\n          },\n          children: [\n            {\n              priority: 10,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive(ExtensionParagraph.name),\n                icon: markRaw(MingcuteParagraphLine),\n                title: i18n.global.t(\"editor.common.heading.paragraph\"),\n                action: () => editor.chain().focus().setParagraph().run(),\n              },\n            },\n            {\n              priority: 20,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive(TiptapHeading.name, { level: 1 }),\n                icon: markRaw(LucideHeading1),\n                title: i18n.global.t(\"editor.common.heading.heading1\"),\n                action: () =>\n                  editor.chain().focus().toggleHeading({ level: 1 }).run(),\n              },\n            },\n            {\n              priority: 30,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive(TiptapHeading.name, { level: 2 }),\n                icon: markRaw(LucideHeading2),\n                title: i18n.global.t(\"editor.common.heading.heading2\"),\n                action: () =>\n                  editor.chain().focus().toggleHeading({ level: 2 }).run(),\n              },\n            },\n            {\n              priority: 40,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive(TiptapHeading.name, { level: 3 }),\n                icon: markRaw(LucideHeading3),\n                title: i18n.global.t(\"editor.common.heading.heading3\"),\n                action: () =>\n                  editor.chain().focus().toggleHeading({ level: 3 }).run(),\n              },\n            },\n            {\n              priority: 50,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive(TiptapHeading.name, { level: 4 }),\n                icon: markRaw(LucideHeading4),\n                title: i18n.global.t(\"editor.common.heading.heading4\"),\n                action: () =>\n                  editor.chain().focus().toggleHeading({ level: 4 }).run(),\n              },\n            },\n            {\n              priority: 60,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive(TiptapHeading.name, { level: 5 }),\n                icon: markRaw(LucideHeading5),\n                title: i18n.global.t(\"editor.common.heading.heading5\"),\n                action: () =>\n                  editor.chain().focus().toggleHeading({ level: 5 }).run(),\n              },\n            },\n            {\n              priority: 70,\n              component: markRaw(ToolbarSubItem),\n              props: {\n                editor,\n                isActive: editor.isActive(TiptapHeading.name, { level: 6 }),\n                icon: markRaw(LucideHeading6),\n                title: i18n.global.t(\"editor.common.heading.heading6\"),\n                action: () =>\n                  editor.chain().focus().toggleHeading({ level: 6 }).run(),\n              },\n            },\n          ],\n        };\n      },\n      getCommandMenuItems() {\n        return [\n          {\n            priority: 10,\n            icon: markRaw(MingcuteParagraphLine),\n            title: \"editor.common.heading.paragraph\",\n            keywords: [\"paragraph\", \"text\", \"putongwenben\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor.chain().focus().deleteRange(range).setParagraph().run();\n            },\n          },\n          {\n            priority: 20,\n            icon: markRaw(LucideHeading1),\n            title: \"editor.common.heading.heading1\",\n            keywords: [\"h1\", \"heading1\", \"1\", \"yijibiaoti\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor\n                .chain()\n                .focus()\n                .deleteRange(range)\n                .setNode(TiptapHeading.name, { level: 1 })\n                .run();\n            },\n          },\n          {\n            priority: 30,\n            icon: markRaw(LucideHeading2),\n            title: \"editor.common.heading.heading2\",\n            keywords: [\"h2\", \"heading2\", \"2\", \"erjibiaoti\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor\n                .chain()\n                .focus()\n                .deleteRange(range)\n                .setNode(TiptapHeading.name, { level: 2 })\n                .run();\n            },\n          },\n          {\n            priority: 40,\n            icon: markRaw(LucideHeading3),\n            title: \"editor.common.heading.heading3\",\n            keywords: [\"h3\", \"heading3\", \"3\", \"sanjibiaoti\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor\n                .chain()\n                .focus()\n                .deleteRange(range)\n                .setNode(TiptapHeading.name, { level: 3 })\n                .run();\n            },\n          },\n          {\n            priority: 50,\n            icon: markRaw(LucideHeading4),\n            title: \"editor.common.heading.heading4\",\n            keywords: [\"h4\", \"heading4\", \"4\", \"sijibiaoti\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor\n                .chain()\n                .focus()\n                .deleteRange(range)\n                .setNode(TiptapHeading.name, { level: 4 })\n                .run();\n            },\n          },\n          {\n            priority: 60,\n            icon: markRaw(LucideHeading5),\n            title: \"editor.common.heading.heading5\",\n            keywords: [\"h5\", \"heading5\", \"5\", \"wujibiaoti\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor\n                .chain()\n                .focus()\n                .deleteRange(range)\n                .setNode(TiptapHeading.name, { level: 5 })\n                .run();\n            },\n          },\n          {\n            priority: 70,\n            icon: markRaw(LucideHeading6),\n            title: \"editor.common.heading.heading6\",\n            keywords: [\"h6\", \"heading6\", \"6\", \"liujibiaoti\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor\n                .chain()\n                .focus()\n                .deleteRange(range)\n                .setNode(TiptapHeading.name, { level: 6 })\n                .run();\n            },\n          },\n        ];\n      },\n      getDraggableMenuItems() {\n        return {\n          extendsKey: CONVERT_TO_KEY,\n          children: {\n            items: [\n              {\n                priority: 10,\n                icon: markRaw(MingcuteParagraphLine),\n                title: i18n.global.t(\"editor.common.heading.paragraph\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setParagraph().run(),\n              },\n              {\n                priority: 20,\n                icon: markRaw(LucideHeading1),\n                title: i18n.global.t(\"editor.common.heading.heading1\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setHeading({ level: 1 }).run(),\n              },\n              {\n                priority: 30,\n                icon: markRaw(LucideHeading2),\n                title: i18n.global.t(\"editor.common.heading.heading2\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setHeading({ level: 2 }).run(),\n              },\n              {\n                priority: 40,\n                icon: markRaw(LucideHeading3),\n                title: i18n.global.t(\"editor.common.heading.heading3\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setHeading({ level: 3 }).run(),\n              },\n              {\n                priority: 50,\n                icon: markRaw(LucideHeading4),\n                title: i18n.global.t(\"editor.common.heading.heading4\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setHeading({ level: 4 }).run(),\n              },\n              {\n                priority: 60,\n                icon: markRaw(LucideHeading5),\n                title: i18n.global.t(\"editor.common.heading.heading5\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setHeading({ level: 5 }).run(),\n              },\n              {\n                priority: 70,\n                icon: markRaw(LucideHeading6),\n                title: i18n.global.t(\"editor.common.heading.heading6\"),\n                action: ({ editor }: { editor: Editor }) =>\n                  editor.chain().focus().setHeading({ level: 6 }).run(),\n              },\n            ],\n          },\n        };\n      },\n    };\n  },\n  addProseMirrorPlugins() {\n    let beforeComposition: boolean | undefined = undefined;\n    return [\n      new Plugin({\n        key: new PluginKey(\"generate-heading-id\"),\n        appendTransaction: (transactions, _oldState, newState) => {\n          const isChangeHeading = transactions.some((transaction) => {\n            const composition = this.editor.view.composing;\n            if (beforeComposition !== undefined && !composition) {\n              beforeComposition = undefined;\n              return true;\n            }\n            if (transaction.docChanged) {\n              if (transaction.getMeta(\"paste\")) {\n                return true;\n              }\n              beforeComposition = composition;\n              const selection = transaction.selection;\n              const { $from } = selection;\n              const node = $from.parent;\n              return node.type.name === ExtensionHeading.name && !composition;\n            }\n            return false;\n          });\n          if (isChangeHeading) {\n            const tr = newState.tr;\n            const headingIds: string[] = [];\n            newState.doc.descendants((node, pos) => {\n              if (node.type.name === ExtensionHeading.name) {\n                const id = generateAnchorId(node.textContent, headingIds);\n                tr.step(new AttrStep(pos, \"id\", id));\n                headingIds.push(id);\n              }\n            });\n            return tr;\n          }\n          return undefined;\n        },\n      }),\n    ];\n  },\n});\n\nfunction getIcon(editor: Editor) {\n  if (editor.isActive(ExtensionParagraph.name)) {\n    return MingcuteParagraphLine;\n  }\n  if (editor.isActive(ExtensionHeading.name, { level: 1 })) {\n    return LucideHeading1;\n  }\n  if (editor.isActive(ExtensionHeading.name, { level: 2 })) {\n    return LucideHeading2;\n  }\n  if (editor.isActive(ExtensionHeading.name, { level: 3 })) {\n    return LucideHeading3;\n  }\n  if (editor.isActive(ExtensionHeading.name, { level: 4 })) {\n    return LucideHeading4;\n  }\n  if (editor.isActive(ExtensionHeading.name, { level: 5 })) {\n    return LucideHeading5;\n  }\n  if (editor.isActive(ExtensionHeading.name, { level: 6 })) {\n    return LucideHeading6;\n  }\n  return MingcuteParagraphLine;\n}\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/highlight/HighlightBubbleItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport MingcuteCloseLine from \"~icons/mingcute/close-line\";\nimport { BubbleItem } from \"@/components\";\nimport ColorPickerDropdown from \"@/components/common/ColorPickerDropdown.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionHighlight } from \".\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nfunction handleSetColor(color?: string) {\n  if (props.editor.isActive(ExtensionHighlight.name)) {\n    props.editor.chain().focus().unsetHighlight().run();\n  }\n\n  props.editor\n    ?.chain()\n    .focus()\n    .setHighlight(color ? { color } : undefined)\n    .run();\n}\n\nfunction handleUnsetColor() {\n  props.editor?.chain().focus().unsetHighlight().run();\n}\n</script>\n\n<template>\n  <ColorPickerDropdown @update:model-value=\"handleSetColor\">\n    <BubbleItem v-bind=\"props\" @click=\"handleSetColor()\" />\n    <template #prefix>\n      <div class=\"p-1\">\n        <div\n          class=\"flex cursor-pointer items-center gap-2 rounded p-1 text-xs text-gray-600 hover:bg-gray-100 hover:text-gray-900\"\n          @click=\"handleUnsetColor\"\n        >\n          <div class=\"inline-flex items-center gap-2\">\n            <MingcuteCloseLine />\n            <span>\n              {{ i18n.global.t(\"editor.extensions.highlight.unset\") }}\n            </span>\n          </div>\n        </div>\n      </div>\n      <div class=\"p-1\">\n        <div\n          class=\"flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100\"\n          @click=\"handleSetColor()\"\n        >\n          <div\n            class=\"size-5 cursor-pointer rounded-sm ring-gray-300 ring-offset-1 hover:ring-1\"\n            :style=\"{ 'background-color': '#fff8c5' }\"\n          ></div>\n          <span class=\"text-xs text-gray-600\">\n            {{ i18n.global.t(\"editor.common.button.restore_default\") }}\n          </span>\n        </div>\n      </div>\n    </template>\n  </ColorPickerDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/highlight/HighlightToolbarItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport MingcuteCloseLine from \"~icons/mingcute/close-line\";\nimport { ToolbarItem } from \"@/components\";\nimport ColorPickerDropdown from \"@/components/common/ColorPickerDropdown.vue\";\nimport { i18n } from \"@/locales\";\nimport type { ToolbarItemComponentProps } from \"@/types\";\nimport { ExtensionHighlight } from \".\";\n\nconst props = defineProps<ToolbarItemComponentProps>();\n\nfunction handleSetColor(color?: string) {\n  if (props.editor?.isActive(ExtensionHighlight.name)) {\n    props.editor?.chain().focus().unsetHighlight().run();\n  }\n\n  props.editor\n    ?.chain()\n    .focus()\n    .setHighlight(color ? { color } : undefined)\n    .run();\n}\n\nfunction handleUnsetColor() {\n  props.editor?.chain().focus().unsetHighlight().run();\n}\n</script>\n\n<template>\n  <ColorPickerDropdown @update:model-value=\"handleSetColor\">\n    <ToolbarItem v-bind=\"props\" @click=\"handleSetColor()\" />\n    <template #prefix>\n      <div class=\"p-1\">\n        <div\n          class=\"flex cursor-pointer items-center gap-2 rounded p-1 text-xs text-gray-600 hover:bg-gray-100 hover:text-gray-900\"\n          @click=\"handleUnsetColor\"\n        >\n          <div class=\"inline-flex items-center gap-2\">\n            <MingcuteCloseLine />\n            <span>\n              {{ i18n.global.t(\"editor.extensions.highlight.unset\") }}\n            </span>\n          </div>\n        </div>\n      </div>\n      <div class=\"p-1\">\n        <div\n          class=\"flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100\"\n          @click=\"handleSetColor()\"\n        >\n          <div\n            class=\"size-5 cursor-pointer rounded-sm ring-gray-300 ring-offset-1 hover:ring-1\"\n            :style=\"{ 'background-color': '#fff8c5' }\"\n          ></div>\n          <span class=\"text-xs text-gray-600\">\n            {{ i18n.global.t(\"editor.common.button.restore_default\") }}\n          </span>\n        </div>\n      </div>\n    </template>\n  </ColorPickerDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/highlight/index.ts",
    "content": "import type { HighlightOptions } from \"@tiptap/extension-highlight\";\nimport TiptapHighlight from \"@tiptap/extension-highlight\";\nimport { markRaw } from \"vue\";\nimport MingcuteMarkPenLine from \"~icons/mingcute/mark-pen-line\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\nimport HighlightToolbarItem from \"./HighlightToolbarItem.vue\";\n\nexport type ExtensionHighlightOptions = ExtensionOptions &\n  Partial<HighlightOptions>;\n\nexport const ExtensionHighlight =\n  TiptapHighlight.extend<ExtensionHighlightOptions>({\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 80,\n            component: markRaw(HighlightToolbarItem),\n            props: {\n              editor,\n              isActive: editor.isActive(TiptapHighlight.name),\n              icon: markRaw(MingcuteMarkPenLine),\n              title: i18n.global.t(\"editor.common.highlight\"),\n            },\n          };\n        },\n      };\n    },\n  }).configure({ multicolor: true });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/history/index.ts",
    "content": "import { UndoRedo as TiptapHistory } from \"@tiptap/extensions\";\nimport { markRaw } from \"vue\";\nimport MdiRedoVariant from \"~icons/mdi/redo-variant\";\nimport MdiUndoVariant from \"~icons/mdi/undo-variant\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport const ExtensionHistory = TiptapHistory.extend<ExtensionOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return [\n          {\n            priority: 10,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: false,\n              icon: markRaw(MdiUndoVariant),\n              title: i18n.global.t(\"editor.menus.undo\"),\n              action: () => editor.chain().undo().focus().run(),\n            },\n          },\n          {\n            priority: 20,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: false,\n              icon: markRaw(MdiRedoVariant),\n              title: i18n.global.t(\"editor.menus.redo\"),\n              action: () => editor.chain().redo().focus().run(),\n            },\n          },\n        ];\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/horizontal-rule/index.ts",
    "content": "export { HorizontalRule as ExtensionHorizontalRule } from \"@tiptap/extension-horizontal-rule\";\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/iframe/BubbleItemIframeAlign.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport MingcuteAlignCenterLine from \"~icons/mingcute/align-center-line\";\nimport MingcuteAlignJustifyLine from \"~icons/mingcute/align-justify-line\";\nimport MingcuteAlignLeftLine from \"~icons/mingcute/align-left-line\";\nimport MingcuteAlignRightLine from \"~icons/mingcute/align-right-line\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionParagraph } from \"..\";\n\nconst props = withDefaults(defineProps<BubbleItemComponentProps>(), {\n  visible: () => true,\n});\n\nconst alignOptions = [\n  {\n    text: i18n.global.t(\"editor.common.align_left\"),\n    value: \"left\",\n    icon: MingcuteAlignLeftLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_center\"),\n    value: \"center\",\n    icon: MingcuteAlignCenterLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_right\"),\n    value: \"right\",\n    icon: MingcuteAlignRightLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_justify\"),\n    value: \"justify\",\n    icon: MingcuteAlignJustifyLine,\n  },\n];\n\nconst currentAlign = computed(() => {\n  const textAlignAttribute = props.editor.getAttributes(\n    ExtensionParagraph.name\n  ).textAlign;\n\n  const alignOption = alignOptions.find(\n    (option) => option.value === textAlignAttribute\n  );\n\n  if (!alignOption) {\n    return alignOptions[0];\n  }\n\n  return alignOption;\n});\n\nconst handleSetAlign = (align: string) => {\n  props.editor.chain().focus().setTextAlign(align).run();\n};\n</script>\n<template>\n  <VDropdown\n    v-if=\"visible({ editor })\"\n    class=\"inline-flex\"\n    :auto-hide=\"true\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.common.align_method')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <component :is=\"currentAlign.icon\" />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <div class=\"relative max-h-72 w-56 overflow-hidden overflow-y-auto\">\n        <DropdownItem\n          v-for=\"option in alignOptions\"\n          :key=\"option.value\"\n          v-close-popper\n          :is-active=\"editor.isActive({ textAlign: option.value })\"\n          @click=\"handleSetAlign(option.value)\"\n        >\n          <template #icon>\n            <component :is=\"option.icon\" />\n          </template>\n          {{ option.text }}\n        </DropdownItem>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/iframe/BubbleItemIframeLink.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport Input from \"@/components/base/Input.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { isAllowedUri } from \"@/utils/is-allowed-uri\";\nimport { ExtensionIframe } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst src = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionIframe.name).src;\n  },\n  set: (src: string) => {\n    if (!src || !isAllowedUri(src)) {\n      return;\n    }\n    props.editor\n      .chain()\n      .updateAttributes(ExtensionIframe.name, { src: src })\n      .run();\n  },\n});\n</script>\n\n<template>\n  <div class=\"w-80\">\n    <Input\n      v-model=\"src\"\n      auto-focus\n      :placeholder=\"i18n.global.t('editor.common.placeholder.link_input')\"\n      :label=\"i18n.global.t('editor.extensions.iframe.src_input_label')\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/iframe/BubbleItemIframeSize.vue",
    "content": "<script setup lang=\"ts\">\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport MdiImageSizeSelectSmall from \"~icons/mdi/image-size-select-small\";\nimport MingcuteCellphoneLine from \"~icons/mingcute/cellphone-line\";\nimport MingcuteImacLine from \"~icons/mingcute/imac-line\";\nimport MingcutePadLine from \"~icons/mingcute/pad-line\";\nimport Input from \"@/components/base/Input.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionIframe } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst width = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionIframe.name).width;\n  },\n  set: (value: string) => {\n    handleSetSize(value, height.value);\n  },\n});\n\nconst height = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionIframe.name).height;\n  },\n  set: (value: string) => {\n    handleSetSize(width.value, value);\n  },\n});\n\nconst handleSetSize = (width: string, height: string) => {\n  props.editor\n    .chain()\n    .updateAttributes(ExtensionIframe.name, { width, height })\n    .focus()\n    .setNodeSelection(props.editor.state.selection.from)\n    .run();\n};\n\nconst presetSizes = [\n  {\n    width: \"390px\",\n    height: \"844px\",\n    icon: MingcuteCellphoneLine,\n    title: i18n.global.t(\"editor.extensions.iframe.phone_size\"),\n  },\n  {\n    width: \"834px\",\n    height: \"1194px\",\n    icon: MingcutePadLine,\n    title: i18n.global.t(\"editor.extensions.iframe.tablet_vertical_size\"),\n  },\n  {\n    width: \"1194px\",\n    height: \"834px\",\n    icon: MingcutePadLine,\n    iconStyle: \"transform: rotate(90deg)\",\n    title: i18n.global.t(\"editor.extensions.iframe.tablet_horizontal_size\"),\n  },\n  {\n    width: \"100%\",\n    height: \"834px\",\n    icon: MingcuteImacLine,\n    title: i18n.global.t(\"editor.extensions.iframe.desktop_size\"),\n  },\n];\n</script>\n<template>\n  <VDropdown class=\"inline-flex\" :auto-hide=\"true\" :distance=\"10\">\n    <BubbleButton\n      :title=\"i18n.global.t('editor.extensions.iframe.resize')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <MdiImageSizeSelectSmall />\n      </template>\n    </BubbleButton>\n    <template #popper>\n      <div class=\"flex w-56 flex-col gap-3\">\n        <div class=\"flex flex-col items-center gap-3\">\n          <Input\n            v-model=\"width\"\n            :label=\"i18n.global.t('editor.common.width')\"\n            :tooltip=\"i18n.global.t('editor.common.tooltip.custom_width_input')\"\n          />\n          <Input\n            v-model=\"height\"\n            :label=\"i18n.global.t('editor.common.height')\"\n            :tooltip=\"\n              i18n.global.t('editor.common.tooltip.custom_height_input')\n            \"\n          />\n        </div>\n\n        <div class=\"flex items-center gap-1 rounded-md bg-gray-100 p-1\">\n          <button\n            v-for=\"item in presetSizes\"\n            :key=\"item.width\"\n            v-tooltip=\"item.title\"\n            class=\"inline-flex flex-1 items-center justify-center rounded px-2 py-1.5 text-gray-600 transition-all hover:text-gray-900\"\n            :class=\"{\n              'bg-white text-gray-900':\n                item.width === width && item.height === height,\n            }\"\n            @click=\"handleSetSize(item.width, item.height)\"\n          >\n            <component :is=\"item.icon\" :style=\"item.iconStyle\" />\n          </button>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/iframe/IframeView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from \"vue\";\nimport Input from \"@/components/base/Input.vue\";\nimport { i18n } from \"@/locales\";\nimport type { NodeViewProps } from \"@/tiptap/vue-3\";\nimport { NodeViewWrapper } from \"@/tiptap/vue-3\";\nimport { isAllowedUri } from \"@/utils/is-allowed-uri\";\n\nconst props = defineProps<NodeViewProps>();\n\nconst src = computed({\n  get: () => {\n    return props.node?.attrs.src;\n  },\n  set: (src: string) => {\n    if (!src || !isAllowedUri(src)) {\n      return;\n    }\n    props.updateAttributes({ src: src });\n  },\n});\n\nconst frameborder = computed(() => {\n  return props.node.attrs.frameborder;\n});\n\nfunction handleSetFocus() {\n  props.editor.commands.setNodeSelection(props.getPos() || 0);\n}\n</script>\n\n<template>\n  <node-view-wrapper as=\"div\" class=\"inline-block w-full\">\n    <div\n      class=\"relative inline-block h-full max-w-full overflow-hidden text-center transition-all\"\n      :style=\"{\n        width: node.attrs.width,\n      }\"\n    >\n      <div v-if=\"!src\" class=\"p-1.5\">\n        <Input\n          ref=\"inputRef\"\n          v-model.lazy=\"src\"\n          :placeholder=\"i18n.global.t('editor.common.placeholder.link_input')\"\n          tabindex=\"-1\"\n          auto-focus\n          @focus=\"handleSetFocus\"\n        />\n      </div>\n      <iframe\n        v-else\n        class=\"rounded-md\"\n        :src=\"node!.attrs.src\"\n        :width=\"node.attrs.width\"\n        :height=\"node.attrs.height\"\n        scrolling=\"yes\"\n        :frameborder=\"frameborder\"\n        framespacing=\"0\"\n        allowfullscreen=\"true\"\n        :class=\"{\n          'border-2': frameborder === '1',\n        }\"\n        @mouseenter=\"handleSetFocus\"\n      ></iframe>\n    </div>\n  </node-view-wrapper>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/iframe/index.ts",
    "content": "import { markRaw } from \"vue\";\nimport MdiBorderAllVariant from \"~icons/mdi/border-all-variant\";\nimport MdiBorderNoneVariant from \"~icons/mdi/border-none-variant\";\nimport MdiWeb from \"~icons/mdi/web\";\nimport MdiWebSync from \"~icons/mdi/web-sync\";\nimport MingcuteLinkLine from \"~icons/mingcute/link-line\";\nimport MingcuteShare3Line from \"~icons/mingcute/share-3-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport ToolboxItem from \"@/components/toolbox/ToolboxItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  Node,\n  PluginKey,\n  VueNodeViewRenderer,\n  isActive,\n  mergeAttributes,\n  nodeInputRule,\n  nodePasteRule,\n  type EditorState,\n  type Range,\n} from \"@/tiptap\";\nimport type { ExtensionOptions, NodeBubbleMenuType } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\nimport { isAllowedUri } from \"@/utils/is-allowed-uri\";\nimport BubbleItemIframeAlign from \"./BubbleItemIframeAlign.vue\";\nimport BubbleIframeLink from \"./BubbleItemIframeLink.vue\";\nimport BubbleIframeSize from \"./BubbleItemIframeSize.vue\";\nimport IframeView from \"./IframeView.vue\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    iframe: {\n      setIframe: (options: { src: string }) => ReturnType;\n    };\n  }\n}\n\nexport const IFRAME_BUBBLE_MENU_KEY = new PluginKey(\"iframeBubbleMenu\");\n\nexport const ExtensionIframe = Node.create<ExtensionOptions>({\n  name: \"iframe\",\n  fakeSelection: true,\n\n  inline() {\n    return true;\n  },\n\n  group() {\n    return \"inline\";\n  },\n\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      src: {\n        default: null,\n        parseHTML: (element) => {\n          const src = element.getAttribute(\"src\");\n          return src;\n        },\n      },\n      width: {\n        default: \"100%\",\n        parseHTML: (element) => {\n          return element.getAttribute(\"width\");\n        },\n        renderHTML(attributes) {\n          return {\n            width: attributes.width,\n          };\n        },\n      },\n      height: {\n        default: \"300px\",\n        parseHTML: (element) => {\n          const height = element.getAttribute(\"height\");\n          return height;\n        },\n        renderHTML: (attributes) => {\n          return {\n            height: attributes.height,\n          };\n        },\n      },\n      scrolling: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"scrolling\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            scrolling: attributes.scrolling,\n          };\n        },\n      },\n      frameborder: {\n        default: \"0\",\n        parseHTML: (element) => {\n          return element.getAttribute(\"frameborder\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            frameborder: attributes.frameborder,\n          };\n        },\n      },\n      allowfullscreen: {\n        default: true,\n        parseHTML: (element) => {\n          return element.getAttribute(\"allowfullscreen\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            allowfullscreen: attributes.allowfullscreen,\n          };\n        },\n      },\n      framespacing: {\n        default: 0,\n        parseHTML: (element) => {\n          const framespacing = element.getAttribute(\"framespacing\");\n          return framespacing ? parseInt(framespacing, 10) : null;\n        },\n        renderHTML: (attributes) => {\n          return {\n            framespacing: attributes.framespacing,\n          };\n        },\n      },\n      style: {\n        renderHTML() {\n          return {\n            style: \"display: inline-block\",\n          };\n        },\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"iframe\",\n        getAttrs: (dom) => {\n          const src = (dom as HTMLElement).getAttribute(\"src\");\n\n          // prevent XSS attacks\n          if (!src || !isAllowedUri(src)) {\n            return false;\n          }\n          return { src };\n        },\n      },\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    // prevent XSS attacks\n    if (!isAllowedUri(HTMLAttributes.src)) {\n      return [\"iframe\", mergeAttributes({ ...HTMLAttributes, src: \"\" })];\n    }\n    return [\"iframe\", mergeAttributes(HTMLAttributes)];\n  },\n\n  addCommands() {\n    return {\n      setIframe:\n        (options) =>\n        ({ commands }) => {\n          return commands.insertContent({\n            type: this.name,\n            attrs: options,\n          });\n        },\n    };\n  },\n\n  addInputRules() {\n    return [\n      nodeInputRule({\n        find: /^\\$iframe\\$$/,\n        type: this.type,\n        getAttributes: () => {\n          return { width: \"100%\" };\n        },\n      }),\n    ];\n  },\n\n  addPasteRules() {\n    return [\n      nodePasteRule({\n        find: /<iframe.*?src=\"(.*?)\".*?<\\/iframe>/g,\n        type: this.type,\n        getAttributes: (match) => {\n          const parse = document\n            .createRange()\n            .createContextualFragment(match[0]);\n\n          const iframe = parse.querySelector(\"iframe\");\n\n          if (!iframe) {\n            return;\n          }\n\n          return {\n            src: iframe.src,\n            width: iframe.width || \"100%\",\n            height: iframe.height || \"300px\",\n          };\n        },\n      }),\n    ];\n  },\n\n  addNodeView() {\n    return VueNodeViewRenderer(IframeView);\n  },\n\n  addOptions() {\n    return {\n      getCommandMenuItems() {\n        return {\n          priority: 90,\n          icon: markRaw(MdiWeb),\n          title: \"editor.extensions.commands_menu.iframe\",\n          keywords: [\"iframe\", \"qianruwangye\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .insertContent([{ type: \"iframe\", attrs: { src: \"\" } }])\n              .run();\n          },\n        };\n      },\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return [\n          {\n            priority: 50,\n            component: markRaw(ToolboxItem),\n            props: {\n              editor,\n              icon: markRaw(MdiWeb),\n              title: i18n.global.t(\"editor.extensions.commands_menu.iframe\"),\n              action: () => {\n                editor\n                  .chain()\n                  .focus()\n                  .insertContent([{ type: \"iframe\", attrs: { src: \"\" } }])\n                  .run();\n              },\n            },\n          },\n        ];\n      },\n      getBubbleMenu({ editor }: { editor: Editor }): NodeBubbleMenuType {\n        return {\n          pluginKey: IFRAME_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }) => {\n            return isActive(state, ExtensionIframe.name);\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                isActive: () =>\n                  editor.getAttributes(ExtensionIframe.name).frameborder ===\n                  \"1\",\n                icon: markRaw(\n                  editor.getAttributes(ExtensionIframe.name).frameborder === \"1\"\n                    ? MdiBorderAllVariant\n                    : MdiBorderNoneVariant\n                ),\n                action: () => {\n                  editor\n                    .chain()\n                    .updateAttributes(ExtensionIframe.name, {\n                      frameborder:\n                        editor.getAttributes(ExtensionIframe.name)\n                          .frameborder === \"1\"\n                          ? \"0\"\n                          : \"1\",\n                    })\n                    .focus()\n                    .setNodeSelection(editor.state.selection.from)\n                    .run();\n                },\n                title:\n                  editor.getAttributes(ExtensionIframe.name).frameborder === \"1\"\n                    ? i18n.global.t(\n                        \"editor.extensions.iframe.disable_frameborder\"\n                      )\n                    : i18n.global.t(\n                        \"editor.extensions.iframe.enable_frameborder\"\n                      ),\n              },\n            },\n            {\n              priority: 20,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 30,\n              component: markRaw(BubbleIframeSize),\n            },\n            {\n              priority: 40,\n              component: markRaw(BubbleItemIframeAlign),\n            },\n            {\n              priority: 50,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 60,\n              props: {\n                icon: markRaw(MdiWebSync),\n                action: () => {\n                  editor\n                    .chain()\n                    .updateAttributes(ExtensionIframe.name, {\n                      src: editor.getAttributes(ExtensionIframe.name).src,\n                    })\n                    .run();\n                },\n              },\n            },\n            {\n              priority: 70,\n              props: {\n                icon: markRaw(MingcuteLinkLine),\n                title: i18n.global.t(\"editor.common.button.edit_link\"),\n                action: () => {\n                  return markRaw(BubbleIframeLink);\n                },\n              },\n            },\n            {\n              priority: 80,\n              props: {\n                icon: markRaw(MingcuteShare3Line),\n                title: i18n.global.t(\"editor.common.tooltip.open_link\"),\n                action: () => {\n                  window.open(\n                    editor.getAttributes(ExtensionIframe.name).src,\n                    \"_blank\"\n                  );\n                },\n              },\n            },\n            {\n              priority: 90,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }) => {\n                  deleteNode(ExtensionIframe.name, editor);\n                },\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/image/BubbleItemImageAlt.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport Input from \"@/components/base/Input.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionImage } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst alt = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionImage.name).alt;\n  },\n  set: (alt: string) => {\n    props.editor\n      .chain()\n      .updateAttributes(ExtensionImage.name, { alt: alt })\n      .setNodeSelection(props.editor.state.selection.from)\n      .focus()\n      .run();\n  },\n});\n</script>\n\n<template>\n  <div v-if=\"visible?.({ editor: props.editor })\" class=\"w-56\">\n    <Input\n      v-model=\"alt\"\n      auto-focus\n      :label=\"i18n.global.t('editor.common.alt')\"\n      :placeholder=\"i18n.global.t('editor.common.placeholder.alt_input')\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/image/BubbleItemImageHref.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport Input from \"@/components/base/Input.vue\";\nimport { ExtensionImage, ExtensionLink } from \"@/extensions\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst href = computed({\n  get: () => {\n    const attrs = props.editor.getAttributes(ExtensionLink.name);\n    return attrs?.href || props.editor.getAttributes(ExtensionImage.name).href;\n  },\n  set: (href: string) => {\n    props.editor.commands.setLink({ href: href, target: \"_blank\" });\n  },\n});\n\nconst target = computed({\n  get() {\n    const attrs = props.editor.getAttributes(ExtensionLink.name);\n    return attrs?.target === \"_blank\";\n  },\n  set(value) {\n    props.editor.commands.setLink({\n      href: href.value,\n      target: value ? \"_blank\" : \"_self\",\n    });\n  },\n});\n</script>\n\n<template>\n  <div class=\"w-80\">\n    <Input\n      v-if=\"visible?.({ editor: props.editor })\"\n      v-model=\"href\"\n      auto-focus\n      :placeholder=\"i18n.global.t('editor.common.placeholder.alt_href')\"\n      :label=\"i18n.global.t('editor.extensions.image.href_input_label')\"\n    />\n    <label class=\"mt-3 inline-flex items-center\">\n      <input v-model=\"target\" type=\"checkbox\" />\n      <span class=\"ml-2 text-sm text-gray-500\">\n        {{ i18n.global.t(\"editor.extensions.link.open_in_new_window\") }}\n      </span>\n    </label>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/image/BubbleItemImageLink.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport Input from \"@/components/base/Input.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionImage } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst src = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionImage.name).src;\n  },\n  set: (src: string) => {\n    props.editor\n      .chain()\n      .updateAttributes(ExtensionImage.name, { src: src })\n      .setNodeSelection(props.editor.state.selection.from)\n      .focus()\n      .run();\n  },\n});\n</script>\n\n<template>\n  <div v-if=\"visible?.({ editor: props.editor })\" class=\"w-80\">\n    <Input\n      v-model=\"src\"\n      auto-focus\n      :label=\"i18n.global.t('editor.extensions.image.src_input_label')\"\n      :placeholder=\"i18n.global.t('editor.common.placeholder.link_input')\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/image/BubbleItemImagePosition.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { isActive } from \"@tiptap/core\";\nimport { computed } from \"vue\";\nimport MingcuteAlignCenterLine from \"~icons/mingcute/align-center-line\";\nimport MingcuteAlignLeftLine from \"~icons/mingcute/align-left-line\";\nimport MingcuteAlignRightLine from \"~icons/mingcute/align-right-line\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionFigure, ExtensionImage } from \"..\";\n\nconst props = withDefaults(defineProps<BubbleItemComponentProps>(), {\n  visible: () => true,\n});\n\nconst positionOptions = [\n  {\n    text: i18n.global.t(\"editor.common.align_left\"),\n    value: \"start\",\n    icon: MingcuteAlignLeftLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_center\"),\n    value: \"center\",\n    icon: MingcuteAlignCenterLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_right\"),\n    value: \"end\",\n    icon: MingcuteAlignRightLine,\n  },\n];\n\nconst currentPosition = computed(() => {\n  const positionAttribute = props.editor.getAttributes(\n    ExtensionImage.name\n  ).position;\n\n  const positionOption = positionOptions.find(\n    (option) => option.value === positionAttribute\n  );\n\n  if (!positionOption) {\n    return positionOptions[0];\n  }\n\n  return positionOption;\n});\n\nconst handleSetPosition = (blockPosition: string) => {\n  props.editor.chain().focus().setBlockPosition(blockPosition).run();\n};\n</script>\n<template>\n  <VDropdown\n    v-if=\"visible({ editor })\"\n    class=\"inline-flex\"\n    :auto-hide=\"true\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.common.align_method')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <component :is=\"currentPosition.icon\" />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <div class=\"relative max-h-72 w-56 overflow-hidden overflow-y-auto\">\n        <DropdownItem\n          v-for=\"option in positionOptions\"\n          :key=\"option.value\"\n          v-close-popper\n          :is-active=\"\n            isActive(props.editor.state, ExtensionFigure.name, {\n              alignItems: option.value,\n            })\n          \"\n          @click=\"handleSetPosition(option.value)\"\n        >\n          <template #icon>\n            <component :is=\"option.icon\" />\n          </template>\n          {{ option.text }}\n        </DropdownItem>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/image/BubbleItemImageSize.vue",
    "content": "<script setup lang=\"ts\">\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport MdiBackupRestore from \"~icons/mdi/backup-restore\";\nimport MdiImageSizeSelectActual from \"~icons/mdi/image-size-select-actual\";\nimport MdiImageSizeSelectLarge from \"~icons/mdi/image-size-select-large\";\nimport MdiImageSizeSelectSmall from \"~icons/mdi/image-size-select-small\";\nimport Input from \"@/components/base/Input.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport { findChildren } from \"@/tiptap\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionImage } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst width = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionImage.name).width;\n  },\n  set: (width: string) => {\n    handleSetSize({ width, height: height.value });\n  },\n});\n\nconst height = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionImage.name).height;\n  },\n  set: (height: string) => {\n    handleSetSize({ width: width.value, height });\n  },\n});\n\nfunction handleSetSize(size: { width?: string; height?: string }) {\n  let figureWidth = size.width;\n  if (!size.width) {\n    const { state } = props.editor;\n    const { selection } = state;\n    const imageNodePosList = findChildren(\n      selection.$from.node(),\n      (node) => node.type.name === ExtensionImage.name\n    );\n    if (imageNodePosList.length === 0) {\n      return;\n    }\n    const imageNodePos = imageNodePosList[0];\n    if (imageNodePos && imageNodePos.node.type.name === ExtensionImage.name) {\n      const pos = selection.$from.pos + imageNodePos.pos;\n      const nodeDOM = props.editor.view.nodeDOM(pos) as HTMLElement;\n      const imageNodeDOM = nodeDOM.querySelector(\"img\");\n      if (imageNodeDOM && imageNodeDOM.naturalWidth) {\n        figureWidth = `${imageNodeDOM.naturalWidth}px`;\n      }\n    }\n  }\n  props.editor\n    .chain()\n    .updateAttributes(ExtensionImage.name, size)\n    .updateFigureContainerWidth(figureWidth)\n    .setNodeSelection(props.editor.state.selection.from)\n    .focus()\n    .run();\n}\n\nconst presetSizes = [\n  {\n    width: \"25%\",\n    height: \"auto\",\n    icon: MdiImageSizeSelectSmall,\n    title: i18n.global.t(\"editor.extensions.image.small_size\"),\n  },\n  {\n    width: \"50%\",\n    height: \"auto\",\n    icon: MdiImageSizeSelectLarge,\n    title: i18n.global.t(\"editor.extensions.image.medium_size\"),\n  },\n  {\n    width: \"100%\",\n    height: \"auto\",\n    icon: MdiImageSizeSelectActual,\n    title: i18n.global.t(\"editor.extensions.image.large_size\"),\n  },\n  {\n    width: undefined,\n    height: undefined,\n    icon: MdiBackupRestore,\n    title: i18n.global.t(\"editor.extensions.image.restore_size\"),\n  },\n];\n</script>\n\n<template>\n  <VDropdown\n    v-if=\"visible?.({ editor })\"\n    class=\"inline-flex\"\n    :auto-hide=\"true\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.extensions.image.resize')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <MdiImageSizeSelectSmall />\n      </template>\n    </BubbleButton>\n    <template #popper>\n      <div class=\"flex w-56 flex-col gap-3\">\n        <div class=\"flex flex-col items-center gap-3\">\n          <Input\n            v-model=\"width\"\n            :label=\"i18n.global.t('editor.common.width')\"\n            :tooltip=\"i18n.global.t('editor.common.tooltip.custom_width_input')\"\n          />\n          <Input\n            v-model=\"height\"\n            :label=\"i18n.global.t('editor.common.height')\"\n            :tooltip=\"\n              i18n.global.t('editor.common.tooltip.custom_height_input')\n            \"\n          />\n        </div>\n\n        <div class=\"flex items-center gap-1 rounded-md bg-gray-100 p-1\">\n          <button\n            v-for=\"item in presetSizes\"\n            :key=\"item.width\"\n            v-tooltip=\"item.title\"\n            class=\"inline-flex flex-1 items-center justify-center rounded px-2 py-1.5 text-gray-600 transition-all hover:text-gray-900\"\n            :class=\"{\n              'bg-white text-gray-900':\n                item.width === width && item.height === height,\n            }\"\n            @click=\"handleSetSize({ width: item.width, height: item.height })\"\n          >\n            <component :is=\"item.icon\" />\n          </button>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/image/ImageView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconImageAddLine, VButton } from \"@halo-dev/components\";\nimport { utils, type AttachmentSimple } from \"@halo-dev/ui-shared\";\nimport { computed, onMounted, onUnmounted, ref, watch } from \"vue\";\nimport { EditorLinkObtain } from \"@/components\";\nimport { ResourceReplaceButton } from \"@/components/upload\";\nimport { useExternalAssetsTransfer } from \"@/composables/use-attachment\";\nimport { i18n } from \"@/locales\";\nimport {\n  findParentNodeClosestToPos,\n  NodeViewWrapper,\n  type NodeViewProps,\n} from \"@/tiptap\";\nimport { fileToBase64 } from \"@/utils/upload\";\nimport { ExtensionFigure } from \"../figure\";\nimport { ExtensionImage } from \"./index\";\n\nconst props = defineProps<NodeViewProps>();\n\nconst src = computed({\n  get: () => {\n    return props.node?.attrs.src;\n  },\n  set: (src: string) => {\n    props.updateAttributes({\n      src: src,\n    });\n  },\n});\n\nconst alt = computed({\n  get: () => {\n    return props.node?.attrs.alt;\n  },\n  set: (alt: string) => {\n    props.updateAttributes({ alt: alt });\n  },\n});\n\nconst href = computed({\n  get: () => {\n    return props.node?.attrs.href;\n  },\n  set: (href: string) => {\n    props.updateAttributes({ href: href });\n  },\n});\n\n// Get the align items of the image from the figure parent\nconst alignItems = computed(() => {\n  const pos = props.getPos();\n  if (!pos) {\n    return \"start\";\n  }\n  const $pos = props.editor.state.doc.resolve(pos);\n  const figureParent = findParentNodeClosestToPos(\n    $pos,\n    (node) => node.type.name === ExtensionFigure.name\n  );\n\n  if (figureParent) {\n    return figureParent.node.attrs.alignItems;\n  }\n\n  return \"start\";\n});\n\nconst fileBase64 = ref<string>();\nconst uploadProgress = ref<number | undefined>(undefined);\nconst retryFlag = ref<boolean>(false);\nconst editorLinkObtain = ref();\n\nconst handleUploadAbort = () => {\n  editorLinkObtain.value?.abort();\n};\n\nconst initialization = computed(() => {\n  return !src.value && !fileBase64.value;\n});\n\nconst handleUploadReady = async (file: File) => {\n  fileBase64.value = await fileToBase64(file);\n  retryFlag.value = false;\n};\n\nconst handleSetExternalLink = (attachment?: AttachmentSimple) => {\n  if (!attachment) return;\n  props.updateAttributes({\n    src: attachment.url,\n    alt: attachment.alt,\n  });\n};\n\nconst handleUploadRetry = () => {\n  editorLinkObtain.value?.retry();\n};\n\nconst handleUploadProgress = (progress: number) => {\n  uploadProgress.value = progress;\n};\n\nconst handleUploadError = () => {\n  retryFlag.value = true;\n};\n\nconst resetUpload = () => {\n  fileBase64.value = undefined;\n  uploadProgress.value = undefined;\n\n  const { file } = props.node.attrs;\n  if (file) {\n    props.updateAttributes({\n      width: undefined,\n      file: undefined,\n    });\n  }\n};\n\nconst aspectRatio = ref<number>(0);\nconst resizeRef = ref<HTMLDivElement>();\nconst inputRef = ref<HTMLInputElement>();\n\nfunction onImageLoaded(event: Event) {\n  if (!resizeRef.value) return;\n\n  aspectRatio.value =\n    resizeRef.value.clientWidth / resizeRef.value.clientHeight;\n  const target = event.target;\n\n  if (props.node?.attrs.width) {\n    return;\n  }\n  if (target instanceof HTMLImageElement) {\n    props.editor\n      .chain()\n      .updateAttributes(ExtensionImage.name, {\n        width: `${target.naturalWidth}px`,\n      })\n      .updateFigureContainerWidth(`${target.naturalWidth}px`)\n      .setNodeSelection(props.getPos() || 0)\n      .focus()\n      .run();\n  }\n}\n\nlet cleanupResize: (() => void) | null = null;\nconst resizeHandleRef = ref<HTMLDivElement>();\n\nfunction setupResizeListener() {\n  if (!resizeHandleRef.value) {\n    return;\n  }\n\n  if (cleanupResize) {\n    cleanupResize();\n  }\n\n  const handleElement = resizeHandleRef.value;\n  let startX: number, startWidth: number;\n  let rafId: number | null = null;\n\n  function handleMouseDown(e: MouseEvent) {\n    if (e.button !== 0) {\n      return;\n    }\n\n    e.preventDefault();\n    e.stopPropagation();\n    startX = e.clientX;\n    startWidth = resizeRef.value?.clientWidth || 1;\n    document.documentElement.addEventListener(\"mousemove\", doDrag, false);\n    document.documentElement.addEventListener(\"mouseup\", stopDrag, false);\n    document.documentElement.addEventListener(\n      \"contextmenu\",\n      handleContextMenu,\n      false\n    );\n    document.documentElement.addEventListener(\"mouseleave\", stopDrag, false);\n    window.addEventListener(\"blur\", stopDrag, false);\n  }\n\n  function doDrag(e: MouseEvent) {\n    if (!resizeRef.value) {\n      return;\n    }\n\n    if (rafId !== null) {\n      cancelAnimationFrame(rafId);\n    }\n\n    rafId = requestAnimationFrame(() => {\n      if (!resizeRef.value) {\n        return;\n      }\n\n      const newWidth = Math.max(\n        1,\n        Math.min(\n          startWidth + e.clientX - startX,\n          props.editor.view.dom?.clientWidth || 0\n        )\n      );\n\n      const width = newWidth.toFixed(0) + \"px\";\n      props.editor\n        .chain()\n        .updateAttributes(ExtensionImage.name, { width })\n        .updateFigureContainerWidth(width)\n        .setNodeSelection(props.getPos() || 0)\n        .focus()\n        .run();\n    });\n  }\n\n  function handleContextMenu(e: MouseEvent) {\n    e.preventDefault();\n    stopDrag();\n  }\n\n  function stopDrag() {\n    if (rafId !== null) {\n      cancelAnimationFrame(rafId);\n      rafId = null;\n    }\n    document.documentElement.removeEventListener(\"mousemove\", doDrag, false);\n    document.documentElement.removeEventListener(\"mouseup\", stopDrag, false);\n    document.documentElement.removeEventListener(\n      \"contextmenu\",\n      handleContextMenu,\n      false\n    );\n    document.documentElement.removeEventListener(\"mouseleave\", stopDrag, false);\n    window.removeEventListener(\"blur\", stopDrag, false);\n  }\n\n  handleElement.addEventListener(\"mousedown\", handleMouseDown);\n\n  cleanupResize = () => {\n    handleElement.removeEventListener(\"mousedown\", handleMouseDown);\n    document.documentElement.removeEventListener(\"mousemove\", doDrag, false);\n    document.documentElement.removeEventListener(\"mouseup\", stopDrag, false);\n    document.documentElement.removeEventListener(\n      \"contextmenu\",\n      handleContextMenu,\n      false\n    );\n    document.documentElement.removeEventListener(\"mouseleave\", stopDrag, false);\n    window.removeEventListener(\"blur\", stopDrag, false);\n    if (rafId !== null) {\n      cancelAnimationFrame(rafId);\n    }\n    cleanupResize = null;\n  };\n}\n\nonMounted(() => {\n  if (!src.value) {\n    inputRef.value?.focus();\n    return;\n  }\n  setupResizeListener();\n});\n\nonUnmounted(() => {\n  if (cleanupResize) {\n    cleanupResize();\n  }\n});\n\nwatch([src, resizeHandleRef], () => {\n  if (src.value && resizeHandleRef.value) {\n    setupResizeListener();\n  }\n});\n\nconst { isExternalAsset, transferring, handleTransfer } =\n  useExternalAssetsTransfer(src, handleSetExternalLink);\n\nconst isPercentageWidth = computed(() => {\n  return props.node?.attrs.width?.includes(\"%\");\n});\n</script>\n\n<template>\n  <NodeViewWrapper\n    as=\"div\"\n    class=\"flex w-full\"\n    :class=\"{\n      [`justify-${alignItems}`]: true,\n    }\"\n  >\n    <div\n      ref=\"resizeRef\"\n      class=\"group relative inline-block max-w-full overflow-hidden rounded-md text-center\"\n      :class=\"{\n        'rounded ring-2': selected,\n        'resize-container': !!src || !!fileBase64,\n      }\"\n      :style=\"{\n        width: initialization ? '100%' : node.attrs.width,\n      }\"\n    >\n      <div v-if=\"src || fileBase64\" class=\"relative\">\n        <img\n          :src=\"src || fileBase64\"\n          :title=\"node.attrs.title\"\n          :alt=\"alt\"\n          :href=\"href\"\n          :width=\"isPercentageWidth ? '100%' : node.attrs.width\"\n          :height=\"node.attrs.height\"\n          :style=\"{\n            width: isPercentageWidth ? '100%' : node.attrs.width,\n            height: node.attrs.height,\n          }\"\n          class=\"max-w-full rounded-md\"\n          @load=\"onImageLoaded\"\n        />\n\n        <div\n          v-if=\"selected\"\n          ref=\"resizeHandleRef\"\n          class=\"resizer-handler resizer-br\"\n        ></div>\n\n        <div\n          v-if=\"src\"\n          class=\"absolute left-0 top-0 hidden w-full cursor-pointer justify-end gap-2 rounded-md bg-gradient-to-b from-gray-300 to-transparent p-2 ease-in-out group-hover:flex\"\n        >\n          <VButton\n            v-if=\"\n              utils.permission.has([\n                'uc:attachments:manage',\n                'system:attachments:manage',\n              ]) && isExternalAsset\n            \"\n            v-tooltip=\"\n              i18n.global.t(\n                'editor.extensions.upload.operations.transfer.tooltip'\n              )\n            \"\n            :loading=\"transferring\"\n            size=\"sm\"\n            ghost\n            @click=\"handleTransfer\"\n          >\n            {{\n              i18n.global.t(\n                \"editor.extensions.upload.operations.transfer.button\"\n              )\n            }}\n          </VButton>\n          <ResourceReplaceButton\n            accept=\"image/*\"\n            :original-link=\"src\"\n            :upload=\"extension.options.uploadImage\"\n            @change=\"handleSetExternalLink\"\n          />\n        </div>\n\n        <div\n          v-if=\"fileBase64\"\n          class=\"absolute top-0 size-full bg-black bg-opacity-20\"\n        >\n          <div class=\"absolute top-[50%] w-full space-y-2 text-white\">\n            <template v-if=\"retryFlag\">\n              <div class=\"px-10\">\n                <div\n                  class=\"relative h-4 w-full overflow-hidden rounded-full bg-gray-200\"\n                >\n                  <div class=\"size-full bg-red-600\"></div>\n                  <div\n                    class=\"absolute left-[50%] top-0 -translate-x-[50%] text-xs leading-4 text-white\"\n                  >\n                    {{ i18n.global.t(\"editor.extensions.upload.error\") }}\n                  </div>\n                </div>\n              </div>\n              <div\n                class=\"inline-block cursor-pointer text-sm hover:opacity-70\"\n                @click=\"handleUploadRetry\"\n              >\n                {{ i18n.global.t(\"editor.extensions.upload.click_retry\") }}\n              </div>\n            </template>\n            <template v-else>\n              <div class=\"px-10\">\n                <div\n                  class=\"relative h-4 w-full overflow-hidden rounded-full bg-gray-200\"\n                >\n                  <div\n                    class=\"h-full bg-primary\"\n                    :style=\"{\n                      width: `${uploadProgress || 0}%`,\n                    }\"\n                  ></div>\n                  <div\n                    class=\"absolute left-[50%] top-0 -translate-x-[50%] text-xs leading-4 text-white\"\n                  >\n                    {{\n                      uploadProgress\n                        ? `${uploadProgress}%`\n                        : `${i18n.global.t(\"editor.extensions.upload.loading\")}...`\n                    }}\n                  </div>\n                </div>\n              </div>\n\n              <div\n                class=\"inline-block cursor-pointer text-sm hover:opacity-70\"\n                @click=\"handleUploadAbort\"\n              >\n                {{ i18n.global.t(\"editor.common.button.cancel\") }}\n              </div>\n            </template>\n          </div>\n        </div>\n      </div>\n      <div v-show=\"!src && !fileBase64\">\n        <EditorLinkObtain\n          ref=\"editorLinkObtain\"\n          :accept=\"'image/*'\"\n          :editor=\"editor\"\n          :upload-to-attachment-file=\"extension.options.uploadImage\"\n          :uploaded-file=\"node?.attrs.file\"\n          @set-external-link=\"handleSetExternalLink\"\n          @on-upload-ready=\"handleUploadReady\"\n          @on-upload-progress=\"handleUploadProgress\"\n          @on-upload-finish=\"resetUpload\"\n          @on-upload-error=\"handleUploadError\"\n          @on-upload-abort=\"resetUpload\"\n        >\n          <template #icon>\n            <IconImageAddLine class=\"text-xl text-primary\" />\n          </template>\n        </EditorLinkObtain>\n      </div>\n    </div>\n  </NodeViewWrapper>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/image/index.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport type { ImageOptions } from \"@tiptap/extension-image\";\nimport TiptapImage from \"@tiptap/extension-image\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { isEmpty } from \"es-toolkit/compat\";\nimport { markRaw } from \"vue\";\nimport MingcuteBookmarkEditLine from \"~icons/mingcute/bookmark-edit-line\";\nimport MingcuteCopy3Fill from \"~icons/mingcute/copy-3-fill\";\nimport MingcuteEdit4Line from \"~icons/mingcute/edit-4-line\";\nimport MingcuteLink2Line from \"~icons/mingcute/link-2-line\";\nimport MingcuteLinkLine from \"~icons/mingcute/link-line\";\nimport MingcutePicLine from \"~icons/mingcute/pic-line\";\nimport MingcuteShare3Line from \"~icons/mingcute/share-3-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport ToolboxItem from \"@/components/toolbox/ToolboxItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  findChildren,\n  findParentNode,\n  findParentNodeClosestToPos,\n  isActive,\n  mergeAttributes,\n  Plugin,\n  PluginKey,\n  PMNode,\n  TextSelection,\n  VueNodeViewRenderer,\n  type Editor,\n  type EditorState,\n  type Range,\n} from \"@/tiptap\";\nimport type { ExtensionOptions, NodeBubbleMenuType } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\nimport { ExtensionFigure } from \"../figure\";\nimport { ExtensionFigureCaption } from \"../figure/figure-caption\";\nimport { ExtensionParagraph } from \"../paragraph\";\nimport BubbleItemImageAlt from \"./BubbleItemImageAlt.vue\";\nimport BubbleItemImageHref from \"./BubbleItemImageHref.vue\";\nimport BubbleItemImageLink from \"./BubbleItemImageLink.vue\";\nimport BubbleItemImagePosition from \"./BubbleItemImagePosition.vue\";\nimport BubbleItemImageSize from \"./BubbleItemImageSize.vue\";\nimport ImageView from \"./ImageView.vue\";\n\nexport const IMAGE_BUBBLE_MENU_KEY = new PluginKey(\"imageBubbleMenu\");\n\nexport type ExtensionImageOptions = ExtensionOptions &\n  Partial<ImageOptions> & {\n    uploadImage?: (\n      file: File,\n      options?: AxiosRequestConfig\n    ) => Promise<Attachment>;\n  };\n\nexport const ExtensionImage = TiptapImage.extend<ExtensionImageOptions>({\n  fakeSelection: true,\n\n  inline: false,\n\n  group: \"block\",\n\n  defining: false,\n\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      src: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"src\");\n        },\n      },\n      width: {\n        default: undefined,\n        parseHTML: (element) => {\n          const width =\n            element.getAttribute(\"width\") || element.style.width || null;\n          return width;\n        },\n        renderHTML: (attributes) => {\n          return {\n            width: attributes.width,\n          };\n        },\n      },\n      height: {\n        default: undefined,\n        parseHTML: (element) => {\n          const height =\n            element.getAttribute(\"height\") || element.style.height || null;\n          return height;\n        },\n        renderHTML: (attributes) => {\n          return {\n            height: attributes.height,\n          };\n        },\n      },\n      href: {\n        default: null,\n        parseHTML: (element) => {\n          const href = element.getAttribute(\"href\") || null;\n          return href;\n        },\n        renderHTML: (attributes) => {\n          return {\n            href: attributes.href,\n          };\n        },\n      },\n      file: {\n        default: null,\n        renderHTML() {\n          return {};\n        },\n        parseHTML() {\n          return null;\n        },\n      },\n    };\n  },\n\n  addNodeView() {\n    return VueNodeViewRenderer(ImageView);\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: this.options.allowBase64 ? \"img\" : 'img:not([src^=\"data:\"])',\n      },\n    ];\n  },\n\n  addProseMirrorPlugins() {\n    return [\n      new Plugin({\n        key: new PluginKey(\"imageLegacyFormat\"),\n        appendTransaction: (transactions, _oldState, newState) => {\n          const docChanged = transactions.some((tr) => tr.docChanged);\n          if (!docChanged) {\n            return null;\n          }\n\n          const tr = newState.tr;\n\n          const modifications: Array<{\n            pos: number;\n            node: PMNode;\n            figureNode: PMNode;\n            deletePreviousNode: boolean;\n            previousNodePos: number;\n            previousNodeSize: number;\n          }> = [];\n\n          newState.doc.descendants((node, pos) => {\n            if (node.type.name !== ExtensionImage.name) {\n              return;\n            }\n\n            const $pos = newState.doc.resolve(pos);\n            if ($pos.parent.type.name === ExtensionFigure.name) {\n              return;\n            }\n\n            let blockPosition = \"start\";\n            let deletePreviousNode = false;\n            let previousNodePos = -1;\n            let previousNodeSize = 0;\n\n            const previousNode = $pos.nodeBefore;\n            if (\n              previousNode &&\n              previousNode.type.name === ExtensionParagraph.name\n            ) {\n              if (previousNode.attrs.textAlign) {\n                const textAlignToBlockPositionMap: Record<string, string> = {\n                  left: \"start\",\n                  center: \"center\",\n                  right: \"end\",\n                  justify: \"center\",\n                };\n                blockPosition =\n                  textAlignToBlockPositionMap[previousNode.attrs.textAlign] ??\n                  \"start\";\n              }\n              if (previousNode.textContent?.trim().length === 0) {\n                deletePreviousNode = true;\n                previousNodePos = pos - previousNode.nodeSize;\n                previousNodeSize = previousNode.nodeSize;\n              }\n            }\n\n            const figureNode = newState.schema.nodes.figure.create(\n              {\n                contentType: \"image\",\n                alignItems: blockPosition,\n              },\n              [node]\n            );\n\n            modifications.push({\n              pos,\n              node,\n              figureNode,\n              deletePreviousNode,\n              previousNodePos,\n              previousNodeSize,\n            });\n          });\n\n          modifications.reverse().forEach((mod) => {\n            if (mod.deletePreviousNode) {\n              tr.delete(\n                mod.previousNodePos,\n                mod.previousNodePos + mod.previousNodeSize\n              );\n              tr.replaceRangeWith(\n                mod.pos - mod.previousNodeSize,\n                mod.pos - mod.previousNodeSize + mod.node.nodeSize,\n                mod.figureNode\n              );\n            } else {\n              tr.replaceRangeWith(\n                mod.pos,\n                mod.pos + mod.node.nodeSize,\n                mod.figureNode\n              );\n            }\n          });\n\n          return modifications.length > 0 ? tr : null;\n        },\n      }),\n    ];\n  },\n\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      uploadImage: undefined,\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return [\n          {\n            priority: 10,\n            component: markRaw(ToolboxItem),\n            props: {\n              editor,\n              icon: markRaw(MingcutePicLine),\n              title: i18n.global.t(\"editor.common.image\"),\n              action: () => {\n                editor\n                  .chain()\n                  .focus()\n                  .insertContent([\n                    {\n                      type: \"figure\",\n                      attrs: { contentType: \"image\" },\n                      content: [{ type: \"image\" }],\n                    },\n                  ])\n                  .run();\n              },\n            },\n          },\n        ];\n      },\n      getCommandMenuItems() {\n        return {\n          priority: 95,\n          icon: markRaw(MingcutePicLine),\n          title: \"editor.extensions.commands_menu.image\",\n          keywords: [\"image\", \"tupian\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .insertContent([\n                {\n                  type: \"figure\",\n                  attrs: { contentType: \"image\" },\n                  content: [{ type: \"image\" }],\n                },\n              ])\n              .run();\n          },\n        };\n      },\n      getBubbleMenu({ editor }: { editor: Editor }): NodeBubbleMenuType {\n        return {\n          pluginKey: IMAGE_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }): boolean => {\n            return isActive(state, ExtensionImage.name);\n          },\n          options: {\n            placement: \"top-start\",\n          },\n          items: [\n            {\n              priority: 10,\n              component: markRaw(BubbleItemImageSize),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionImage.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 20,\n              component: markRaw(BubbleItemImagePosition),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionImage.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 25,\n              props: {\n                icon: markRaw(MingcuteCopy3Fill),\n                title: i18n.global.t(\"editor.extensions.image.copy_width\"),\n                action: () => {\n                  const width = editor.getAttributes(ExtensionImage.name).width;\n\n                  const tr = editor.state.tr;\n                  editor.state.doc.descendants((node, pos, parent) => {\n                    if (node.type.name === ExtensionImage.name) {\n                      tr.setNodeAttribute(pos, \"width\", width);\n\n                      if (parent?.type.name === ExtensionFigure.name) {\n                        const $pos = editor.state.doc.resolve(pos);\n                        const figurePos = findParentNodeClosestToPos(\n                          $pos,\n                          (node) => node.type.name === ExtensionFigure.name\n                        );\n                        if (figurePos) {\n                          // TODO: image should not update figureCaption's width every time, it should be optimized in figure\n                          const figureCaptionPos = findChildren(\n                            figurePos.node,\n                            (node) =>\n                              node.type.name === ExtensionFigureCaption.name\n                          )[0];\n                          if (figureCaptionPos) {\n                            const imageHTMLElement = editor.view.nodeDOM(\n                              $pos.pos\n                            ) as HTMLElement;\n                            const imageNode =\n                              imageHTMLElement.querySelector(\"img\");\n                            const imageNaturalWidth = width\n                              ? width\n                              : imageNode?.naturalWidth\n                                ? `${imageNode.naturalWidth}px`\n                                : undefined;\n                            tr.setNodeAttribute(\n                              figurePos.pos + figureCaptionPos.pos + 1,\n                              \"width\",\n                              imageNaturalWidth\n                            );\n                          }\n                        }\n                      }\n                    }\n                  });\n\n                  editor.view.dispatch(tr);\n                },\n              },\n            },\n            {\n              priority: 30,\n              component: markRaw(BlockActionSeparator),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionImage.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 40,\n              props: {\n                icon: markRaw(MingcuteLinkLine),\n                title: i18n.global.t(\"editor.common.button.edit_link\"),\n                action: () => {\n                  return markRaw(BubbleItemImageLink);\n                },\n              },\n            },\n            {\n              priority: 50,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionImage.name).src\n                  );\n                },\n                icon: markRaw(MingcuteShare3Line),\n                title: i18n.global.t(\"editor.common.tooltip.open_link\"),\n                action: () => {\n                  window.open(\n                    editor.getAttributes(ExtensionImage.name).src,\n                    \"_blank\"\n                  );\n                },\n              },\n            },\n            {\n              priority: 60,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionImage.name).src\n                  );\n                },\n                icon: markRaw(MingcuteEdit4Line),\n                title: i18n.global.t(\"editor.extensions.image.edit_alt\"),\n                action: () => {\n                  return markRaw(BubbleItemImageAlt);\n                },\n              },\n            },\n            {\n              priority: 70,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionImage.name).src\n                  );\n                },\n                icon: markRaw(MingcuteLink2Line),\n                title: i18n.global.t(\"editor.extensions.image.edit_href\"),\n                action: () => {\n                  return markRaw(BubbleItemImageHref);\n                },\n              },\n            },\n            {\n              priority: 80,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionImage.name).src\n                  );\n                },\n                icon: markRaw(MingcuteBookmarkEditLine),\n                title: i18n.global.t(\"editor.extensions.image.edit_caption\"),\n                action: ({ editor }) => {\n                  const figureParent = findParentNode(\n                    (node) => node.type.name === ExtensionFigure.name\n                  )(editor.state.selection);\n\n                  if (!figureParent) {\n                    return;\n                  }\n\n                  const { node, pos } = figureParent;\n                  let captionPos = -1;\n                  node.forEach((child, offset) => {\n                    if (child.type.name === ExtensionFigureCaption.name) {\n                      captionPos = pos + offset + 1;\n                    }\n                  });\n                  if (captionPos !== -1) {\n                    editor.chain().focus().setTextSelection(captionPos).run();\n                    return;\n                  }\n                  const imageNodePos = findChildren(\n                    editor.state.selection.$from.node(),\n                    (node) => node.type.name === ExtensionImage.name\n                  )[0];\n                  const figureCaptionNode =\n                    editor.schema.nodes.figureCaption.create({\n                      width: imageNodePos.node.attrs.width,\n                    });\n                  editor\n                    .chain()\n                    .focus()\n                    .command(({ tr }) => {\n                      const insertPos = pos + node.nodeSize - 1;\n                      tr.insert(insertPos, figureCaptionNode);\n                      tr.setSelection(\n                        TextSelection.near(tr.doc.resolve(insertPos + 1))\n                      );\n                      return true;\n                    })\n                    .run();\n                },\n              },\n            },\n            {\n              priority: 90,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 100,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }) => {\n                  const figureParent = findParentNode(\n                    (node) => node.type.name === ExtensionFigure.name\n                  )(editor.state.selection);\n\n                  deleteNode(\n                    figureParent ? ExtensionFigure.name : ExtensionImage.name,\n                    editor\n                  );\n                },\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n  renderHTML({ HTMLAttributes }) {\n    if (HTMLAttributes.href) {\n      return [\n        \"a\",\n        { href: HTMLAttributes.href },\n        [\"img\", mergeAttributes(HTMLAttributes)],\n      ];\n    }\n    return [\"img\", mergeAttributes(HTMLAttributes)];\n  },\n}).configure({\n  inline: true,\n  allowBase64: false,\n  HTMLAttributes: {\n    loading: \"lazy\",\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/indent/index.ts",
    "content": "import { ListItem } from \"@tiptap/extension-list\";\nimport {\n  Editor,\n  Extension,\n  isList,\n  type CommandProps,\n  type Extensions,\n  type KeyboardShortcutCommand,\n} from \"@/tiptap\";\nimport { TextSelection, Transaction } from \"@/tiptap/pm\";\nimport type { ExtensionOptions } from \"@/types\";\nimport { isListActive } from \"@/utils/is-list-active\";\nimport { ExtensionColumns } from \"../columns\";\nimport { ExtensionTable } from \"../table\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    indent: {\n      indent: () => ReturnType;\n      outdent: () => ReturnType;\n    };\n  }\n}\n\nexport interface ExtensionIndentOptions extends ExtensionOptions {\n  names: Array<string>;\n  indentRange: number;\n  minIndentLevel: number;\n  maxIndentLevel: number;\n  defaultIndentLevel: number;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  HTMLAttributes: Record<string, any>;\n  firstLineIndent: boolean;\n}\n\nexport const ExtensionIndent = Extension.create<ExtensionIndentOptions>({\n  name: \"indent\",\n  priority: 800,\n\n  addOptions() {\n    return {\n      names: [\"heading\", \"paragraph\"],\n      indentRange: 24,\n      minIndentLevel: 0,\n      maxIndentLevel: 24 * 10,\n      defaultIndentLevel: 0,\n      HTMLAttributes: {},\n      firstLineIndent: true,\n    };\n  },\n\n  addGlobalAttributes() {\n    return [\n      {\n        types: this.options.names,\n        attributes: {\n          indent: {\n            default: this.options.defaultIndentLevel,\n            renderHTML: (attributes) => ({\n              style:\n                attributes.indent != 0\n                  ? `margin-left: ${attributes.indent}px!important;`\n                  : \"\",\n            }),\n            parseHTML: (element) =>\n              parseInt(element.style.marginLeft, 10) ||\n              this.options.defaultIndentLevel,\n          },\n          lineIndent: {\n            default: false,\n            renderHTML: (attributes) => ({\n              style: attributes.lineIndent ? \"text-indent: 2em\" : \"\",\n            }),\n            parseHTML: (element) => element.style.textIndent === \"2em\",\n          },\n        },\n      },\n    ];\n  },\n\n  addCommands(this) {\n    return {\n      indent:\n        () =>\n        ({ tr, state, dispatch, editor }: CommandProps) => {\n          const { selection } = state;\n          tr = tr.setSelection(selection);\n          tr = updateIndentLevel(\n            tr,\n            this.options,\n            editor.extensionManager.extensions,\n            \"indent\"\n          );\n          if (tr.docChanged && dispatch) {\n            dispatch(tr);\n          }\n          return true;\n        },\n      outdent:\n        () =>\n        ({ tr, state, dispatch, editor }: CommandProps) => {\n          const { selection } = state;\n          tr = tr.setSelection(selection);\n          tr = updateIndentLevel(\n            tr,\n            this.options,\n            editor.extensionManager.extensions,\n            \"outdent\"\n          );\n          if (tr.docChanged && dispatch) {\n            dispatch(tr);\n          }\n          return true;\n        },\n    };\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      Tab: getIndent(),\n      \"Shift-Tab\": getOutdent(false),\n      \"Mod-]\": getIndent(),\n      \"Mod-[\": getOutdent(false),\n      Backspace: ({ editor }) => {\n        const { selection } = editor.state;\n        const { $from } = selection;\n\n        if ($from.parentOffset === 0) {\n          const node = $from.parent;\n\n          if (node.attrs.lineIndent) {\n            return editor\n              .chain()\n              .focus()\n              .updateAttributes(node.type.name, { lineIndent: false })\n              .run();\n          }\n\n          if (node.attrs.indent && node.attrs.indent > 0) {\n            return getOutdent(false)({ editor });\n          }\n        }\n        return false;\n      },\n    };\n  },\n\n  onUpdate() {\n    const { editor } = this;\n    if (editor.isActive(ListItem.name)) {\n      const node = editor.state.selection.$head.node();\n      if (node.attrs.indent) {\n        editor.commands.updateAttributes(node.type.name, { indent: 0 });\n      }\n    }\n  },\n});\n\nexport const clamp = (val: number, min: number, max: number): number => {\n  if (val < min) {\n    return min;\n  }\n  if (val > max) {\n    return max;\n  }\n  return val;\n};\n\nfunction setNodeIndentMarkup(\n  tr: Transaction,\n  pos: number,\n  dir: number,\n  options: ExtensionIndentOptions\n): Transaction {\n  if (!tr.doc) {\n    return tr;\n  }\n  const node = tr.doc.nodeAt(pos);\n  if (!node) {\n    return tr;\n  }\n  if (options.firstLineIndent && isLineIndent(tr)) {\n    if (node.attrs.lineIndent !== dir > 0) {\n      const nodeAttrs = { ...node.attrs, lineIndent: dir > 0 };\n      return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks);\n    }\n  }\n\n  const delta = options.indentRange * dir;\n  const min = options.minIndentLevel;\n  const max = options.maxIndentLevel;\n  const indent = clamp((node.attrs.indent || 0) + delta, min, max);\n  if (indent === node.attrs.indent) {\n    return tr;\n  }\n  const nodeAttrs = { ...node.attrs, indent };\n  return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks);\n}\n\nconst isLineIndent = (tr: Transaction) => {\n  const { selection } = tr;\n  const { $from, from, to } = selection;\n  if (from == 0) {\n    return true;\n  }\n\n  if (from != to) {\n    return false;\n  }\n\n  return $from.textOffset == 0;\n};\n\ntype IndentType = \"indent\" | \"outdent\";\nconst updateIndentLevel = (\n  tr: Transaction,\n  options: ExtensionIndentOptions,\n  extensions: Extensions,\n  type: IndentType\n): Transaction => {\n  const { doc, selection } = tr;\n  if (!doc || !selection) return tr;\n  if (!(selection instanceof TextSelection)) {\n    return tr;\n  }\n  const { from, to } = selection;\n  doc.nodesBetween(from, to, (node, pos) => {\n    if (options.names.includes(node.type.name)) {\n      if (isTextIndent(tr, pos) && type === \"indent\") {\n        tr.insertText(\"\\t\", from, to);\n      } else {\n        tr = setNodeIndentMarkup(tr, pos, type === \"indent\" ? 1 : -1, options);\n      }\n      return false;\n    }\n    return !isList(node.type.name, extensions);\n  });\n\n  return tr;\n};\n\nconst isTextIndent = (tr: Transaction, currNodePos: number) => {\n  const { selection } = tr;\n  const { from, to } = selection;\n  if (from == 0) {\n    return false;\n  }\n  if (from - to == 0 && currNodePos != from - 1) {\n    return true;\n  }\n  return false;\n};\n\nconst isFilterActive = (editor: Editor) => {\n  return (\n    editor.isActive(ExtensionTable.name) ||\n    editor.isActive(ExtensionColumns.name)\n  );\n};\n\nexport const getIndent: () => KeyboardShortcutCommand =\n  () =>\n  ({ editor }) => {\n    if (isFilterActive(editor)) {\n      return false;\n    }\n    if (isListActive(editor)) {\n      const name = editor.can().sinkListItem(\"listItem\")\n        ? \"listItem\"\n        : \"taskItem\";\n      return editor.chain().focus().sinkListItem(name).run();\n    }\n    return editor.chain().focus().indent().run();\n  };\nexport const getOutdent: (\n  outdentOnlyAtHead: boolean\n) => KeyboardShortcutCommand =\n  (outdentOnlyAtHead) =>\n  ({ editor }) => {\n    if (outdentOnlyAtHead && editor.state.selection.$head.parentOffset > 0) {\n      return false;\n    }\n\n    if (isFilterActive(editor)) {\n      return false;\n    }\n\n    if (isListActive(editor)) {\n      const name = editor.can().liftListItem(\"listItem\")\n        ? \"listItem\"\n        : \"taskItem\";\n      return editor.chain().focus().liftListItem(name).run();\n    }\n    return editor.chain().focus().outdent().run();\n  };\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/index.ts",
    "content": "export * from \"./extensions-kit\";\n\nexport * from \"./audio\";\nexport * from \"./blockquote\";\nexport * from \"./bold\";\nexport * from \"./bullet-list\";\nexport * from \"./character-count\";\nexport * from \"./clear-format\";\nexport * from \"./code\";\nexport * from \"./code-block\";\nexport * from \"./color\";\nexport * from \"./columns\";\nexport * from \"./commands-menu\";\nexport * from \"./details\";\nexport * from \"./document\";\nexport * from \"./drop-cursor\";\nexport * from \"./figure\";\nexport * from \"./font-size\";\nexport * from \"./format-brush\";\nexport * from \"./gallery\";\nexport * from \"./gap-cursor\";\nexport * from \"./hard-break\";\nexport * from \"./heading\";\nexport * from \"./highlight\";\nexport * from \"./history\";\nexport * from \"./horizontal-rule\";\nexport * from \"./iframe\";\nexport * from \"./image\";\nexport * from \"./indent\";\nexport * from \"./italic\";\nexport * from \"./link\";\nexport * from \"./list-keymap\";\nexport * from \"./node-selected\";\nexport * from \"./ordered-list\";\nexport * from \"./paragraph\";\nexport * from \"./placeholder\";\nexport * from \"./range-selection\";\nexport * from \"./search-and-replace\";\nexport * from \"./smart-scroll\";\nexport * from \"./strike\";\nexport * from \"./subscript\";\nexport * from \"./superscript\";\nexport * from \"./table\";\nexport * from \"./task-list\";\nexport * from \"./text\";\nexport * from \"./text-align\";\nexport * from \"./text-style\";\nexport * from \"./trailing-node\";\nexport * from \"./underline\";\nexport * from \"./upload\";\nexport * from \"./video\";\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/italic/index.ts",
    "content": "import TiptapItalic, { type ItalicOptions } from \"@tiptap/extension-italic\";\nimport { markRaw } from \"vue\";\nimport MingcuteItalicLine from \"~icons/mingcute/italic-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionItalicOptions = ExtensionOptions & Partial<ItalicOptions>;\n\nexport const ExtensionItalic = TiptapItalic.extend<ExtensionItalicOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 50,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: editor.isActive(TiptapItalic.name),\n            icon: markRaw(MingcuteItalicLine),\n            title: i18n.global.t(\"editor.common.italic\"),\n            action: () => editor.chain().focus().toggleItalic().run(),\n          },\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/link/LinkBubbleButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown, vTooltip } from \"@halo-dev/components\";\nimport { TextSelection } from \"@tiptap/pm/state\";\nimport { test } from \"linkifyjs\";\nimport { computed } from \"vue\";\nimport MingcuteLinkLine from \"~icons/mingcute/link-line\";\nimport Input from \"@/components/base/Input.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionLink } from \".\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst href = computed({\n  get() {\n    const attrs = props.editor.getAttributes(ExtensionLink.name);\n    return attrs?.href;\n  },\n  set(value) {\n    props.editor.commands.setLink({\n      href: value,\n      target: target.value ? \"_blank\" : \"_self\",\n      rel: rel.value ? \"nofollow\" : \"\",\n    });\n  },\n});\n\nconst target = computed({\n  get() {\n    const attrs = props.editor.getAttributes(ExtensionLink.name);\n    return attrs?.target === \"_blank\";\n  },\n  set(value) {\n    props.editor.commands.setLink({\n      href: href.value,\n      target: value ? \"_blank\" : \"_self\",\n      rel: rel.value ? \"nofollow\" : \"\",\n    });\n  },\n});\n\nconst rel = computed({\n  get() {\n    const attrs = props.editor.getAttributes(ExtensionLink.name);\n    return attrs?.rel === \"nofollow\";\n  },\n  set(value) {\n    props.editor.commands.setLink({\n      href: href.value,\n      target: target.value ? \"_blank\" : \"_self\",\n      rel: value ? \"nofollow\" : \"\",\n    });\n  },\n});\n\n/**\n * Convert the currently selected text when clicking the link\n */\nconst handleLinkBubbleButton = () => {\n  if (props.isActive?.({ editor: props.editor })) {\n    return;\n  }\n  const { state } = props.editor;\n  const { selection } = state;\n  const { empty } = selection;\n\n  if (selection instanceof TextSelection) {\n    if (empty) {\n      return false;\n    }\n    const { content } = selection.content();\n    if (!content || content.childCount !== 1) {\n      return false;\n    }\n    const text = content.firstChild?.textContent;\n    if (text && test(text, \"url\")) {\n      props.editor.commands.setLink({\n        href: text,\n        target: \"_self\",\n        rel: \"\",\n      });\n    }\n  }\n};\n</script>\n\n<template>\n  <VDropdown\n    class=\"inline-flex\"\n    :triggers=\"['click']\"\n    :distance=\"10\"\n    @click=\"handleLinkBubbleButton\"\n  >\n    <button\n      v-tooltip=\"\n        isActive?.({ editor })\n          ? i18n.global.t('editor.extensions.link.edit_link')\n          : i18n.global.t('editor.extensions.link.add_link')\n      \"\n      class=\"inline-flex size-8 items-center justify-center rounded-md text-lg text-gray-600 hover:bg-gray-100 active:!bg-gray-200\"\n      :class=\"{ 'bg-gray-200 !text-black': isActive?.({ editor }) }\"\n    >\n      <MingcuteLinkLine />\n    </button>\n\n    <template #popper>\n      <div class=\"relative w-96\">\n        <Input\n          v-model.lazy=\"href\"\n          auto-focus\n          :placeholder=\"i18n.global.t('editor.extensions.link.placeholder')\"\n        />\n        <label class=\"mr-2 mt-2 inline-flex items-center\">\n          <input v-model=\"target\" type=\"checkbox\" />\n          <span class=\"ml-2 text-sm text-gray-500\">\n            {{ i18n.global.t(\"editor.extensions.link.open_in_new_window\") }}\n          </span>\n        </label>\n        <label class=\"mt-2 inline-flex items-center\">\n          <!-- nofollow -->\n          <input v-model=\"rel\" type=\"checkbox\" />\n          <span class=\"ml-2 text-sm text-gray-500\">\n            {{ i18n.global.t(\"editor.extensions.link.nofollow\") }}\n          </span>\n        </label>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/link/index.ts",
    "content": "import TiptapLink, { type LinkOptions } from \"@tiptap/extension-link\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionLinkOptions = ExtensionOptions & Partial<LinkOptions>;\n\nexport const ExtensionLink = TiptapLink.extend<ExtensionLinkOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      ...{\n        HTMLAttributes: {\n          rel: null,\n        },\n      },\n    };\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\"a\", HTMLAttributes, 0];\n  },\n\n  addPasteRules() {\n    // Remove the function of pasted text parsing as a link\n    return [];\n  },\n}).configure({\n  autolink: false,\n  openOnClick: false,\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/list-extra/index.ts",
    "content": "import { Editor, Extension } from \"@tiptap/core\";\nimport { markRaw } from \"vue\";\nimport MingcuteListCheck3Line from \"~icons/mingcute/list-check-3-line\";\nimport MingcuteListCheckLine from \"~icons/mingcute/list-check-line\";\nimport MingcuteListOrderedLine from \"~icons/mingcute/list-ordered-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport ToolbarSubItem from \"@/components/toolbar/ToolbarSubItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { ExtensionOptions, ToolbarItemType } from \"@/types\";\nimport { ExtensionBulletList } from \"../bullet-list\";\nimport { ExtensionOrderedList } from \"../ordered-list\";\nimport { ExtensionTaskList } from \"../task-list\";\n\nconst listExtensionNames = [\n  ExtensionBulletList.name,\n  ExtensionOrderedList.name,\n  ExtensionTaskList.name,\n];\n\n/**\n * Add toolbar items for list, include bullet list, ordered list and task list.\n */\nexport const ExtensionListExtra = Extension.create<ExtensionOptions>({\n  name: \"list-extra\",\n  addOptions() {\n    return {\n      getToolbarItems({ editor }) {\n        const isListExtensionLoaded = editor.extensionManager.extensions.some(\n          (extension) => listExtensionNames.includes(extension.name)\n        );\n        if (!isListExtensionLoaded) {\n          return [];\n        }\n\n        return {\n          priority: 130,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: isListActive(editor),\n            icon: markRaw(getListIcon(editor)),\n            title: i18n.global.t(\"editor.common.list\"),\n          },\n          children: getListItems(editor),\n        };\n      },\n    };\n  },\n});\n\nfunction isListActive(editor: Editor) {\n  return (\n    editor.isActive(ExtensionBulletList.name) ||\n    editor.isActive(ExtensionOrderedList.name) ||\n    editor.isActive(ExtensionTaskList.name)\n  );\n}\n\nfunction getListIcon(editor: Editor) {\n  if (editor.isActive(ExtensionBulletList.name)) {\n    return MingcuteListCheckLine;\n  }\n  if (editor.isActive(ExtensionOrderedList.name)) {\n    return MingcuteListOrderedLine;\n  }\n  if (editor.isActive(ExtensionTaskList.name)) {\n    return MingcuteListCheck3Line;\n  }\n  return MingcuteListCheckLine;\n}\n\nfunction isListExtensionLoaded(editor: Editor, extensionName: string) {\n  return editor.extensionManager.extensions.some(\n    (extension) => extension.name === extensionName\n  );\n}\n\nfunction getListItems(editor: Editor) {\n  const items: ToolbarItemType[] = [];\n  if (isListExtensionLoaded(editor, ExtensionBulletList.name)) {\n    items.push({\n      priority: 10,\n      component: markRaw(ToolbarSubItem),\n      props: {\n        editor,\n        isActive: editor.isActive(ExtensionBulletList.name),\n        icon: markRaw(MingcuteListCheckLine),\n        title: i18n.global.t(\"editor.common.bullet_list\"),\n        action: () => editor.chain().focus().toggleBulletList().run(),\n      },\n    });\n  }\n\n  if (isListExtensionLoaded(editor, ExtensionOrderedList.name)) {\n    items.push({\n      priority: 20,\n      component: markRaw(ToolbarSubItem),\n      props: {\n        editor,\n        isActive: editor.isActive(ExtensionOrderedList.name),\n        icon: markRaw(MingcuteListOrderedLine),\n        title: i18n.global.t(\"editor.common.ordered_list\"),\n        action: () => editor.chain().focus().toggleOrderedList().run(),\n      },\n    });\n  }\n\n  if (isListExtensionLoaded(editor, ExtensionTaskList.name)) {\n    items.push({\n      priority: 30,\n      component: markRaw(ToolbarSubItem),\n      props: {\n        editor,\n        isActive: editor.isActive(ExtensionTaskList.name),\n        icon: markRaw(MingcuteListCheck3Line),\n        title: i18n.global.t(\"editor.common.task_list\"),\n        action: () => editor.chain().focus().toggleTaskList().run(),\n      },\n    });\n  }\n\n  return items;\n}\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/list-keymap/index.ts",
    "content": "import { Editor } from \"@tiptap/core\";\nimport {\n  listHelpers,\n  ListKeymap,\n  type ListKeymapOptions,\n} from \"@tiptap/extension-list\";\n\nexport type ExtensionListKeymapOptions = Partial<ListKeymapOptions>;\n\n/**\n *  Optimize the listKeymap extension until the issue with @tiptap/extension-list is resolved.\n *  https://github.com/ueberdosis/tiptap/issues/4395\n */\nexport const ExtensionListKeymap =\n  ListKeymap.extend<ExtensionListKeymapOptions>({\n    addKeyboardShortcuts() {\n      const backspaceHandle = (editor: Editor) => {\n        let handled = false;\n\n        if (!editor.state.selection.empty) {\n          return false;\n        }\n\n        this.options.listTypes?.forEach(\n          ({\n            itemName,\n            wrapperNames,\n          }: {\n            itemName: string;\n            wrapperNames: string[];\n          }) => {\n            if (listHelpers.handleBackspace(editor, itemName, wrapperNames)) {\n              handled = true;\n            }\n          }\n        );\n\n        return handled;\n      };\n\n      return {\n        Backspace: ({ editor }: { editor: Editor }) => backspaceHandle(editor),\n\n        \"Mod-Backspace\": ({ editor }: { editor: Editor }) =>\n          backspaceHandle(editor),\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/node-selected/index.ts",
    "content": "import {\n  Decoration,\n  DecorationSet,\n  Extension,\n  Plugin,\n  PluginKey,\n} from \"@/tiptap\";\n\nexport interface ExtensionNodeSelectedOptions {\n  className: string;\n}\n\nexport const ExtensionNodeSelected =\n  Extension.create<ExtensionNodeSelectedOptions>({\n    name: \"nodeSelected\",\n\n    addOptions() {\n      return {\n        className: \"has-node-selected\",\n      };\n    },\n\n    addProseMirrorPlugins() {\n      return [\n        new Plugin({\n          key: new PluginKey(\"nodeSelectedByAttr\"),\n          props: {\n            decorations: ({ doc }) => {\n              const { isEditable, isFocused } = this.editor;\n              const decorations: Decoration[] = [];\n\n              if (!isEditable || !isFocused) {\n                return DecorationSet.create(doc, []);\n              }\n\n              doc.descendants((node, pos) => {\n                if (node.isText) {\n                  return false;\n                }\n\n                const isSelected = node.attrs.selected;\n                if (!isSelected) {\n                  return false;\n                }\n\n                decorations.push(\n                  Decoration.node(pos, pos + node.nodeSize, {\n                    class: this.options.className,\n                  })\n                );\n              });\n\n              return DecorationSet.create(doc, decorations);\n            },\n          },\n        }),\n      ];\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/ordered-list/index.ts",
    "content": "import {\n  ListItem,\n  OrderedList as TiptapOrderedList,\n  type OrderedListOptions,\n} from \"@tiptap/extension-list\";\nimport { markRaw } from \"vue\";\nimport MingcuteListOrderedLine from \"~icons/mingcute/list-ordered-line\";\nimport type { Editor, Range } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionOrderedListOptions = Partial<OrderedListOptions> &\n  ExtensionOptions;\n\nexport const ExtensionOrderedList =\n  TiptapOrderedList.extend<ExtensionOrderedListOptions>({\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getCommandMenuItems() {\n          return {\n            priority: 140,\n            icon: markRaw(MingcuteListOrderedLine),\n            title: \"editor.common.ordered_list\",\n            keywords: [\"orderedlist\", \"youxuliebiao\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor\n                .chain()\n                .focus()\n                .deleteRange(range)\n                .toggleOrderedList()\n                .run();\n            },\n          };\n        },\n      };\n    },\n    addExtensions() {\n      return [ListItem];\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/paragraph/index.ts",
    "content": "import TiptapParagraph, {\n  type ParagraphOptions,\n} from \"@tiptap/extension-paragraph\";\nimport { markRaw } from \"vue\";\nimport MingcuteLineHeightLine from \"~icons/mingcute/line-height-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport ToolbarSubItem from \"@/components/toolbar/ToolbarSubItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  EditorState,\n  ResolvedPos,\n  TextSelection,\n  isActive,\n  type Dispatch,\n} from \"@/tiptap\";\nimport type { ExtensionOptions, ToolbarItemType } from \"@/types\";\nimport { deleteNodeByPos } from \"@/utils\";\nimport { isListActive } from \"@/utils/is-list-active\";\n\nexport type ExtensionParagraphOptions = ExtensionOptions &\n  Partial<ParagraphOptions>;\n\nexport const ExtensionParagraph =\n  TiptapParagraph.extend<ExtensionParagraphOptions>({\n    addAttributes() {\n      return {\n        lineHeight: {\n          default: null,\n          parseHTML: (element) => {\n            return element.style.lineHeight;\n          },\n          renderHTML: (attributes) => {\n            const lineHeight = attributes.lineHeight;\n            if (!lineHeight) {\n              return {};\n            }\n            return {\n              style: `line-height: ${lineHeight}`,\n            };\n          },\n        },\n      };\n    },\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getToolbarItems({ editor }: { editor: Editor }): ToolbarItemType {\n          return {\n            priority: 220,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: !!editor.getAttributes(ExtensionParagraph.name)\n                ?.lineHeight,\n              icon: markRaw(MingcuteLineHeightLine),\n              title: i18n.global.t(\"editor.common.line_height\"),\n            },\n            children: [0, 1, 1.5, 2, 2.5, 3].map((lineHeight) => {\n              return {\n                priority: lineHeight,\n                component: markRaw(ToolbarSubItem),\n                props: {\n                  editor,\n                  isActive:\n                    editor.getAttributes(ExtensionParagraph.name)\n                      ?.lineHeight === lineHeight,\n                  title: !lineHeight\n                    ? i18n.global.t(\"editor.common.text.default\")\n                    : String(lineHeight),\n                  action: () =>\n                    editor\n                      .chain()\n                      .focus()\n                      .updateAttributes(ExtensionParagraph.name, {\n                        lineHeight,\n                      })\n                      .run(),\n                },\n              };\n            }),\n          };\n        },\n      };\n    },\n\n    addKeyboardShortcuts() {\n      return {\n        Backspace: ({ editor }: { editor: Editor }) => {\n          const { state, view } = editor;\n          const { selection } = state;\n          if (\n            isListActive(editor) ||\n            !isActive(state, ExtensionParagraph.name)\n          ) {\n            return false;\n          }\n\n          if (!(selection instanceof TextSelection) || !selection.empty) {\n            return false;\n          }\n\n          const { $from } = selection;\n\n          if ($from.parentOffset !== 0) {\n            return false;\n          }\n\n          const beforePos = $from.before($from.depth);\n          if (beforePos === 0) {\n            return true;\n          }\n\n          return handleDeletePreviousNode(\n            $from,\n            beforePos,\n            state,\n            view.dispatch\n          );\n        },\n      };\n    },\n  });\n\nexport function deleteCurrentNodeAndSetSelection(\n  $from: ResolvedPos,\n  beforePos: number,\n  state: EditorState,\n  dispatch: Dispatch\n) {\n  const { tr } = state;\n  if (deleteNodeByPos($from)(tr) && dispatch) {\n    if (beforePos !== 0) {\n      tr.setSelection(TextSelection.near(tr.doc.resolve(beforePos - 1), -1));\n    }\n    dispatch(tr);\n    return true;\n  }\n  return false;\n}\n\nexport function handleDeletePreviousNode(\n  $from: ResolvedPos,\n  beforePos: number,\n  state: EditorState,\n  dispatch: Dispatch\n) {\n  const { tr } = state;\n  if (!dispatch) {\n    return false;\n  }\n\n  const $beforePos = $from.doc.resolve(beforePos);\n  const nodeBefore = $beforePos.nodeBefore;\n\n  if (\n    !nodeBefore ||\n    !nodeBefore.type.isBlock ||\n    nodeBefore.type.isText ||\n    nodeBefore.type.name === ExtensionParagraph.name\n  ) {\n    return false;\n  }\n\n  const allowGapCursor = nodeBefore.type.spec.allowGapCursor;\n  if (!allowGapCursor) {\n    return false;\n  }\n\n  if (deleteNodeByPos($from.doc.resolve(beforePos - 1))(tr)) {\n    dispatch(tr);\n    return true;\n  }\n  return false;\n}\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/placeholder/index.ts",
    "content": "import { Placeholder } from \"@tiptap/extensions\";\nimport { i18n } from \"@/locales\";\n\nexport const ExtensionPlaceholder = Placeholder.configure({\n  placeholder: i18n.global.t(\"editor.extensions.commands_menu.placeholder\"),\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/range-selection/index.ts",
    "content": "import {\n  Decoration,\n  DecorationSet,\n  EditorView,\n  Extension,\n  Plugin,\n  PluginKey,\n  callOrReturn,\n  getExtensionField,\n  type ParentConfig,\n} from \"@/tiptap\";\nimport RangeSelection from \"./range-selection\";\n\ndeclare module \"@tiptap/core\" {\n  export interface NodeConfig<Options, Storage> {\n    /**\n     * Whether to allow displaying a fake selection state on the node.\n     *\n     * Typically, it is only necessary to display a fake selection state on child nodes,\n     * so the parent node can be set to false.\n     *\n     * default: true\n     */\n    fakeSelection?:\n      | boolean\n      | null\n      | ((this: {\n          name: string;\n          options: Options;\n          storage: Storage;\n          parent: ParentConfig<NodeConfig<Options>>[\"fakeSelection\"];\n        }) => boolean | null);\n  }\n}\n\nconst range = {\n  anchor: 0,\n  head: 0,\n  enable: false,\n};\n\nexport const ExtensionRangeSelection = Extension.create({\n  priority: 100,\n  name: \"rangeSelectionExtension\",\n\n  addProseMirrorPlugins() {\n    return [\n      new Plugin({\n        key: new PluginKey(\"rangeSelectionPlugin\"),\n        props: {\n          decorations: ({ doc, selection }) => {\n            const { isEditable, isFocused } = this.editor;\n            if (!isEditable || !isFocused) {\n              return null;\n            }\n            if (!(selection instanceof RangeSelection)) {\n              return null;\n            }\n            const { $from, $to } = selection;\n            const decorations: Decoration[] = [];\n            doc.nodesBetween($from.pos, $to.pos, (node, pos) => {\n              if (node.isText || node.type.name === \"paragraph\") {\n                return;\n              }\n              if (node.type.spec.fakeSelection) {\n                decorations.push(\n                  Decoration.node(pos, pos + node.nodeSize, {\n                    class: \"no-selection range-fake-selection\",\n                  })\n                );\n              }\n            });\n            return DecorationSet.create(doc, decorations);\n          },\n\n          createSelectionBetween: (view, anchor, head) => {\n            if (anchor.pos === head.pos) {\n              return null;\n            }\n            return RangeSelection.valid(view.state, anchor.pos, head.pos)\n              ? new RangeSelection(anchor, head)\n              : null;\n          },\n          handleDOMEvents: {\n            mousedown: (view: EditorView, event) => {\n              const coords = { left: event.clientX, top: event.clientY };\n              const $pos = view.posAtCoords(coords);\n              if (!$pos || !$pos.pos) {\n                return;\n              }\n              range.enable = true;\n              range.anchor = $pos.pos;\n            },\n            mousemove: (view, event) => {\n              if (!range.enable) {\n                return;\n              }\n              const coords = { left: event.clientX, top: event.clientY };\n              const $pos = view.posAtCoords(coords);\n              if (\n                !$pos ||\n                !$pos.pos ||\n                $pos.pos === range.anchor ||\n                $pos.pos === range.head\n              ) {\n                return;\n              }\n              range.head = $pos.pos;\n              const selection = RangeSelection.between(\n                view.state.doc.resolve(range.anchor),\n                view.state.doc.resolve(range.head)\n              );\n              if (selection) {\n                view.dispatch(view.state.tr.setSelection(selection));\n              }\n            },\n            mouseup: () => {\n              range.enable = false;\n              range.anchor = 0;\n              range.head = 0;\n            },\n            mouseleave: () => {\n              range.enable = false;\n              range.anchor = 0;\n              range.head = 0;\n            },\n          },\n        },\n      }),\n    ];\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      \"Mod-a\": ({ editor }) => {\n        editor.view.dispatch(\n          editor.view.state.tr.setSelection(\n            RangeSelection.allRange(editor.view.state.doc)\n          )\n        );\n        return true;\n      },\n    };\n  },\n\n  extendNodeSchema(extension) {\n    const context = {\n      name: extension.name,\n      options: extension.options,\n      storage: extension.storage,\n    };\n\n    return {\n      fakeSelection:\n        callOrReturn(getExtensionField(extension, \"fakeSelection\", context)) ??\n        false,\n    };\n  },\n});\n\nexport { RangeSelection };\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/range-selection/range-selection.ts",
    "content": "import {\n  EditorState,\n  Node,\n  ResolvedPos,\n  Selection,\n  type Mappable,\n} from \"@/tiptap/pm\";\n\n/**\n * The RangeSelection class represents a selection range within a document.\n * The content can include text, paragraphs, block-level content, etc.\n *\n * It has a starting position and an ending position. When the given range includes block-level content,\n * the RangeSelection will automatically expand to include the block-level content at the corresponding depth.\n *\n * The RangeSelection must not contain empty content.\n */\nclass RangeSelection extends Selection {\n  /**\n   * Creates a RangeSelection between the specified positions.\n   *\n   * @param $anchor - The starting position of the selection.\n   * @param $head - The ending position of the selection.\n   */\n  constructor($anchor: ResolvedPos, $head: ResolvedPos) {\n    checkRangeSelection($anchor, $head);\n    super($anchor, $head);\n  }\n\n  map(doc: Node, mapping: Mappable): Selection {\n    const $head = doc.resolve(mapping.map(this.head));\n    const $anchor = doc.resolve(mapping.map(this.anchor));\n    return new RangeSelection($anchor, $head);\n  }\n\n  eq(other: Selection): boolean {\n    return (\n      other instanceof RangeSelection &&\n      other.anchor == this.anchor &&\n      other.head == this.head\n    );\n  }\n\n  getBookmark() {\n    return new RangeBookmark(this.anchor, this.head);\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  toJSON(): any {\n    return { type: \"range\", anchor: this.anchor, head: this.head };\n  }\n\n  /**\n   * Validates if the given positions can form a valid RangeSelection in the given state.\n   *\n   * @param state - The editor state.\n   * @param anchor - The starting position.\n   * @param head - The ending position.\n   * @returns True if the positions form a valid RangeSelection, otherwise false.\n   */\n  static valid(state: EditorState, anchor: number, head: number) {\n    const nodes = rangeNodesBetween(\n      state.doc.resolve(anchor),\n      state.doc.resolve(head)\n    );\n\n    if (nodes.length === 0) {\n      return false;\n    }\n\n    if (nodes.reverse()[0].pos < 0) {\n      return false;\n    }\n\n    return true;\n  }\n\n  /**\n   * Returns a RangeSelection spanning the given positions.\n   *\n   * When the given range includes block-level content, if only a part is included,\n   * the selection will be expanded to encompass the block-level content at the corresponding depth.\n   *\n   * Expansion: If the selection includes all depth nodes of the current block-level content but not the entire last node,\n   * the selection will be expanded to include the node at that depth.\n   *\n   * @param $anchor - The starting position of the selection.\n   * @param $head - The ending position of the selection.\n   * @returns A new RangeSelection that spans the given positions.\n   */\n  static between($anchor: ResolvedPos, $head: ResolvedPos) {\n    checkRangeSelection($anchor, $head);\n\n    const doc = $anchor.doc;\n    const dir = $anchor.pos < $head.pos ? 1 : -1;\n    const anchorPos = dir > 0 ? $anchor.pos : $head.pos;\n    const headPos = dir > 0 ? $head.pos : $anchor.pos;\n    const nodes = rangeNodesBetween($anchor, $head);\n\n    if (nodes.length === 0) {\n      return null;\n    }\n\n    const lastNode = nodes[nodes.length - 1];\n    if (lastNode.pos < 0) {\n      return null;\n    }\n\n    let fromOffset = 0;\n    nodes.forEach(({ pos }) => {\n      if (pos < 0) {\n        fromOffset = pos;\n      }\n    });\n\n    const toOffset =\n      headPos - anchorPos - lastNode.pos - lastNode.node.nodeSize;\n    const anchor =\n      dir > 0\n        ? anchorPos + fromOffset\n        : headPos - (toOffset > 0 ? 0 : toOffset);\n    const head =\n      dir > 0\n        ? headPos - (toOffset > 0 ? 0 : toOffset)\n        : anchorPos + fromOffset;\n    return new RangeSelection(doc.resolve(anchor), doc.resolve(head));\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  static fromJSON(doc: Node, json: any) {\n    if (typeof json.anchor != \"number\" || typeof json.head != \"number\") {\n      throw new RangeError(\"Invalid input for RangeSelection.fromJSON\");\n    }\n\n    return new RangeSelection(doc.resolve(json.anchor), doc.resolve(json.head));\n  }\n\n  static create(doc: Node, anchor: number, head: number) {\n    return new this(doc.resolve(anchor), doc.resolve(head));\n  }\n\n  static allRange(doc: Node) {\n    return new RangeSelection(doc.resolve(0), doc.resolve(doc.content.size));\n  }\n}\n\nSelection.jsonID(\"range\", RangeSelection);\n\nclass RangeBookmark {\n  constructor(\n    readonly anchor: number,\n    readonly head: number\n  ) {}\n\n  map(mapping: Mappable) {\n    return new RangeBookmark(mapping.map(this.anchor), mapping.map(this.head));\n  }\n  resolve(doc: Node) {\n    return new RangeSelection(doc.resolve(this.anchor), doc.resolve(this.head));\n  }\n}\n\nexport function checkRangeSelection($anchor: ResolvedPos, $head: ResolvedPos) {\n  if ($anchor.pos === $head.pos) {\n    console.warn(\"The RangeSelection cannot be empty.\");\n  }\n}\n\nexport function rangeNodesBetween($anchor: ResolvedPos, $head: ResolvedPos) {\n  const doc = $anchor.doc;\n  const dir = $anchor.pos < $head.pos ? 1 : -1;\n  const anchorPos = dir > 0 ? $anchor.pos : $head.pos;\n  const headPos = dir > 0 ? $head.pos : $anchor.pos;\n\n  const nodes: Array<{\n    node: Node;\n    pos: number;\n    parent: Node | null;\n    index: number;\n  }> = [];\n  doc.nodesBetween(\n    anchorPos,\n    headPos,\n    (node, pos, parent, index) => {\n      if (node.isText || node.type.name === \"paragraph\") {\n        return true;\n      }\n      nodes.push({ node, pos, parent, index });\n    },\n    -anchorPos\n  );\n  return nodes;\n}\n\nexport default RangeSelection;\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/search-and-replace/IconButton.vue",
    "content": "<script lang=\"ts\" setup>\ndefineProps<{\n  disabled?: boolean;\n}>();\n</script>\n<template>\n  <button\n    type=\"button\"\n    class=\"rounded p-1\"\n    :class=\"{\n      'text-gray-600 opacity-100 hover:bg-gray-100 hover:text-gray-900 active:bg-gray-200':\n        !disabled,\n      'opacity-50': disabled,\n    }\"\n    :disabled=\"disabled\"\n  >\n    <slot />\n  </button>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/search-and-replace/MatchToggleButton.vue",
    "content": "<script lang=\"ts\" setup>\ndefineProps<{\n  isActive: boolean;\n}>();\n</script>\n<template>\n  <button\n    type=\"button\"\n    class=\"rounded p-0.5 transition-all\"\n    :class=\"{\n      'text-gray-600 hover:bg-gray-100 hover:text-gray-900 active:bg-gray-200':\n        !isActive,\n      'bg-primary/20 text-gray-900 outline outline-1 outline-primary/50 hover:bg-primary/30':\n        isActive,\n    }\"\n  >\n    <slot />\n  </button>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/search-and-replace/SearchAndReplace.vue",
    "content": "<script setup lang=\"ts\" name=\"BubbleMenu\">\nimport { vTooltip } from \"@halo-dev/components\";\nimport { computed, nextTick, ref, watch, type PropType } from \"vue\";\nimport LucideReplace from \"~icons/lucide/replace\";\nimport LucideReplaceAll from \"~icons/lucide/replace-all\";\nimport MdiFormatLetterCase from \"~icons/mdi/format-letter-case\";\nimport MdiFormatLetterMatches from \"~icons/mdi/format-letter-matches\";\nimport MdiRegex from \"~icons/mdi/regex\";\nimport MingcuteArrowDownLine from \"~icons/mingcute/arrow-down-line\";\nimport MingcuteArrowUpLine from \"~icons/mingcute/arrow-up-line\";\nimport MingcuteCloseLine from \"~icons/mingcute/close-line\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport { PluginKey } from \"@/tiptap/pm\";\nimport IconButton from \"./IconButton.vue\";\nimport MatchToggleButton from \"./MatchToggleButton.vue\";\nimport type { SearchAndReplacePluginState } from \"./SearchAndReplacePlugin\";\n\nconst props = defineProps({\n  editor: {\n    type: Object as PropType<Editor>,\n    required: true,\n  },\n  pluginKey: {\n    type: Object as PropType<PluginKey<SearchAndReplacePluginState>>,\n    required: true,\n  },\n  visible: {\n    type: Boolean,\n    default: false,\n  },\n});\n\nconst searchTerm = ref<string>(\"\");\nconst replaceTerm = ref<string>(\"\");\nconst regex = ref<boolean>(false);\nconst caseSensitive = ref<boolean>(false);\nconst matchWord = ref<boolean>(false);\nconst flag = ref<boolean>(false);\n\nconst findState = computed(() => {\n  void flag.value;\n  const { editor, pluginKey } = props;\n  if (!editor || !pluginKey) {\n    return {\n      findIndex: 0,\n      findCount: 0,\n    };\n  }\n  const state = pluginKey.getState(editor.state);\n  return {\n    findIndex: state?.findIndex || 0,\n    findCount: state?.findCount || 0,\n  };\n});\n\nconst findNextSearchResult = () => {\n  props.editor.commands.findNext();\n};\n\nconst findPreviousSearchResult = () => {\n  props.editor.commands.findPrevious();\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst updateSearchReplace = (value: any) => {\n  const { editor, pluginKey } = props;\n  if (!editor || !pluginKey) {\n    return;\n  }\n  const tr = editor.state.tr;\n  tr.setMeta(pluginKey, value);\n  editor.view.dispatch(tr);\n  flag.value = !flag.value;\n};\n\nconst replace = () => {\n  props.editor.commands.replace();\n  flag.value = !flag.value;\n};\n\nconst replaceAll = () => {\n  props.editor.commands.replaceAll();\n  flag.value = !flag.value;\n};\n\nconst handleCloseSearch = () => {\n  props.editor.commands.closeSearch();\n};\n\nwatch(\n  () => searchTerm.value.trim(),\n  (val, oldVal) => {\n    if (val !== oldVal) {\n      updateSearchReplace({\n        setSearchTerm: val,\n      });\n    }\n  }\n);\n\nwatch(\n  () => replaceTerm.value.trim(),\n  (val, oldVal) => {\n    if (val !== oldVal) {\n      updateSearchReplace({\n        setReplaceTerm: val,\n      });\n    }\n  }\n);\n\nwatch(\n  () => regex.value,\n  (val, oldVal) => {\n    if (val !== oldVal) {\n      updateSearchReplace({\n        setRegex: val,\n      });\n    }\n  }\n);\n\nwatch(\n  () => caseSensitive.value,\n  (val, oldVal) => {\n    if (val !== oldVal) {\n      updateSearchReplace({\n        setCaseSensitive: val,\n      });\n    }\n  }\n);\n\nwatch(\n  () => matchWord.value,\n  (val, oldVal) => {\n    if (val !== oldVal) {\n      updateSearchReplace({\n        setMatchWord: val,\n      });\n    }\n  }\n);\n\nconst searchInput = ref<HTMLInputElement | null>(null);\n\nwatch(\n  () => props.visible,\n  (val) => {\n    if (val) {\n      nextTick(() => {\n        searchInput.value?.focus();\n      });\n    }\n  }\n);\n</script>\n<template>\n  <Transition v-show=\"visible\" appear name=\"slide\">\n    <div\n      class=\"absolute right-5 top-0 z-50 float-right flex min-w-[500px] justify-end rounded bg-white p-1 !pt-2 shadow\"\n      @keydown.escape.prevent=\"handleCloseSearch\"\n    >\n      <section class=\"flex w-full flex-col gap-2\">\n        <div class=\"relative flex items-center\">\n          <div class=\"relative w-full max-w-[55%]\">\n            <input\n              ref=\"searchInput\"\n              v-model=\"searchTerm\"\n              type=\"text\"\n              class=\"block size-full h-9 rounded-md bg-white px-3 text-sm text-gray-900 ring-1 ring-gray-100 transition-all placeholder:text-gray-400 focus:!ring-1 focus:!ring-primary\"\n              :placeholder=\"\n                i18n.global.t(\n                  'editor.extensions.search_and_replace.search_placeholder'\n                )\n              \"\n              tabindex=\"2\"\n              @keydown.enter.prevent=\"findNextSearchResult\"\n            />\n            <div class=\"absolute inset-y-0 end-0 flex items-center gap-1 pr-1\">\n              <MatchToggleButton\n                v-tooltip=\"\n                  i18n.global.t(\n                    'editor.extensions.search_and_replace.case_sensitive'\n                  )\n                \"\n                :is-active=\"caseSensitive\"\n                @click=\"caseSensitive = !caseSensitive\"\n              >\n                <MdiFormatLetterCase />\n              </MatchToggleButton>\n              <MatchToggleButton\n                v-tooltip=\"\n                  i18n.global.t(\n                    'editor.extensions.search_and_replace.match_word'\n                  )\n                \"\n                :is-active=\"matchWord\"\n                @click=\"matchWord = !matchWord\"\n              >\n                <MdiFormatLetterMatches />\n              </MatchToggleButton>\n              <MatchToggleButton\n                v-tooltip=\"\n                  i18n.global.t(\n                    'editor.extensions.search_and_replace.use_regex'\n                  )\n                \"\n                :is-active=\"regex\"\n                @click=\"regex = !regex\"\n              >\n                <MdiRegex />\n              </MatchToggleButton>\n            </div>\n          </div>\n          <div class=\"mx-2 min-w-[130px] text-sm\">\n            <span\n              v-if=\"findState.findCount === 0\"\n              :class=\"{ 'text-red-600': searchTerm.length > 0 }\"\n            >\n              {{\n                i18n.global.t(\"editor.extensions.search_and_replace.not_found\")\n              }}\n            </span>\n            <span v-else>\n              {{\n                i18n.global.t(\n                  \"editor.extensions.search_and_replace.occurrence_found\",\n                  {\n                    index: findState.findIndex + 1,\n                    total: findState.findCount,\n                  }\n                )\n              }}\n            </span>\n          </div>\n          <div class=\"absolute right-0 flex h-full items-center\">\n            <IconButton\n              v-tooltip=\"\n                i18n.global.t(\n                  'editor.extensions.search_and_replace.find_previous'\n                )\n              \"\n              :disabled=\"findState.findCount === 0\"\n              @click=\"findPreviousSearchResult\"\n            >\n              <MingcuteArrowUpLine />\n            </IconButton>\n            <IconButton\n              v-tooltip=\"\n                i18n.global.t('editor.extensions.search_and_replace.find_next')\n              \"\n              :disabled=\"findState.findCount === 0\"\n              @click=\"findNextSearchResult\"\n            >\n              <MingcuteArrowDownLine />\n            </IconButton>\n            <IconButton\n              v-tooltip=\"\n                i18n.global.t('editor.extensions.search_and_replace.close')\n              \"\n              @click=\"handleCloseSearch\"\n            >\n              <MingcuteCloseLine />\n            </IconButton>\n          </div>\n        </div>\n\n        <div class=\"flex items-center\">\n          <div class=\"relative w-full max-w-[55%]\">\n            <input\n              v-model=\"replaceTerm\"\n              type=\"text\"\n              class=\"block size-full h-9 rounded-md bg-white px-3 text-sm text-gray-900 ring-1 ring-gray-100 transition-all placeholder:text-gray-400 focus:!ring-1 focus:!ring-primary\"\n              :placeholder=\"\n                i18n.global.t(\n                  'editor.extensions.search_and_replace.replace_placeholder'\n                )\n              \"\n              tabindex=\"2\"\n              @keydown.enter.prevent=\"replace\"\n            />\n          </div>\n          <div class=\"mx-2 flex items-center gap-2\">\n            <IconButton\n              v-tooltip=\"\n                i18n.global.t('editor.extensions.search_and_replace.replace')\n              \"\n              :disabled=\"findState.findCount === 0\"\n              @click=\"replace\"\n            >\n              <LucideReplace />\n            </IconButton>\n\n            <IconButton\n              v-tooltip=\"\n                i18n.global.t(\n                  'editor.extensions.search_and_replace.replace_all'\n                )\n              \"\n              :disabled=\"findState.findCount === 0\"\n              @click=\"replaceAll\"\n            >\n              <LucideReplaceAll />\n            </IconButton>\n          </div>\n        </div>\n      </section>\n    </div>\n  </Transition>\n</template>\n<style>\n.slide-enter-active,\n.slide-leave-active {\n  transition: transform 0.25s;\n}\n\n.slide-enter-from,\n.slide-leave-to {\n  transform: translateY(-100%);\n}\n\n.slide-enter-to,\n.slide-leave-from {\n  transform: translateY(0);\n}\n</style>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/search-and-replace/SearchAndReplacePlugin.ts",
    "content": "import scrollIntoView from \"scroll-into-view-if-needed\";\nimport type { PMNode } from \"@/tiptap\";\nimport {\n  Decoration,\n  DecorationSet,\n  EditorView,\n  Plugin,\n  PluginKey,\n  Selection,\n  Transaction,\n} from \"@/tiptap/pm\";\nimport { Editor } from \"@/tiptap/vue-3\";\nexport interface SearchAndReplacePluginProps {\n  editor: Editor;\n  element: HTMLElement;\n  searchResultClass?: string;\n  findSearchClass?: string;\n}\n\nexport const searchAndReplacePluginKey =\n  new PluginKey<SearchAndReplacePluginState>(\"searchAndReplace\");\n\nexport type SearchAndReplacePluginViewProps = SearchAndReplacePluginProps & {\n  view: EditorView;\n};\n\nexport class SearchAndReplacePluginView {\n  public editor: Editor;\n\n  public view: EditorView;\n\n  public containerElement: HTMLElement;\n\n  public init: boolean;\n\n  constructor({ view, editor, element }: SearchAndReplacePluginViewProps) {\n    this.editor = editor;\n    this.view = view;\n    this.containerElement = element;\n    this.init = false;\n  }\n\n  update() {\n    const headerParentElement = this.findEditorEntryElement();\n    if (!this.init && headerParentElement) {\n      headerParentElement.insertAdjacentElement(\n        \"afterbegin\",\n        this.containerElement\n      );\n      this.init = true;\n    }\n    return false;\n  }\n\n  destroy() {\n    return false;\n  }\n\n  private findEditorEntryElement() {\n    const editorElement = this.editor.options.element as HTMLElement;\n    let currentElement = editorElement;\n    while (currentElement) {\n      if (currentElement.classList.contains(\"editor-main\")) {\n        return currentElement;\n      }\n\n      if (!currentElement.parentElement) {\n        break;\n      }\n      currentElement = currentElement.parentElement;\n    }\n    return null;\n  }\n}\n\nexport interface TextNodesWithPosition {\n  text: string;\n  pos: number;\n  index: number;\n}\n\nexport interface SearchResultWithPosition {\n  pos: number;\n  index: number;\n  from: number;\n  to: number;\n}\n\nexport class SearchAndReplacePluginState {\n  private _findIndex: number;\n  public editor: Editor;\n  public enable: boolean;\n  // Whether it is necessary to reset the findIndex based on the cursor position.\n  public findIndexFlag: boolean;\n  public findCount: number;\n  public searchTerm: string;\n  public replaceTerm: string;\n  public regex: boolean;\n  public caseSensitive: boolean;\n  public wholeWord: boolean;\n  public results: SearchResultWithPosition[] = [];\n  public searchResultDecorations: Decoration[] = [];\n  public findIndexDecoration: Decoration | undefined;\n\n  constructor({\n    editor,\n    enable,\n    regex,\n    caseSensitive,\n    wholeWord,\n  }: {\n    editor: Editor;\n    enable?: boolean;\n    regex?: boolean;\n    caseSensitive?: boolean;\n    wholeWord?: boolean;\n  }) {\n    this.editor = editor;\n    this.enable = enable || false;\n    this.searchTerm = \"\";\n    this.replaceTerm = \"\";\n    this.regex = regex || false;\n    this.caseSensitive = caseSensitive || false;\n    this.wholeWord = wholeWord || false;\n    this._findIndex = 0;\n    this.findCount = 0;\n    this.searchResultDecorations = [];\n    this.findIndexDecoration = undefined;\n    this.results = [];\n    this.findIndexFlag = true;\n  }\n\n  get findIndex() {\n    return this._findIndex;\n  }\n\n  set findIndex(newValue) {\n    this._findIndex = this.verifySetIndex(newValue);\n  }\n\n  apply(tr: Transaction): SearchAndReplacePluginState {\n    const action = tr.getMeta(searchAndReplacePluginKey);\n\n    if (action && \"setEnable\" in action) {\n      if (action.setEnable && !this.enable) {\n        action.setSearchTerm = this.searchTerm;\n      }\n      this.enable = action.setEnable;\n    }\n\n    if (!this.enable) {\n      return this;\n    }\n\n    // The refresh method needs to be called before setFindIndex\n    // Because setFindIndex depends on the refreshed results\n    if (action && action.refresh) {\n      this.processSearches(tr);\n    }\n\n    if (action && \"setReplaceTerm\" in action) {\n      this.replaceTerm = action.setReplaceTerm;\n    }\n\n    if (action && \"setFindIndex\" in action) {\n      const { setFindIndex } = action;\n      this.findIndex = setFindIndex;\n      this.processFindIndexDecoration();\n    }\n\n    if (action && \"setScrollView\") {\n      this.scrollIntoFindIndexView();\n    }\n\n    if (action && \"setRegex\" in action) {\n      if (this.regex !== action.setRegex) {\n        this.regex = action.setRegex;\n        action.setSearchTerm = this.searchTerm;\n      }\n    }\n\n    if (action && \"setWholeWord\" in action) {\n      if (this.wholeWord !== action.setWholeWord) {\n        this.wholeWord = action.setWholeWord;\n        action.setSearchTerm = this.searchTerm;\n      }\n    }\n\n    if (action && \"setCaseSensitive\" in action) {\n      if (this.caseSensitive !== action.setCaseSensitive) {\n        this.caseSensitive = action.setCaseSensitive;\n        action.setSearchTerm = this.searchTerm;\n      }\n    }\n\n    if (action && \"setSearchTerm\" in action) {\n      this.searchTerm = action.setSearchTerm;\n      this.findIndexFlag = true;\n      // If the searchTerm is modified or replaced, perform a new\n      // search throughout the entire document.\n      this.processSearches(tr);\n      this.scrollIntoFindIndexView();\n      return this;\n    }\n\n    if (tr.docChanged) {\n      return this.processSearches(tr);\n    } else if (tr.getMeta(\"pointer\")) {\n      this.getNearestResultBySelection(tr.selection);\n      this.processFindIndexDecoration();\n    }\n\n    return this;\n  }\n\n  scrollIntoFindIndexView() {\n    const { results, editor, _findIndex } = this;\n    if (results.length > _findIndex && _findIndex >= 0) {\n      const result = results[_findIndex];\n      if (result) {\n        const { pos } = result;\n        const { view } = editor;\n        let node = view.nodeDOM(pos - 1);\n        if (!(node instanceof HTMLElement)) {\n          node = view.domAtPos(pos, 0).node;\n        }\n        if (node instanceof HTMLElement) {\n          scrollIntoView(node, {\n            behavior: \"smooth\",\n            scrollMode: \"if-needed\",\n          });\n        }\n      }\n    }\n  }\n\n  /**\n   * Validate if findIndex is within the range\n   * If results.length === 0, take 0\n   * If less than or equal to -1, take results.length - 1\n   * If greater than results.length - 1, take 0\n   *\n   * @param index new findIndex\n   * @returns validated findIndex\n   */\n  verifySetIndex(index: number) {\n    const { results } = this;\n    if (results.length === 0) {\n      return 0;\n    } else if (index <= -1) {\n      return results.length - 1;\n    } else if (index > results.length - 1) {\n      return 0;\n    } else {\n      return index;\n    }\n  }\n\n  /**\n   * Execute full-text search functionality.\n   *\n   * @param Transaction\n   * @returns\n   * @memberof SearchAndReplacePluginState\n   */\n  processSearches({\n    doc,\n    selection,\n  }: Transaction): SearchAndReplacePluginState {\n    const textNodesWithPosition = this.getFullText(doc);\n    const searchTerm = this.getRegex();\n    this.results.length = 0;\n    for (let i = 0; i < textNodesWithPosition.length; i += 1) {\n      const { text, pos, index } = textNodesWithPosition[i];\n\n      const matches = Array.from(text.matchAll(searchTerm)).filter(\n        ([matchText]) => matchText.trim()\n      );\n\n      for (let j = 0; j < matches.length; j += 1) {\n        const m = matches[j];\n\n        if (m[0] === \"\") {\n          break;\n        }\n\n        if (m.index !== undefined) {\n          this.results.push({\n            pos: pos,\n            index: index,\n            from: pos + m.index,\n            to: pos + m.index + m[0].length,\n          });\n        }\n      }\n    }\n\n    this.processResultDecorations();\n    if (this.findIndexFlag) {\n      this.getNearestResultBySelection(selection);\n      this.findIndexFlag = false;\n    }\n    this.processFindIndexDecoration();\n    return this;\n  }\n\n  /**\n   * Highlight the current result based on findIndex.\n   *\n   * @memberof SearchAndReplacePluginState\n   */\n  processFindIndexDecoration() {\n    const { results, findIndex } = this;\n    const result = results[findIndex];\n    if (result) {\n      this.findIndexDecoration = Decoration.inline(result.from, result.to, {\n        class: \"search-result-current\",\n      });\n    }\n  }\n\n  /**\n   * Generate highlighted results based on the 'results'.\n   *\n   * @memberof SearchAndReplacePluginState\n   */\n  processResultDecorations() {\n    const { results } = this;\n    this.findCount = results.length;\n    this.searchResultDecorations.length = 0;\n    for (let i = 0; i < results.length; i += 1) {\n      const result = results[i];\n      this.searchResultDecorations.push(\n        Decoration.inline(result.from, result.to, {\n          class: \"search-result\",\n        })\n      );\n    }\n  }\n\n  /**\n   * Reset findIndex based on the current cursor position.\n   *\n   * @param selection Current cursor position.\n   */\n  getNearestResultBySelection(selection: Selection) {\n    const { results } = this;\n    for (let i = 0; i < results.length; i += 1) {\n      const result = results[i];\n      if (selection && selection.to <= result.from) {\n        this.findIndex = i;\n        break;\n      }\n    }\n  }\n\n  /**\n   * Convert the entire text into flattened text with positions.\n   *\n   * @param doc The entire document\n   * @returns Flattened text with positions\n   */\n  getFullText(doc: PMNode): TextNodesWithPosition[] {\n    const textNodesWithPosition: TextNodesWithPosition[] = [];\n    doc.descendants((node, pos, _parent, index) => {\n      if (node.isText) {\n        textNodesWithPosition.push({\n          text: `${node.text}`,\n          pos,\n          index,\n        });\n      }\n    });\n    return textNodesWithPosition;\n  }\n\n  /**\n   * Get the regular expression object based on the current search term.\n   *\n   * @returns Regular expression object\n   */\n  getRegex = (): RegExp => {\n    const { searchTerm, regex, caseSensitive, wholeWord } = this;\n    let pattern = regex\n      ? searchTerm\n      : searchTerm.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n    if (wholeWord) {\n      pattern = `\\\\b${pattern}\\\\b`;\n    }\n    return new RegExp(pattern, caseSensitive ? \"gu\" : \"gui\");\n  };\n}\n\nexport const SearchAndReplacePlugin = (\n  options: SearchAndReplacePluginProps\n) => {\n  return new Plugin({\n    key: searchAndReplacePluginKey,\n    view: (view) => new SearchAndReplacePluginView({ view, ...options }),\n    state: {\n      init: () => {\n        return new SearchAndReplacePluginState({ ...options });\n      },\n      apply: (tr, prev) => {\n        return prev.apply(tr);\n      },\n    },\n    props: {\n      decorations: (state) => {\n        const searchAndReplaceState = searchAndReplacePluginKey.getState(state);\n        if (searchAndReplaceState) {\n          const { searchResultDecorations, findIndexDecoration, enable } =\n            searchAndReplaceState;\n          if (!enable) {\n            return DecorationSet.empty;\n          }\n          const decorations = [...searchResultDecorations];\n          if (findIndexDecoration) {\n            decorations.push(findIndexDecoration);\n          }\n          if (decorations.length > 0) {\n            return DecorationSet.create(state.doc, decorations);\n          }\n        }\n        return DecorationSet.empty;\n      },\n    },\n  });\n};\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/search-and-replace/index.ts",
    "content": "import { h, markRaw, render } from \"vue\";\nimport MingcuteListSearchLine from \"~icons/mingcute/list-search-line\";\nimport { ToolbarItem } from \"@/components\";\nimport { i18n } from \"@/locales\";\nimport { EditorState } from \"@/tiptap/pm\";\nimport { Editor, Extension } from \"@/tiptap/vue-3\";\nimport type { ExtensionOptions } from \"@/types\";\nimport SearchAndReplaceVue from \"./SearchAndReplace.vue\";\nimport {\n  SearchAndReplacePlugin,\n  searchAndReplacePluginKey,\n} from \"./SearchAndReplacePlugin\";\n\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    searchAndReplace: {\n      /**\n       * @description Replace first instance of search result with given replace term.\n       */\n      replace: () => ReturnType;\n      /**\n       * @description Replace all instances of search result with given replace term.\n       */\n      replaceAll: () => ReturnType;\n      /**\n       * @description Find next instance of search result.\n       */\n      findNext: () => ReturnType;\n      /**\n       * @description Find previous instance of search result.\n       */\n      findPrevious: () => ReturnType;\n      /**\n       * @description Open search panel.\n       */\n      openSearch: () => ReturnType;\n      /**\n       * @description Close search panel.\n       */\n      closeSearch: () => ReturnType;\n    };\n  }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst instance = h<any>(SearchAndReplaceVue);\nfunction isShowSearch() {\n  const searchAndReplaceInstance = instance.component;\n  if (searchAndReplaceInstance) {\n    return searchAndReplaceInstance.props.visible;\n  }\n  return false;\n}\n\nexport const ExtensionSearchAndReplace = Extension.create<ExtensionOptions>({\n  name: \"searchAndReplace\",\n\n  // @ts-ignore\n  addOptions() {\n    return {\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return [\n          {\n            priority: 230,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: isShowSearch(),\n              icon: markRaw(MingcuteListSearchLine),\n              title: i18n.global.t(\n                \"editor.extensions.search_and_replace.title\"\n              ),\n              action: () => {\n                const searchAndReplaceInstance = instance.component;\n                if (searchAndReplaceInstance) {\n                  const visible = searchAndReplaceInstance.props.visible;\n                  if (visible) {\n                    editor.commands.closeSearch();\n                  } else {\n                    editor.commands.openSearch();\n                  }\n                }\n              },\n            },\n          },\n        ];\n      },\n    };\n  },\n\n  addCommands() {\n    return {\n      replace:\n        () =>\n        ({\n          state,\n          dispatch,\n        }: {\n          state: EditorState;\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          dispatch: ((args?: any) => any) | undefined;\n        }) => {\n          const searchAndReplaceState =\n            searchAndReplacePluginKey.getState(state);\n          if (!searchAndReplaceState) {\n            return false;\n          }\n          const { replaceTerm, results, findIndex } = searchAndReplaceState;\n          const result = results[findIndex];\n          if (!result) {\n            return false;\n          }\n\n          const { from, to } = result;\n\n          if (dispatch) {\n            const tr = state.tr;\n            tr.insertText(replaceTerm, from, to);\n            tr.setMeta(searchAndReplacePluginKey, {\n              setFindIndex: findIndex,\n              refresh: true,\n            });\n            dispatch(tr);\n          }\n\n          return false;\n        },\n\n      replaceAll:\n        () =>\n        ({\n          state,\n          dispatch,\n        }: {\n          state: EditorState;\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          dispatch: ((args?: any) => any) | undefined;\n        }) => {\n          const searchAndReplaceState =\n            searchAndReplacePluginKey.getState(state);\n          if (!searchAndReplaceState) {\n            return false;\n          }\n          const { replaceTerm, results } = searchAndReplaceState;\n          const tr = state.tr;\n          let offset = 0;\n          results.forEach((result) => {\n            const { from, to } = result;\n            tr.insertText(replaceTerm, offset + from, offset + to);\n            // when performing multi-text replacement, it is necessary\n            // to calculate the offset between 'form' and 'to'.\n            offset = offset + replaceTerm.length - (to - from);\n          });\n\n          if (dispatch) {\n            dispatch(tr);\n          }\n          return false;\n        },\n\n      findNext:\n        () =>\n        ({\n          state,\n          dispatch,\n        }: {\n          state: EditorState;\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          dispatch: ((args?: any) => any) | undefined;\n        }) => {\n          if (dispatch) {\n            const tr = state.tr;\n            const searchAndReplaceState =\n              searchAndReplacePluginKey.getState(state);\n            if (!searchAndReplaceState) {\n              return false;\n            }\n            const { findIndex } = searchAndReplaceState;\n\n            tr.setMeta(searchAndReplacePluginKey, {\n              setFindIndex: findIndex + 1,\n            });\n            dispatch(tr);\n          }\n          return false;\n        },\n\n      findPrevious:\n        () =>\n        ({\n          state,\n          dispatch,\n        }: {\n          state: EditorState;\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          dispatch: ((args?: any) => any) | undefined;\n        }) => {\n          if (dispatch) {\n            const searchAndReplaceState =\n              searchAndReplacePluginKey.getState(state);\n            if (!searchAndReplaceState) {\n              return false;\n            }\n            const { findIndex } = searchAndReplaceState;\n            const tr = state.tr;\n            tr.setMeta(searchAndReplacePluginKey, {\n              setFindIndex: findIndex - 1,\n            });\n            dispatch(tr);\n          }\n          return false;\n        },\n\n      openSearch:\n        () =>\n        ({\n          state,\n          dispatch,\n        }: {\n          state: EditorState;\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          dispatch: ((args?: any) => any) | undefined;\n        }) => {\n          const searchAndReplaceState =\n            searchAndReplacePluginKey.getState(state);\n          if (!searchAndReplaceState) {\n            return false;\n          }\n          const searchAndReplaceInstance = instance.component;\n          if (searchAndReplaceInstance) {\n            searchAndReplaceInstance.props.visible = true;\n            const tr = state.tr;\n            tr.setMeta(searchAndReplacePluginKey, {\n              setEnable: true,\n            });\n            if (dispatch) {\n              dispatch(tr);\n            }\n          }\n          return false;\n        },\n\n      closeSearch:\n        () =>\n        ({\n          state,\n          dispatch,\n        }: {\n          state: EditorState;\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          dispatch: ((args?: any) => any) | undefined;\n        }) => {\n          const searchAndReplaceState =\n            searchAndReplacePluginKey.getState(state);\n          if (!searchAndReplaceState) {\n            return false;\n          }\n          const searchAndReplaceInstance = instance.component;\n          if (searchAndReplaceInstance) {\n            searchAndReplaceInstance.props.visible = false;\n            const tr = state.tr;\n            tr.setMeta(searchAndReplacePluginKey, {\n              setEnable: false,\n            });\n            if (dispatch) {\n              dispatch(tr);\n            }\n          }\n          return false;\n        },\n    };\n  },\n\n  addProseMirrorPlugins() {\n    const containerDom = document.createElement(\"div\");\n    containerDom.style.position = \"sticky\";\n    containerDom.style.top = \"0\";\n    containerDom.style.zIndex = \"50\";\n    instance.props = {\n      editor: this.editor,\n      pluginKey: searchAndReplacePluginKey,\n      visible: false,\n    };\n    render(instance, containerDom);\n    return [\n      SearchAndReplacePlugin({\n        editor: this.editor as Editor,\n        element: containerDom,\n      }),\n    ];\n  },\n\n  addKeyboardShortcuts() {\n    return {\n      \"Mod-f\": () => {\n        this.editor.commands.openSearch();\n        return true;\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/smart-scroll/index.ts",
    "content": "import { Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { EditorView, Extension } from \"@/tiptap\";\n\nexport interface SmartScrollOptions {\n  /**\n   * The container to scroll\n   */\n  scrollContainer?:\n    | string\n    | HTMLElement\n    | ((editor: EditorView) => HTMLElement);\n  /**\n   * Top threshold (pixels), when the cursor is less than this value from the top of the viewport, trigger scrolling\n   * @default 150\n   */\n  topThreshold: number;\n\n  /**\n   * Bottom threshold (pixels), when the cursor is less than this value from the bottom of the viewport, trigger scrolling\n   * @default 150\n   */\n  bottomThreshold: number;\n\n  /**\n   * Whether to enable smooth scrolling\n   * @default false\n   */\n  smooth: boolean;\n}\n\n/**\n * Smart scroll extension\n *\n * When the cursor is close to the top or bottom of the viewport, trigger scrolling to keep the cursor in the center of the viewport\n */\nexport const ExtensionSmartScroll = Extension.create<SmartScrollOptions>({\n  name: \"smartScroll\",\n\n  addOptions() {\n    return {\n      scrollContainer: undefined,\n      topThreshold: 150,\n      bottomThreshold: 150,\n      smooth: false,\n    };\n  },\n\n  addProseMirrorPlugins() {\n    const options = this.options;\n    let lastMouseDownTime = 0;\n\n    return [\n      new Plugin({\n        key: new PluginKey(\"smartScroll\"),\n        view() {\n          return {\n            update(view, prevState) {\n              if (!prevState) {\n                return;\n              }\n\n              const { state } = view;\n              const { doc, selection } = state;\n\n              const docChanged = prevState.doc !== doc;\n              const selectionChanged = !prevState.selection.eq(selection);\n\n              const isRecentMouseClick = Date.now() - lastMouseDownTime < 100;\n\n              // The conditions for triggering scrolling:\n              // 1. Document content changed (input, delete, etc.)\n              // 2. Selection changed but not caused by mouse click (arrow keys, etc.)\n              if (docChanged || (selectionChanged && !isRecentMouseClick)) {\n                requestAnimationFrame(() => {\n                  smartScroll(view, options);\n                });\n              }\n            },\n          };\n        },\n        props: {\n          handleDOMEvents: {\n            mousedown: () => {\n              lastMouseDownTime = Date.now();\n              return false;\n            },\n          },\n        },\n      }),\n    ];\n  },\n});\n\nconst smartScroll = (view: EditorView, options: SmartScrollOptions): void => {\n  try {\n    const { state } = view;\n    const { selection } = state;\n\n    const coords = view.coordsAtPos(selection.$head.pos);\n    if (!coords) {\n      return;\n    }\n\n    let scrollContainer: HTMLElement | null = null;\n    if (!options.scrollContainer) {\n      const editorElement = view.dom as HTMLElement;\n      scrollContainer = findScrollContainer(editorElement);\n    } else {\n      if (typeof options.scrollContainer === \"function\") {\n        scrollContainer = options.scrollContainer(view);\n      } else if (typeof options.scrollContainer === \"string\") {\n        scrollContainer = document.querySelector(\n          options.scrollContainer\n        ) as HTMLElement;\n      } else {\n        scrollContainer = options.scrollContainer;\n      }\n    }\n\n    if (!scrollContainer) {\n      return;\n    }\n\n    const containerRect = scrollContainer.getBoundingClientRect();\n    const viewportTop = containerRect.top;\n    const viewportBottom = containerRect.bottom;\n\n    const cursorTop = coords.top;\n    const cursorBottom = coords.bottom;\n\n    const distanceFromTop = cursorTop - viewportTop;\n    const distanceFromBottom = viewportBottom - cursorBottom;\n\n    let scrollAmount = 0;\n\n    if (distanceFromTop < options.topThreshold && distanceFromTop > 0) {\n      scrollAmount = distanceFromTop - options.topThreshold;\n    } else if (\n      distanceFromBottom < options.bottomThreshold &&\n      distanceFromBottom > 0\n    ) {\n      scrollAmount = options.bottomThreshold - distanceFromBottom;\n    } else if (distanceFromTop < 0) {\n      scrollAmount = distanceFromTop - options.topThreshold;\n    } else if (distanceFromBottom < 0) {\n      scrollAmount = options.bottomThreshold - distanceFromBottom;\n    }\n\n    if (scrollAmount !== 0) {\n      const currentScrollTop = scrollContainer.scrollTop;\n      const targetScrollTop = currentScrollTop + scrollAmount;\n\n      if (scrollContainer.scrollTo) {\n        scrollContainer.scrollTo({\n          top: targetScrollTop,\n          behavior: options.smooth ? \"smooth\" : \"instant\",\n        });\n      } else {\n        scrollContainer.scrollTop = targetScrollTop;\n      }\n    }\n  } catch (error) {\n    console.debug(\"Smart scroll error:\", error);\n  }\n};\n\n/**\n * Find the scrollable container element\n */\nconst findScrollContainer = (element: HTMLElement): HTMLElement | null => {\n  let current: HTMLElement | null = element;\n\n  while (current && current !== document.body) {\n    const style = window.getComputedStyle(current);\n    const overflowY = style.overflowY;\n\n    if (\n      (overflowY === \"auto\" || overflowY === \"scroll\") &&\n      current.scrollHeight > current.clientHeight\n    ) {\n      return current;\n    }\n\n    current = current.parentElement;\n  }\n\n  return document.documentElement;\n};\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/strike/index.ts",
    "content": "import TiptapStrike, { type StrikeOptions } from \"@tiptap/extension-strike\";\nimport { markRaw } from \"vue\";\nimport MingcuteStrikethroughLine from \"~icons/mingcute/strikethrough-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionStrikeOptions = ExtensionOptions & Partial<StrikeOptions>;\n\nexport const ExtensionStrike = TiptapStrike.extend<ExtensionStrikeOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getToolbarItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 70,\n          component: markRaw(ToolbarItem),\n          props: {\n            editor,\n            isActive: editor.isActive(TiptapStrike.name),\n            icon: markRaw(MingcuteStrikethroughLine),\n            title: i18n.global.t(\"editor.common.strike\"),\n            action: () => editor.chain().focus().toggleStrike().run(),\n          },\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/subscript/index.ts",
    "content": "import TiptapSubscript, {\n  type SubscriptExtensionOptions,\n} from \"@tiptap/extension-subscript\";\nimport { markRaw } from \"vue\";\nimport PhTextSubscript from \"~icons/ph/text-subscript\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionSubscriptOptions = Partial<SubscriptExtensionOptions> &\n  ExtensionOptions;\n\nexport const ExtensionSubscript =\n  TiptapSubscript.extend<ExtensionSubscriptOptions>({\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 120,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: editor.isActive(TiptapSubscript.name),\n              icon: markRaw(PhTextSubscript),\n              title: i18n.global.t(\"editor.common.subscript\"),\n              action: () => editor.chain().focus().toggleSubscript().run(),\n            },\n          };\n        },\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/superscript/index.ts",
    "content": "import TiptapSuperscript, {\n  type SuperscriptExtensionOptions,\n} from \"@tiptap/extension-superscript\";\nimport { markRaw } from \"vue\";\nimport PhTextSuperscript from \"~icons/ph/text-superscript\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionSuperscriptOptions = Partial<SuperscriptExtensionOptions> &\n  ExtensionOptions;\n\nexport const ExtensionSuperscript =\n  TiptapSuperscript.extend<ExtensionSuperscriptOptions>({\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 110,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: editor.isActive(TiptapSuperscript.name),\n              icon: markRaw(PhTextSuperscript),\n              title: i18n.global.t(\"editor.common.superscript\"),\n              action: () => editor.chain().focus().toggleSuperscript().run(),\n            },\n          };\n        },\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/table/index.ts",
    "content": "import {\n  createColGroup,\n  Table as TiptapTable,\n  type TableOptions,\n} from \"@tiptap/extension-table\";\nimport { markRaw } from \"vue\";\nimport FluentTableColumnTopBottom24Regular from \"~icons/fluent/table-column-top-bottom-24-regular\";\nimport MdiTable from \"~icons/mdi/table\";\nimport MdiTableColumnPlusAfter from \"~icons/mdi/table-column-plus-after\";\nimport MdiTableColumnPlusBefore from \"~icons/mdi/table-column-plus-before\";\nimport MdiTableColumnRemove from \"~icons/mdi/table-column-remove\";\nimport MdiTableHeadersEye from \"~icons/mdi/table-headers-eye\";\nimport MdiTableMergeCells from \"~icons/mdi/table-merge-cells\";\nimport MdiTablePlus from \"~icons/mdi/table-plus\";\nimport MdiTableRemove from \"~icons/mdi/table-remove\";\nimport MdiTableRowPlusAfter from \"~icons/mdi/table-row-plus-after\";\nimport MdiTableRowPlusBefore from \"~icons/mdi/table-row-plus-before\";\nimport MdiTableRowRemove from \"~icons/mdi/table-row-remove\";\nimport MdiTableSplitCell from \"~icons/mdi/table-split-cell\";\nimport { BlockActionSeparator, ToolboxItem } from \"@/components\";\nimport { CONVERT_TO_KEY } from \"@/components/drag/default-drag\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  findParentNode,\n  isActive,\n  isNodeActive,\n  mergeAttributes,\n  posToDOMRect,\n  type Range,\n} from \"@/tiptap\";\nimport {\n  PluginKey,\n  TextSelection,\n  type DOMOutputSpec,\n  type EditorState,\n  type NodeView,\n  type Node as ProseMirrorNode,\n  type ViewMutationRecord,\n} from \"@/tiptap/pm\";\nimport type { ExtensionOptions, NodeBubbleMenuType } from \"@/types\";\nimport TableCell from \"./table-cell\";\nimport TableHeader from \"./table-header\";\nimport TableRow from \"./table-row\";\nimport {\n  findNextCell,\n  findPreviousCell,\n  hasTableBefore,\n  isCellSelection,\n  isTableSelected,\n  selectTable,\n} from \"./util\";\n\nfunction updateColumns(\n  node: ProseMirrorNode,\n  colgroup: Element,\n  table: HTMLElement,\n  cellMinWidth: number,\n  overrideCol?: number,\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  overrideValue?: any\n) {\n  let totalWidth = 0;\n  let fixedWidth = true;\n  let nextDOM = colgroup.firstChild as HTMLElement;\n  const row = node.firstChild;\n  if (!row) return;\n\n  for (let i = 0, col = 0; i < row.childCount; i += 1) {\n    const { colspan, colwidth } = row.child(i).attrs;\n\n    for (let j = 0; j < colspan; j += 1, col += 1) {\n      const hasWidth =\n        overrideCol === col ? overrideValue : colwidth && colwidth[j];\n      const cssWidth = hasWidth ? `${hasWidth}px` : \"\";\n\n      totalWidth += hasWidth || cellMinWidth;\n\n      if (!hasWidth) {\n        fixedWidth = false;\n      }\n\n      if (!nextDOM) {\n        colgroup.appendChild(document.createElement(\"col\")).style.width =\n          cssWidth;\n      } else {\n        if (nextDOM.style.width !== cssWidth) {\n          nextDOM.style.width = cssWidth;\n        }\n\n        nextDOM = nextDOM.nextSibling as HTMLElement;\n      }\n    }\n  }\n\n  while (nextDOM) {\n    const after = nextDOM.nextSibling as HTMLElement;\n\n    nextDOM.parentNode?.removeChild(nextDOM);\n    nextDOM = after;\n  }\n\n  if (fixedWidth) {\n    table.style.width = `${totalWidth}px`;\n    table.style.minWidth = \"\";\n  } else {\n    table.style.width = \"\";\n    table.style.minWidth = `${totalWidth}px`;\n  }\n}\n\nlet editor: Editor | undefined = undefined;\n\nclass TableView implements NodeView {\n  node: ProseMirrorNode;\n\n  cellMinWidth: number;\n\n  dom: HTMLElement;\n\n  scrollDom: HTMLElement;\n\n  table: HTMLElement;\n\n  colgroup: HTMLElement;\n\n  contentDOM: HTMLElement;\n\n  containerDOM: HTMLElement;\n\n  constructor(node: ProseMirrorNode, cellMinWidth: number) {\n    this.node = node;\n    this.cellMinWidth = cellMinWidth;\n    this.dom = document.createElement(\"div\");\n    this.dom.className = \"table-container\";\n\n    this.containerDOM = this.dom.appendChild(document.createElement(\"div\"));\n\n    this.containerDOM.className = \"tableWrapper\";\n    this.containerDOM.addEventListener(\"wheel\", (e) => {\n      return this.handleHorizontalWheel(this.containerDOM, e);\n    });\n\n    this.containerDOM.addEventListener(\"scroll\", () => {\n      if (!editor) {\n        return false;\n      }\n      const { view } = editor;\n      view.dispatch(view.state.tr);\n    });\n\n    this.scrollDom = document.createElement(\"div\");\n    this.scrollDom.className = \"scrollWrapper\";\n    this.containerDOM.appendChild(this.scrollDom);\n\n    this.table = this.scrollDom.appendChild(document.createElement(\"table\"));\n    this.colgroup = this.table.appendChild(document.createElement(\"colgroup\"));\n    updateColumns(node, this.colgroup, this.table, cellMinWidth);\n    this.contentDOM = this.table.appendChild(document.createElement(\"tbody\"));\n    // delay execution during initialization, otherwise\n    // the correct scrollWidth cannot be obtained.\n    setTimeout(() => {\n      this.updateTableShadow();\n    });\n  }\n\n  update(node: ProseMirrorNode) {\n    if (node.type !== this.node.type) {\n      return false;\n    }\n\n    this.node = node;\n    updateColumns(node, this.colgroup, this.table, this.cellMinWidth);\n    this.updateTableShadow();\n    return true;\n  }\n\n  updateTableShadow() {\n    const { scrollWidth, clientWidth, scrollLeft } = this\n      .containerDOM as HTMLElement;\n    if (scrollWidth > clientWidth && scrollLeft < scrollWidth - clientWidth) {\n      this.dom.classList.add(\"table-right-shadow\");\n    } else {\n      this.dom.classList.remove(\"table-right-shadow\");\n    }\n    if (scrollLeft > 0) {\n      this.dom.classList.add(\"table-left-shadow\");\n    } else {\n      this.dom.classList.remove(\"table-left-shadow\");\n    }\n  }\n\n  ignoreMutation(mutation: ViewMutationRecord) {\n    return (\n      mutation.type === \"attributes\" &&\n      (mutation.target === this.table ||\n        mutation.target === this.dom ||\n        this.colgroup.contains(mutation.target))\n    );\n  }\n\n  handleHorizontalWheel(dom: HTMLElement, event: WheelEvent) {\n    const { scrollWidth, clientWidth } = dom;\n    const hasScrollWidth = scrollWidth > clientWidth;\n    if (hasScrollWidth) {\n      event.stopPropagation();\n      event.preventDefault();\n      dom.scrollBy({ left: event.deltaY });\n    }\n  }\n}\n\nexport const TABLE_BUBBLE_MENU_KEY = new PluginKey(\"tableBubbleMenu\");\n\nexport type ExtensionTableOptions = ExtensionOptions & Partial<TableOptions>;\n\nexport const ExtensionTable = TiptapTable.extend<ExtensionTableOptions>({\n  allowGapCursor: true,\n\n  addExtensions() {\n    return [TableCell, TableRow, TableHeader];\n  },\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      HTMLAttributes: {},\n      resizable: true,\n      handleWidth: 5,\n      cellMinWidth: 25,\n      View: TableView,\n      lastColumnResizable: true,\n      allowTableNodeSelection: false,\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return {\n          priority: 40,\n          component: markRaw(ToolboxItem),\n          props: {\n            editor,\n            icon: markRaw(MdiTablePlus),\n            title: i18n.global.t(\"editor.menus.table.add\"),\n            action: () =>\n              editor\n                .chain()\n                .focus()\n                .insertTable({ rows: 3, cols: 3, withHeaderRow: true })\n                .run(),\n          },\n        };\n      },\n      getCommandMenuItems() {\n        return {\n          priority: 120,\n          icon: markRaw(MdiTable),\n          title: \"editor.extensions.commands_menu.table\",\n          keywords: [\"table\", \"biaoge\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .insertTable({ rows: 3, cols: 3, withHeaderRow: true })\n              .run();\n          },\n        };\n      },\n      getBubbleMenu({ editor }): NodeBubbleMenuType {\n        return {\n          pluginKey: TABLE_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }): boolean => {\n            return isActive(state, ExtensionTable.name);\n          },\n          options: {\n            placement: \"bottom-start\",\n          },\n          getReferencedVirtualElement() {\n            const editor = this.editor;\n            if (!editor) {\n              return null;\n            }\n            const parentNode = findParentNode(\n              (node) => node.type.name === ExtensionTable.name\n            )(editor.state.selection);\n            if (parentNode) {\n              const domRect = posToDOMRect(\n                editor.view,\n                parentNode.start,\n                parentNode.start + parentNode.node.nodeSize - 2\n              );\n              return {\n                getBoundingClientRect: () => domRect,\n                getClientRects: () => [domRect],\n              };\n            }\n            return null;\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                icon: markRaw(MdiTableColumnPlusBefore),\n                title: i18n.global.t(\"editor.menus.table.add_column_before\"),\n                action: () => {\n                  editor.chain().focus().addColumnBefore().run();\n                },\n              },\n            },\n            {\n              priority: 20,\n              props: {\n                icon: markRaw(MdiTableColumnPlusAfter),\n                title: i18n.global.t(\"editor.menus.table.add_column_after\"),\n                action: () => editor.chain().focus().addColumnAfter().run(),\n              },\n            },\n            {\n              priority: 30,\n              props: {\n                icon: markRaw(MdiTableColumnRemove),\n                title: i18n.global.t(\"editor.menus.table.delete_column\"),\n                action: () => editor.chain().focus().deleteColumn().run(),\n              },\n            },\n            {\n              priority: 40,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 50,\n              props: {\n                icon: markRaw(MdiTableRowPlusBefore),\n                title: i18n.global.t(\"editor.menus.table.add_row_before\"),\n                action: () => editor.chain().focus().addRowBefore().run(),\n              },\n            },\n            {\n              priority: 60,\n              props: {\n                icon: markRaw(MdiTableRowPlusAfter),\n                title: i18n.global.t(\"editor.menus.table.add_row_after\"),\n                action: () => editor.chain().focus().addRowAfter().run(),\n              },\n            },\n            {\n              priority: 70,\n              props: {\n                icon: markRaw(MdiTableRowRemove),\n                title: i18n.global.t(\"editor.menus.table.delete_row\"),\n                action: () => editor.chain().focus().deleteRow().run(),\n              },\n            },\n            {\n              priority: 80,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 90,\n              props: {\n                icon: markRaw(MdiTableHeadersEye),\n                title: i18n.global.t(\"editor.menus.table.toggle_header_column\"),\n                action: () => editor.chain().focus().toggleHeaderColumn().run(),\n              },\n            },\n            {\n              priority: 100,\n              props: {\n                icon: markRaw(MdiTableHeadersEye),\n                title: i18n.global.t(\"editor.menus.table.toggle_header_row\"),\n                action: () => editor.chain().focus().toggleHeaderRow().run(),\n              },\n            },\n            {\n              priority: 101,\n              props: {\n                icon: markRaw(FluentTableColumnTopBottom24Regular),\n                title: i18n.global.t(\"editor.menus.table.toggle_header_cell\"),\n                action: () => editor.chain().focus().toggleHeaderCell().run(),\n              },\n            },\n            {\n              priority: 110,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 120,\n              props: {\n                icon: markRaw(MdiTableMergeCells),\n                title: i18n.global.t(\"editor.menus.table.merge_cells\"),\n                action: () => editor.chain().focus().mergeCells().run(),\n              },\n            },\n            {\n              priority: 130,\n              props: {\n                icon: markRaw(MdiTableSplitCell),\n                title: i18n.global.t(\"editor.menus.table.split_cell\"),\n                action: () => editor.chain().focus().splitCell().run(),\n              },\n            },\n            {\n              priority: 140,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 150,\n              props: {\n                icon: markRaw(MdiTableRemove),\n                title: i18n.global.t(\"editor.menus.table.delete_table\"),\n                action: () => editor.chain().focus().deleteTable().run(),\n              },\n            },\n          ],\n        };\n      },\n      getDraggableMenuItems() {\n        return {\n          extendsKey: CONVERT_TO_KEY,\n          visible({ editor }) {\n            if (isActive(editor.state, \"table\")) {\n              return false;\n            }\n            return true;\n          },\n        };\n      },\n    };\n  },\n\n  addKeyboardShortcuts() {\n    const handleBackspace = () => {\n      const { editor } = this;\n      if (editor.commands.undoInputRule()) {\n        return true;\n      }\n\n      const { selection } = editor.state;\n      // the node in the current active state is not a table\n      // and the previous node is a table\n      if (\n        !isNodeActive(editor.state, ExtensionTable.name) &&\n        hasTableBefore(editor.state) &&\n        selection.empty\n      ) {\n        editor.commands.selectNodeBackward();\n        return true;\n      }\n\n      if (!isNodeActive(editor.state, ExtensionTable.name)) {\n        return false;\n      }\n\n      // If the table is currently selected,\n      // then delete the whole table\n      if (isTableSelected(editor.state.selection)) {\n        editor.commands.deleteTable();\n        return true;\n      }\n\n      return false;\n    };\n\n    return {\n      Backspace: () => handleBackspace(),\n\n      \"Mod-Backspace\": () => handleBackspace(),\n\n      \"Mod-a\": ({ editor }) => {\n        if (!isNodeActive(editor.state, ExtensionTable.name)) {\n          return false;\n        }\n\n        const { tr, selection } = editor.state;\n        // If the entire table is already selected, no longer perform the select all operation.\n        if (isTableSelected(selection)) {\n          return true;\n        }\n\n        if (isCellSelection(selection)) {\n          selectTable(tr);\n          editor.view.dispatch(tr);\n          return true;\n        }\n\n        let cellNodePos = findParentNode(\n          (node) => node.type.name === TableCell.name\n        )(selection);\n        if (!cellNodePos) {\n          cellNodePos = findParentNode(\n            (node) => node.type.name === TableHeader.name\n          )(selection);\n        }\n        if (!cellNodePos) {\n          return false;\n        }\n        editor.commands.setNodeSelection(cellNodePos.pos);\n        return true;\n      },\n      Tab: ({ editor }) => {\n        const { state } = editor;\n        if (!isActive(editor.state, ExtensionTable.name)) {\n          return false;\n        }\n        let nextView = editor.view;\n        let nextTr = editor.state.tr;\n\n        let nextCell = findNextCell(state);\n        if (!nextCell) {\n          // If it is the last cell, create a new line and jump to the first cell of the new line.\n          editor\n            .chain()\n            .addRowAfter()\n            .command(({ tr, view, state }) => {\n              nextView = view;\n              nextTr = tr;\n              nextCell = findNextCell(state);\n              return true;\n            });\n        }\n        if (nextCell) {\n          nextTr.setSelection(\n            new TextSelection(\n              nextTr.doc.resolve(nextCell.start),\n              nextTr.doc.resolve(\n                nextCell.start + (nextCell.node?.nodeSize || 0) - 4\n              )\n            )\n          );\n          nextTr.scrollIntoView();\n          nextView.dispatch(nextTr);\n          return true;\n        }\n        return false;\n      },\n      \"Shift-Tab\": ({ editor }) => {\n        const { tr } = editor.state;\n        if (!isActive(editor.state, ExtensionTable.name)) {\n          return false;\n        }\n        const previousCell = findPreviousCell(editor.state);\n        if (previousCell) {\n          tr.setSelection(\n            new TextSelection(\n              tr.doc.resolve(previousCell.start),\n              tr.doc.resolve(\n                previousCell.start + (previousCell.node?.nodeSize || 0) - 4\n              )\n            )\n          );\n          tr.scrollIntoView();\n          editor.view.dispatch(tr);\n        }\n        return true;\n      },\n    };\n  },\n\n  renderHTML({ node, HTMLAttributes }) {\n    const { colgroup, tableWidth, tableMinWidth } = createColGroup(\n      node,\n      this.options.cellMinWidth ?? 25\n    );\n\n    const table: DOMOutputSpec = [\n      \"div\",\n      { style: \"overflow-x: auto; overflow-y: hidden;\" },\n      [\n        \"table\",\n        mergeAttributes(\n          this.options.HTMLAttributes ?? {},\n          HTMLAttributes ?? {},\n          {\n            style: tableWidth\n              ? `width: ${tableWidth}`\n              : `minWidth: ${tableMinWidth}`,\n          }\n        ),\n        colgroup,\n        [\"tbody\", 0],\n      ],\n    ];\n\n    return table;\n  },\n\n  onTransaction() {\n    editor = this.editor;\n  },\n}).configure({ resizable: true });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/table/table-cell.ts",
    "content": "import { VTooltipComponent } from \"@halo-dev/components\";\nimport { h, render } from \"vue\";\nimport MdiPlus from \"~icons/mdi/plus\";\nimport { i18n } from \"@/locales\";\nimport {\n  addRowAfter,\n  Decoration,\n  DecorationSet,\n  Plugin,\n  PluginKey,\n} from \"@/tiptap/pm\";\nimport { mergeAttributes, Node } from \"@/tiptap/vue-3\";\nimport {\n  getCellsInColumn,\n  isRowSelected,\n  isTableSelected,\n  selectRow,\n  selectTable,\n} from \"./util\";\n\nexport interface TableCellOptions {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  HTMLAttributes: Record<string, any>;\n}\n\nconst TableCell = Node.create<TableCellOptions>({\n  name: \"tableCell\",\n  content: \"block+\",\n  tableRole: \"cell\",\n  isolating: true,\n  fakeSelection: true,\n\n  addOptions() {\n    return {\n      HTMLAttributes: {},\n    };\n  },\n\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      colspan: {\n        default: 1,\n        parseHTML: (element) => {\n          const colspan = element.getAttribute(\"colspan\");\n          const value = colspan ? parseInt(colspan, 10) : 1;\n          return value;\n        },\n      },\n      rowspan: {\n        default: 1,\n        parseHTML: (element) => {\n          const rowspan = element.getAttribute(\"rowspan\");\n          const value = rowspan ? parseInt(rowspan, 10) : 1;\n          return value;\n        },\n      },\n      colwidth: {\n        default: [100],\n        parseHTML: (element) => {\n          const colwidth = element.getAttribute(\"colwidth\");\n          const value = colwidth\n            ? colwidth.split(\",\").map((width) => parseInt(width, 10))\n            : null;\n          return value;\n        },\n      },\n      style: {\n        default: null,\n      },\n    };\n  },\n\n  parseHTML() {\n    return [{ tag: \"td\" }];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\n      \"td\",\n      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n      0,\n    ];\n  },\n\n  addStorage() {\n    const gripMap = new Map<string, HTMLElement>();\n    return {\n      gripMap,\n    };\n  },\n\n  onDestroy() {\n    this.storage.gripMap.clear();\n  },\n\n  addProseMirrorPlugins() {\n    const editor = this.editor;\n    const storage = this.storage;\n    return [\n      new Plugin({\n        key: new PluginKey(\"table-cell-control\"),\n        props: {\n          decorations(state) {\n            const { doc, selection } = state;\n            const decorations: Decoration[] = [];\n            const cells = getCellsInColumn(0)(selection);\n            if (cells) {\n              cells.forEach(({ pos }, index) => {\n                if (index === 0) {\n                  decorations.push(\n                    Decoration.widget(pos + 1, () => {\n                      const key = \"table\" + index;\n                      let className = \"grip-table\";\n                      const selected = isTableSelected(selection);\n                      if (selected) {\n                        className += \" selected\";\n                      }\n                      let grip = storage.gripMap.get(key);\n                      if (!grip) {\n                        grip = document.createElement(\"a\") as HTMLElement;\n                        grip.addEventListener(\"mousedown\", (event: Event) => {\n                          event.preventDefault();\n                          event.stopImmediatePropagation();\n                          editor.view.dispatch(selectTable(editor.state.tr));\n                        });\n                      }\n                      grip.className = className;\n                      storage.gripMap.set(key, grip);\n                      return grip;\n                    })\n                  );\n                }\n\n                decorations.push(\n                  Decoration.widget(pos + 1, () => {\n                    const key = \"row\" + index;\n                    const rowSelected = isRowSelected(index)(selection);\n                    let className = \"grip-row\";\n                    if (rowSelected) {\n                      className += \" selected\";\n                    }\n                    if (index === 0) {\n                      className += \" first\";\n                    }\n                    if (index === cells.length - 1) {\n                      className += \" last\";\n                    }\n\n                    let grip = storage.gripMap.get(key);\n                    if (!grip) {\n                      grip = document.createElement(\"a\");\n                      const instance = h(\n                        VTooltipComponent,\n                        {\n                          triggers: [\"hover\"],\n                        },\n                        {\n                          default: () => h(MdiPlus, { class: \"plus-icon\" }),\n                          popper: () =>\n                            i18n.global.t(\"editor.menus.table.add_row_after\"),\n                        }\n                      );\n                      render(instance, grip);\n                      grip.addEventListener(\n                        \"mousedown\",\n                        (event: Event) => {\n                          event.preventDefault();\n                          event.stopImmediatePropagation();\n\n                          editor.view.dispatch(\n                            selectRow(index)(editor.state.tr)\n                          );\n\n                          if (event.target !== grip) {\n                            addRowAfter(editor.state, editor.view.dispatch);\n                          }\n                        },\n                        true\n                      );\n                    }\n                    grip.className = className;\n                    storage.gripMap.set(key, grip);\n                    return grip;\n                  })\n                );\n              });\n            }\n            return DecorationSet.create(doc, decorations);\n          },\n        },\n      }),\n    ];\n  },\n});\n\nexport default TableCell;\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/table/table-header.ts",
    "content": "import { VTooltipComponent } from \"@halo-dev/components\";\nimport { h, render } from \"vue\";\nimport MdiPlus from \"~icons/mdi/plus\";\nimport { i18n } from \"@/locales\";\nimport {\n  addColumnAfter,\n  Decoration,\n  DecorationSet,\n  Plugin,\n  PluginKey,\n} from \"@/tiptap/pm\";\nimport { mergeAttributes, Node } from \"@/tiptap/vue-3\";\nimport { getCellsInRow, isColumnSelected, selectColumn } from \"./util\";\n\nexport interface TableCellOptions {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  HTMLAttributes: Record<string, any>;\n}\n\nconst TableHeader = Node.create<TableCellOptions>({\n  name: \"tableHeader\",\n  content: \"block+\",\n  tableRole: \"header_cell\",\n  isolating: true,\n  fakeSelection: true,\n\n  addOptions() {\n    return {\n      HTMLAttributes: {},\n    };\n  },\n\n  addAttributes() {\n    return {\n      colspan: {\n        default: 1,\n      },\n      rowspan: {\n        default: 1,\n      },\n      colwidth: {\n        default: [100],\n        parseHTML: (element) => {\n          const colwidth = element.getAttribute(\"colwidth\");\n          const value = colwidth\n            ? colwidth.split(\",\").map((width) => parseInt(width, 10))\n            : null;\n          return value;\n        },\n      },\n      style: {\n        default: null,\n      },\n    };\n  },\n\n  parseHTML() {\n    return [{ tag: \"th\" }];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\n      \"th\",\n      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n      0,\n    ];\n  },\n\n  addStorage() {\n    const gripMap = new Map<string, HTMLElement>();\n    return {\n      gripMap,\n    };\n  },\n\n  onDestroy() {\n    this.storage.gripMap.clear();\n  },\n\n  addProseMirrorPlugins() {\n    const editor = this.editor;\n    const storage = this.storage;\n    return [\n      new Plugin({\n        key: new PluginKey(\"table-header-control\"),\n        props: {\n          decorations(state) {\n            const { doc, selection } = state;\n            const decorations: Decoration[] = [];\n            const cells = getCellsInRow(0)(selection);\n            if (cells) {\n              cells.forEach(({ pos }, index) => {\n                decorations.push(\n                  Decoration.widget(pos + 1, () => {\n                    const key = \"column\" + index;\n                    const colSelected = isColumnSelected(index)(selection);\n                    let className = \"grip-column\";\n                    if (colSelected) {\n                      className += \" selected\";\n                    }\n                    if (index === 0) {\n                      className += \" first\";\n                    } else if (index === cells.length - 1) {\n                      className += \" last\";\n                    }\n\n                    let grip = storage.gripMap.get(key) as HTMLElement;\n                    if (!grip) {\n                      grip = document.createElement(\"a\");\n                      const instance = h(\n                        VTooltipComponent,\n                        {\n                          triggers: [\"hover\"],\n                        },\n                        {\n                          default: () => h(MdiPlus, { class: \"plus-icon\" }),\n                          popper: () =>\n                            i18n.global.t(\n                              \"editor.menus.table.add_column_after\"\n                            ),\n                        }\n                      );\n                      render(instance, grip);\n                      grip.addEventListener(\"mousedown\", (event) => {\n                        event.preventDefault();\n                        event.stopImmediatePropagation();\n\n                        editor.view.dispatch(\n                          selectColumn(index)(editor.state.tr)\n                        );\n\n                        if (event.target !== grip) {\n                          addColumnAfter(editor.state, editor.view.dispatch);\n                        }\n                      });\n                    }\n                    grip.className = className;\n                    storage.gripMap.set(key, grip);\n                    return grip;\n                  })\n                );\n              });\n            }\n            return DecorationSet.create(doc, decorations);\n          },\n        },\n      }),\n    ];\n  },\n});\n\nexport default TableHeader;\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/table/table-row.ts",
    "content": "import { TableRow as BuiltInTableRow } from \"@tiptap/extension-table\";\n\nconst TableRow = BuiltInTableRow.extend({\n  allowGapCursor: false,\n\n  addAttributes() {\n    return {\n      style: {\n        default: \"height: 60px;\",\n        parseHTML: (element: HTMLElement) => element.getAttribute(\"style\"),\n      },\n    };\n  },\n});\n\nexport default TableRow;\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/table/util.ts",
    "content": "import type { EditorState, Rect, Selection, Transaction } from \"@/tiptap/pm\";\nimport { CellSelection, Node, selectedRect, TableMap } from \"@/tiptap/pm\";\nimport { findParentNode } from \"@/tiptap/vue-3\";\n\nexport const selectTable = (tr: Transaction) => {\n  const table = findTable(tr.selection);\n  if (table) {\n    const { node } = table;\n    const { map } = TableMap.get(node);\n    if (map && map.length) {\n      const head = table.start + map[0];\n      const anchor = table.start + map[map.length - 1];\n      const $head = tr.doc.resolve(head);\n      const $anchor = tr.doc.resolve(anchor);\n      return tr.setSelection(new CellSelection($anchor, $head));\n    }\n  }\n  return tr;\n};\n\nconst select =\n  (type: \"row\" | \"column\") => (index: number) => (tr: Transaction) => {\n    const table = findTable(tr.selection);\n    const isRowSelection = type === \"row\";\n\n    if (table) {\n      const map = TableMap.get(table.node);\n      if (index >= 0 && index < (isRowSelection ? map.height : map.width)) {\n        const cells = isRowSelection\n          ? map.cellsInRect({\n              left: 0,\n              right: 1,\n              top: 0,\n              bottom: map.height,\n            })\n          : map.cellsInRect({\n              left: 0,\n              right: map.width,\n              top: 0,\n              bottom: 1,\n            });\n\n        const startCellRect = map.findCell(cells[index]);\n        const rect = {\n          left: isRowSelection ? map.width - 1 : startCellRect.left,\n          right: isRowSelection ? map.width : startCellRect.right,\n          top: isRowSelection ? startCellRect.top : map.height - 1,\n          bottom: isRowSelection ? startCellRect.bottom : map.height,\n        };\n        let endCellRect = map.cellsInRect(rect);\n        while (endCellRect.length === 0) {\n          if (isRowSelection) {\n            rect.left -= 1;\n          } else {\n            rect.top -= 1;\n          }\n          endCellRect = map.cellsInRect(rect);\n        }\n\n        const head = table.start + cells[index];\n        const anchor = table.start + endCellRect[endCellRect.length - 1];\n        const $head = tr.doc.resolve(head);\n        const $anchor = tr.doc.resolve(anchor);\n\n        return tr.setSelection(new CellSelection($anchor, $head));\n      }\n    }\n    return tr;\n  };\n\nexport const selectColumn = select(\"column\");\n\nexport const selectRow = select(\"row\");\n\nexport const getCellsInColumn =\n  (columnIndex: number | number[]) => (selection: Selection) => {\n    const table = findTable(selection);\n    if (table) {\n      const map = TableMap.get(table.node);\n      const indexes = Array.isArray(columnIndex)\n        ? columnIndex\n        : Array.from([columnIndex]);\n      return indexes.reduce(\n        (acc, index) => {\n          if (index >= 0 && index <= map.width - 1) {\n            const cells = map.cellsInRect({\n              left: index,\n              right: index + 1,\n              top: 0,\n              bottom: map.height,\n            });\n            return acc.concat(\n              cells.map((nodePos: number) => {\n                const node = table.node.nodeAt(nodePos);\n                const pos = nodePos + table.start;\n                return { pos, start: pos + 1, node };\n              }) as unknown as {\n                pos: number;\n                start: number;\n                node: Node | null | undefined;\n              }[]\n            );\n          }\n          return acc;\n        },\n        [] as { pos: number; start: number; node: Node | null | undefined }[]\n      );\n    }\n  };\n\nexport const getCellsInRow =\n  (rowIndex: number | number[]) => (selection: Selection) => {\n    const table = findTable(selection);\n    if (table) {\n      const map = TableMap.get(table.node);\n      const indexes = Array.isArray(rowIndex)\n        ? rowIndex\n        : Array.from([rowIndex]);\n      return indexes.reduce(\n        (acc, index) => {\n          if (index >= 0 && index <= map.height - 1) {\n            const cells = map.cellsInRect({\n              left: 0,\n              right: map.width,\n              top: index,\n              bottom: index + 1,\n            });\n            return acc.concat(\n              cells.map((nodePos) => {\n                const node = table.node.nodeAt(nodePos);\n                const pos = nodePos + table.start;\n                return { pos, start: pos + 1, node };\n              }) as unknown as {\n                pos: number;\n                start: number;\n                node: Node | null | undefined;\n              }[]\n            );\n          }\n          return acc;\n        },\n        [] as { pos: number; start: number; node: Node | null | undefined }[]\n      );\n    }\n  };\n\nexport const findTable = (selection: Selection) => {\n  return findParentNode((node) => node.type.spec.tableRole === \"table\")(\n    selection\n  ) as\n    | {\n        pos: number;\n        start: number;\n        depth: number;\n        node: Node;\n      }\n    | undefined;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const isRectSelected = (rect: any) => (selection: CellSelection) => {\n  const map = TableMap.get(selection.$anchorCell.node(-1));\n  const start = selection.$anchorCell.start(-1);\n  const cells = map.cellsInRect(rect);\n  const selectedCells = map.cellsInRect(\n    map.rectBetween(\n      selection.$anchorCell.pos - start,\n      selection.$headCell.pos - start\n    )\n  );\n\n  for (let i = 0, count = cells.length; i < count; i++) {\n    if (selectedCells.indexOf(cells[i]) === -1) {\n      return false;\n    }\n  }\n\n  return true;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const isCellSelection = (selection: any) => {\n  return selection instanceof CellSelection;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const isColumnSelected = (columnIndex: number) => (selection: any) => {\n  if (isCellSelection(selection)) {\n    const map = TableMap.get(selection.$anchorCell.node(-1));\n    const cells = map.cellsInRect({\n      left: 0,\n      right: map.width,\n      top: 0,\n      bottom: 1,\n    });\n    if (columnIndex >= cells.length) {\n      return false;\n    }\n    const startCellRect = map.findCell(cells[columnIndex]);\n    const isSelect = isRectSelected({\n      left: startCellRect.left,\n      right: startCellRect.right,\n      top: 0,\n      bottom: map.height,\n    })(selection);\n    return isSelect;\n  }\n\n  return false;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const isRowSelected = (rowIndex: number) => (selection: any) => {\n  if (isCellSelection(selection)) {\n    const map = TableMap.get(selection.$anchorCell.node(-1));\n    const cells = map.cellsInRect({\n      left: 0,\n      right: 1,\n      top: 0,\n      bottom: map.height,\n    });\n    if (rowIndex >= cells.length) {\n      return false;\n    }\n    const startCellRect = map.findCell(cells[rowIndex]);\n    return isRectSelected({\n      left: 0,\n      right: map.width,\n      top: startCellRect.top,\n      bottom: startCellRect.bottom,\n    })(selection);\n  }\n\n  return false;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const isTableSelected = (selection: any) => {\n  if (isCellSelection(selection)) {\n    const map = TableMap.get(selection.$anchorCell.node(-1));\n    return isRectSelected({\n      left: 0,\n      right: map.width,\n      top: 0,\n      bottom: map.height,\n    })(selection);\n  }\n\n  return false;\n};\n\nexport const hasTableBefore = (editorState: EditorState) => {\n  const { $anchor } = editorState.selection;\n\n  const previousNodePos = Math.max(0, $anchor.pos - 2);\n\n  const previousNode = editorState.doc.resolve(previousNodePos).node();\n\n  if (!previousNode || !(previousNode.type.name === \"table\")) {\n    return false;\n  }\n\n  return true;\n};\n\nexport const findNextCell = (state: EditorState) => {\n  return findAdjacentCell(1)(state);\n};\n\nexport const findPreviousCell = (state: EditorState) => {\n  return findAdjacentCell(-1)(state);\n};\n\nexport const findAdjacentCell = (dir: number) => (state: EditorState) => {\n  const selectionPosRect = selectedRect(state);\n  if (selectionPosRect.table) {\n    const map = selectionPosRect.map;\n    // currentPos is the position of the current cell in the table map, which is between two cells.\n    const selectedCells = map.cellsInRect(selectionPosRect);\n    // Get the currently selected cell boundary\n    const rect = nextCell(map)(selectedCells[selectedCells.length - 1], dir);\n    if (rect) {\n      const { top, left } = rect;\n      // Get the pos of the current cell according to the boundary\n      const nextPos = map.map[top * map.width + left];\n      return {\n        start: nextPos + selectionPosRect.tableStart + 2,\n        node: selectionPosRect.table.nodeAt(nextPos),\n      };\n    }\n    return undefined;\n  }\n};\n\nexport const nextCell = (map: TableMap) => (pos: number, dir: number) => {\n  function findNextCellPos({ top, left, right, bottom }: Rect) {\n    const nextCellRect = {\n      top,\n      left,\n      right,\n      bottom,\n    };\n    if (right + 1 > map.width) {\n      if (bottom === map.height) {\n        return undefined;\n      }\n      nextCellRect.top++;\n      nextCellRect.left = 0;\n      nextCellRect.right = 1;\n      nextCellRect.bottom++;\n    } else {\n      nextCellRect.left++;\n      nextCellRect.right++;\n    }\n    const temporaryPos =\n      map.map[nextCellRect.top * map.width + nextCellRect.left];\n    const temporaryRect = map.findCell(temporaryPos);\n    if (\n      temporaryRect.top != nextCellRect.top ||\n      temporaryRect.left < nextCellRect.left\n    ) {\n      return findNextCellPos({\n        ...nextCellRect,\n        right: temporaryRect.right,\n      });\n    }\n    return temporaryPos;\n  }\n\n  function findPreviousCellPos({ top, left, right, bottom }: Rect) {\n    const nextCellRect = {\n      top,\n      left,\n      right,\n      bottom,\n    };\n    if (left - 1 < 0) {\n      if (top === 0) {\n        return undefined;\n      }\n      nextCellRect.top--;\n      nextCellRect.left = map.width - 1;\n      nextCellRect.right = map.width;\n      nextCellRect.bottom--;\n    } else {\n      nextCellRect.left--;\n      nextCellRect.right--;\n    }\n    const temporaryPos =\n      map.map[nextCellRect.top * map.width + nextCellRect.left];\n    const temporaryRect = map.findCell(temporaryPos);\n    if (temporaryRect.top != nextCellRect.top) {\n      return findPreviousCellPos(nextCellRect);\n    }\n    return temporaryPos;\n  }\n\n  function nextCellRectByPos(innerPos: number, innerDir: number) {\n    // Get the current cell boundary\n    const { top, left, right, bottom } = map.findCell(innerPos);\n    if (innerDir == 0) {\n      return {\n        top,\n        left,\n        right,\n        bottom,\n      };\n    }\n\n    const nextCellRect = {\n      top,\n      left,\n      right,\n      bottom,\n    };\n    let nextPos;\n    if (innerDir > 0) {\n      nextPos = findNextCellPos(nextCellRect);\n      innerDir--;\n    } else {\n      nextPos = findPreviousCellPos(nextCellRect);\n      innerDir++;\n    }\n    if (!nextPos) {\n      return undefined;\n    }\n    return nextCellRectByPos(nextPos, innerDir);\n  }\n\n  return nextCellRectByPos(pos, dir);\n};\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/task-list/index.ts",
    "content": "import {\n  TaskItem,\n  TaskList as TiptapTaskList,\n  type TaskListOptions,\n} from \"@tiptap/extension-list\";\nimport { markRaw } from \"vue\";\nimport MingcuteListCheck3Line from \"~icons/mingcute/list-check-3-line\";\nimport type { Editor, Range } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionTaskListOptions = Partial<TaskListOptions> &\n  ExtensionOptions;\n\nexport const ExtensionTaskList =\n  TiptapTaskList.extend<ExtensionTaskListOptions>({\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getCommandMenuItems() {\n          return {\n            priority: 150,\n            icon: markRaw(MingcuteListCheck3Line),\n            title: \"editor.common.task_list\",\n            keywords: [\"tasklist\", \"renwuliebiao\"],\n            command: ({ editor, range }: { editor: Editor; range: Range }) => {\n              editor.chain().focus().deleteRange(range).toggleTaskList().run();\n            },\n          };\n        },\n      };\n    },\n    addExtensions() {\n      return [TaskItem];\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/text/BubbleItemTextType.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport LucideHeading1 from \"~icons/lucide/heading-1\";\nimport LucideHeading2 from \"~icons/lucide/heading-2\";\nimport LucideHeading3 from \"~icons/lucide/heading-3\";\nimport LucideHeading4 from \"~icons/lucide/heading-4\";\nimport LucideHeading5 from \"~icons/lucide/heading-5\";\nimport LucideHeading6 from \"~icons/lucide/heading-6\";\nimport MingcuteParagraphLine from \"~icons/mingcute/paragraph-line\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionHeading } from \"../heading\";\nimport { ExtensionParagraph } from \"../paragraph\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst typeOptions = [\n  {\n    text: i18n.global.t(\"editor.common.heading.paragraph\"),\n    icon: MingcuteParagraphLine,\n    action: () => props.editor.chain().focus().setParagraph().run(),\n    isActive: () => props.editor.isActive(ExtensionParagraph.name),\n  },\n  {\n    text: i18n.global.t(\"editor.common.heading.heading1\"),\n    icon: LucideHeading1,\n    action: () => props.editor.chain().focus().setHeading({ level: 1 }).run(),\n    isActive: () => props.editor.isActive(ExtensionHeading.name, { level: 1 }),\n  },\n  {\n    text: i18n.global.t(\"editor.common.heading.heading2\"),\n    icon: LucideHeading2,\n    action: () => props.editor.chain().focus().setHeading({ level: 2 }).run(),\n    isActive: () => props.editor.isActive(ExtensionHeading.name, { level: 2 }),\n  },\n  {\n    text: i18n.global.t(\"editor.common.heading.heading3\"),\n    icon: LucideHeading3,\n    action: () => props.editor.chain().focus().setHeading({ level: 3 }).run(),\n    isActive: () => props.editor.isActive(ExtensionHeading.name, { level: 3 }),\n  },\n  {\n    text: i18n.global.t(\"editor.common.heading.heading4\"),\n    icon: LucideHeading4,\n    action: () => props.editor.chain().focus().setHeading({ level: 4 }).run(),\n    isActive: () => props.editor.isActive(ExtensionHeading.name, { level: 4 }),\n  },\n  {\n    text: i18n.global.t(\"editor.common.heading.heading5\"),\n    icon: LucideHeading5,\n    action: () => props.editor.chain().focus().setHeading({ level: 5 }).run(),\n    isActive: () => props.editor.isActive(ExtensionHeading.name, { level: 5 }),\n  },\n  {\n    text: i18n.global.t(\"editor.common.heading.heading6\"),\n    icon: LucideHeading6,\n    action: () => props.editor.chain().focus().setHeading({ level: 6 }).run(),\n    isActive: () => props.editor.isActive(ExtensionHeading.name, { level: 6 }),\n  },\n];\n\nconst currentType = computed(() => {\n  return typeOptions.find((option) => option.isActive?.());\n});\n</script>\n<template>\n  <VDropdown class=\"inline-flex\" :auto-hide=\"true\" :distance=\"10\">\n    <BubbleButton\n      :title=\"i18n.global.t('editor.common.heading.title')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <component :is=\"currentType?.icon\" />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <div class=\"relative max-h-72 w-56 overflow-hidden overflow-y-auto\">\n        <DropdownItem\n          v-for=\"option in typeOptions\"\n          :key=\"option.text\"\n          v-close-popper\n          :is-active=\"option.isActive?.()\"\n          @click=\"option.action?.()\"\n        >\n          <template #icon>\n            <component :is=\"option.icon\" />\n          </template>\n          {{ option.text }}\n        </DropdownItem>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/text/index.ts",
    "content": "import { Text as TiptapText } from \"@tiptap/extension-text\";\nimport { markRaw } from \"vue\";\nimport MingcuteBoldLine from \"~icons/mingcute/bold-line\";\nimport MingcuteCodeLine from \"~icons/mingcute/code-line\";\nimport MingcuteItalicLine from \"~icons/mingcute/italic-line\";\nimport MingcuteMarkPenLine from \"~icons/mingcute/mark-pen-line\";\nimport MingcuteShare3Line from \"~icons/mingcute/share-3-line\";\nimport MingcuteStrikethroughLine from \"~icons/mingcute/strikethrough-line\";\nimport MingcuteTextColorLine from \"~icons/mingcute/text-color-line\";\nimport MingcuteUnderlineLine from \"~icons/mingcute/underline-line\";\nimport MingcuteUnlinkLine from \"~icons/mingcute/unlink-line\";\nimport PhTextSubscript from \"~icons/ph/text-subscript\";\nimport PhTextSuperscript from \"~icons/ph/text-superscript\";\nimport BlockActionSeparator from \"@/components/block/BlockActionSeparator.vue\";\nimport ColorBubbleItem from \"@/extensions/color/ColorBubbleItem.vue\";\nimport HighlightBubbleItem from \"@/extensions/highlight/HighlightBubbleItem.vue\";\nimport LinkBubbleButton from \"@/extensions/link/LinkBubbleButton.vue\";\nimport { RangeSelection } from \"@/extensions/range-selection\";\nimport { i18n } from \"@/locales\";\nimport { PluginKey, type EditorState } from \"@/tiptap/pm\";\nimport { isActive, isTextSelection } from \"@/tiptap/vue-3\";\nimport type { ExtensionOptions, NodeBubbleMenuType } from \"@/types\";\nimport { ExtensionBold } from \"../bold\";\nimport { ExtensionCode } from \"../code\";\nimport { ExtensionColor } from \"../color\";\nimport { ExtensionHighlight } from \"../highlight\";\nimport { ExtensionItalic } from \"../italic\";\nimport { ExtensionLink } from \"../link\";\nimport { ExtensionStrike } from \"../strike\";\nimport { ExtensionSubscript } from \"../subscript\";\nimport { ExtensionSuperscript } from \"../superscript\";\nimport { ExtensionUnderline } from \"../underline\";\nimport BubbleItemTextType from \"./BubbleItemTextType.vue\";\n\nconst OTHER_BUBBLE_MENU_TYPES = [\n  \"audio\",\n  \"video\",\n  \"image\",\n  \"iframe\",\n  \"codeBlock\",\n];\n\nexport const TEXT_BUBBLE_MENU_KEY = new PluginKey(\"textBubbleMenu\");\n\nexport type ExtensionTextOptions = ExtensionOptions;\n\nexport const ExtensionText = TiptapText.extend<ExtensionTextOptions>({\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      getBubbleMenu(): NodeBubbleMenuType {\n        return {\n          pluginKey: TEXT_BUBBLE_MENU_KEY,\n          shouldShow: ({ state, from, to }) => {\n            const { doc, selection } = state as EditorState;\n            const { empty } = selection;\n            if (empty) {\n              return false;\n            }\n\n            if (\n              OTHER_BUBBLE_MENU_TYPES.some((type) =>\n                isActive(state as EditorState, type)\n              )\n            ) {\n              return false;\n            }\n\n            const isEmptyTextBlock =\n              doc.textBetween(from || 0, to || 0).length === 0;\n\n            if (isEmptyTextBlock) {\n              return false;\n            }\n\n            if (\n              !isTextSelection(selection) &&\n              !(selection instanceof RangeSelection)\n            ) {\n              return false;\n            }\n\n            return true;\n          },\n          items: [\n            {\n              priority: 10,\n              component: markRaw(BubbleItemTextType),\n            },\n            {\n              priority: 20,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 30,\n              props: {\n                isActive: ({ editor }) => editor.isActive(ExtensionBold.name),\n                icon: markRaw(MingcuteBoldLine),\n                title: i18n.global.t(\"editor.common.bold\"),\n                action: ({ editor }) => {\n                  editor.chain().focus().toggleBold().run();\n                },\n              },\n            },\n            {\n              priority: 40,\n              props: {\n                isActive: ({ editor }) => editor.isActive(ExtensionItalic.name),\n                icon: markRaw(MingcuteItalicLine),\n                title: i18n.global.t(\"editor.common.italic\"),\n                action: ({ editor }) => {\n                  editor.chain().focus().toggleItalic().run();\n                },\n              },\n            },\n            {\n              priority: 50,\n              props: {\n                isActive: ({ editor }) =>\n                  editor.isActive(ExtensionUnderline.name),\n                icon: markRaw(MingcuteUnderlineLine),\n                title: i18n.global.t(\"editor.common.underline\"),\n                action: ({ editor }) =>\n                  editor.chain().focus().toggleUnderline().run(),\n              },\n            },\n            {\n              priority: 60,\n              props: {\n                isActive: ({ editor }) => editor.isActive(ExtensionStrike.name),\n                icon: markRaw(MingcuteStrikethroughLine),\n                title: i18n.global.t(\"editor.common.strike\"),\n                action: ({ editor }) =>\n                  editor.chain().focus().toggleStrike().run(),\n              },\n            },\n            {\n              priority: 70,\n              component: markRaw(HighlightBubbleItem),\n              props: {\n                isActive: ({ editor }) =>\n                  editor.isActive(ExtensionHighlight.name),\n                icon: markRaw(MingcuteMarkPenLine),\n                title: i18n.global.t(\"editor.common.highlight\"),\n              },\n            },\n            {\n              priority: 80,\n              component: markRaw(ColorBubbleItem),\n              props: {\n                isActive: ({ editor }) => editor.isActive(ExtensionColor.name),\n                icon: markRaw(MingcuteTextColorLine),\n                title: i18n.global.t(\"editor.common.color\"),\n              },\n            },\n            {\n              priority: 90,\n              props: {\n                isActive: ({ editor }) => editor.isActive(ExtensionCode.name),\n                icon: markRaw(MingcuteCodeLine),\n                title: i18n.global.t(\"editor.common.code\"),\n                action: ({ editor }) =>\n                  editor.chain().focus().toggleCode().run(),\n              },\n            },\n            {\n              priority: 100,\n              props: {\n                isActive: ({ editor }) =>\n                  editor.isActive(ExtensionSuperscript.name),\n                icon: markRaw(PhTextSuperscript),\n                title: i18n.global.t(\"editor.common.superscript\"),\n                action: ({ editor }) =>\n                  editor.chain().focus().toggleSuperscript().run(),\n              },\n            },\n            {\n              priority: 110,\n              props: {\n                isActive: ({ editor }) =>\n                  editor.isActive(ExtensionSubscript.name),\n                icon: markRaw(PhTextSubscript),\n                title: i18n.global.t(\"editor.common.subscript\"),\n                action: ({ editor }) =>\n                  editor.chain().focus().toggleSubscript().run(),\n              },\n            },\n            {\n              priority: 120,\n              component: markRaw(LinkBubbleButton),\n              props: {\n                isActive: ({ editor }) => editor.isActive(ExtensionLink.name),\n              },\n            },\n            {\n              priority: 130,\n              props: {\n                isActive: () => false,\n                visible: ({ editor }) => editor.isActive(ExtensionLink.name),\n                icon: markRaw(MingcuteUnlinkLine),\n                title: i18n.global.t(\"editor.extensions.link.cancel_link\"),\n                action: ({ editor }) => editor.commands.unsetLink(),\n              },\n            },\n            {\n              priority: 140,\n              props: {\n                isActive: () => false,\n                visible: ({ editor }) => editor.isActive(ExtensionLink.name),\n                icon: markRaw(MingcuteShare3Line),\n                title: i18n.global.t(\"editor.common.tooltip.open_link\"),\n                action: ({ editor }) => {\n                  const attrs = editor.getAttributes(ExtensionLink.name);\n                  if (attrs?.href) {\n                    window.open(attrs.href, \"_blank\");\n                  }\n                },\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/text-align/index.ts",
    "content": "import TiptapTextAlign, {\n  type TextAlignOptions,\n} from \"@tiptap/extension-text-align\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionTextAlignOptions = ExtensionOptions &\n  Partial<TextAlignOptions>;\n\nexport const ExtensionTextAlign =\n  TiptapTextAlign.extend<ExtensionTextAlignOptions>().configure({\n    types: [\"heading\", \"paragraph\"],\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/text-style/index.ts",
    "content": "import {\n  TextStyleKit,\n  type TextStyleKitOptions,\n} from \"@tiptap/extension-text-style\";\n\nexport type ExtensionTextStyleOptions = Partial<TextStyleKitOptions>;\n\nexport const ExtensionTextStyle =\n  TextStyleKit.extend<ExtensionTextStyleOptions>({\n    // Set the priority of this extension to 110 to ensure it loads before other extensions.\n    // It must load before the highlight plugin, otherwise, it will cause span and mark to display in parallel.\n    priority: 110,\n  }).configure({\n    backgroundColor: {\n      types: [\"textStyle\"],\n    },\n    color: {\n      types: [\"textStyle\"],\n    },\n    fontFamily: {\n      types: [\"textStyle\"],\n    },\n    fontSize: {\n      types: [\"textStyle\"],\n    },\n    lineHeight: {\n      types: [\"textStyle\"],\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/trailing-node/index.ts",
    "content": "import { Plugin, PluginKey } from \"@/tiptap/pm\";\nimport { Extension } from \"@/tiptap/vue-3\";\n\n/**\n * @param {object} args Arguments as deconstructable object\n * @param {Array | object} args.types possible types\n * @param {object} args.node node to check\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction nodeEqualsType({ types, node }: { types: any; node: any }) {\n  return (\n    (Array.isArray(types) && types.includes(node.type)) || node.type === types\n  );\n}\n\n/**\n * Extension based on:\n * - https://github.com/ueberdosis/tiptap/tree/main/demos/src/Experiments/TrailingNode\n * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js\n * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts\n */\n\nexport const ExtensionTrailingNode = Extension.create({\n  name: \"trailingNode\",\n\n  addOptions() {\n    return {\n      node: \"paragraph\",\n      notAfter: [\"paragraph\"],\n    };\n  },\n\n  addProseMirrorPlugins() {\n    const plugin = new PluginKey(this.name);\n    const disabledNodes = Object.entries(this.editor.schema.nodes)\n      .map(([, value]) => value)\n      .filter((node) => this.options.notAfter.includes(node.name));\n\n    const isEditable = this.editor.isEditable;\n\n    return [\n      new Plugin({\n        key: plugin,\n        appendTransaction: (_, __, state) => {\n          if (!isEditable) return;\n\n          const { doc, tr, schema } = state;\n          const shouldInsertNodeAtEnd = plugin.getState(state);\n          const endPosition = doc.content.size;\n          const type = schema.nodes[this.options.node];\n\n          if (!shouldInsertNodeAtEnd) {\n            return;\n          }\n\n          return tr.insert(endPosition, type.create());\n        },\n        state: {\n          init: (_, state) => {\n            if (!isEditable) return false;\n            const lastNode = state.tr.doc.lastChild;\n            return !nodeEqualsType({ node: lastNode, types: disabledNodes });\n          },\n          apply: (tr, value) => {\n            if (!isEditable) return value;\n\n            if (!tr.docChanged) {\n              return value;\n            }\n\n            const lastNode = tr.doc.lastChild;\n            return !nodeEqualsType({ node: lastNode, types: disabledNodes });\n          },\n        },\n      }),\n    ];\n  },\n});\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/underline/index.ts",
    "content": "import TiptapUnderline, {\n  type UnderlineOptions,\n} from \"@tiptap/extension-underline\";\nimport { markRaw } from \"vue\";\nimport MingcuteUnderlineLine from \"~icons/mingcute/underline-line\";\nimport ToolbarItem from \"@/components/toolbar/ToolbarItem.vue\";\nimport { i18n } from \"@/locales\";\nimport type { Editor } from \"@/tiptap\";\nimport type { ExtensionOptions } from \"@/types\";\n\nexport type ExtensionUnderlineOptions = ExtensionOptions &\n  Partial<UnderlineOptions>;\n\nexport const ExtensionUnderline =\n  TiptapUnderline.extend<ExtensionUnderlineOptions>({\n    addOptions() {\n      return {\n        ...this.parent?.(),\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 60,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: editor.isActive(TiptapUnderline.name),\n              icon: markRaw(MingcuteUnderlineLine),\n              title: i18n.global.t(\"editor.common.underline\"),\n              action: () => editor.chain().focus().toggleUnderline().run(),\n            },\n          };\n        },\n      };\n    },\n  });\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/upload/index.ts",
    "content": "import { Dialog, Toast } from \"@halo-dev/components\";\nimport { i18n } from \"@/locales\";\nimport { Editor, Extension, Plugin, PluginKey, PMNode, Slice } from \"@/tiptap\";\nimport {\n  batchUploadExternalLink,\n  containsFileClipboardIdentifier,\n  handleFileEvent,\n  isExternalAsset,\n} from \"@/utils/upload\";\nimport { ExtensionAudio } from \"../audio\";\nimport { ExtensionImage } from \"../image\";\nimport { ExtensionVideo } from \"../video\";\n\nexport const ExtensionUpload = Extension.create({\n  name: \"upload\",\n\n  addProseMirrorPlugins() {\n    const { editor }: { editor: Editor } = this;\n\n    return [\n      new Plugin({\n        key: new PluginKey(\"upload\"),\n        props: {\n          handlePaste: (view, event: ClipboardEvent, slice: Slice) => {\n            if (view.props.editable && !view.props.editable(view.state)) {\n              return false;\n            }\n\n            if (!event.clipboardData) {\n              return false;\n            }\n\n            const externalNodes = getAllExternalNodes(slice);\n            if (externalNodes.length > 0) {\n              Dialog.info({\n                title: i18n.global.t(\"editor.common.text.tip\"),\n                description: i18n.global.t(\n                  \"editor.extensions.upload.operations.transfer_in_batch.description\"\n                ),\n                confirmText: i18n.global.t(\"editor.common.button.confirm\"),\n                cancelText: i18n.global.t(\"editor.common.button.cancel\"),\n                async onConfirm() {\n                  await batchUploadExternalLink(editor, externalNodes);\n\n                  Toast.success(\n                    i18n.global.t(\"editor.common.toast.save_success\")\n                  );\n                },\n              });\n            }\n\n            const types = event.clipboardData.types;\n            if (!containsFileClipboardIdentifier(types)) {\n              return false;\n            }\n\n            // If the copied content is Excel, do not process it.\n            if (isExcelPasted(event.clipboardData)) {\n              return false;\n            }\n\n            const files = Array.from(event.clipboardData.files);\n\n            if (files.length) {\n              event.preventDefault();\n              handleFileEvent(editor, files);\n              return true;\n            }\n\n            return false;\n          },\n          handleDrop: (view, event) => {\n            if (view.props.editable && !view.props.editable(view.state)) {\n              return false;\n            }\n\n            if (!event.dataTransfer) {\n              return false;\n            }\n\n            const hasFiles = event.dataTransfer.files.length > 0;\n            if (!hasFiles) {\n              return false;\n            }\n\n            event.preventDefault();\n\n            const files = Array.from(event.dataTransfer.files) as File[];\n            if (files.length) {\n              event.preventDefault();\n              // TODO: For drag-and-drop uploaded files,\n              // perhaps it is necessary to determine the\n              // current position of the drag-and-drop\n              // instead of inserting them directly at the cursor.\n              handleFileEvent(editor, files);\n              return true;\n            }\n\n            return false;\n          },\n        },\n      }),\n    ];\n  },\n});\n\nfunction isExcelPasted(clipboardData: ClipboardEvent[\"clipboardData\"]) {\n  if (!clipboardData) {\n    return false;\n  }\n\n  const types = clipboardData.types;\n  if (\n    types.includes(\"application/vnd.ms-excel\") ||\n    types.includes(\n      \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n    )\n  ) {\n    return true;\n  }\n\n  if (types.includes(\"text/html\")) {\n    try {\n      const html = clipboardData.getData(\"text/html\");\n      if (\n        html.includes('ProgId=\"Excel.Sheet\"') ||\n        html.includes('xmlns:x=\"urn:schemas-microsoft-com:office:excel\"') ||\n        html.includes(\"urn:schemas-microsoft-com:office:spreadsheet\") ||\n        html.includes(\"<x:ExcelWorkbook>\")\n      ) {\n        return true;\n      }\n    } catch (e) {\n      console.warn(\"Failed to read clipboard HTML data:\", e);\n    }\n  }\n\n  return false;\n}\n\nexport function getAllExternalNodes(\n  slice: Slice\n): { node: PMNode; pos: number; index: number; parent: PMNode | null }[] {\n  const externalNodes: {\n    node: PMNode;\n    pos: number;\n    index: number;\n    parent: PMNode | null;\n  }[] = [];\n  slice.content.descendants((node, pos, parent, index) => {\n    if (\n      [ExtensionAudio.name, ExtensionVideo.name, ExtensionImage.name].includes(\n        node.type.name\n      )\n    ) {\n      if (isExternalAsset(node.attrs.src)) {\n        externalNodes.push({\n          node,\n          pos,\n          parent,\n          index,\n        });\n      }\n    }\n  });\n  return externalNodes;\n}\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/video/BubbleItemVideoLink.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport Input from \"@/components/base/Input.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionVideo } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst src = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionVideo.name).src;\n  },\n  set: (src: string) => {\n    props.editor\n      .chain()\n      .updateAttributes(ExtensionVideo.name, { src: src })\n      .run();\n  },\n});\n</script>\n\n<template>\n  <div class=\"w-80\">\n    <Input\n      v-model=\"src\"\n      auto-focus\n      :placeholder=\"i18n.global.t('editor.common.placeholder.link_input')\"\n      :label=\"i18n.global.t('editor.extensions.video.src_input_label')\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/video/BubbleItemVideoPosition.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { findParentNode } from \"@tiptap/core\";\nimport { computed } from \"vue\";\nimport MingcuteAlignCenterLine from \"~icons/mingcute/align-center-line\";\nimport MingcuteAlignLeftLine from \"~icons/mingcute/align-left-line\";\nimport MingcuteAlignRightLine from \"~icons/mingcute/align-right-line\";\nimport DropdownItem from \"@/components/base/DropdownItem.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionVideo } from \"..\";\n\nconst props = withDefaults(defineProps<BubbleItemComponentProps>(), {\n  visible: () => true,\n});\n\nconst positionOptions = [\n  {\n    text: i18n.global.t(\"editor.common.align_left\"),\n    value: \"start\",\n    icon: MingcuteAlignLeftLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_center\"),\n    value: \"center\",\n    icon: MingcuteAlignCenterLine,\n  },\n  {\n    text: i18n.global.t(\"editor.common.align_right\"),\n    value: \"end\",\n    icon: MingcuteAlignRightLine,\n  },\n];\n\nconst currentPosition = computed(() => {\n  const positionAttribute = props.editor.getAttributes(\n    ExtensionVideo.name\n  ).position;\n\n  const positionOption = positionOptions.find(\n    (option) => option.value === positionAttribute\n  );\n\n  if (!positionOption) {\n    return positionOptions[0];\n  }\n\n  return positionOption;\n});\n\nconst isActive = (blockPosition: string) => {\n  const videoParent = findParentNode(\n    (node) => node.type.name === ExtensionVideo.name\n  )(props.editor.state.selection);\n  if (!videoParent) {\n    return false;\n  }\n  return videoParent.node.attrs.alignItems === blockPosition;\n};\n\nconst handleSetPosition = (position: string) => {\n  return props.editor.chain().focus().setBlockPosition(position).run();\n};\n</script>\n<template>\n  <VDropdown\n    v-if=\"visible({ editor })\"\n    class=\"inline-flex\"\n    :auto-hide=\"true\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.common.align_method')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <component :is=\"currentPosition.icon\" />\n      </template>\n    </BubbleButton>\n\n    <template #popper>\n      <div class=\"relative max-h-72 w-56 overflow-hidden overflow-y-auto\">\n        <DropdownItem\n          v-for=\"option in positionOptions\"\n          :key=\"option.value\"\n          v-close-popper\n          :is-active=\"isActive(option.value)\"\n          @click=\"handleSetPosition(option.value)\"\n        >\n          <template #icon>\n            <component :is=\"option.icon\" />\n          </template>\n          {{ option.text }}\n        </DropdownItem>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/video/BubbleItemVideoSize.vue",
    "content": "<script setup lang=\"ts\">\nimport { VDropdown } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\nimport MdiImageSizeSelectActual from \"~icons/mdi/image-size-select-actual\";\nimport MdiImageSizeSelectLarge from \"~icons/mdi/image-size-select-large\";\nimport MdiImageSizeSelectSmall from \"~icons/mdi/image-size-select-small\";\nimport Input from \"@/components/base/Input.vue\";\nimport BubbleButton from \"@/components/bubble/BubbleButton.vue\";\nimport { i18n } from \"@/locales\";\nimport type { BubbleItemComponentProps } from \"@/types\";\nimport { ExtensionVideo } from \"./index\";\n\nconst props = defineProps<BubbleItemComponentProps>();\n\nconst width = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionVideo.name).width;\n  },\n  set: (width: string) => {\n    handleSetSize({ width, height: height.value });\n  },\n});\n\nconst height = computed({\n  get: () => {\n    return props.editor.getAttributes(ExtensionVideo.name).height;\n  },\n  set: (height: string) => {\n    handleSetSize({ width: width.value, height });\n  },\n});\n\nfunction handleSetSize(size: { width?: string; height?: string }) {\n  props.editor\n    .chain()\n    .updateAttributes(ExtensionVideo.name, size)\n    .setNodeSelection(props.editor.state.selection.from)\n    .focus()\n    .run();\n}\n\nconst presetSizes = [\n  {\n    width: \"25%\",\n    height: \"auto\",\n    icon: MdiImageSizeSelectSmall,\n    title: i18n.global.t(\"editor.extensions.video.small_size\"),\n  },\n  {\n    width: \"50%\",\n    height: \"auto\",\n    icon: MdiImageSizeSelectLarge,\n    title: i18n.global.t(\"editor.extensions.video.medium_size\"),\n  },\n  {\n    width: \"100%\",\n    height: \"auto\",\n    icon: MdiImageSizeSelectActual,\n    title: i18n.global.t(\"editor.extensions.video.large_size\"),\n  },\n];\n</script>\n\n<template>\n  <VDropdown\n    v-if=\"visible?.({ editor })\"\n    class=\"inline-flex\"\n    :auto-hide=\"true\"\n    :distance=\"10\"\n  >\n    <BubbleButton\n      :title=\"i18n.global.t('editor.extensions.video.resize')\"\n      show-more-indicator\n    >\n      <template #icon>\n        <MdiImageSizeSelectSmall />\n      </template>\n    </BubbleButton>\n    <template #popper>\n      <div class=\"flex w-56 flex-col gap-3\">\n        <div class=\"flex flex-col items-center gap-3\">\n          <Input\n            v-model=\"width\"\n            :label=\"i18n.global.t('editor.common.width')\"\n            :tooltip=\"i18n.global.t('editor.common.tooltip.custom_width_input')\"\n          />\n          <Input\n            v-model=\"height\"\n            :label=\"i18n.global.t('editor.common.height')\"\n            :tooltip=\"\n              i18n.global.t('editor.common.tooltip.custom_height_input')\n            \"\n          />\n        </div>\n\n        <div class=\"flex items-center gap-1 rounded-md bg-gray-100 p-1\">\n          <button\n            v-for=\"item in presetSizes\"\n            :key=\"item.width\"\n            v-tooltip=\"item.title\"\n            class=\"inline-flex flex-1 items-center justify-center rounded px-2 py-1.5 text-gray-600 transition-all hover:text-gray-900\"\n            :class=\"{\n              'bg-white text-gray-900':\n                item.width === width && item.height === height,\n            }\"\n            @click=\"handleSetSize({ width: item.width, height: item.height })\"\n          >\n            <component :is=\"item.icon\" />\n          </button>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/video/VideoView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton } from \"@halo-dev/components\";\nimport { utils, type AttachmentSimple } from \"@halo-dev/ui-shared\";\nimport { computed, ref } from \"vue\";\nimport MingcuteVideoLine from \"~icons/mingcute/video-line\";\nimport { EditorLinkObtain } from \"@/components\";\nimport { ResourceReplaceButton } from \"@/components/upload\";\nimport { useExternalAssetsTransfer } from \"@/composables/use-attachment\";\nimport { i18n } from \"@/locales\";\nimport {\n  findParentNodeClosestToPos,\n  NodeViewWrapper,\n  type NodeViewProps,\n} from \"@/tiptap\";\nimport { ExtensionFigure } from \"../figure\";\n\nconst props = defineProps<NodeViewProps>();\n\nconst src = computed({\n  get: () => {\n    return props.node?.attrs.src;\n  },\n  set: (src: string) => {\n    props.updateAttributes({ src: src });\n  },\n});\n\nconst controls = computed(() => {\n  return props.node.attrs.controls;\n});\n\nconst autoplay = computed(() => {\n  return props.node.attrs.autoplay;\n});\n\nconst loop = computed(() => {\n  return props.node.attrs.loop;\n});\n\nconst initialization = computed(() => {\n  return !src.value;\n});\n\nconst editorLinkObtain = ref();\n\nconst handleSetExternalLink = (attachment?: AttachmentSimple) => {\n  if (!attachment) return;\n  props.updateAttributes({\n    src: attachment.url,\n  });\n};\n\nconst resetUpload = () => {\n  const { file } = props.node.attrs;\n  if (file) {\n    props.updateAttributes({\n      width: undefined,\n      height: undefined,\n      file: undefined,\n    });\n  }\n};\n\nconst handleUploadRetry = () => {\n  editorLinkObtain.value?.reset();\n};\n\nconst handleUploadAbort = () => {\n  editorLinkObtain.value?.abort();\n};\n\nconst { isExternalAsset, transferring, handleTransfer } =\n  useExternalAssetsTransfer(src, handleSetExternalLink);\n\nconst isPercentageWidth = computed(() => {\n  return props.node?.attrs.width?.includes(\"%\");\n});\n\n// Get the align items of the image from the figure parent\nconst alignItems = computed(() => {\n  const pos = props.getPos();\n  if (!pos) {\n    return \"start\";\n  }\n  const $pos = props.editor.state.doc.resolve(pos);\n  const figureParent = findParentNodeClosestToPos(\n    $pos,\n    (node) => node.type.name === ExtensionFigure.name\n  );\n\n  if (figureParent) {\n    return figureParent.node.attrs.alignItems;\n  }\n\n  return \"start\";\n});\n</script>\n\n<template>\n  <node-view-wrapper\n    as=\"div\"\n    class=\"flex w-full\"\n    :class=\"{\n      [`justify-${alignItems}`]: true,\n    }\"\n  >\n    <div\n      class=\"relative inline-block h-full max-w-full overflow-hidden rounded-md text-center transition-all\"\n      :class=\"{\n        'rounded ring-2': selected,\n      }\"\n      :style=\"{\n        width: initialization ? '100%' : node.attrs.width,\n        height: initialization ? '100%' : node.attrs.height,\n      }\"\n    >\n      <div v-if=\"src\" class=\"group relative\">\n        <video\n          :src=\"src\"\n          :controls=\"controls\"\n          :autoplay=\"autoplay\"\n          :loop=\"loop\"\n          playsinline\n          preload=\"metadata\"\n          class=\"m-0 rounded-md\"\n          :style=\"{\n            width: isPercentageWidth ? '100%' : node.attrs.width,\n            height: isPercentageWidth ? '100%' : node.attrs.height,\n          }\"\n        ></video>\n        <div\n          v-if=\"src\"\n          class=\"absolute left-0 top-0 hidden w-full cursor-pointer justify-end gap-2 bg-gradient-to-b from-gray-300 to-transparent p-2 ease-in-out group-hover:flex\"\n        >\n          <VButton\n            v-if=\"\n              utils.permission.has([\n                'uc:attachments:manage',\n                'system:attachments:manage',\n              ]) && isExternalAsset\n            \"\n            v-tooltip=\"\n              i18n.global.t(\n                'editor.extensions.upload.operations.transfer.tooltip'\n              )\n            \"\n            :loading=\"transferring\"\n            size=\"sm\"\n            ghost\n            @click=\"handleTransfer\"\n          >\n            {{\n              i18n.global.t(\n                \"editor.extensions.upload.operations.transfer.button\"\n              )\n            }}\n          </VButton>\n          <ResourceReplaceButton\n            accept=\"video/*\"\n            :original-link=\"src\"\n            :upload=\"extension.options.uploadVideo\"\n            @change=\"handleSetExternalLink\"\n          />\n        </div>\n      </div>\n      <div v-show=\"!src\" class=\"relative\">\n        <EditorLinkObtain\n          ref=\"editorLinkObtain\"\n          :accept=\"'video/*'\"\n          :editor=\"editor\"\n          :upload-to-attachment-file=\"extension.options.uploadVideo\"\n          :uploaded-file=\"node?.attrs.file\"\n          @set-external-link=\"handleSetExternalLink\"\n          @on-upload-finish=\"resetUpload\"\n          @on-upload-abort=\"resetUpload\"\n        >\n          <template #icon>\n            <MingcuteVideoLine class=\"text-xl text-primary\" />\n          </template>\n          <template #uploading=\"{ progress }\">\n            <div class=\"absolute top-0 size-full bg-black bg-opacity-20\">\n              <div class=\"absolute top-[50%] w-full space-y-2 text-white\">\n                <div class=\"px-10\">\n                  <div\n                    class=\"relative h-4 w-full overflow-hidden rounded-full bg-gray-200\"\n                  >\n                    <div\n                      class=\"h-full bg-primary\"\n                      :style=\"{\n                        width: `${progress || 0}%`,\n                      }\"\n                    ></div>\n                    <div\n                      class=\"absolute left-[50%] top-0 -translate-x-[50%] text-xs leading-4 text-white\"\n                    >\n                      {{\n                        progress\n                          ? `${progress}%`\n                          : `${i18n.global.t(\"editor.extensions.upload.loading\")}...`\n                      }}\n                    </div>\n                  </div>\n                </div>\n\n                <div\n                  class=\"inline-block cursor-pointer text-sm hover:opacity-70\"\n                  @click=\"handleUploadAbort\"\n                >\n                  {{ i18n.global.t(\"editor.common.button.cancel\") }}\n                </div>\n              </div>\n            </div>\n          </template>\n          <template #error>\n            <div class=\"absolute top-0 size-full bg-black bg-opacity-20\">\n              <div class=\"absolute top-[50%] w-full space-y-2 text-white\">\n                <div class=\"px-10\">\n                  <div\n                    class=\"relative h-4 w-full overflow-hidden rounded-full bg-gray-200\"\n                  >\n                    <div class=\"size-full bg-red-600\"></div>\n                    <div\n                      class=\"absolute left-[50%] top-0 -translate-x-[50%] text-xs leading-4 text-white\"\n                    >\n                      {{ i18n.global.t(\"editor.extensions.upload.error\") }}\n                    </div>\n                  </div>\n                </div>\n                <div\n                  class=\"inline-block cursor-pointer text-sm hover:opacity-70\"\n                  @click=\"handleUploadRetry\"\n                >\n                  {{ i18n.global.t(\"editor.extensions.upload.click_retry\") }}\n                </div>\n              </div>\n            </div>\n          </template>\n        </EditorLinkObtain>\n      </div>\n    </div>\n  </node-view-wrapper>\n</template>\n"
  },
  {
    "path": "ui/packages/editor/src/extensions/video/index.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { isEmpty } from \"es-toolkit/compat\";\nimport { markRaw } from \"vue\";\nimport LucideCaptions from \"~icons/lucide/captions\";\nimport MdiCogPlay from \"~icons/mdi/cog-play\";\nimport MdiCogPlayOutline from \"~icons/mdi/cog-play-outline\";\nimport MdiMotionPlay from \"~icons/mdi/motion-play\";\nimport MdiMotionPlayOutline from \"~icons/mdi/motion-play-outline\";\nimport MdiPlayCircle from \"~icons/mdi/play-circle\";\nimport MdiPlayCircleOutline from \"~icons/mdi/play-circle-outline\";\nimport MingcuteLinkLine from \"~icons/mingcute/link-line\";\nimport MingcuteShare3Line from \"~icons/mingcute/share-3-line\";\nimport MingcuteVideoLine from \"~icons/mingcute/video-line\";\nimport { BlockActionSeparator } from \"@/components\";\nimport MingcuteDelete2Line from \"@/components/icon/MingcuteDelete2Line.vue\";\nimport ToolboxItem from \"@/components/toolbox/ToolboxItem.vue\";\nimport { i18n } from \"@/locales\";\nimport {\n  Editor,\n  findChildren,\n  findParentNode,\n  isActive,\n  mergeAttributes,\n  Node,\n  nodeInputRule,\n  Plugin,\n  PluginKey,\n  TextSelection,\n  VueNodeViewRenderer,\n  type EditorState,\n  type Range,\n} from \"@/tiptap\";\nimport type { ExtensionOptions, NodeBubbleMenuType } from \"@/types\";\nimport { deleteNode } from \"@/utils\";\nimport { ExtensionFigure } from \"../figure\";\nimport { ExtensionFigureCaption } from \"../figure/figure-caption\";\nimport { ExtensionParagraph } from \"../paragraph\";\nimport BubbleItemVideoLink from \"./BubbleItemVideoLink.vue\";\nimport BubbleItemVideoPosition from \"./BubbleItemVideoPosition.vue\";\nimport BubbleItemVideoSize from \"./BubbleItemVideoSize.vue\";\nimport VideoView from \"./VideoView.vue\";\ndeclare module \"@/tiptap\" {\n  interface Commands<ReturnType> {\n    video: {\n      setVideo: (options: { src: string }) => ReturnType;\n    };\n  }\n}\n\nexport const VIDEO_BUBBLE_MENU_KEY = new PluginKey(\"videoBubbleMenu\");\n\nexport type ExtensionVideoOptions = ExtensionOptions & {\n  uploadVideo?: (\n    file: File,\n    options?: AxiosRequestConfig\n  ) => Promise<Attachment>;\n};\n\nexport const ExtensionVideo = Node.create<ExtensionVideoOptions>({\n  name: \"video\",\n  fakeSelection: true,\n\n  inline: false,\n\n  group: \"block\",\n\n  addAttributes() {\n    return {\n      ...this.parent?.(),\n      src: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"src\");\n        },\n      },\n      width: {\n        default: \"100%\",\n        parseHTML: (element) => {\n          return element.getAttribute(\"width\") || element.style.width || null;\n        },\n        renderHTML(attributes) {\n          return {\n            width: attributes.width,\n          };\n        },\n      },\n      height: {\n        default: \"auto\",\n        parseHTML: (element) => {\n          return element.getAttribute(\"height\") || element.style.height || null;\n        },\n        renderHTML: (attributes) => {\n          return {\n            height: attributes.height,\n          };\n        },\n      },\n      autoplay: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"autoplay\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            autoplay: attributes.autoplay,\n          };\n        },\n      },\n      controls: {\n        default: true,\n        parseHTML: (element) => {\n          return element.getAttribute(\"controls\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            controls: attributes.controls,\n          };\n        },\n      },\n      loop: {\n        default: null,\n        parseHTML: (element) => {\n          return element.getAttribute(\"loop\");\n        },\n        renderHTML: (attributes) => {\n          return {\n            loop: attributes.loop,\n          };\n        },\n      },\n      file: {\n        default: null,\n        renderHTML() {\n          return {};\n        },\n        parseHTML() {\n          return null;\n        },\n      },\n    };\n  },\n\n  parseHTML() {\n    return [\n      {\n        tag: \"video\",\n      },\n    ];\n  },\n\n  renderHTML({ HTMLAttributes }) {\n    return [\"video\", mergeAttributes(HTMLAttributes)];\n  },\n\n  addCommands() {\n    return {\n      setVideo:\n        (options) =>\n        ({ commands }) => {\n          return commands.insertContent({\n            type: this.name,\n            attrs: options,\n          });\n        },\n    };\n  },\n\n  addInputRules() {\n    return [\n      nodeInputRule({\n        find: /^\\$video\\$$/,\n        type: this.type,\n      }),\n    ];\n  },\n\n  addNodeView() {\n    return VueNodeViewRenderer(VideoView);\n  },\n\n  addProseMirrorPlugins() {\n    return [\n      new Plugin({\n        key: new PluginKey(\"videoLegacyFormat\"),\n        appendTransaction: (transactions, _oldState, newState) => {\n          const docChanged = transactions.some((tr) => tr.docChanged);\n          if (!docChanged) {\n            return null;\n          }\n\n          const tr = newState.tr;\n          let modified = false;\n\n          newState.doc.descendants((node, pos) => {\n            if (node.type.name !== ExtensionVideo.name) {\n              return;\n            }\n\n            const $pos = newState.doc.resolve(pos);\n            if ($pos.parent.type.name === ExtensionFigure.name) {\n              return;\n            }\n\n            let blockPosition = \"start\";\n            let deletePreviousNode = false;\n            let previousNodePos = -1;\n            let previousNodeSize = 0;\n\n            const previousNode = $pos.nodeBefore;\n            if (\n              previousNode &&\n              previousNode.type.name === ExtensionParagraph.name\n            ) {\n              if (previousNode.attrs.textAlign) {\n                const textAlignToBlockPositionMap: Record<string, string> = {\n                  left: \"start\",\n                  center: \"center\",\n                  right: \"end\",\n                  justify: \"center\",\n                };\n                blockPosition =\n                  textAlignToBlockPositionMap[previousNode.attrs.textAlign] ??\n                  \"start\";\n              }\n              if (previousNode.textContent?.trim().length === 0) {\n                deletePreviousNode = true;\n                previousNodePos = pos - previousNode.nodeSize;\n                previousNodeSize = previousNode.nodeSize;\n              }\n            }\n\n            const figureNode = newState.schema.nodes.figure.create(\n              {\n                contentType: \"video\",\n                alignItems: blockPosition,\n              },\n              [node]\n            );\n\n            if (deletePreviousNode) {\n              tr.delete(previousNodePos, previousNodePos + previousNodeSize);\n              tr.replaceRangeWith(\n                pos - previousNodeSize,\n                pos - previousNodeSize + node.nodeSize,\n                figureNode\n              );\n            } else {\n              tr.replaceRangeWith(pos, pos + node.nodeSize, figureNode);\n            }\n\n            modified = true;\n          });\n\n          return modified ? tr : null;\n        },\n      }),\n    ];\n  },\n\n  addOptions() {\n    return {\n      ...this.parent?.(),\n      uploadVideo: undefined,\n      getCommandMenuItems() {\n        return {\n          priority: 100,\n          icon: markRaw(MingcuteVideoLine),\n          title: \"editor.extensions.commands_menu.video\",\n          keywords: [\"video\", \"shipin\"],\n          command: ({ editor, range }: { editor: Editor; range: Range }) => {\n            editor\n              .chain()\n              .focus()\n              .deleteRange(range)\n              .insertContent([\n                {\n                  type: \"figure\",\n                  attrs: { contentType: \"video\" },\n                  content: [{ type: \"video\", attrs: { src: \"\" } }],\n                },\n                { type: \"paragraph\", content: \"\" },\n              ])\n              .run();\n          },\n        };\n      },\n      getToolboxItems({ editor }: { editor: Editor }) {\n        return [\n          {\n            priority: 20,\n            component: markRaw(ToolboxItem),\n            props: {\n              editor,\n              icon: markRaw(MingcuteVideoLine),\n              title: i18n.global.t(\"editor.extensions.commands_menu.video\"),\n              action: () => {\n                editor\n                  .chain()\n                  .focus()\n                  .insertContent([\n                    {\n                      type: \"figure\",\n                      attrs: { contentType: \"video\" },\n                      content: [{ type: \"video\", attrs: { src: \"\" } }],\n                    },\n                  ])\n                  .run();\n              },\n            },\n          },\n        ];\n      },\n      getBubbleMenu({ editor }: { editor: Editor }): NodeBubbleMenuType {\n        return {\n          pluginKey: VIDEO_BUBBLE_MENU_KEY,\n          shouldShow: ({ state }: { state: EditorState }) => {\n            return isActive(state, ExtensionVideo.name);\n          },\n          options: {\n            placement: \"top-start\",\n          },\n          items: [\n            {\n              priority: 10,\n              props: {\n                isActive: () =>\n                  editor.getAttributes(ExtensionVideo.name).controls,\n                icon: markRaw(\n                  editor.getAttributes(ExtensionVideo.name).controls\n                    ? MdiCogPlay\n                    : MdiCogPlayOutline\n                ),\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n                action: () => {\n                  return editor\n                    .chain()\n                    .updateAttributes(ExtensionVideo.name, {\n                      controls: editor.getAttributes(ExtensionVideo.name)\n                        .controls\n                        ? null\n                        : true,\n                    })\n                    .setNodeSelection(editor.state.selection.from)\n                    .focus()\n                    .run();\n                },\n                title: editor.getAttributes(ExtensionVideo.name).controls\n                  ? i18n.global.t(\"editor.extensions.video.disable_controls\")\n                  : i18n.global.t(\"editor.extensions.video.enable_controls\"),\n              },\n            },\n            {\n              priority: 20,\n              props: {\n                isActive: () => {\n                  return editor.getAttributes(ExtensionVideo.name).autoplay;\n                },\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n                icon: markRaw(\n                  editor.getAttributes(ExtensionVideo.name).autoplay\n                    ? MdiPlayCircle\n                    : MdiPlayCircleOutline\n                ),\n                action: () => {\n                  return editor\n                    .chain()\n                    .updateAttributes(ExtensionVideo.name, {\n                      autoplay: editor.getAttributes(ExtensionVideo.name)\n                        .autoplay\n                        ? null\n                        : true,\n                    })\n                    .setNodeSelection(editor.state.selection.from)\n                    .focus()\n                    .run();\n                },\n                title: editor.getAttributes(ExtensionVideo.name).autoplay\n                  ? i18n.global.t(\"editor.extensions.video.disable_autoplay\")\n                  : i18n.global.t(\"editor.extensions.video.enable_autoplay\"),\n              },\n            },\n            {\n              priority: 30,\n              props: {\n                isActive: () => {\n                  return editor.getAttributes(ExtensionVideo.name).loop;\n                },\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n                icon: markRaw(\n                  editor.getAttributes(ExtensionVideo.name).loop\n                    ? MdiMotionPlay\n                    : MdiMotionPlayOutline\n                ),\n                action: () => {\n                  editor\n                    .chain()\n                    .updateAttributes(ExtensionVideo.name, {\n                      loop: editor.getAttributes(ExtensionVideo.name).loop\n                        ? null\n                        : true,\n                    })\n                    .setNodeSelection(editor.state.selection.from)\n                    .focus()\n                    .run();\n                },\n                title: editor.getAttributes(ExtensionVideo.name).loop\n                  ? i18n.global.t(\"editor.extensions.video.disable_loop\")\n                  : i18n.global.t(\"editor.extensions.video.enable_loop\"),\n              },\n            },\n            {\n              priority: 40,\n              component: markRaw(BlockActionSeparator),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 50,\n              component: markRaw(BubbleItemVideoSize),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 60,\n              component: markRaw(BubbleItemVideoPosition),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 70,\n              component: markRaw(BlockActionSeparator),\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n              },\n            },\n            {\n              priority: 80,\n              props: {\n                icon: markRaw(MingcuteLinkLine),\n                title: i18n.global.t(\"editor.common.button.edit_link\"),\n                action: () => {\n                  return markRaw(BubbleItemVideoLink);\n                },\n              },\n            },\n            {\n              priority: 90,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n                icon: markRaw(MingcuteShare3Line),\n                title: i18n.global.t(\"editor.common.tooltip.open_link\"),\n                action: () => {\n                  window.open(\n                    editor.getAttributes(ExtensionVideo.name).src,\n                    \"_blank\"\n                  );\n                },\n              },\n            },\n            {\n              priority: 100,\n              props: {\n                visible({ editor }) {\n                  return !isEmpty(\n                    editor.getAttributes(ExtensionVideo.name).src\n                  );\n                },\n                icon: markRaw(LucideCaptions),\n                title: i18n.global.t(\"editor.extensions.video.edit_caption\"),\n                action: ({ editor }) => {\n                  const figureParent = findParentNode(\n                    (node) => node.type.name === ExtensionFigure.name\n                  )(editor.state.selection);\n\n                  if (!figureParent) {\n                    return;\n                  }\n\n                  const { node, pos } = figureParent;\n                  let captionPos = -1;\n\n                  node.forEach((child, offset) => {\n                    if (child.type.name === ExtensionFigureCaption.name) {\n                      captionPos = pos + offset + 1;\n                    }\n                  });\n\n                  if (captionPos !== -1) {\n                    editor.chain().focus().setTextSelection(captionPos).run();\n                    return;\n                  }\n                  const imageNodePos = findChildren(\n                    editor.state.selection.$from.node(),\n                    (node) => node.type.name === ExtensionVideo.name\n                  )[0];\n                  const figureCaptionNode =\n                    editor.schema.nodes.figureCaption.create({\n                      width: imageNodePos.node.attrs.width,\n                    });\n                  editor\n                    .chain()\n                    .focus()\n                    .command(({ tr }) => {\n                      const insertPos = pos + node.nodeSize - 1;\n                      tr.insert(insertPos, figureCaptionNode);\n                      tr.setSelection(\n                        TextSelection.near(tr.doc.resolve(insertPos + 1))\n                      );\n                      return true;\n                    })\n                    .run();\n                },\n              },\n            },\n            {\n              priority: 110,\n              component: markRaw(BlockActionSeparator),\n            },\n            {\n              priority: 120,\n              props: {\n                icon: markRaw(MingcuteDelete2Line),\n                title: i18n.global.t(\"editor.common.button.delete\"),\n                action: ({ editor }) => {\n                  const figureParent = findParentNode(\n                    (node) => node.type.name === \"figure\"\n                  )(editor.state.selection);\n\n                  deleteNode(\n                    figureParent ? \"figure\" : ExtensionVideo.name,\n                    editor\n                  );\n                },\n              },\n            },\n          ],\n        };\n      },\n    };\n  },\n});\n\nexport const getVideoElement = (editor: Editor): HTMLVideoElement | null => {\n  const { view, state } = editor;\n  const { from } = state.selection;\n\n  let domNode = view.nodeDOM(from);\n  if (!domNode && from > 0) {\n    const $pos = state.doc.resolve(from);\n    if ($pos.parent) {\n      domNode = view.domAtPos(from).node as HTMLElement;\n    }\n  }\n\n  if (domNode instanceof HTMLElement) {\n    let video = domNode.querySelector(\"video\");\n    if (video) {\n      return video;\n    }\n\n    if (domNode.tagName === \"VIDEO\") {\n      return domNode as HTMLVideoElement;\n    }\n\n    const parent = domNode.parentElement;\n    if (parent) {\n      video = parent.querySelector(\"video\");\n      if (video) {\n        return video;\n      }\n    }\n  }\n\n  return null;\n};\n\nexport const getVideoSizePercentage = (\n  editor: Editor,\n  percentage: number,\n  videoElement?: HTMLVideoElement | null\n): { width: number; height: number } | undefined => {\n  const element = videoElement || getVideoElement(editor);\n  if (!element || element.readyState < 1) {\n    return undefined;\n  }\n  const videoWidth = element.videoWidth;\n  const videoHeight = element.videoHeight;\n  const aspectRatio = videoWidth / videoHeight;\n  const newWidth = Math.round(videoWidth * (percentage / 100));\n  const newHeight = Math.round(newWidth / aspectRatio);\n  return { width: newWidth, height: newHeight };\n};\n\nexport const handleSetSize = (\n  editor: Editor,\n  size: { width?: string; height?: string }\n) => {\n  editor\n    .chain()\n    .updateAttributes(ExtensionVideo.name, size)\n    .setNodeSelection(editor.state.selection.from)\n    .focus()\n    .run();\n};\n"
  },
  {
    "path": "ui/packages/editor/src/index.ts",
    "content": "import \"github-markdown-css/github-markdown-light.css\";\nimport \"./styles/index.scss\";\nimport \"./styles/tailwind.css\";\n\nexport * from \"./components\";\nexport * from \"./extensions\";\nexport * from \"./tiptap\";\nexport * from \"./types\";\nexport {\n  convertToMediaContents,\n  deleteNode,\n  deleteNodeByPos,\n  filterDuplicateExtensions,\n  generateAnchor,\n  generateAnchorId,\n  isAllowedUri,\n  isBlockEmpty,\n  isEmpty,\n  isListActive,\n  isNodeContentEmpty,\n} from \"./utils\";\n"
  },
  {
    "path": "ui/packages/editor/src/locales/en.json",
    "content": "{\n  \"editor.drag.button.insert_block\": \"Insert block\",\n  \"editor.drag.button.drag_handle\": \"Drag handle\",\n  \"editor.drag.menu.convert_to\": \"Convert to\",\n  \"editor.drag.menu.copy\": \"Copy\",\n  \"editor.drag.menu.cut\": \"Cut\",\n  \"editor.drag.menu.delete\": \"Delete\",\n  \"editor.menus.undo\": \"Undo\",\n  \"editor.menus.redo\": \"Redo\",\n  \"editor.menus.table.title\": \"Table\",\n  \"editor.menus.table.add\": \"Add table\",\n  \"editor.menus.table.add_column_before\": \"Add column before\",\n  \"editor.menus.table.add_column_after\": \"Add column after\",\n  \"editor.menus.table.delete_column\": \"Delete column\",\n  \"editor.menus.table.add_row_before\": \"Add row before\",\n  \"editor.menus.table.add_row_after\": \"Add row after\",\n  \"editor.menus.table.toggle_header_column\": \"Set/Unset First Column Header\",\n  \"editor.menus.table.toggle_header_row\": \"Set/Unset First Row Header\",\n  \"editor.menus.table.toggle_header_cell\": \"Set/Unset Current Cell as Header\",\n  \"editor.menus.table.delete_row\": \"Delete row\",\n  \"editor.menus.table.merge_cells\": \"Merge cells\",\n  \"editor.menus.table.split_cell\": \"Split cell\",\n  \"editor.menus.table.delete_table\": \"Delete table\",\n  \"editor.extensions.commands_menu.columns\": \"Column Card\",\n  \"editor.extensions.commands_menu.iframe\": \"Iframe\",\n  \"editor.extensions.commands_menu.image\": \"Image\",\n  \"editor.extensions.commands_menu.video\": \"Video\",\n  \"editor.extensions.commands_menu.audio\": \"Audio\",\n  \"editor.extensions.commands_menu.table\": \"Table\",\n  \"editor.extensions.commands_menu.gallery\": \"Gallery\",\n  \"editor.extensions.commands_menu.no_results\": \"No results\",\n  \"editor.extensions.commands_menu.placeholder\": \"Enter / to select input type\",\n  \"editor.extensions.figure_caption.empty_placeholder\": \"Add description\",\n  \"editor.extensions.link.add_link\": \"Add link\",\n  \"editor.extensions.link.edit_link\": \"Edit link\",\n  \"editor.extensions.link.placeholder\": \"Link address\",\n  \"editor.extensions.link.open_in_new_window\": \"Open in new window\",\n  \"editor.extensions.link.cancel_link\": \"Cancel link\",\n  \"editor.extensions.link.nofollow\": \"Nofollow\",\n  \"editor.extensions.audio.src_input_label\": \"Audio link\",\n  \"editor.extensions.audio.disable_autoplay\": \"Disable auto play\",\n  \"editor.extensions.audio.enable_autoplay\": \"Enable auto play\",\n  \"editor.extensions.audio.disable_loop\": \"Disable loop\",\n  \"editor.extensions.audio.enable_loop\": \"Enable loop\",\n  \"editor.extensions.iframe.src_input_label\": \"Web link\",\n  \"editor.extensions.iframe.resize\": \"Resize\",\n  \"editor.extensions.iframe.disable_frameborder\": \"Hide frameborder\",\n  \"editor.extensions.iframe.enable_frameborder\": \"Show frameborder\",\n  \"editor.extensions.iframe.phone_size\": \"Mobile phone size\",\n  \"editor.extensions.iframe.tablet_vertical_size\": \"Tablet portrait size\",\n  \"editor.extensions.iframe.tablet_horizontal_size\": \"Tablet landscape size\",\n  \"editor.extensions.iframe.desktop_size\": \"Desktop size\",\n  \"editor.extensions.image.src_input_label\": \"Image link\",\n  \"editor.extensions.image.href_input_label\": \"Image hyperlink\",\n  \"editor.extensions.image.resize\": \"Resize\",\n  \"editor.extensions.image.small_size\": \"Small size\",\n  \"editor.extensions.image.medium_size\": \"Medium size\",\n  \"editor.extensions.image.large_size\": \"Large size\",\n  \"editor.extensions.image.restore_size\": \"Restore original size\",\n  \"editor.extensions.image.edit_link\": \"Edit link\",\n  \"editor.extensions.image.copy_width\": \"Apply width to all text\",\n  \"editor.extensions.image.edit_alt\": \"Edit alt\",\n  \"editor.extensions.image.edit_href\": \"Edit the image hyperlink\",\n  \"editor.extensions.image.edit_caption\": \"Image description\",\n  \"editor.extensions.image.image_load_error\": \"Image load failed\",\n  \"editor.extensions.video.src_input_label\": \"Video link\",\n  \"editor.extensions.video.resize\": \"Resize\",\n  \"editor.extensions.video.disable_controls\": \"Hide controls\",\n  \"editor.extensions.video.enable_controls\": \"Show controls\",\n  \"editor.extensions.video.disable_autoplay\": \"Disable auto play\",\n  \"editor.extensions.video.enable_autoplay\": \"Enable auto play\",\n  \"editor.extensions.video.disable_loop\": \"Disable loop\",\n  \"editor.extensions.video.enable_loop\": \"Enable loop\",\n  \"editor.extensions.video.small_size\": \"Small size\",\n  \"editor.extensions.video.medium_size\": \"Medium size\",\n  \"editor.extensions.video.large_size\": \"Large size\",\n  \"editor.extensions.video.edit_caption\": \"Video description\",\n  \"editor.extensions.highlight.unset\": \"Unset\",\n  \"editor.extensions.columns.add_column_before\": \"Add column before\",\n  \"editor.extensions.columns.add_column_after\": \"Add column after\",\n  \"editor.extensions.columns.delete_column\": \"Delete column\",\n  \"editor.extensions.search_and_replace.title\": \"Search and Replace\",\n  \"editor.extensions.search_and_replace.search_placeholder\": \"Search\",\n  \"editor.extensions.search_and_replace.not_found\": \"Not found\",\n  \"editor.extensions.search_and_replace.occurrence_found\": \"{index} of {total} occurrences\",\n  \"editor.extensions.search_and_replace.find_previous\": \"Find previous\",\n  \"editor.extensions.search_and_replace.find_next\": \"Find next\",\n  \"editor.extensions.search_and_replace.replace_placeholder\": \"Replace\",\n  \"editor.extensions.search_and_replace.replace\": \"Replace\",\n  \"editor.extensions.search_and_replace.replace_all\": \"Replace all\",\n  \"editor.extensions.search_and_replace.case_sensitive\": \"Case sensitive\",\n  \"editor.extensions.search_and_replace.match_word\": \"Match whole word\",\n  \"editor.extensions.search_and_replace.use_regex\": \"Use regular expression\",\n  \"editor.extensions.search_and_replace.close\": \"Close\",\n  \"editor.extensions.format_brush.toolbar_item.title\": \"Format Brush\",\n  \"editor.extensions.format_brush.toolbar_item.cancel\": \"Cancel Format Brush\",\n  \"editor.extensions.details.command_item\": \"Details\",\n  \"editor.extensions.gallery.title\": \"Gallery\",\n  \"editor.extensions.gallery.add_image\": \"Add image\",\n  \"editor.extensions.gallery.empty_prompt\": \"Click to select images or drag images here\",\n  \"editor.extensions.gallery.group_size\": \"Images per row\",\n  \"editor.extensions.gallery.group_size_label\": \"{count} images per row\",\n  \"editor.extensions.gallery.layout.title\": \"Layout\",\n  \"editor.extensions.gallery.layout.auto\": \"Auto\",\n  \"editor.extensions.gallery.layout.square\": \"Square\",\n  \"editor.extensions.gallery.gap\": \"Gap\",\n  \"editor.extensions.upload.error\": \"Upload failed\",\n  \"editor.extensions.upload.click_retry\": \"Click to retry\",\n  \"editor.extensions.upload.loading\": \"Loading\",\n  \"editor.extensions.upload.attachment.title\": \"Attachment Library\",\n  \"editor.extensions.upload.permalink.title\": \"Input link\",\n  \"editor.extensions.upload.permalink.placeholder\": \"Input link and press Enter to confirm\",\n  \"editor.extensions.upload.operations.remove.button\": \"Delete\",\n  \"editor.extensions.upload.operations.replace.button\": \"Replace\",\n  \"editor.extensions.upload.operations.transfer.button\": \"Save locally\",\n  \"editor.extensions.upload.operations.transfer.tooltip\": \"This resource is detected as an external resource. Click this button to save it to the attachment library.\",\n  \"editor.extensions.upload.operations.transfer_in_batch.description\": \"External link detected. Would you like to automatically upload it to the attachment library?\",\n  \"editor.extensions.font_size.title\": \"Font size\",\n  \"editor.components.color_picker.more_color\": \"More\",\n  \"editor.common.alt\": \"Alternative text\",\n  \"editor.common.width\": \"Width\",\n  \"editor.common.height\": \"Height\",\n  \"editor.common.align_method\": \"Align method\",\n  \"editor.common.align_left\": \"Align left\",\n  \"editor.common.align_center\": \"Align center\",\n  \"editor.common.align_right\": \"Align right\",\n  \"editor.common.align_justify\": \"Align justify\",\n  \"editor.common.bold\": \"Bold\",\n  \"editor.common.italic\": \"Italic\",\n  \"editor.common.underline\": \"Underline\",\n  \"editor.common.strike\": \"Strike\",\n  \"editor.common.quote\": \"Quote\",\n  \"editor.common.code\": \"Code\",\n  \"editor.common.superscript\": \"Superscript\",\n  \"editor.common.subscript\": \"Subscript\",\n  \"editor.common.codeblock.title\": \"Code block\",\n  \"editor.common.codeblock.copy_code\": \"Copy code\",\n  \"editor.common.codeblock.copy_code_success\": \"Copy success\",\n  \"editor.common.image\": \"Image\",\n  \"editor.common.heading.title\": \"Text type\",\n  \"editor.common.heading.paragraph\": \"Paragraph\",\n  \"editor.common.heading.heading1\": \"Heading 1\",\n  \"editor.common.heading.heading2\": \"Heading 2\",\n  \"editor.common.heading.heading3\": \"Heading 3\",\n  \"editor.common.heading.heading4\": \"Heading 4\",\n  \"editor.common.heading.heading5\": \"Heading 5\",\n  \"editor.common.heading.heading6\": \"Heading 6\",\n  \"editor.common.list\": \"List\",\n  \"editor.common.bullet_list\": \"Bullet list\",\n  \"editor.common.ordered_list\": \"Ordered list\",\n  \"editor.common.task_list\": \"Task list\",\n  \"editor.common.highlight\": \"Highlight\",\n  \"editor.common.color\": \"Color\",\n  \"editor.common.tooltip.custom_width_input\": \"Custom width, press Enter to take effect\",\n  \"editor.common.tooltip.custom_height_input\": \"Customize height, press Enter to take effect\",\n  \"editor.common.tooltip.open_link\": \"Open link\",\n  \"editor.common.placeholder.link_input\": \"Enter the link and press Enter to confirm.\",\n  \"editor.common.placeholder.alt_input\": \"Enter the image alt text and press Enter to confirm.\",\n  \"editor.common.placeholder.alt_href\": \"Enter the image hyperlink and press Enter to confirm.\",\n  \"editor.common.button.new_line\": \"New line\",\n  \"editor.common.button.delete\": \"Delete\",\n  \"editor.common.button.edit_link\": \"Edit link\",\n  \"editor.common.button.refresh\": \"Refresh\",\n  \"editor.common.button.restore_default\": \"Restore default\",\n  \"editor.common.button.add\": \"Add\",\n  \"editor.common.button.cancel\": \"Cancel\",\n  \"editor.common.button.upload\": \"Upload\",\n  \"editor.common.button.confirm\": \"Confirm\",\n  \"editor.common.text.default\": \"Default\",\n  \"editor.common.text.tip\": \"Tip\",\n  \"editor.common.toast.save_success\": \"Saved successfully\",\n  \"editor.common.line_height\": \"Line height\",\n  \"editor.common.clear_format\": \"Clear format\"\n}\n"
  },
  {
    "path": "ui/packages/editor/src/locales/es.json",
    "content": "{\n  \"editor.drag.button.insert_block\": \"Insertar bloque\",\n  \"editor.drag.button.drag_handle\": \"Tirador de arrastre\",\n  \"editor.drag.menu.convert_to\": \"Convertir a\",\n  \"editor.drag.menu.copy\": \"Copiar\",\n  \"editor.drag.menu.cut\": \"Cortar\",\n  \"editor.drag.menu.delete\": \"Eliminar\",\n  \"editor.menus.undo\": \"Deshacer\",\n  \"editor.menus.redo\": \"Rehacer\",\n  \"editor.menus.table.title\": \"Tabla\",\n  \"editor.menus.table.add\": \"Añadir tabla\",\n  \"editor.menus.table.add_column_before\": \"Añadir columna antes\",\n  \"editor.menus.table.add_column_after\": \"Añadir columna después\",\n  \"editor.menus.table.delete_column\": \"Eliminar columna\",\n  \"editor.menus.table.add_row_before\": \"Añadir fila antes\",\n  \"editor.menus.table.add_row_after\": \"Añadir fila después\",\n  \"editor.menus.table.toggle_header_column\": \"Activar/Desactivar encabezado de primera columna\",\n  \"editor.menus.table.toggle_header_row\": \"Activar/Desactivar encabezado de primera fila\",\n  \"editor.menus.table.toggle_header_cell\": \"Activar/Desactivar celda actual como encabezado\",\n  \"editor.menus.table.delete_row\": \"Eliminar fila\",\n  \"editor.menus.table.merge_cells\": \"Combinar celdas\",\n  \"editor.menus.table.split_cell\": \"Dividir celda\",\n  \"editor.menus.table.delete_table\": \"Eliminar tabla\",\n  \"editor.extensions.commands_menu.columns\": \"Tarjeta de columnas\",\n  \"editor.extensions.commands_menu.iframe\": \"Iframe\",\n  \"editor.extensions.commands_menu.image\": \"Imagen\",\n  \"editor.extensions.commands_menu.video\": \"Vídeo\",\n  \"editor.extensions.commands_menu.audio\": \"Audio\",\n  \"editor.extensions.commands_menu.table\": \"Tabla\",\n  \"editor.extensions.commands_menu.gallery\": \"Galería\",\n  \"editor.extensions.commands_menu.no_results\": \"Sin resultados\",\n  \"editor.extensions.commands_menu.placeholder\": \"Introduce / para seleccionar el tipo de entrada\",\n  \"editor.extensions.figure_caption.empty_placeholder\": \"Añadir descripción\",\n  \"editor.extensions.link.add_link\": \"Añadir enlace\",\n  \"editor.extensions.link.edit_link\": \"Editar enlace\",\n  \"editor.extensions.link.placeholder\": \"Dirección del enlace\",\n  \"editor.extensions.link.open_in_new_window\": \"Abrir en una ventana nueva\",\n  \"editor.extensions.link.cancel_link\": \"Cancelar enlace\",\n  \"editor.extensions.link.nofollow\": \"Nofollow\",\n  \"editor.extensions.audio.src_input_label\": \"Enlace de audio\",\n  \"editor.extensions.audio.disable_autoplay\": \"Desactivar reproducción automática\",\n  \"editor.extensions.audio.enable_autoplay\": \"Activar reproducción automática\",\n  \"editor.extensions.audio.disable_loop\": \"Desactivar bucle\",\n  \"editor.extensions.audio.enable_loop\": \"Activar bucle\",\n  \"editor.extensions.iframe.src_input_label\": \"Enlace web\",\n  \"editor.extensions.iframe.resize\": \"Redimensionar\",\n  \"editor.extensions.iframe.disable_frameborder\": \"Ocultar borde de marco\",\n  \"editor.extensions.iframe.enable_frameborder\": \"Mostrar borde de marco\",\n  \"editor.extensions.iframe.phone_size\": \"Tamaño de teléfono móvil\",\n  \"editor.extensions.iframe.tablet_vertical_size\": \"Tamaño de tableta vertical\",\n  \"editor.extensions.iframe.tablet_horizontal_size\": \"Tamaño de tableta horizontal\",\n  \"editor.extensions.iframe.desktop_size\": \"Tamaño de escritorio\",\n  \"editor.extensions.image.src_input_label\": \"Enlace de imagen\",\n  \"editor.extensions.image.href_input_label\": \"Hipervínculo de imagen\",\n  \"editor.extensions.image.resize\": \"Redimensionar\",\n  \"editor.extensions.image.small_size\": \"Tamaño pequeño\",\n  \"editor.extensions.image.medium_size\": \"Tamaño mediano\",\n  \"editor.extensions.image.large_size\": \"Tamaño grande\",\n  \"editor.extensions.image.restore_size\": \"Restaurar tamaño original\",\n  \"editor.extensions.image.edit_link\": \"Editar enlace\",\n  \"editor.extensions.image.copy_width\": \"Aplicar ancho a todo el texto\",\n  \"editor.extensions.image.edit_alt\": \"Editar texto alternativo\",\n  \"editor.extensions.image.edit_href\": \"Editar el hipervínculo de la imagen\",\n  \"editor.extensions.image.edit_caption\": \"Descripción de la imagen\",\n  \"editor.extensions.image.image_load_error\": \"Error al cargar la imagen\",\n  \"editor.extensions.video.src_input_label\": \"Enlace de vídeo\",\n  \"editor.extensions.video.resize\": \"Redimensionar\",\n  \"editor.extensions.video.disable_controls\": \"Ocultar controles\",\n  \"editor.extensions.video.enable_controls\": \"Mostrar controles\",\n  \"editor.extensions.video.disable_autoplay\": \"Desactivar reproducción automática\",\n  \"editor.extensions.video.enable_autoplay\": \"Activar reproducción automática\",\n  \"editor.extensions.video.disable_loop\": \"Desactivar bucle\",\n  \"editor.extensions.video.enable_loop\": \"Activar bucle\",\n  \"editor.extensions.video.small_size\": \"Tamaño pequeño\",\n  \"editor.extensions.video.medium_size\": \"Tamaño mediano\",\n  \"editor.extensions.video.large_size\": \"Tamaño grande\",\n  \"editor.extensions.video.edit_caption\": \"Descripción del vídeo\",\n  \"editor.extensions.highlight.unset\": \"Desactivar\",\n  \"editor.extensions.columns.add_column_before\": \"Añadir columna antes\",\n  \"editor.extensions.columns.add_column_after\": \"Añadir columna después\",\n  \"editor.extensions.columns.delete_column\": \"Eliminar columna\",\n  \"editor.extensions.search_and_replace.title\": \"Buscar y reemplazar\",\n  \"editor.extensions.search_and_replace.search_placeholder\": \"Buscar\",\n  \"editor.extensions.search_and_replace.not_found\": \"No encontrado\",\n  \"editor.extensions.search_and_replace.occurrence_found\": \"{index} de {total} ocurrencias\",\n  \"editor.extensions.search_and_replace.find_previous\": \"Buscar anterior\",\n  \"editor.extensions.search_and_replace.find_next\": \"Buscar siguiente\",\n  \"editor.extensions.search_and_replace.replace_placeholder\": \"Reemplazar\",\n  \"editor.extensions.search_and_replace.replace\": \"Reemplazar\",\n  \"editor.extensions.search_and_replace.replace_all\": \"Reemplazar todo\",\n  \"editor.extensions.search_and_replace.case_sensitive\": \"Coincidir mayúsculas y minúsculas\",\n  \"editor.extensions.search_and_replace.match_word\": \"Palabras completas\",\n  \"editor.extensions.search_and_replace.use_regex\": \"Usar expresión regular\",\n  \"editor.extensions.search_and_replace.close\": \"Cerrar\",\n  \"editor.extensions.format_brush.toolbar_item.title\": \"Pincel de formato\",\n  \"editor.extensions.format_brush.toolbar_item.cancel\": \"Cancelar pincel de formato\",\n  \"editor.extensions.details.command_item\": \"Detalles\",\n  \"editor.extensions.gallery.title\": \"Galería\",\n  \"editor.extensions.gallery.add_image\": \"Añadir imagen\",\n  \"editor.extensions.gallery.empty_prompt\": \"Haz clic para seleccionar imágenes o arrastra imágenes aquí\",\n  \"editor.extensions.gallery.group_size\": \"Imágenes por fila\",\n  \"editor.extensions.gallery.group_size_label\": \"{count} imágenes por fila\",\n  \"editor.extensions.gallery.layout.title\": \"Diseño\",\n  \"editor.extensions.gallery.layout.auto\": \"Automático\",\n  \"editor.extensions.gallery.layout.square\": \"Cuadrado\",\n  \"editor.extensions.gallery.gap\": \"Espaciado\",\n  \"editor.extensions.upload.error\": \"Error al subir\",\n  \"editor.extensions.upload.click_retry\": \"Haz clic para reintentar\",\n  \"editor.extensions.upload.loading\": \"Subiendo\",\n  \"editor.extensions.upload.attachment.title\": \"Biblioteca de adjuntos\",\n  \"editor.extensions.upload.permalink.title\": \"Introducir enlace\",\n  \"editor.extensions.upload.permalink.placeholder\": \"Introduce el enlace y presiona Enter para confirmar\",\n  \"editor.extensions.upload.operations.remove.button\": \"Eliminar\",\n  \"editor.extensions.upload.operations.replace.button\": \"Reemplazar\",\n  \"editor.extensions.upload.operations.transfer.button\": \"Guardar localmente\",\n  \"editor.extensions.upload.operations.transfer.tooltip\": \"Este recurso se detecta como recurso externo. Haz clic en este botón para guardarlo en la biblioteca de adjuntos.\",\n  \"editor.extensions.upload.operations.transfer_in_batch.description\": \"Se detectó un enlace externo. ¿Deseas subirlo automáticamente a la biblioteca de adjuntos?\",\n  \"editor.extensions.font_size.title\": \"Tamaño de fuente\",\n  \"editor.components.color_picker.more_color\": \"Más\",\n  \"editor.common.alt\": \"Texto alternativo\",\n  \"editor.common.width\": \"Ancho\",\n  \"editor.common.height\": \"Alto\",\n  \"editor.common.align_method\": \"Método de alineación\",\n  \"editor.common.align_left\": \"Alinear a la izquierda\",\n  \"editor.common.align_center\": \"Centrar\",\n  \"editor.common.align_right\": \"Alinear a la derecha\",\n  \"editor.common.align_justify\": \"Justificar\",\n  \"editor.common.bold\": \"Negrita\",\n  \"editor.common.italic\": \"Cursiva\",\n  \"editor.common.underline\": \"Subrayado\",\n  \"editor.common.strike\": \"Tachado\",\n  \"editor.common.quote\": \"Cita\",\n  \"editor.common.code\": \"Código\",\n  \"editor.common.superscript\": \"Superíndice\",\n  \"editor.common.subscript\": \"Subíndice\",\n  \"editor.common.codeblock.title\": \"Bloque de código\",\n  \"editor.common.codeblock.copy_code\": \"Copiar código\",\n  \"editor.common.codeblock.copy_code_success\": \"Copiado con éxito\",\n  \"editor.common.image\": \"Imagen\",\n  \"editor.common.heading.title\": \"Tipo de texto\",\n  \"editor.common.heading.paragraph\": \"Párrafo\",\n  \"editor.common.heading.heading1\": \"Título 1\",\n  \"editor.common.heading.heading2\": \"Título 2\",\n  \"editor.common.heading.heading3\": \"Título 3\",\n  \"editor.common.heading.heading4\": \"Título 4\",\n  \"editor.common.heading.heading5\": \"Título 5\",\n  \"editor.common.heading.heading6\": \"Título 6\",\n  \"editor.common.list\": \"Lista\",\n  \"editor.common.bullet_list\": \"Lista con viñetas\",\n  \"editor.common.ordered_list\": \"Lista ordenada\",\n  \"editor.common.task_list\": \"Lista de tareas\",\n  \"editor.common.highlight\": \"Resaltar\",\n  \"editor.common.color\": \"Color\",\n  \"editor.common.tooltip.custom_width_input\": \"Ancho personalizado, presiona Enter para aplicar\",\n  \"editor.common.tooltip.custom_height_input\": \"Alto personalizado, presiona Enter para aplicar\",\n  \"editor.common.tooltip.open_link\": \"Abrir enlace\",\n  \"editor.common.placeholder.link_input\": \"Introduce el enlace y presiona Enter para confirmar.\",\n  \"editor.common.placeholder.alt_input\": \"Introduce el texto alternativo y presiona Enter para confirmar.\",\n  \"editor.common.placeholder.alt_href\": \"Introduce el hipervínculo de la imagen y presiona Enter para confirmar.\",\n  \"editor.common.button.new_line\": \"Nueva línea\",\n  \"editor.common.button.delete\": \"Eliminar\",\n  \"editor.common.button.edit_link\": \"Editar enlace\",\n  \"editor.common.button.refresh\": \"Actualizar\",\n  \"editor.common.button.restore_default\": \"Restaurar predeterminado\",\n  \"editor.common.button.add\": \"Añadir\",\n  \"editor.common.button.cancel\": \"Cancelar\",\n  \"editor.common.button.upload\": \"Cargar\",\n  \"editor.common.button.confirm\": \"Confirmar\",\n  \"editor.common.text.default\": \"Predeterminado\",\n  \"editor.common.text.tip\": \"Consejo\",\n  \"editor.common.toast.save_success\": \"Guardado con éxito\",\n  \"editor.common.line_height\": \"Altura de línea\",\n  \"editor.common.clear_format\": \"Limpiar formato\"\n}\n"
  },
  {
    "path": "ui/packages/editor/src/locales/index.ts",
    "content": "import { createI18n } from \"vue-i18n\";\nimport en from \"./en.json\";\nimport es from \"./es.json\";\nimport zhCN from \"./zh-CN.json\";\n\nconst messages = {\n  en: en,\n  zh: zhCN,\n  es: es,\n  \"en-US\": en,\n  \"zh-CN\": zhCN,\n  \"es-ES\": es,\n};\n\nconst i18n = createI18n({\n  legacy: false,\n  locale: \"en\",\n  fallbackLocale: \"en\",\n  messages,\n});\n\nexport { i18n };\n"
  },
  {
    "path": "ui/packages/editor/src/locales/zh-CN.json",
    "content": "{\n  \"editor.drag.button.insert_block\": \"插入块\",\n  \"editor.drag.button.drag_handle\": \"按住拖动\",\n  \"editor.drag.menu.convert_to\": \"转换为\",\n  \"editor.drag.menu.copy\": \"复制\",\n  \"editor.drag.menu.cut\": \"剪切\",\n  \"editor.drag.menu.delete\": \"删除\",\n  \"editor.menus.undo\": \"撤销\",\n  \"editor.menus.redo\": \"恢复\",\n  \"editor.menus.table.title\": \"表格\",\n  \"editor.menus.table.add\": \"插入表格\",\n  \"editor.menus.table.add_column_before\": \"向前插入列\",\n  \"editor.menus.table.add_column_after\": \"向后插入列\",\n  \"editor.menus.table.delete_column\": \"删除当前列\",\n  \"editor.menus.table.add_row_before\": \"向上插入行\",\n  \"editor.menus.table.add_row_after\": \"向下插入行\",\n  \"editor.menus.table.toggle_header_column\": \"设置/取消首列表头\",\n  \"editor.menus.table.toggle_header_row\": \"设置/取消首行表头\",\n  \"editor.menus.table.toggle_header_cell\": \"设置/取消当前单元格为表头\",\n  \"editor.menus.table.delete_row\": \"删除当前行\",\n  \"editor.menus.table.merge_cells\": \"合并单元格\",\n  \"editor.menus.table.split_cell\": \"拆分单元格\",\n  \"editor.menus.table.delete_table\": \"删除表格\",\n  \"editor.extensions.commands_menu.columns\": \"分栏卡片\",\n  \"editor.extensions.commands_menu.iframe\": \"嵌入网页\",\n  \"editor.extensions.commands_menu.image\": \"图片\",\n  \"editor.extensions.commands_menu.video\": \"视频\",\n  \"editor.extensions.commands_menu.audio\": \"音频\",\n  \"editor.extensions.commands_menu.table\": \"表格\",\n  \"editor.extensions.commands_menu.gallery\": \"图片集\",\n  \"editor.extensions.commands_menu.no_results\": \"没有搜索结果\",\n  \"editor.extensions.commands_menu.placeholder\": \"输入 / 以选择输入类型\",\n  \"editor.extensions.figure_caption.empty_placeholder\": \"添加描述\",\n  \"editor.extensions.link.add_link\": \"添加链接\",\n  \"editor.extensions.link.edit_link\": \"修改链接\",\n  \"editor.extensions.link.placeholder\": \"链接地址\",\n  \"editor.extensions.link.open_in_new_window\": \"在新窗口中打开\",\n  \"editor.extensions.link.cancel_link\": \"取消链接\",\n  \"editor.extensions.link.nofollow\": \"搜索引擎忽略链接关系\",\n  \"editor.extensions.audio.src_input_label\": \"音频链接\",\n  \"editor.extensions.audio.disable_autoplay\": \"关闭自动播放\",\n  \"editor.extensions.audio.enable_autoplay\": \"开启自动播放\",\n  \"editor.extensions.audio.disable_loop\": \"关闭循环播放\",\n  \"editor.extensions.audio.enable_loop\": \"开启循环播放\",\n  \"editor.extensions.iframe.src_input_label\": \"网页链接\",\n  \"editor.extensions.iframe.resize\": \"调整尺寸\",\n  \"editor.extensions.iframe.disable_frameborder\": \"取消边框\",\n  \"editor.extensions.iframe.enable_frameborder\": \"设置边框\",\n  \"editor.extensions.iframe.phone_size\": \"手机尺寸\",\n  \"editor.extensions.iframe.tablet_vertical_size\": \"平板电脑纵向尺寸\",\n  \"editor.extensions.iframe.tablet_horizontal_size\": \"平板电脑横向尺寸\",\n  \"editor.extensions.iframe.desktop_size\": \"桌面电脑尺寸\",\n  \"editor.extensions.image.src_input_label\": \"图片链接\",\n  \"editor.extensions.image.href_input_label\": \"图片跳转链接\",\n  \"editor.extensions.image.resize\": \"调整尺寸\",\n  \"editor.extensions.image.small_size\": \"小尺寸\",\n  \"editor.extensions.image.medium_size\": \"中尺寸\",\n  \"editor.extensions.image.large_size\": \"大尺寸\",\n  \"editor.extensions.image.restore_size\": \"恢复原始尺寸\",\n  \"editor.extensions.image.edit_link\": \"修改链接\",\n  \"editor.extensions.image.copy_width\": \"将宽度应用至全文\",\n  \"editor.extensions.image.edit_alt\": \"修改图片的替代文本（alt 属性）\",\n  \"editor.extensions.image.edit_href\": \"修改图片跳转链接\",\n  \"editor.extensions.image.edit_caption\": \"图片描述文本\",\n  \"editor.extensions.image.image_load_error\": \"图片加载失败\",\n  \"editor.extensions.video.src_input_label\": \"视频链接\",\n  \"editor.extensions.video.resize\": \"调整尺寸\",\n  \"editor.extensions.video.disable_controls\": \"隐藏控制面板\",\n  \"editor.extensions.video.enable_controls\": \"显示控制面板\",\n  \"editor.extensions.video.disable_autoplay\": \"关闭自动播放\",\n  \"editor.extensions.video.enable_autoplay\": \"开启自动播放\",\n  \"editor.extensions.video.disable_loop\": \"关闭循环播放\",\n  \"editor.extensions.video.enable_loop\": \"开启循环播放\",\n  \"editor.extensions.video.small_size\": \"小尺寸\",\n  \"editor.extensions.video.medium_size\": \"中尺寸\",\n  \"editor.extensions.video.large_size\": \"大尺寸\",\n  \"editor.extensions.video.edit_caption\": \"视频描述\",\n  \"editor.extensions.highlight.unset\": \"移除高亮颜色\",\n  \"editor.extensions.columns.add_column_before\": \"向前插入列\",\n  \"editor.extensions.columns.add_column_after\": \"向后插入列\",\n  \"editor.extensions.columns.delete_column\": \"删除当前列\",\n  \"editor.extensions.search_and_replace.title\": \"查找替换\",\n  \"editor.extensions.search_and_replace.search_placeholder\": \"查找\",\n  \"editor.extensions.search_and_replace.not_found\": \"无结果\",\n  \"editor.extensions.search_and_replace.occurrence_found\": \"第 {index} 项，共 {total} 项\",\n  \"editor.extensions.search_and_replace.find_previous\": \"上一个匹配项\",\n  \"editor.extensions.search_and_replace.find_next\": \"下一个匹配项\",\n  \"editor.extensions.search_and_replace.replace_placeholder\": \"替换\",\n  \"editor.extensions.search_and_replace.replace\": \"替换\",\n  \"editor.extensions.search_and_replace.replace_all\": \"全部替换\",\n  \"editor.extensions.search_and_replace.case_sensitive\": \"区分大小写\",\n  \"editor.extensions.search_and_replace.match_word\": \"全字匹配\",\n  \"editor.extensions.search_and_replace.use_regex\": \"使用正则表达式\",\n  \"editor.extensions.search_and_replace.close\": \"关闭\",\n  \"editor.extensions.format_brush.toolbar_item.title\": \"格式刷\",\n  \"editor.extensions.format_brush.toolbar_item.cancel\": \"取消格式刷\",\n  \"editor.extensions.details.command_item\": \"折叠内容\",\n  \"editor.extensions.gallery.title\": \"图片集\",\n  \"editor.extensions.gallery.add_image\": \"添加图片\",\n  \"editor.extensions.gallery.empty_prompt\": \"点击选择图片或拖拽图片到这里\",\n  \"editor.extensions.gallery.group_size\": \"每行图片数\",\n  \"editor.extensions.gallery.group_size_label\": \"每行 {count} 张图片\",\n  \"editor.extensions.gallery.layout.title\": \"布局\",\n  \"editor.extensions.gallery.layout.auto\": \"自适应\",\n  \"editor.extensions.gallery.layout.square\": \"等比矩形\",\n  \"editor.extensions.gallery.gap\": \"间距\",\n  \"editor.extensions.upload.error\": \"上传失败\",\n  \"editor.extensions.upload.click_retry\": \"点击重试\",\n  \"editor.extensions.upload.loading\": \"上传中\",\n  \"editor.extensions.upload.attachment.title\": \"附件库\",\n  \"editor.extensions.upload.permalink.title\": \"输入链接\",\n  \"editor.extensions.upload.permalink.placeholder\": \"输入链接，按回车确定\",\n  \"editor.extensions.upload.operations.remove.button\": \"删除\",\n  \"editor.extensions.upload.operations.replace.button\": \"替换\",\n  \"editor.extensions.upload.operations.transfer.button\": \"转存\",\n  \"editor.extensions.upload.operations.transfer.tooltip\": \"检测到此资源为外部资源，点击此按钮以转存到附件库\",\n  \"editor.extensions.upload.operations.transfer_in_batch.description\": \"检测到外部链接，是否需要自动上传到附件库？\",\n  \"editor.extensions.font_size.title\": \"字体大小\",\n  \"editor.components.color_picker.more_color\": \"更多颜色\",\n  \"editor.common.alt\": \"替代文本\",\n  \"editor.common.width\": \"宽度\",\n  \"editor.common.height\": \"高度\",\n  \"editor.common.align_method\": \"对齐方式\",\n  \"editor.common.align_left\": \"左对齐\",\n  \"editor.common.align_center\": \"居中\",\n  \"editor.common.align_right\": \"右对齐\",\n  \"editor.common.align_justify\": \"两端对齐\",\n  \"editor.common.bold\": \"粗体\",\n  \"editor.common.italic\": \"斜体\",\n  \"editor.common.underline\": \"下划线\",\n  \"editor.common.strike\": \"删除线\",\n  \"editor.common.quote\": \"引用\",\n  \"editor.common.code\": \"行内代码\",\n  \"editor.common.superscript\": \"上角标\",\n  \"editor.common.subscript\": \"下角标\",\n  \"editor.common.codeblock.title\": \"代码块\",\n  \"editor.common.codeblock.copy_code\": \"复制代码\",\n  \"editor.common.codeblock.copy_code_success\": \"复制成功\",\n  \"editor.common.image\": \"图片\",\n  \"editor.common.heading.title\": \"文本类型\",\n  \"editor.common.heading.paragraph\": \"普通文本\",\n  \"editor.common.heading.heading1\": \"一级标题\",\n  \"editor.common.heading.heading2\": \"二级标题\",\n  \"editor.common.heading.heading3\": \"三级标题\",\n  \"editor.common.heading.heading4\": \"四级标题\",\n  \"editor.common.heading.heading5\": \"五级标题\",\n  \"editor.common.heading.heading6\": \"六级标题\",\n  \"editor.common.list\": \"列表\",\n  \"editor.common.bullet_list\": \"无序列表\",\n  \"editor.common.ordered_list\": \"有序列表\",\n  \"editor.common.task_list\": \"任务列表\",\n  \"editor.common.highlight\": \"高亮\",\n  \"editor.common.color\": \"字体颜色\",\n  \"editor.common.tooltip.custom_width_input\": \"自定义宽度，按回车键生效\",\n  \"editor.common.tooltip.custom_height_input\": \"自定义高度，按回车键生效\",\n  \"editor.common.tooltip.open_link\": \"打开链接\",\n  \"editor.common.placeholder.link_input\": \"输入链接，按回车确定\",\n  \"editor.common.placeholder.alt_input\": \"输入图片 alt 属性值，按回车确认\",\n  \"editor.common.placeholder.alt_href\": \"输入图片跳转链接，按回车确认\",\n  \"editor.common.button.new_line\": \"换行\",\n  \"editor.common.button.delete\": \"删除\",\n  \"editor.common.button.edit_link\": \"修改链接\",\n  \"editor.common.button.refresh\": \"刷新\",\n  \"editor.common.button.restore_default\": \"恢复为默认\",\n  \"editor.common.button.add\": \"添加\",\n  \"editor.common.button.cancel\": \"取消\",\n  \"editor.common.button.upload\": \"上传\",\n  \"editor.common.button.confirm\": \"确定\",\n  \"editor.common.text.default\": \"默认\",\n  \"editor.common.text.tip\": \"提示\",\n  \"editor.common.toast.save_success\": \"保存成功\",\n  \"editor.common.line_height\": \"行高\",\n  \"editor.common.clear_format\": \"清除格式\"\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/base.scss",
    "content": ".halo-rich-text-editor {\n  $editorVerticalPadding: 15px;\n  $editorHorizontalPadding: 1rem;\n  $editorContentMaxWidth: 70rem;\n\n  box-sizing: border-box;\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n\n  .editor-header {\n    height: 48px;\n    flex: none;\n  }\n\n  .editor-entry {\n    display: flex;\n    width: 100%;\n    height: 100%;\n    min-height: 0;\n    flex-shrink: 1;\n    flex: 1 1 0%;\n    overflow: hidden;\n\n    .editor-main {\n      position: relative;\n      min-width: 0;\n      flex: 1 1 0%;\n      flex-shrink: 1;\n      overflow-y: auto;\n      background-color: theme(\"colors.white\");\n\n      &-extra {\n        padding: $editorVerticalPadding $editorHorizontalPadding;\n        max-width: $editorContentMaxWidth;\n        margin: 0 auto;\n\n        @media (min-width: theme(\"screens.sm\")) {\n          padding-left: 3.5rem;\n          padding-right: 3.5rem;\n        }\n      }\n    }\n\n    &-extra {\n      display: none;\n      height: 100%;\n      width: theme(\"width.72\");\n      flex: none;\n      flex-shrink: 0;\n\n      @media (min-width: theme(\"screens.sm\")) {\n        display: block;\n      }\n    }\n  }\n\n  .editor-main-content {\n    width: 100%;\n    position: relative;\n\n    .ProseMirror {\n      outline: none !important;\n      padding-left: $editorHorizontalPadding;\n      padding-right: $editorHorizontalPadding;\n      padding-top: $editorVerticalPadding;\n      max-width: $editorContentMaxWidth;\n      padding-bottom: 150px;\n      margin: 0 auto;\n\n      p {\n        margin-top: 0.75em;\n        margin-bottom: 0;\n      }\n\n      p.is-empty::before {\n        content: attr(data-placeholder);\n        float: left;\n        color: #adb5bd;\n        pointer-events: none;\n        height: 0;\n      }\n\n      pre {\n        background-color: transparent;\n        padding: 0.75rem 1rem;\n        margin: 0;\n\n        code {\n          background: none;\n          font-size: 0.8rem;\n          padding: 0 !important;\n          border-radius: 0;\n        }\n      }\n\n      ul[data-type=\"taskList\"] {\n        list-style: none;\n        padding: 0;\n\n        p {\n          margin: 0;\n        }\n\n        li {\n          display: flex;\n\n          > label {\n            flex: 0 0 auto;\n            margin-right: 0.5rem;\n            user-select: none;\n          }\n\n          > div {\n            flex: 1 1 auto;\n          }\n        }\n      }\n\n      ul,\n      ol {\n        list-style-type: revert-layer;\n      }\n\n      code br {\n        display: initial;\n      }\n\n      @media (min-width: theme(\"screens.sm\")) {\n        padding-left: 3.5rem;\n        padding-right: 3.5rem;\n      }\n    }\n\n    .v-popper__arrow-container {\n      display: none;\n    }\n  }\n\n  .v-popper--theme-tooltip {\n    pointer-events: all;\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/columns.scss",
    "content": ".halo-rich-text-editor {\n  .ProseMirror {\n    .columns {\n      margin-top: 0.75em;\n\n      .column {\n        border: 1px solid #e7e9e8;\n        border-radius: 6px;\n        position: relative;\n        padding: 16px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/details.scss",
    "content": ".halo-rich-text-editor {\n  .details {\n    display: flex;\n    gap: 0.25rem;\n    margin: 1.5rem 0;\n    border: 1px solid theme(\"colors.gray.200\");\n    border-radius: 0.5rem;\n    padding: 0.5rem;\n\n    summary {\n      all: unset;\n      font-weight: 700;\n    }\n\n    > button {\n      align-items: center;\n      background: transparent;\n      border-radius: 4px;\n      display: flex;\n      font-size: 0.625rem;\n      height: 1.25rem;\n      justify-content: center;\n      line-height: 1;\n      margin-top: 0.1rem;\n      padding: 0;\n      width: 1.25rem;\n\n      &:hover {\n        background-color: theme(\"colors.gray.100\");\n      }\n\n      &:active {\n        background-color: theme(\"colors.gray.200\");\n      }\n\n      &::before {\n        content: \"\\25B6\";\n      }\n    }\n\n    &.is-open > button::before {\n      transform: rotate(90deg);\n    }\n\n    > div {\n      display: flex;\n      flex-direction: column;\n      gap: 1rem;\n      width: 100%;\n\n      > [data-type=\"detailsContent\"] > :last-child {\n        margin-bottom: 0.5rem;\n      }\n    }\n\n    .details {\n      margin: 0.5rem 0;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/draggable.scss",
    "content": "[data-drag-handle] {\n  position: absolute;\n  z-index: 100;\n  display: inline;\n  width: 16px;\n  height: 16px;\n  cursor: move;\n  background-color: #fff;\n  opacity: 0;\n  transition: opacity 0.3s ease-out;\n  background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='3' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3C/svg%3E\");\n  background-size: contain;\n  background-position: center 0;\n  background-repeat: no-repeat;\n\n  &.show {\n    opacity: 0.3;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n\n  &.hide {\n    opacity: 0;\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/figure.scss",
    "content": ".halo-rich-text-editor {\n  figure {\n    display: flex !important;\n    flex-direction: column;\n    margin: 0 !important;\n    margin-top: 0.5rem !important;\n    max-width: 100%;\n\n    img,\n    video {\n      display: block;\n      max-width: 100%;\n      height: auto;\n    }\n\n    figcaption {\n      display: block;\n      width: var(--media-width, 100%);\n      max-width: 100%;\n      padding: 0.5rem;\n      font-size: 0.875rem;\n      color: #6b7280;\n      text-align: center;\n      font-style: italic;\n      min-height: 1.5rem;\n      border-radius: 0.25rem;\n      transition: all 0.2s ease;\n      cursor: text;\n      box-sizing: border-box;\n      position: relative;\n\n      &[data-empty=\"true\"]::before {\n        content: attr(data-placeholder);\n        color: #9ca3af;\n        pointer-events: none;\n        position: absolute;\n        left: 50%;\n        top: 50%;\n        transform: translate(-50%, -50%);\n      }\n\n      &:focus {\n        outline: none;\n        background-color: #f3f4f6;\n        border: 1px solid #d1d5db;\n      }\n\n      &:hover {\n        background-color: #f9fafb;\n      }\n\n      &:not([data-empty=\"true\"]) {\n        background-color: transparent;\n      }\n    }\n\n    &.ProseMirror-selectednode {\n      figcaption {\n        background-color: #f3f4f6;\n      }\n    }\n\n    & .ProseMirror-selectednode {\n      background-color: transparent;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/format-brush.scss",
    "content": ".halo-rich-text-editor {\n  .ProseMirror {\n    &.format-brush-mode {\n      cursor:\n        url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHBhdGggZD0iTTI0IDB2MjRIMFYwek0xMi41OTQgMjMuMjU4bC0uMDEyLjAwMmwtLjA3MS4wMzVsLS4wMi4wMDRsLS4wMTQtLjAwNGwtLjA3MS0uMDM2Yy0uMDEtLjAwMy0uMDE5IDAtLjAyNC4wMDZsLS4wMDQuMDFsLS4wMTcuNDI4bC4wMDUuMDJsLjAxLjAxM2wuMTA0LjA3NGwuMDE1LjAwNGwuMDEyLS4wMDRsLjEwNC0uMDc0bC4wMTItLjAxNmwuMDA0LS4wMTdsLS4wMTctLjQyN2MtLjAwMi0uMDEtLjAwOS0uMDE3LS4wMTYtLjAxOG0uMjY0LS4xMTNsLS4wMTQuMDAybC0uMTg0LjA5M2wtLjAxLjAxbC0uMDAzLjAxMWwuMDE4LjQzbC4wMDUuMDEybC4wMDguMDA4bC4yMDEuMDkyYy4wMTIuMDA0LjAyMyAwIC4wMjktLjAwOGwuMDA0LS4wMTRsLS4wMzQtLjYxNGMtLjAwMy0uMDEyLS4wMS0uMDItLjAyLS4wMjJtLS43MTUuMDAyYS4wMjMuMDIzIDAgMCAwLS4wMjcuMDA2bC0uMDA2LjAxNGwtLjAzNC42MTRjMCAuMDEyLjAwNy4wMi4wMTcuMDI0bC4wMTUtLjAwMmwuMjAxLS4wOTNsLjAxLS4wMDhsLjAwMy0uMDExbC4wMTgtLjQzbC0uMDAzLS4wMTJsLS4wMS0uMDF6Ii8+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMTQuMzMgMi4zMDdhMyAzIDAgMCAxIDIuMTIgMy42NzRsLTEuMDM1IDMuODY0bDEuMzU4LjM2M2MxLjY1NC40NDQgMi41MSAyLjEyIDIuMjc4IDMuNjU2Yy0uMTEuNzI5LS4xNTQgMS40NDYtLjA2NSAxLjk2OGMuMTg1IDEuMDg0LjgwOCAyLjE0NiAxLjQ1OCAzLjA3M2wuMzU0LjQ5MkExIDEgMCAwIDEgMjAgMjFINS41Yy0uMzIgMC0uNjI3LS4xNTgtLjgxMy0uNDE4Yy0uNDg5LS42ODQtLjgzOC0xLjQ1OC0xLjEyNS0yLjI0NGMtLjUtMS4zNjQtLjkxNy0zLjI5My0uNTQ4LTUuNTAyYTExLjYzMyAxMS42MzMgMCAwIDEgMS40MjgtMy45OTRjLjY1NS0xLjEwOCAxLjkzMS0xLjQyIDIuOTkxLTEuMTM2bDIuMTg3LjU4NmwxLjAzNS0zLjg2NGEzIDMgMCAwIDEgMy42NzQtMi4xMjFaTTcuOTc4IDE1YTEgMSAwIDAgMC0uOTkzIDEuMDA3Yy4wMDcgMS4wMjQuMzI3IDIuMDA1LjY2OSAyLjc3bC4xMDIuMjIzaDIuMjc3YTguOTQ0IDguOTQ0IDAgMCAxLS4zOTItLjY5OWMtLjMxLS42MTctLjU4My0xLjM0OC0uNjQ0LTIuMDQ3bC0uMDEyLS4yNjFBMSAxIDAgMCAwIDcuOTc5IDE1Wm00LjYwOC0xMC4wNTRsLTEuMjk0IDQuODNhMSAxIDAgMCAxLTEuMjI1LjcwNmwtMy4xNTItLjg0NGMtLjM4My0uMTAzLS42NDYuMDQzLS43NTEuMjIxYTkuMTExIDkuMTExIDAgMCAwLS43MDMgMS40OTRsMTEuNTEgMy4wNTNjLjAyNS0uMjkzLjA2MS0uNTc4LjEwMS0uODQyYy4xMDUtLjY5LS4yOTctMS4yODQtLjgxOC0xLjQyNGwtMi4zMjMtLjYyMmExIDEgMCAwIDEtLjcwNy0xLjIyNWwxLjI5NC00LjgzYTEgMSAwIDAgMC0xLjkzMi0uNTE3Ii8+PC9nPjwvc3ZnPg==)\n          5 10,\n        text;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/gap-cursor.scss",
    "content": ".halo-rich-text-editor {\n  .ProseMirror {\n    &.ProseMirror-focused {\n      .card-gap-cursor {\n        &::before {\n          display: block;\n        }\n      }\n    }\n    .card-gap-cursor {\n      position: relative;\n    }\n\n    .card-gap-cursor {\n      &::before {\n        content: \"\";\n        display: none;\n        position: absolute;\n        min-width: 1px;\n        height: 1em;\n        font-size: 1em;\n        border-left: 1px solid black;\n        animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;\n      }\n    }\n\n    .start-card-gap-cursor::before {\n      top: -2px;\n      left: -1px;\n    }\n\n    .end-card-gap-cursor::before {\n      bottom: -2px;\n      right: -1px;\n    }\n  }\n}\n\n@keyframes ProseMirror-cursor-blink {\n  to {\n    visibility: hidden;\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/index.scss",
    "content": "@use \"base.scss\";\n@use \"resizer.scss\";\n@use \"table.scss\";\n@use \"draggable.scss\";\n@use \"columns.scss\";\n@use \"search.scss\";\n@use \"format-brush.scss\";\n@use \"gap-cursor.scss\";\n@use \"node-select.scss\";\n@use \"range-selection.scss\";\n@use \"details.scss\";\n@use \"figure.scss\";\n"
  },
  {
    "path": "ui/packages/editor/src/styles/node-select.scss",
    "content": ".halo-rich-text-editor {\n  $editorNodeCardBorderSelected: rgba(47, 142, 244);\n  $editorNodeCardBgSelected: rgba(47, 142, 244, 0.1);\n\n  .has-node-selected {\n    &.code-node {\n      border-color: $editorNodeCardBorderSelected;\n    }\n  }\n\n  .ProseMirror-selectednode {\n    background-color: $editorNodeCardBgSelected;\n    border-radius: 4px;\n\n    & {\n      &.code-node,\n      img,\n      iframe,\n      video,\n      audio {\n        border-color: $editorNodeCardBorderSelected;\n        background-color: transparent;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/range-selection.scss",
    "content": ".halo-rich-text-editor {\n  $editorRangeFakeSelection: rgba(27, 162, 227, 0.2);\n\n  .no-selection {\n    *::selection {\n      background-color: transparent;\n      color: inherit;\n    }\n  }\n\n  .range-fake-selection {\n    position: relative;\n\n    &::after {\n      content: \" \";\n      position: absolute;\n      top: 0;\n      left: 0;\n      right: 0;\n      bottom: 0;\n      background-color: $editorRangeFakeSelection;\n      pointer-events: none;\n      z-index: 99;\n      caret-color: transparent;\n    }\n\n    &.column {\n      &::after {\n        border-radius: 5px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/resizer.scss",
    "content": ".resize-container {\n  position: relative;\n  overflow: visible !important;\n}\n\n.resizer-handler {\n  position: absolute;\n  z-index: 3;\n  width: 12px;\n  height: 12px;\n  border: none;\n  background-color: rgba(59, 130, 246, 0.8);\n  border-radius: 50%;\n  transition: all 0.2s ease;\n  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);\n\n  &:hover {\n    background-color: rgba(37, 99, 235, 1);\n    transform: scale(1.3);\n    box-shadow: 0 2px 6px rgba(59, 130, 246, 0.5);\n  }\n\n  &:active {\n    background-color: rgba(29, 78, 216, 1);\n    transform: scale(1.1);\n  }\n}\n\n.resizer-br {\n  bottom: -6px;\n  right: -6px;\n  cursor: nwse-resize;\n}\n\n.resizer-bl {\n  bottom: -6px;\n  left: -6px;\n  cursor: nesw-resize;\n}\n\n.resizer-tr {\n  top: -6px;\n  right: -6px;\n  cursor: nesw-resize;\n}\n\n.resizer-tl {\n  top: -6px;\n  left: -6px;\n  cursor: nwse-resize;\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/search.scss",
    "content": ".halo-rich-text-editor {\n  .ProseMirror {\n    .search-result {\n      background-color: #ffd90050;\n\n      &.search-result-current {\n        background-color: #ffd900;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/table.scss",
    "content": ".halo-rich-text-editor {\n  $tableBorderColor: #d8d8d8;\n  $tableHeaderBgColor: #f6f8fa;\n  $tableSelectedBorderColor: rgb(0 101 255);\n  $tableSelectedCellBgColor: transparent;\n  $tableResizeHandleBgColor: #adf;\n\n  .ProseMirror {\n    .table-container {\n      position: relative;\n\n      &.table-right-shadow {\n        &::after {\n          bottom: 15px;\n          background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.08));\n          right: 0;\n          content: \" \";\n          position: absolute;\n          pointer-events: none;\n          top: 0;\n          width: 8px;\n          margin-top: 27px;\n        }\n      }\n\n      &.table-left-shadow {\n        &::before {\n          bottom: 15px;\n          background: linear-gradient(-90deg, transparent, rgba(0, 0, 0, 0.08));\n          left: 0;\n          content: \" \";\n          position: absolute;\n          pointer-events: none;\n          top: 0;\n          width: 8px;\n          margin-top: 27px;\n        }\n      }\n    }\n\n    .tableWrapper {\n      position: relative;\n      overflow-x: auto;\n      overflow-y: hidden;\n      cursor: default;\n\n      &.has-focus {\n        .scrollWrapper {\n          margin-top: -20px;\n        }\n      }\n    }\n\n    .scrollWrapper {\n      width: max-content;\n      border-left: 1px solid transparent;\n      border-right: 1px solid transparent;\n      -webkit-transition: border 250ms ease-in-out 0s;\n      transition: border 250ms ease-in-out 0s;\n    }\n\n    table {\n      width: 100%;\n      border-collapse: collapse;\n      border-radius: 4px;\n      overflow: initial;\n      padding-left: 12px;\n      padding-top: 12px;\n\n      box-sizing: border-box;\n\n      * {\n        box-sizing: border-box;\n      }\n\n      tr {\n        position: relative;\n        border-bottom: 1px solid $tableBorderColor;\n        cursor: text;\n      }\n\n      th {\n        background: $tableHeaderBgColor;\n      }\n\n      td {\n        background: #fff;\n      }\n\n      td,\n      th {\n        position: relative;\n        vertical-align: top;\n        border: 1px solid $tableBorderColor;\n        position: relative;\n        padding: 12px 8px;\n        text-align: left;\n        min-width: 100px;\n\n        > * {\n          margin-top: 0;\n          margin-bottom: 0;\n        }\n      }\n\n      .selectedCell {\n        position: relative;\n        border: 1px solid $tableSelectedBorderColor;\n\n        &::after {\n          box-sizing: content-box;\n          height: 100%;\n          width: 100%;\n          border: 1px solid $tableSelectedBorderColor;\n          content: \"\";\n          position: absolute;\n          left: -1px;\n          top: -1px;\n          bottom: 0px;\n          display: inline-block;\n          pointer-events: none;\n        }\n      }\n    }\n\n    .grip-column {\n      position: absolute;\n      top: -12px;\n      left: -1px;\n      width: 100%;\n\n      > div {\n        position: absolute;\n        top: -18px;\n        left: 100%;\n        transform: translateX(-8px);\n\n        display: inline-block;\n        width: 16px;\n        height: 16px;\n\n        font-size: 0;\n        cursor: pointer;\n\n        .plus-icon {\n          font-size: inherit;\n        }\n      }\n\n      &::before {\n        content: \"\";\n        position: absolute;\n        left: 100%;\n        bottom: 4px;\n        transform: translateX(-1px);\n\n        width: 4px;\n        height: 4px;\n        background-color: #d8d8d8;\n        border-radius: 50%;\n        display: block;\n      }\n\n      &::after {\n        box-sizing: content-box;\n        content: \"\";\n        cursor: pointer;\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 10px;\n        background: #f6f8fa;\n        border: 1px solid #d8d8d8;\n        display: block;\n      }\n\n      &:hover {\n        color: rgb(0 101 255);\n\n        > div {\n          font-size: 14px;\n        }\n\n        &::before {\n          display: none;\n        }\n\n        &::after {\n          background: #2584ff;\n          border-color: #2584ff;\n        }\n      }\n\n      &.last::after {\n        border-top-right-radius: 3px;\n      }\n\n      &.selected::after {\n        background: #2584ff;\n        border-color: #2584ff;\n      }\n    }\n\n    .grip-row {\n      position: absolute;\n      left: -12px;\n      top: -1px;\n      height: 100%;\n\n      > div {\n        transform: translateY(8px);\n        position: absolute;\n        left: -16px;\n        bottom: 4px;\n\n        display: inline-block;\n        width: 16px;\n        height: 16px;\n\n        font-size: 0;\n        cursor: pointer;\n\n        .plus-icon {\n          font-size: inherit;\n        }\n      }\n\n      &::before {\n        content: \"\";\n        position: absolute;\n        left: -10px;\n        bottom: -2px;\n        width: 4px;\n        height: 4px;\n        background-color: #d8d8d8;\n        border-radius: 50%;\n        display: block;\n      }\n\n      &::after {\n        box-sizing: content-box;\n        content: \"\";\n        cursor: pointer;\n        position: absolute;\n        left: 0;\n        top: 0;\n        height: 100%;\n        width: 10px;\n        background: #f6f8fa;\n        border: 1px solid #d8d8d8;\n        display: block;\n      }\n\n      &:hover {\n        color: rgb(0 101 255);\n\n        > div {\n          font-size: 14px;\n        }\n\n        &::before {\n          display: none;\n        }\n\n        &::after {\n          background: #2584ff;\n          border-color: rgb(0 101 255);\n        }\n      }\n\n      &.last::after {\n        border-bottom-left-radius: 3px;\n      }\n\n      &.selected::after {\n        background: #2584ff;\n        border-color: rgb(0 101 255);\n      }\n    }\n\n    .grip-table {\n      &::after {\n        box-sizing: content-box;\n        content: \"\";\n        cursor: pointer;\n        position: absolute;\n        top: -12px;\n        left: -12px;\n        display: block;\n        background: #f6f8fa;\n        width: 10px;\n        height: 10px;\n        border: 1px solid #d8d8d8;\n        border-top-left-radius: 3px;\n      }\n\n      &:hover::after {\n        background: #2584ff;\n        border-color: rgb(0 101 255);\n      }\n\n      &.selected::after {\n        background: #2584ff;\n        border-color: rgb(0 101 255);\n      }\n    }\n\n    .column-resize-handle {\n      position: absolute;\n      top: 0;\n      right: -2px;\n      bottom: -2px;\n      width: 4px;\n      background-color: $tableResizeHandleBgColor;\n      cursor: col-resize;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/styles/tailwind.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "ui/packages/editor/src/tiptap/core/index.ts",
    "content": "export * from \"@tiptap/core\";\n"
  },
  {
    "path": "ui/packages/editor/src/tiptap/index.ts",
    "content": "export * from \"./core\";\nexport { Editor, MarkView, type MarkType, type NodeType } from \"./core\";\nexport * from \"./pm\";\nexport {\n  Fragment,\n  InputRule as PMInputRule,\n  Mark as PMMark,\n  Node as PMNode,\n  liftListItem,\n  textblockTypeInputRule as pmTextblockTypeInputRule,\n  wrappingInputRule as pmWrappingInputRule,\n  sinkListItem,\n  splitListItem,\n  undoInputRule,\n  wrapInList,\n  type Command as PMCommand,\n  type MarkType as PMMarkType,\n  type MarkView as PMMarkView,\n  type NodeRange as PMNodeRange,\n  type NodeType as PMNodeType,\n  type NodeView as PMNodeView,\n} from \"./pm\";\nexport * from \"./vue-3\";\nexport {\n  InputRule,\n  Mark,\n  Node,\n  NodeView,\n  Editor as VueEditor,\n  textblockTypeInputRule,\n  wrappingInputRule,\n  type Command,\n  type NodeRange,\n} from \"./vue-3\";\n"
  },
  {
    "path": "ui/packages/editor/src/tiptap/pm/index.ts",
    "content": "export * from \"@tiptap/pm/changeset\";\nexport * from \"@tiptap/pm/collab\";\nexport * from \"@tiptap/pm/dropcursor\";\nexport * from \"@tiptap/pm/gapcursor\";\nexport * from \"@tiptap/pm/history\";\nexport * from \"@tiptap/pm/inputrules\";\nexport * from \"@tiptap/pm/keymap\";\nexport * from \"@tiptap/pm/markdown\";\nexport { schema as markDownSchema } from \"@tiptap/pm/markdown\";\nexport * from \"@tiptap/pm/menu\";\nexport * from \"@tiptap/pm/model\";\nexport * from \"@tiptap/pm/schema-basic\";\nexport { schema } from \"@tiptap/pm/schema-basic\";\nexport * from \"@tiptap/pm/schema-list\";\nexport * from \"@tiptap/pm/state\";\nexport * from \"@tiptap/pm/tables\";\nexport * from \"@tiptap/pm/trailing-node\";\nexport * from \"@tiptap/pm/transform\";\nexport * from \"@tiptap/pm/view\";\n"
  },
  {
    "path": "ui/packages/editor/src/tiptap/vue-3/index.ts",
    "content": "export * from \"@tiptap/vue-3\";\n"
  },
  {
    "path": "ui/packages/editor/src/types/index.ts",
    "content": "import type {\n  arrow,\n  autoPlacement,\n  flip,\n  hide,\n  inline,\n  offset,\n  shift,\n  size,\n  VirtualElement,\n} from \"@floating-ui/dom\";\nimport type { Component } from \"vue\";\nimport type {\n  Editor,\n  EditorState,\n  EditorView,\n  PluginKey,\n  PMNode,\n  Range,\n  ResolvedPos,\n} from \"@/tiptap\";\n\nexport interface ToolbarItemType {\n  priority: number;\n  component: Component;\n  props: Omit<ToolbarItemComponentProps, \"children\"> & Record<string, unknown>;\n  children?: ToolbarItemType[];\n}\n\nexport interface ToolbarItemComponentProps {\n  editor: Editor;\n  isActive: boolean;\n  disabled?: boolean;\n  icon?: Component;\n  title?: string;\n  action?: () => void;\n  children?: ToolbarItemType[];\n}\n\nexport interface BubbleMenuOptions {\n  strategy?: \"absolute\" | \"fixed\";\n  placement?:\n    | \"top\"\n    | \"right\"\n    | \"bottom\"\n    | \"left\"\n    | \"top-start\"\n    | \"top-end\"\n    | \"right-start\"\n    | \"right-end\"\n    | \"bottom-start\"\n    | \"bottom-end\"\n    | \"left-start\"\n    | \"left-end\";\n  offset?: Parameters<typeof offset>[0] | boolean;\n  flip?: Parameters<typeof flip>[0] | boolean;\n  shift?: Parameters<typeof shift>[0] | boolean;\n  arrow?: Parameters<typeof arrow>[0] | false;\n  size?: Parameters<typeof size>[0] | boolean;\n  autoPlacement?: Parameters<typeof autoPlacement>[0] | boolean;\n  hide?: Parameters<typeof hide>[0] | boolean;\n  inline?: Parameters<typeof inline>[0] | boolean;\n  onShow?: () => void;\n  onHide?: () => void;\n  onUpdate?: () => void;\n  onDestroy?: () => void;\n  /**\n   * The scrollable element that should be listened to when updating the position of the bubble menu.\n   * If not provided, the window will be used.\n   * @type {HTMLElement | Window}\n   */\n  scrollTarget?: HTMLElement | Window;\n}\n\nexport interface DragButtonItemProps {\n  extendsKey?: string;\n  priority?: number;\n  title?:\n    | string\n    | (({\n        editor,\n        node,\n        pos,\n      }: {\n        editor: Editor;\n        node: PMNode | null;\n        pos: number;\n      }) => string);\n  icon?: Component;\n  key?: string;\n  action?: ({\n    editor,\n    node,\n    pos,\n    close,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n    close: () => void;\n  }) => Component | boolean | void | Promise<Component | boolean | void>;\n  iconStyle?: string;\n  class?: string;\n  visible?: ({\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => boolean;\n  isActive?: ({\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => boolean;\n  disabled?: ({\n    editor,\n    node,\n    pos,\n  }: {\n    editor: Editor;\n    node: PMNode | null;\n    pos: number;\n  }) => boolean;\n  keyboard?: string;\n  component?: Component;\n  [key: string]: unknown;\n}\nexport interface DragButtonType extends DragButtonItemProps {\n  children?: {\n    component?: Component;\n    items?: DragButtonItemProps[];\n  };\n}\n\nexport interface BubbleMenuProps {\n  pluginKey?: string | PluginKey;\n  editor?: Editor;\n  shouldShow?: (props: {\n    editor: Editor;\n    element: HTMLElement;\n    view: EditorView;\n    state: EditorState;\n    oldState?: EditorState;\n    from: number;\n    to: number;\n  }) => boolean;\n  appendTo?: HTMLElement | (() => HTMLElement) | undefined;\n  getReferencedVirtualElement?: () => VirtualElement | null;\n  options?: BubbleMenuOptions | null;\n}\n\nexport interface NodeBubbleMenuType extends BubbleMenuProps {\n  component?: Component;\n  items?: BubbleItemType[];\n  extendsKey?: string | PluginKey;\n}\n\nexport interface BubbleItemType {\n  priority: number;\n  component?: Component;\n  key?: string;\n  props?: Omit<BubbleItemComponentProps, \"editor\"> & Record<string, unknown>;\n}\n\nexport interface BubbleItemComponentProps {\n  editor: Editor;\n  isActive?: ({ editor }: { editor: Editor }) => boolean;\n  visible?: ({ editor }: { editor: Editor }) => boolean;\n  icon?: Component;\n  iconStyle?: string;\n  title?: string;\n  action?: ({ editor }: { editor: Editor }) => Component | boolean | void;\n}\n\nexport interface ToolboxItemType {\n  priority: number;\n  component: Component;\n  props: ToolboxItemComponentProps & Record<string, unknown>;\n}\n\nexport interface ToolboxItemComponentProps {\n  editor: Editor;\n  icon?: Component;\n  title?: string;\n  description?: string;\n  action?: () => void;\n}\n\nexport interface ExtensionOptions {\n  getToolbarItems?: ({\n    editor,\n  }: {\n    editor: Editor;\n  }) => ToolbarItemType | ToolbarItemType[];\n\n  getCommandMenuItems?: () => CommandMenuItemType | CommandMenuItemType[];\n\n  getBubbleMenu?: ({ editor }: { editor: Editor }) => NodeBubbleMenuType;\n\n  getToolboxItems?: ({\n    editor,\n  }: {\n    editor: Editor;\n  }) => ToolboxItemType | ToolboxItemType[];\n\n  getDraggableMenuItems?: ({\n    editor,\n  }: {\n    editor: Editor;\n  }) => DragButtonType | DragButtonType[];\n}\n\nexport interface CommandMenuItemType {\n  priority: number;\n  icon: Component;\n  title: string;\n  keywords: string[];\n  command: ({ editor, range }: { editor: Editor; range: Range }) => void;\n}\n\nexport interface DragSelectionNodeType {\n  $pos?: ResolvedPos;\n  node?: PMNode;\n  el: HTMLElement;\n  nodeOffset?: number;\n  dragDomOffset?: {\n    x?: number;\n    y?: number;\n  };\n}\n"
  },
  {
    "path": "ui/packages/editor/src/utils/anchor.ts",
    "content": "export function generateAnchor(text: string) {\n  return encodeURIComponent(\n    String(text).trim().toLowerCase().replace(/\\s+/g, \"-\")\n  );\n}\n\nexport const generateAnchorId = (text: string, ids: string[]) => {\n  const originId = generateAnchor(text);\n  let id = originId;\n  while (ids.includes(id)) {\n    const temporarySuffix = id.replace(originId, \"\");\n    const match = temporarySuffix.match(/-(\\d+)$/);\n    if (match) {\n      id = `${originId}-${Number(match[1]) + 1}`;\n    } else {\n      id = `${originId}-1`;\n    }\n  }\n  return id;\n};\n"
  },
  {
    "path": "ui/packages/editor/src/utils/attachment.ts",
    "content": "import { utils, type AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { Editor, generateJSON, type Content } from \"@/tiptap\";\n\nexport function convertToMediaContents(\n  editor: Editor,\n  attachments: AttachmentLike[]\n): Content[] {\n  return attachments\n    .map((attachment) => {\n      if (typeof attachment === \"string\") {\n        return {\n          type: \"image\",\n          attrs: {\n            src: attachment,\n          },\n        };\n      }\n\n      const attachmentSimple = utils.attachment.convertToSimple(attachment);\n\n      if (!attachmentSimple) {\n        return;\n      }\n\n      const { mediaType, alt, url, caption } = attachmentSimple;\n\n      if (mediaType?.startsWith(\"image/\")) {\n        return createFigureContent(editor, {\n          contentType: \"image\",\n          url,\n          alt,\n          caption,\n        });\n      }\n\n      if (mediaType?.startsWith(\"video/\")) {\n        return createFigureContent(editor, {\n          contentType: \"video\",\n          url,\n          caption,\n        });\n      }\n\n      if (mediaType?.startsWith(\"audio/\")) {\n        return createFigureContent(editor, {\n          contentType: \"audio\",\n          url,\n          caption,\n        });\n      }\n\n      return {\n        type: \"text\",\n        marks: [\n          {\n            type: \"link\",\n            attrs: {\n              href: url,\n            },\n          },\n        ],\n        text: alt || url,\n      };\n    })\n    .filter(Boolean) as Content[];\n}\n\nfunction createFigureContent(\n  editor: Editor,\n  {\n    contentType,\n    url,\n    alt,\n    caption,\n  }: {\n    contentType: \"image\" | \"video\" | \"audio\";\n    url: string;\n    alt?: string;\n    caption?: string;\n  }\n) {\n  const baseContent: Content = {\n    type: \"figure\",\n    attrs: {\n      contentType,\n    },\n    content: [\n      {\n        type: contentType,\n        attrs: {\n          src: url,\n          alt,\n        },\n      },\n    ],\n  };\n\n  if (caption) {\n    const captionContent = generateJSON(\n      caption,\n      editor.extensionManager.extensions\n    );\n    baseContent.content?.push({\n      type: \"figureCaption\",\n      content: captionContent.content[0].content,\n    });\n  }\n\n  return baseContent;\n}\n"
  },
  {
    "path": "ui/packages/editor/src/utils/clipboard.ts",
    "content": "import type { Editor, VueEditor } from \"@/tiptap\";\n\n/**\n * Copy the selected content from the editor to the clipboard\n * Supports both HTML and plain text formats\n *\n * @param editor - TipTap editor instance\n * @returns Promise<boolean> - Whether the copy operation was successful\n */\nexport async function copySelectionToClipboard(\n  editor: Editor | VueEditor\n): Promise<boolean> {\n  try {\n    const slice = editor.state.selection.content();\n    const { dom, text } = editor.view.serializeForClipboard(slice);\n    if (navigator.clipboard && window.ClipboardItem) {\n      try {\n        const htmlBlob = new Blob([dom.innerHTML], { type: \"text/html\" });\n        const textBlob = new Blob([text], { type: \"text/plain\" });\n        const clipboardItem = new ClipboardItem({\n          \"text/html\": htmlBlob,\n          \"text/plain\": textBlob,\n        });\n        await navigator.clipboard.write([clipboardItem]);\n        return true;\n      } catch {\n        await navigator.clipboard.writeText(text);\n        return true;\n      }\n    }\n    await navigator.clipboard.writeText(text);\n    return true;\n  } catch (error) {\n    console.error(\"Failed to copy to clipboard:\", error);\n    return false;\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/src/utils/delete-node.ts",
    "content": "import type { Editor } from \"@/tiptap\";\nimport { NodeSelection, Transaction, type ResolvedPos } from \"@/tiptap/pm\";\n\nexport const deleteNodeByPos = ($pos: ResolvedPos) => {\n  return (tr: Transaction) => {\n    if ($pos.depth) {\n      for (let d = $pos.depth; d > 0; d--) {\n        tr.delete($pos.before(d), $pos.after(d)).scrollIntoView();\n        return true;\n      }\n    } else {\n      const node = $pos.parent;\n      if (!node.isTextblock && node.nodeSize) {\n        tr.setSelection(\n          NodeSelection.create($pos.doc, $pos.pos)\n        ).deleteSelection();\n        return true;\n      }\n    }\n\n    const pos = $pos.pos;\n\n    if (pos) {\n      tr.delete(pos, pos + $pos.node().nodeSize);\n      return true;\n    }\n    return false;\n  };\n};\n\nexport const deleteNode = (nodeType: string, editor: Editor) => {\n  const { state } = editor;\n  const $pos = state.selection.$anchor;\n  const done = false;\n\n  if ($pos.depth) {\n    for (let d = $pos.depth; d > 0; d--) {\n      const node = $pos.node(d);\n      if (node.type.name === nodeType) {\n        // @ts-ignore\n        if (editor.dispatchTransaction)\n          // @ts-ignore\n          editor.dispatchTransaction(\n            state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView()\n          );\n        return true;\n      }\n    }\n  } else {\n    // @ts-ignore\n    const node = state.selection.node;\n    if (node && node.type.name === nodeType) {\n      editor.chain().deleteSelection().run();\n      return true;\n    }\n  }\n\n  if (!done) {\n    const pos = $pos.pos;\n\n    if (pos) {\n      const node = state.tr.doc.nodeAt(pos);\n\n      if (node && node.type.name === nodeType) {\n        // @ts-ignore\n        if (editor.dispatchTransaction)\n          // @ts-ignore\n          editor.dispatchTransaction(state.tr.delete(pos, pos + node.nodeSize));\n        return true;\n      }\n    }\n  }\n\n  return done;\n};\n"
  },
  {
    "path": "ui/packages/editor/src/utils/filter-duplicate-extensions.ts",
    "content": "import { utils } from \"@halo-dev/ui-shared\";\nimport {\n  getExtensionField,\n  type AnyConfig,\n  type AnyExtension,\n  type Extensions,\n} from \"@/tiptap\";\n\nexport const filterDuplicateExtensions = (\n  extensions: Extensions | undefined\n) => {\n  if (!extensions) {\n    return;\n  }\n  const resolvedExtensions = sort(extensions);\n  const map = new Map<string, AnyExtension>();\n  resolvedExtensions.filter(Boolean).forEach((extension) => {\n    if (!extension.name) {\n      console.warn(\n        `Extension name is missing for Extension, type: ${extension.type}.`\n      );\n      const key = utils.id.uuid();\n      map.set(key, extension);\n      return;\n    }\n    const key = `${extension.type}-${extension.name}`;\n    if (map.has(key)) {\n      console.warn(\n        `Duplicate found for Extension, type: ${extension.type}, name: ${extension.name}. Keeping the later one.`\n      );\n    }\n    map.set(key, extension);\n  });\n  return Array.from(map.values());\n};\n\n/**\n * Sort extensions by priority.\n * @param extensions An array of Tiptap extensions\n * @returns A sorted array of Tiptap extensions by priority\n */\nconst sort = (extensions: Extensions): Extensions => {\n  const defaultPriority = 100;\n\n  return extensions.sort((a, b) => {\n    const priorityA =\n      getExtensionField<AnyConfig[\"priority\"]>(a, \"priority\") ||\n      defaultPriority;\n    const priorityB =\n      getExtensionField<AnyConfig[\"priority\"]>(b, \"priority\") ||\n      defaultPriority;\n\n    if (priorityA > priorityB) {\n      return -1;\n    }\n\n    if (priorityA < priorityB) {\n      return 1;\n    }\n\n    return 0;\n  });\n};\n"
  },
  {
    "path": "ui/packages/editor/src/utils/index.ts",
    "content": "export * from \"./anchor\";\nexport * from \"./attachment\";\nexport * from \"./clipboard\";\nexport * from \"./delete-node\";\nexport * from \"./filter-duplicate-extensions\";\nexport * from \"./is-allowed-uri\";\nexport * from \"./is-list-active\";\nexport * from \"./is-node-empty\";\nexport * from \"./keyboard\";\nexport * from \"./upload\";\n"
  },
  {
    "path": "ui/packages/editor/src/utils/is-allowed-uri.ts",
    "content": "// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js\n// see https://github.com/ueberdosis/tiptap/pull/5160\n\nconst ATTR_WHITESPACE =\n  /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g;\nconst IS_ALLOWED_URI =\n  /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i;\n\nexport function isAllowedUri(uri: string | undefined) {\n  return !uri || uri.replace(ATTR_WHITESPACE, \"\").match(IS_ALLOWED_URI);\n}\n"
  },
  {
    "path": "ui/packages/editor/src/utils/is-list-active.ts",
    "content": "import {\n  callOrReturn,\n  Editor,\n  getExtensionField,\n  isActive,\n  type NodeConfig,\n} from \"@/tiptap\";\n\n/**\n * Check if a list is active\n *\n * @param state - The state of the editor\n * @returns Whether a list is active\n * @example\n * ```ts\n * const isActive = isListActive(editor.state);\n * ```\n **/\nexport const isListActive = (editor: Editor) => {\n  const extensions = editor.extensionManager.extensions;\n  const listExtensions = extensions.filter((extension) => {\n    const context = {\n      name: extension.name,\n      options: extension.options,\n      storage: extension.storage,\n    };\n\n    const group = callOrReturn(\n      getExtensionField<NodeConfig[\"group\"]>(extension, \"group\", context)\n    );\n\n    if (typeof group !== \"string\") {\n      return false;\n    }\n\n    return group.split(\" \").includes(\"list\");\n  });\n\n  return listExtensions.some((extension) => {\n    return isActive(editor.state, extension.name);\n  });\n};\n"
  },
  {
    "path": "ui/packages/editor/src/utils/is-node-empty.ts",
    "content": "import type { Node } from \"@tiptap/pm/model\";\n\nexport const isEmpty = (node: Node) => {\n  return isNodeDefault(node) || isNodeContentEmpty(node);\n};\n\nexport const isNodeDefault = (node: Node) => {\n  const defaultContent = node.type.createAndFill()?.toJSON();\n  const content = node.toJSON();\n  return JSON.stringify(defaultContent) === JSON.stringify(content);\n};\n\nexport const isNodeContentEmpty = (node: Node) => {\n  if (node.isTextblock) {\n    return node.textContent.length === 0;\n  }\n\n  if (node.childCount === 0) {\n    return true;\n  }\n\n  let allChildrenEmpty = true;\n  node.forEach((child) => {\n    if (!isEmpty(child)) {\n      allChildrenEmpty = false;\n    }\n  });\n\n  return allChildrenEmpty;\n};\n\nexport const isParagraphEmpty = (node: Node) => {\n  if (node.type.name !== \"paragraph\") {\n    return false;\n  }\n\n  return node.textContent.length === 0;\n};\n\nexport const isBlockEmpty = (node: Node) => {\n  if (!node.isTextblock) {\n    return false;\n  }\n\n  return isParagraphEmpty(node);\n};\n"
  },
  {
    "path": "ui/packages/editor/src/utils/keyboard.ts",
    "content": "/**\n * Keyboard shortcut utilities\n * Reference: tiptap keyboard shortcuts format https://tiptap.dev/docs/editor/core-concepts/keyboard-shortcuts\n */\n\ninterface ParsedShortcut {\n  key: string;\n  ctrlKey: boolean;\n  shiftKey: boolean;\n  altKey: boolean;\n  metaKey: boolean;\n}\n\n/**\n * Check if the current platform is Mac\n */\nconst isMac = () =>\n  typeof navigator !== \"undefined\" && /Mac/.test(navigator.platform);\n\n/**\n * Parse shortcut string\n * Supported formats:\n * - Mod-l (Mac: Cmd+L, Others: Ctrl+L)\n * - Control-Shift-Enter\n * - Shift-Tab\n * - Alt-1\n *\n * @param shortcut - Shortcut string\n * @returns Parsed shortcut object\n */\nexport function parseShortcut(shortcut: string): ParsedShortcut {\n  const parts = shortcut.split(\"-\");\n  const result: ParsedShortcut = {\n    key: \"\",\n    ctrlKey: false,\n    shiftKey: false,\n    altKey: false,\n    metaKey: false,\n  };\n\n  for (let i = 0; i < parts.length; i++) {\n    const part = parts[i];\n\n    if (i === parts.length - 1) {\n      result.key = normalizeKey(part);\n      continue;\n    }\n\n    const modifier = part.toLowerCase();\n    switch (modifier) {\n      case \"mod\":\n        if (isMac()) {\n          result.metaKey = true;\n        } else {\n          result.ctrlKey = true;\n        }\n        break;\n      case \"ctrl\":\n      case \"control\":\n        result.ctrlKey = true;\n        break;\n      case \"shift\":\n        result.shiftKey = true;\n        break;\n      case \"alt\":\n      case \"option\":\n        result.altKey = true;\n        break;\n      case \"cmd\":\n      case \"command\":\n      case \"meta\":\n        result.metaKey = true;\n        break;\n    }\n  }\n\n  return result;\n}\n\n/**\n * Normalize key name\n *\n * @param key - Key string\n * @returns Normalized key name\n */\nfunction normalizeKey(key: string): string {\n  const keyMap: Record<string, string> = {\n    space: \" \",\n    enter: \"Enter\",\n    tab: \"Tab\",\n    backspace: \"Backspace\",\n    delete: \"Delete\",\n    escape: \"Escape\",\n    esc: \"Escape\",\n    arrowleft: \"ArrowLeft\",\n    arrowright: \"ArrowRight\",\n    arrowup: \"ArrowUp\",\n    arrowdown: \"ArrowDown\",\n  };\n\n  const lowerKey = key.toLowerCase();\n  if (keyMap[lowerKey]) {\n    return keyMap[lowerKey];\n  }\n\n  if (key.length === 1) {\n    return key.toLowerCase();\n  }\n\n  return key;\n}\n\n/**\n * Check if a keyboard event matches a shortcut\n * @param event - Keyboard event\n * @param shortcut - Shortcut string\n * @returns Whether the event matches the shortcut\n */\nexport function matchShortcut(event: KeyboardEvent, shortcut: string): boolean {\n  const parsed = parseShortcut(shortcut);\n\n  let eventKey = event.key.toLowerCase();\n\n  if (event.key === \" \") {\n    eventKey = \" \";\n  }\n\n  if (eventKey !== parsed.key.toLowerCase()) {\n    return false;\n  }\n\n  if (event.ctrlKey !== parsed.ctrlKey) {\n    return false;\n  }\n  if (event.shiftKey !== parsed.shiftKey) {\n    return false;\n  }\n  if (event.altKey !== parsed.altKey) {\n    return false;\n  }\n  if (event.metaKey !== parsed.metaKey) {\n    return false;\n  }\n\n  return true;\n}\n\n/**\n * Format shortcut to display text\n * @param shortcut - Shortcut string\n * @returns Formatted display text\n */\nexport function formatShortcut(shortcut: string): string {\n  const parsed = parseShortcut(shortcut);\n  const parts: string[] = [];\n\n  if (isMac()) {\n    if (parsed.ctrlKey) {\n      parts.push(\"⌃\");\n    }\n    if (parsed.altKey) {\n      parts.push(\"⌥\");\n    }\n    if (parsed.shiftKey) {\n      parts.push(\"⇧\");\n    }\n    if (parsed.metaKey) {\n      parts.push(\"⌘\");\n    }\n  } else {\n    if (parsed.ctrlKey) {\n      parts.push(\"Ctrl\");\n    }\n    if (parsed.altKey) {\n      parts.push(\"Alt\");\n    }\n    if (parsed.shiftKey) parts.push(\"Shift\");\n    if (parsed.metaKey) {\n      parts.push(\"Meta\");\n    }\n  }\n\n  let key = parsed.key;\n  switch (key) {\n    case \" \":\n      key = \"Space\";\n      break;\n    case \"Enter\":\n      key = \"↵\";\n      break;\n    case \"Delete\":\n      key = \"Del\";\n      break;\n  }\n  if (key.length === 1) {\n    key = key.toUpperCase();\n  }\n  parts.push(key);\n\n  return isMac() ? parts.join(\"\") : parts.join(\"+\");\n}\n"
  },
  {
    "path": "ui/packages/editor/src/utils/upload.ts",
    "content": "import { ucApiClient, type Attachment } from \"@halo-dev/api-client\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { chunk } from \"es-toolkit\";\nimport { ExtensionAudio } from \"@/extensions/audio\";\nimport { ExtensionImage } from \"@/extensions/image\";\nimport { ExtensionVideo } from \"@/extensions/video\";\nimport { Editor, PMNode } from \"@/tiptap\";\n\nexport interface FileProps {\n  file: File;\n  editor: Editor;\n}\n\n/**\n * Creates an editor node from a file.\n *\n * @param editor - Editor instance\n * @param file - File to be uploaded\n * @returns - Editor node\n */\nexport const createEditorNodeFormFile = (editor: Editor, file: File) => {\n  if (file.type.startsWith(\"image/\")) {\n    return uploadImage(editor, file);\n  }\n\n  if (file.type.startsWith(\"video/\")) {\n    return uploadVideo(editor, file);\n  }\n\n  if (file.type.startsWith(\"audio/\")) {\n    return uploadAudio(editor, file);\n  }\n};\n\n/**\n * Handles file events, determining if the file is an image and triggering the appropriate upload process.\n *\n * @param {FileProps} { file, editor } - File and editor instances\n * @returns {boolean} - True if a file is handled, otherwise false\n */\nexport const handleFileEvent = (editor: Editor, files: File[]) => {\n  if (!files.length) {\n    return false;\n  }\n\n  if (\n    !utils.permission.has([\n      \"uc:attachments:manage\",\n      \"system:attachments:manage\",\n    ])\n  ) {\n    return false;\n  }\n\n  const nodes = files\n    .map((file) => createEditorNodeFormFile(editor, file))\n    .filter((node) => node !== undefined);\n\n  if (nodes.length) {\n    const tr = editor.view.state.tr;\n    tr.insert(editor.view.state.selection.from, nodes);\n    editor.view.dispatch(tr);\n  }\n};\n\n/**\n * Uploads an image file and inserts it into the editor.\n *\n * @param editor - Editor instance\n * @param file - File to be uploaded\n */\nexport const uploadImage = (editor: Editor, file: File) => {\n  const { state } = editor;\n  return state.schema.nodes[ExtensionImage.name].create({\n    file: file,\n  });\n};\n\n/**\n * Uploads a video file and inserts it into the editor.\n *\n * @param editor - Editor instance\n * @param file - File to be uploaded\n */\nexport const uploadVideo = (editor: Editor, file: File) => {\n  const { state } = editor;\n  return state.schema.nodes[ExtensionVideo.name].create({\n    file: file,\n  });\n};\n\n/**\n * Uploads an audio file and inserts it into the editor.\n *\n * @param editor - Editor instance\n * @param file - File to be uploaded\n */\nexport const uploadAudio = (editor: Editor, file: File) => {\n  const { state } = editor;\n  return state.schema.nodes[ExtensionAudio.name].create({\n    file: file,\n  });\n};\n\nexport interface UploadFetchResponse {\n  controller: AbortController;\n  onUploadProgress: (progress: number) => void;\n  onFinish: (attachment?: Attachment) => void;\n  onError: (error: Error) => void;\n}\n\n/**\n * Uploads a file with progress monitoring, cancellation support, and callbacks for completion and errors.\n *\n * @param {File} file - File to be uploaded\n * @param {Function} upload - Function to handle the file upload, should return a Promise\n * @returns {Promise<UploadFetchResponse>} - Returns an object with control and callback methods\n */\nexport const uploadFile = async (\n  file: File,\n  upload: (file: File, options?: AxiosRequestConfig) => Promise<Attachment>,\n  uploadResponse: UploadFetchResponse\n) => {\n  const { signal } = uploadResponse.controller;\n\n  upload(file, {\n    signal,\n    onUploadProgress(progressEvent) {\n      const progress = Math.round(\n        (progressEvent.loaded * 100) / (progressEvent.total || 0)\n      );\n      uploadResponse.onUploadProgress(progress);\n    },\n  })\n    .then((attachment) => {\n      uploadResponse.onFinish(attachment);\n    })\n    .catch((error) => {\n      uploadResponse.onError(error);\n    });\n};\n\n/**\n * Converts a file to a Base64 string.\n *\n * @param {File} file - File to be converted\n * @returns {Promise<string>} - A promise that resolves with the Base64 string\n */\nexport function fileToBase64(file: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = function () {\n      resolve(reader.result as string);\n    };\n    reader.onerror = function (error) {\n      reject(error);\n    };\n    reader.readAsDataURL(file);\n  });\n}\n\nexport function containsFileClipboardIdentifier(types: readonly string[]) {\n  const fileTypes = [\"files\", \"application/x-moz-file\", \"public.file-url\"];\n  return types.some((type) => fileTypes.includes(type.toLowerCase()));\n}\n\nexport async function batchUploadExternalLink(\n  editor: Editor,\n  nodes: { node: PMNode; pos: number; index: number; parent: PMNode | null }[]\n) {\n  const chunks = chunk(nodes, 5);\n\n  for (const chunkNodes of chunks) {\n    await Promise.all(\n      chunkNodes.map((node) => uploadExternalLink(editor, node))\n    );\n  }\n}\n\nexport async function uploadExternalLink(\n  editor: Editor,\n  nodeWithPos: {\n    node: PMNode;\n    pos: number;\n    index: number;\n    parent: PMNode | null;\n  }\n) {\n  const { node, pos } = nodeWithPos;\n  const { src } = node.attrs;\n\n  if (!isExternalAsset(src)) {\n    return;\n  }\n\n  try {\n    const { data } = await ucApiClient.storage.attachment.uploadAttachmentForUc(\n      {\n        url: src,\n      }\n    );\n\n    const url = data.status?.permalink;\n    const name = data.spec.displayName;\n    const tr = editor.view.state.tr;\n    tr.setNodeMarkup(pos, node.type, {\n      ...node.attrs,\n      src: url,\n      name,\n    });\n    editor.view.dispatch(tr);\n  } catch (error) {\n    console.error(\"Failed to upload external link:\", error);\n  }\n}\n\nexport function isExternalAsset(src: string) {\n  if (!src) {\n    return false;\n  }\n\n  if (src.startsWith(\"/\")) {\n    return false;\n  }\n\n  const localProtocols = [\"data:\", \"blob:\", \"file:\"];\n  if (localProtocols.some((protocol) => src.startsWith(protocol))) {\n    return false;\n  }\n\n  const currentOrigin = window.location.origin;\n  if (src.startsWith(currentOrigin)) {\n    return false;\n  }\n\n  return src.startsWith(\"http://\") || src.startsWith(\"https://\");\n}\n"
  },
  {
    "path": "ui/packages/editor/tailwind.config.ts",
    "content": "import tailwindConfig from \"../../tailwind.config\";\n\nexport default tailwindConfig;\n"
  },
  {
    "path": "ui/packages/editor/tsconfig.app.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"include\": [\n    \"./env.d.ts\",\n    \"./src/**/*\",\n    \"./src/**/*.vue\",\n    \"./src/locales/*.json\"\n  ],\n  \"exclude\": [\"./src/**/__tests__/*\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"types\": [\"unplugin-icons/types/vue\"]\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.vitest.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "ui/packages/editor/tsconfig.node.json",
    "content": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"include\": [\n    \"vite.config.*\",\n    \"tailwind.config.ts\",\n    \"../../tailwind.config.ts\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"types\": [\"node\", \"vitest\"]\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/tsconfig.vitest.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"exclude\": [],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"lib\": [],\n    \"types\": [\"node\", \"jsdom\"]\n  }\n}\n"
  },
  {
    "path": "ui/packages/editor/vite.config.ts",
    "content": "import path from \"node:path\";\nimport { fileURLToPath, URL } from \"node:url\";\nimport VueI18nPlugin from \"@intlify/unplugin-vue-i18n/vite\";\nimport Vue from \"@vitejs/plugin-vue\";\nimport Icons from \"unplugin-icons/vite\";\nimport { defineConfig } from \"vite\";\nimport dts from \"vite-plugin-dts\";\n\nexport default ({ mode }: { mode: string }) => {\n  const isProduction = mode === \"production\";\n\n  return defineConfig({\n    plugins: [\n      Vue(),\n      Icons({\n        compiler: \"vue3\",\n      }),\n      isProduction &&\n        dts({\n          tsconfigPath: \"./tsconfig.app.json\",\n          entryRoot: \"./src\",\n          outDir: \"./dist\",\n          insertTypesEntry: true,\n        }),\n      VueI18nPlugin({\n        include: [path.resolve(__dirname, \"./src/locales/*.json\")],\n      }),\n    ],\n    define: {\n      \"process.env.NODE_ENV\": '\"production\"',\n    },\n    resolve: {\n      alias: {\n        \"@\": fileURLToPath(new URL(\"./src\", import.meta.url)),\n      },\n    },\n    build: {\n      outDir: path.resolve(__dirname, \"dist\"),\n      lib: {\n        entry: path.resolve(__dirname, \"src/index.ts\"),\n        name: \"RichTextEditor\",\n        formats: [\"es\", \"iife\"],\n        fileName: (format) => `index.${format}.js`,\n        cssFileName: \"style\",\n      },\n      minify: isProduction,\n      rollupOptions: {\n        external: [\n          \"vue\",\n          \"@halo-dev/ui-shared\",\n          \"@halo-dev/api-client\",\n          \"@halo-dev/components\",\n        ],\n        output: {\n          globals: {\n            vue: \"Vue\",\n            \"@halo-dev/ui-shared\": \"HaloUiShared\",\n            \"@halo-dev/api-client\": \"HaloApiClient\",\n            \"@halo-dev/components\": \"HaloComponents\",\n          },\n          exports: \"named\",\n        },\n      },\n      sourcemap: false,\n    },\n  });\n};\n"
  },
  {
    "path": "ui/packages/shared/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\nexport {};\n\nimport type { Component } from \"vue\";\nimport type { CoreMenuGroupId } from \"./src/types/menus\";\n\ndeclare module \"vue-router\" {\n  interface RouteMeta {\n    title?: string;\n    searchable?: boolean;\n    permissions?: string[];\n    core?: boolean;\n    menu?: {\n      name: string;\n      group?: CoreMenuGroupId;\n      icon?: Component;\n      priority: number;\n      mobile?: boolean;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/packages/shared/package.json",
    "content": "{\n  \"name\": \"@halo-dev/ui-shared\",\n  \"version\": \"2.23.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"homepage\": \"https://github.com/halo-dev/halo/tree/main/ui/packages/shared#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/halo-dev/halo/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"@halo-dev\",\n    \"url\": \"https://github.com/halo-dev\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/halo-dev/halo.git\",\n    \"directory\": \"ui/packages/shared\"\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": \"./dist/index.js\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"dev\": \"tsdown --watch\",\n    \"typecheck\": \"tsc --noEmit -p tsconfig.app.json --composite false\",\n    \"prepublishOnly\": \"pnpm run build\"\n  },\n  \"dependencies\": {\n    \"@halo-dev/api-client\": \"workspace:*\",\n    \"@halo-dev/richtext-editor\": \"workspace:*\",\n    \"mitt\": \"^3.0.1\"\n  },\n  \"devDependencies\": {\n    \"dayjs\": \"^1.11.19\",\n    \"uuid\": \"^13.0.0\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"^3.5.x\",\n    \"vue-router\": \"^5.0.x\"\n  },\n  \"inlinedDependencies\": {\n    \"mitt\": \"3.0.1\",\n    \"dayjs\": \"1.11.19\",\n    \"uuid\": \"13.0.0\"\n  }\n}\n"
  },
  {
    "path": "ui/packages/shared/src/events/index.ts",
    "content": "import mitt from \"mitt\";\n\ntype Events = {\n  /**\n   * Emitted when a plugin's configuration map is updated.\n   */\n  \"core:plugin:configMap:updated\": { pluginName: string; group: string };\n};\n\nconst events = mitt<Events>();\n\nexport { events };\n"
  },
  {
    "path": "ui/packages/shared/src/index.ts",
    "content": "export * from \"./events\";\nexport * from \"./plugin\";\nexport * from \"./stores\";\nexport * from \"./types\";\nexport * from \"./utils\";\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/index.ts",
    "content": "export * from \"./types\";\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/attachment-selector.ts",
    "content": "import type { Attachment } from \"@halo-dev/api-client\";\nimport type { Component } from \"vue\";\n\n/**\n * Represents a simplified attachment object with essential properties.\n * Used for lightweight attachment representations without full API client dependencies.\n */\nexport interface AttachmentSimple {\n  /**\n   * The URL of the attachment.\n   */\n  url: string;\n\n  /**\n   * The MIME type of the attachment (e.g., 'image/png', 'video/mp4').\n   */\n  mediaType?: string;\n\n  /**\n   * Alternative text for the attachment, used for accessibility.\n   */\n  alt?: string;\n\n  /**\n   * Caption or description for the attachment.\n   */\n  caption?: string;\n}\n\n/**\n * Union type representing different forms of attachment references.\n *\n * @remarks\n * This flexible type allows working with attachments in various formats:\n * - Full `Attachment` object from the API client\n * - Simplified `AttachmentSimple` object with basic properties\n * - String URL directly referencing the attachment\n */\nexport type AttachmentLike = Attachment | AttachmentSimple | string;\n\n/**\n * Defines a custom attachment selector provider that plugins can register.\n *\n * @remarks\n * Attachment selector providers allow plugins to create custom UI components\n * for selecting attachments from different sources (e.g., local upload,\n * external services, galleries, etc.).\n */\nexport interface AttachmentSelectProvider {\n  /**\n   * Unique identifier for this attachment selector provider.\n   * Should follow a namespaced pattern to avoid conflicts (e.g., 'plugin-name:selector-id').\n   */\n  id: string;\n\n  /**\n   * Display label for the attachment selector.\n   * This label will be shown in the UI where users can choose between different selectors.\n   */\n  label: string;\n\n  /**\n   * The Vue component that implements the attachment selection interface.\n   * Can be either a component definition or a component name string.\n   *\n   * @remarks\n   * The component should emit selected attachments through the provided callback\n   * or emit events that the parent component can handle.\n   */\n  component: Component | string;\n\n  /**\n   * Optional callback function invoked when attachments are selected.\n   *\n   * @param attachments - Array of selected attachments in various formats.\n   *\n   * @remarks\n   * This callback is triggered after the user confirms their selection.\n   * It allows the provider to handle post-selection logic like validation,\n   * transformation, or triggering additional actions.\n   */\n  callback?: (attachments: AttachmentLike[]) => void;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/backup.ts",
    "content": "import type { Component, Raw } from \"vue\";\n\n/**\n * Defines a custom tab for the backup management page.\n *\n * @remarks\n * Backup tabs allow plugins to extend the backup interface with custom\n * functionality such as cloud backup providers, automated backup schedules,\n * or specialized restore operations.\n */\nexport interface BackupTab {\n  /**\n   * Unique identifier for the backup tab.\n   * Should follow a namespaced pattern to avoid conflicts (e.g., 'plugin-name:tab-id').\n   */\n  id: string;\n\n  /**\n   * Display label for the tab.\n   * This text will be shown in the tab navigation.\n   */\n  label: string;\n\n  /**\n   * The Vue component that renders the tab content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional array of permission identifiers required to view this tab.\n   * The tab will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/comment.ts",
    "content": "import type { Extension } from \"@halo-dev/api-client\";\nimport type { Component, Raw } from \"vue\";\nimport type { RouteLocationRaw } from \"vue-router\";\n\n/**\n * Represents the result of resolving a comment subject reference.\n * Contains display information and navigation details for the commented content.\n */\nexport interface CommentSubjectRefResult {\n  /**\n   * Short label or type identifier for the subject (e.g., 'Post', 'Page', 'Custom').\n   */\n  label: string;\n\n  /**\n   * The title or name of the content being commented on.\n   */\n  title: string;\n\n  /**\n   * Internal route location to navigate to the subject.\n   * Used for navigating within the Halo console.\n   */\n  route?: RouteLocationRaw;\n\n  /**\n   * External URL to the subject if it's hosted outside the console.\n   * Typically used for public-facing content.\n   */\n  externalUrl?: string;\n}\n\n/**\n * Defines a provider that resolves comment subject references.\n *\n * @remarks\n * Comment subject reference providers allow plugins to make their custom content types\n * commentable. The provider resolves extension references into human-readable information\n * that can be displayed in the comment management interface.\n */\nexport type CommentSubjectRefProvider = {\n  /**\n   * The kind of the extension that this provider handles.\n   * Must match the `kind` field in the extension's metadata.\n   */\n  kind: string;\n\n  /**\n   * The API group of the extension that this provider handles.\n   * Must match the `group` field in the extension's metadata.\n   */\n  group: string;\n\n  /**\n   * Resolves an extension reference into displayable comment subject information.\n   *\n   * @param subject - The extension object representing the commented content.\n   * @returns The resolved subject information including label, title, and navigation details.\n   */\n  resolve: (subject: Extension) => CommentSubjectRefResult;\n};\n\n/**\n * Defines a custom comment editor provider.\n *\n * @remarks\n * Allows plugins to replace the default comment editor with a custom implementation.\n * Useful for providing rich-text editing, markdown support, or specialized input methods.\n */\nexport interface CommentEditorProvider {\n  /**\n   * The Vue component that implements the custom comment editor.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component should emit appropriate events for content changes and\n   * integrate with the parent form for submission handling.\n   */\n  component: Raw<Component>;\n}\n\n/**\n * Defines a custom comment content renderer.\n *\n * @remarks\n * Allows plugins to customize how comment content is displayed in comment lists.\n * Useful when comments are stored in a custom format that requires special rendering.\n */\nexport interface CommentContentProvider {\n  /**\n   * The Vue component that renders the comment content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component receives the comment content as props and should handle\n   * rendering, sanitization, and any interactive features.\n   */\n  component: Raw<Component>;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/dashboard-widget.ts",
    "content": "import type { Component, Raw } from \"vue\";\nimport type { RouteLocationRaw } from \"vue-router\";\n\n/**\n * Defines responsive layout configurations for dashboard widgets across different screen sizes.\n *\n * @remarks\n * Each breakpoint can have a different layout arrangement. If a breakpoint is not specified,\n * it will fall back to the next larger breakpoint's layout.\n */\nexport interface DashboardResponsiveLayout {\n  /**\n   * Layout for large screens (typically ≥ 1200px).\n   */\n  lg?: DashboardWidget[];\n\n  /**\n   * Layout for medium screens (typically ≥ 996px).\n   */\n  md?: DashboardWidget[];\n\n  /**\n   * Layout for small screens (typically ≥ 768px).\n   */\n  sm?: DashboardWidget[];\n\n  /**\n   * Layout for extra small screens (typically ≥ 480px).\n   */\n  xs?: DashboardWidget[];\n\n  /**\n   * Layout for double extra small screens (typically < 480px).\n   */\n  xxs?: DashboardWidget[];\n}\n\n/**\n * Represents an instance of a dashboard widget with its position, size, and configuration.\n *\n * @remarks\n * Dashboard widgets are arranged in a grid layout system. The grid typically has 12 columns,\n * and widgets can span multiple rows and columns.\n */\nexport interface DashboardWidget {\n  /**\n   * The x-coordinate (column position) in the grid, starting from 0.\n   */\n  x: number;\n\n  /**\n   * The y-coordinate (row position) in the grid, starting from 0.\n   */\n  y: number;\n\n  /**\n   * The width of the widget in grid columns.\n   */\n  w: number;\n\n  /**\n   * The height of the widget in grid rows.\n   */\n  h: number;\n\n  /**\n   * Unique identifier for this widget instance in the layout.\n   * Can be a number or string.\n   */\n  i: number | string;\n\n  /**\n   * Minimum width constraint in grid columns.\n   */\n  minW?: number;\n\n  /**\n   * Minimum height constraint in grid rows.\n   */\n  minH?: number;\n\n  /**\n   * Maximum width constraint in grid columns.\n   */\n  maxW?: number;\n\n  /**\n   * Maximum height constraint in grid rows.\n   */\n  maxH?: number;\n\n  /**\n   * Reference to the widget definition ID that this instance represents.\n   */\n  id: string;\n\n  /**\n   * User-specific configuration for this widget instance.\n   * The structure depends on the widget's definition schema.\n   */\n  config?: Record<string, unknown>;\n\n  /**\n   * Optional array of permission identifiers required to view this widget instance.\n   */\n  permissions?: string[];\n}\n\n/**\n * Defines a dashboard widget type that can be added to the dashboard.\n *\n * @remarks\n * Widget definitions serve as templates from which users can create widget instances.\n * Each definition specifies the component, default configuration, and size constraints.\n *\n * @example\n * ```typescript\n * const widgetDef: DashboardWidgetDefinition = {\n *   id: 'plugin-name:widget-id',\n *   component: markRaw(MyWidgetComponent),\n *   group: 'statistics',\n *   defaultSize: { w: 4, h: 2, minW: 2, minH: 1 },\n *   defaultConfig: { refreshInterval: 60000 },\n *   configFormKitSchema: [\n *     { $formkit: 'number', name: 'refreshInterval', label: 'Refresh Interval (ms)' }\n *   ]\n * };\n * ```\n */\nexport interface DashboardWidgetDefinition {\n  /**\n   * Unique identifier for the widget definition.\n   * Should follow a namespaced pattern (e.g., 'plugin-name:widget-id').\n   */\n  id: string;\n\n  /**\n   * The Vue component that renders the widget.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Category or group for organizing widgets in the selection interface.\n   * Common groups include 'statistics', 'content', 'system', etc.\n   */\n  group: string;\n\n  /**\n   * FormKit schema for the widget's configuration form.\n   * Can be an array, a function returning an array, or a function returning a Promise.\n   *\n   * @remarks\n   * Defines the editable settings that users can configure for widget instances.\n   * The schema uses FormKit's schema format.\n   */\n  configFormKitSchema?:\n    | Record<string, unknown>[]\n    | (() => Promise<Record<string, unknown>[]>)\n    | (() => Record<string, unknown>[]);\n\n  /**\n   * Default configuration values for new widget instances.\n   * These values will be used when a user first adds the widget to their dashboard.\n   */\n  defaultConfig?: Record<string, unknown>;\n\n  /**\n   * Default size and constraints for the widget.\n   *\n   * @remarks\n   * Specifies the initial dimensions and any size limitations when the widget is added.\n   */\n  defaultSize: {\n    /**\n     * Default width in grid columns.\n     */\n    w: number;\n\n    /**\n     * Default height in grid rows.\n     */\n    h: number;\n\n    /**\n     * Minimum width constraint in grid columns.\n     */\n    minW?: number;\n\n    /**\n     * Minimum height constraint in grid rows.\n     */\n    minH?: number;\n\n    /**\n     * Maximum width constraint in grid columns.\n     */\n    maxW?: number;\n\n    /**\n     * Maximum height constraint in grid rows.\n     */\n    maxH?: number;\n  };\n\n  /**\n   * Optional array of permission identifiers required to add or view this widget.\n   */\n  permissions?: string[];\n}\n\n/**\n * Base interface for dashboard widget quick action items.\n */\ninterface DashboardWidgetQuickActionBaseItem {\n  /**\n   * Unique identifier for the quick action.\n   */\n  id: string;\n\n  /**\n   * Optional array of permission identifiers required to see this action.\n   */\n  permissions?: string[];\n}\n\n/**\n * A quick action item that uses a custom component for rendering.\n * Provides maximum flexibility for complex action UI.\n */\ninterface DashboardWidgetQuickActionComponentItem extends DashboardWidgetQuickActionBaseItem {\n  /**\n   * Custom Vue component for rendering the quick action.\n   * When provided, standard properties (icon, title, action) are optional.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional icon component displayed alongside the action.\n   */\n  icon?: Raw<Component>;\n\n  /**\n   * Optional title text for the action.\n   */\n  title?: string;\n\n  /**\n   * Optional click handler for the action.\n   */\n  action?: () => void;\n}\n\n/**\n * A standard quick action item with icon, title, and action handler.\n * Uses the default rendering style for consistency.\n */\ninterface DashboardWidgetQuickActionStandardItem extends DashboardWidgetQuickActionBaseItem {\n  /**\n   * Cannot provide a custom component for standard items.\n   */\n  component?: never;\n\n  /**\n   * Icon component displayed with the action (required for standard items).\n   */\n  icon: Raw<Component>;\n\n  /**\n   * Display title for the action (required for standard items).\n   */\n  title: string;\n\n  /**\n   * Click handler invoked when the action is triggered (required for standard items).\n   */\n  action: () => void;\n}\n\n/**\n * A quick action item that navigates to a route when triggered.\n */\ninterface DashboardWidgetQuickActionRouteItem extends DashboardWidgetQuickActionBaseItem {\n  /**\n   * Cannot provide a custom component for route items.\n   */\n  component?: never;\n\n  /**\n   * Cannot provide an action handler for route items.\n   */\n  action?: never;\n\n  /**\n   * Icon component displayed with the action (required for route items).\n   */\n  icon: Raw<Component>;\n\n  /**\n   * Display title for the action (required for route items).\n   */\n  title: string;\n\n  /**\n   * Route to navigate to when the action is triggered (required for route items).\n   */\n  route: RouteLocationRaw;\n}\n\n/**\n * Represents a quick action button that can be added to dashboard widgets.\n *\n * @remarks\n * Quick actions provide shortcuts to common operations directly from the dashboard.\n * They can either use a custom component for full control or follow the standard\n * pattern with an icon, title, and action handler.\n */\nexport type DashboardWidgetQuickActionItem =\n  | DashboardWidgetQuickActionComponentItem\n  | DashboardWidgetQuickActionStandardItem\n  | DashboardWidgetQuickActionRouteItem;\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/editor-provider.ts",
    "content": "import type { Component } from \"vue\";\n\n/**\n * Defines a custom content editor provider.\n *\n * @remarks\n * Editor providers allow plugins to register alternative content editors beyond\n * the default editor. This enables support for different content formats, editing\n * experiences, and specialized workflows (e.g., Markdown, rich-text, code, visual builders).\n *\n * @example\n * ```typescript\n * const markdownEditor: EditorProvider = {\n *   name: 'markdown-editor',\n *   displayName: 'Markdown Editor',\n *   logo: '/path/to/markdown-logo.svg',\n *   component: MarkdownEditorComponent,\n *   rawType: 'markdown'\n * };\n * ```\n */\nexport interface EditorProvider {\n  /**\n   * Unique identifier for the editor provider.\n   * Should use kebab-case naming (e.g., 'markdown-editor', 'wysiwyg-editor').\n   */\n  name: string;\n\n  /**\n   * Human-readable display name shown in the editor selection UI.\n   */\n  displayName: string;\n\n  /**\n   * Optional URL or path to the editor's logo image.\n   * Displayed in the editor selection interface for visual identification.\n   */\n  logo?: string;\n\n  /**\n   * The Vue component that implements the editor interface.\n   *\n   * @remarks\n   * The component should handle content editing, emit appropriate events for\n   * content changes, and integrate with the parent form for validation and submission.\n   */\n  component: Component;\n\n  /**\n   * The content format or MIME type that this editor produces.\n   *\n   * @remarks\n   * This identifier is stored with the content to indicate which editor should be\n   * used when reopening for editing. Common values include:\n   * - 'markdown' for Markdown content\n   * - 'html' for HTML content\n   * - 'json' for structured JSON data\n   * - Custom identifiers for proprietary formats\n   */\n  rawType: string;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/index.ts",
    "content": "export * from \"./attachment-selector\";\nexport * from \"./backup\";\nexport * from \"./comment\";\nexport * from \"./dashboard-widget\";\nexport * from \"./editor-provider\";\nexport * from \"./list-entity-field\";\nexport * from \"./list-operation\";\nexport * from \"./plugin-installation-tab\";\nexport * from \"./plugin-tab\";\nexport * from \"./theme-list-tab\";\nexport * from \"./ui-plugin-entry\";\nexport * from \"./ui-plugin-module\";\nexport * from \"./user-tab\";\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/list-entity-field.ts",
    "content": "import type { Component, Raw } from \"vue\";\n\n/**\n * Defines a custom field (column) that can be added to entity list views.\n *\n * @remarks\n * Entity field items allow plugins to add custom columns or metadata displays\n * to list views such as posts, pages, comments, etc. Fields can be positioned\n * at the start or end of the list and can include custom rendering logic.\n *\n * @example\n * ```typescript\n * const customField: EntityFieldItem = {\n *   priority: 10,\n *   position: 'end',\n *   component: markRaw(CustomFieldComponent),\n *   props: { format: 'short' },\n *   permissions: ['plugin:my-plugin:view']\n * };\n * ```\n */\nexport interface EntityFieldItem {\n  /**\n   * Priority for ordering multiple custom fields.\n   * Higher priority fields are displayed first within their position group.\n   */\n  priority: number;\n\n  /**\n   * The position where this field should be inserted in the list.\n   * - 'start': Before the default fields\n   * - 'end': After the default fields\n   */\n  position: \"start\" | \"end\";\n\n  /**\n   * The Vue component that renders the field content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component typically receives the entity object as a prop and should\n   * render the custom field information in a list-friendly format.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional props to pass to the component.\n   * These can be used to configure the field's behavior or appearance.\n   */\n  props?: Record<string, unknown>;\n\n  /**\n   * Optional array of permission identifiers required to view this field.\n   * The field will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n\n  /**\n   * Whether the field should be hidden by default.\n   * Can be used to implement toggleable columns or conditional visibility.\n   */\n  hidden?: boolean;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/list-operation.ts",
    "content": "import type { Component, Raw } from \"vue\";\n\n/**\n * Defines a custom operation (action button or menu item) for entity list items.\n *\n * @remarks\n * Operation items allow plugins to add custom actions to list rows across various\n * entity types (posts, pages, comments, etc.). Operations can be standalone buttons\n * or nested within dropdown menus, and can include permission checks and conditional visibility.\n *\n * @typeParam T - The type of entity this operation applies to (e.g., ListedPost, Plugin, Theme).\n *\n * @example\n * ```typescript\n * const exportOperation: OperationItem<ListedPost> = {\n *   priority: 20,\n *   component: markRaw(VButton),\n *   label: 'Export',\n *   action: (post) => {\n *     console.log('Exporting post:', post.metadata.name);\n *   },\n *   permissions: ['plugin:export:use']\n * };\n *\n * // Operation with children (dropdown menu)\n * const shareOperation: OperationItem<ListedPost> = {\n *   priority: 15,\n *   component: markRaw(VDropdown),\n *   label: 'Share',\n *   children: [\n *     {\n *       priority: 10,\n *       component: markRaw(VButton),\n *       label: 'Share to Twitter',\n *       action: (post) => { // ... }\n *     },\n *     {\n *       priority: 20,\n *       component: markRaw(VButton),\n *       label: 'Copy Link',\n *       action: (post) => { // ... }\n *     }\n *   ]\n * };\n * ```\n */\nexport interface OperationItem<T> {\n  /**\n   * Priority for ordering multiple operations.\n   * Higher priority operations are displayed first (leftmost or at the top).\n   */\n  priority: number;\n\n  /**\n   * The Vue component that renders the operation.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * Common components include buttons, dropdown menus, or custom action components.\n   * The component receives props and should handle the visual representation and\n   * interaction of the operation.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional props to pass to the component.\n   * Can be used to configure appearance, behavior, or additional data.\n   */\n  props?: Record<string, unknown>;\n\n  /**\n   * Optional action handler invoked when the operation is triggered.\n   *\n   * @param item - The entity instance that this operation was triggered on.\n   *\n   * @remarks\n   * This is typically used for operations that need to perform actions on the entity,\n   * such as exporting, sharing, or custom processing. The handler can be async.\n   */\n  action?: (item?: T) => void;\n\n  /**\n   * Optional display label for the operation.\n   * Used for button text, menu items, or tooltips.\n   */\n  label?: string;\n\n  /**\n   * Whether the operation should be hidden by default.\n   * Can be used to implement conditional visibility based on entity state or user context.\n   */\n  hidden?: boolean;\n\n  /**\n   * Optional array of permission identifiers required to see this operation.\n   * The operation will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n\n  /**\n   * Optional nested operations for creating hierarchical menus.\n   *\n   * @remarks\n   * When provided, this operation becomes a dropdown menu or submenu containing\n   * the child operations. Useful for grouping related actions together.\n   */\n  children?: OperationItem<T>[];\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/plugin-installation-tab.ts",
    "content": "import type { Component, Raw } from \"vue\";\n\n/**\n * Defines a custom tab for the plugin installation page.\n *\n * @remarks\n * Plugin installation tabs allow plugins to provide alternative installation methods\n * or additional functionality during the plugin installation process. Examples include\n * installing from a URL, importing from a marketplace, or batch installation features.\n *\n * @example\n * ```typescript\n * const remoteInstallTab: PluginInstallationTab = {\n *   id: 'remote-install',\n *   label: 'Install from URL',\n *   component: markRaw(RemoteInstallComponent),\n *   priority: 10,\n *   permissions: ['plugin:install:remote']\n * };\n * ```\n */\nexport interface PluginInstallationTab {\n  /**\n   * Unique identifier for the installation tab.\n   * Should follow a namespaced pattern to avoid conflicts (e.g., 'plugin-name:tab-id').\n   */\n  id: string;\n\n  /**\n   * Display label for the tab.\n   * This text will be shown in the tab navigation.\n   */\n  label: string;\n\n  /**\n   * The Vue component that renders the tab content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component should implement the installation interface and handle\n   * the complete installation workflow, including validation and error handling.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional props to pass to the component.\n   * Can be used to configure the installation behavior or provide additional context.\n   */\n  props?: Record<string, unknown>;\n\n  /**\n   * Optional array of permission identifiers required to view this tab.\n   * The tab will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n\n  /**\n   * Priority for ordering multiple installation tabs.\n   * Higher priority tabs appear first (leftmost) in the tab navigation.\n   */\n  priority: number;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/plugin-tab.ts",
    "content": "import type { Component, Raw } from \"vue\";\n\n/**\n * Defines a custom tab for a plugin's own detail/settings page.\n *\n * @remarks\n * Plugin tabs allow plugins to organize their settings, configuration, and\n * management interfaces into multiple sections. This is useful for plugins\n * with extensive configuration options or multiple functional areas.\n *\n * @example\n * ```typescript\n * const settingsTab: PluginTab = {\n *   id: 'settings',\n *   label: 'Settings',\n *   component: markRaw(SettingsComponent),\n *   permissions: ['plugin:my-plugin:manage']\n * };\n * ```\n */\nexport interface PluginTab {\n  /**\n   * Unique identifier for the plugin tab.\n   * Should follow a namespaced pattern to avoid conflicts (e.g., 'plugin-name:tab-id').\n   */\n  id: string;\n\n  /**\n   * Display label for the tab.\n   * This text will be shown in the tab navigation.\n   */\n  label: string;\n\n  /**\n   * The Vue component that renders the tab content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component should implement the tab's functionality, which might include\n   * configuration forms, status displays, or management interfaces.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional array of permission identifiers required to view this tab.\n   * The tab will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/theme-list-tab.ts",
    "content": "import type { Component, Raw } from \"vue\";\n\n/**\n * Defines a custom tab for the theme list page.\n *\n * @remarks\n * Theme list tabs allow plugins to extend the theme management interface with\n * custom functionality such as theme marketplaces, remote theme browsers,\n * theme backup/restore tools, or advanced theme customization interfaces.\n *\n * @example\n * ```typescript\n * const themeMarketplaceTab: ThemeListTab = {\n *   id: 'marketplace',\n *   label: 'Theme Marketplace',\n *   component: markRaw(MarketplaceComponent),\n *   priority: 20,\n *   permissions: ['plugin:theme-marketplace:view']\n * };\n * ```\n */\nexport interface ThemeListTab {\n  /**\n   * Unique identifier for the theme list tab.\n   * Should follow a namespaced pattern to avoid conflicts (e.g., 'plugin-name:tab-id').\n   */\n  id: string;\n\n  /**\n   * Display label for the tab.\n   * This text will be shown in the tab navigation.\n   */\n  label: string;\n\n  /**\n   * The Vue component that renders the tab content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component should implement the theme-related functionality and integrate\n   * appropriately with the theme management system.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional props to pass to the component.\n   * Can be used to configure behavior or provide additional context.\n   */\n  props?: Record<string, unknown>;\n\n  /**\n   * Optional array of permission identifiers required to view this tab.\n   * The tab will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n\n  /**\n   * Priority for ordering multiple theme list tabs.\n   * Higher priority tabs appear first (leftmost) in the tab navigation.\n   */\n  priority: number;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/ui-plugin-entry.ts",
    "content": "import type { PluginModule } from \"./ui-plugin-module\";\n\n/**\n * Helper function to define a Halo UI plugin module with type safety.\n *\n * @param plugin - The plugin module definition containing components, routes, and extension points.\n * @returns The same plugin module, properly typed for Halo's plugin system.\n *\n * @remarks\n * This function is a type-safe wrapper that provides IDE support and type checking\n * for plugin definitions. It doesn't perform any runtime transformations, but ensures\n * that the plugin structure conforms to the expected interface.\n *\n * @example\n * ```typescript\n * import { definePlugin } from \"@halo-dev/ui-shared\";\n * import { markRaw } from \"vue\";\n *\n * export default definePlugin({\n *   components: {\n *     \"my-widget\": MyWidgetComponent\n *   },\n *   routes: [\n *     {\n *       path: \"/my-plugin\",\n *       name: \"MyPlugin\",\n *       component: MyPluginView\n *     }\n *   ],\n *   extensionPoints: {\n *     \"post:list-item:operation:create\": (post) => [\n *       {\n *         priority: 10,\n *         component: markRaw(VButton),\n *         label: \"Custom Action\",\n *         action: () => {\n *           console.log(\"Action triggered\");\n *         }\n *       }\n *     ]\n *   }\n * });\n * ```\n */\nexport function definePlugin(plugin: PluginModule): PluginModule {\n  return plugin;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/ui-plugin-module.ts",
    "content": "import type {\n  Attachment,\n  Backup,\n  ListedComment,\n  ListedPost,\n  ListedReply,\n  ListedSinglePage,\n  Plugin,\n  Theme,\n} from \"@halo-dev/api-client\";\nimport type { AnyExtension } from \"@halo-dev/richtext-editor\";\nimport type { Component, Ref } from \"vue\";\nimport type { RouteRecordName, RouteRecordRaw } from \"vue-router\";\nimport type { AttachmentSelectProvider } from \"./attachment-selector\";\nimport type { BackupTab } from \"./backup\";\nimport type {\n  CommentContentProvider,\n  CommentEditorProvider,\n  CommentSubjectRefProvider,\n} from \"./comment\";\nimport type {\n  DashboardWidgetDefinition,\n  DashboardWidgetQuickActionItem,\n} from \"./dashboard-widget\";\nimport type { EditorProvider } from \"./editor-provider\";\nimport type { EntityFieldItem } from \"./list-entity-field\";\nimport type { OperationItem } from \"./list-operation\";\nimport type { PluginInstallationTab } from \"./plugin-installation-tab\";\nimport type { PluginTab } from \"./plugin-tab\";\nimport type { ThemeListTab } from \"./theme-list-tab\";\nimport type { UserProfileTab, UserTab } from \"./user-tab\";\n\n/**\n * Represents a route record that will be appended to a parent route.\n * Used to extend existing routes with additional child routes.\n */\nexport interface RouteRecordAppend {\n  /**\n   * The name of the parent route to which this route will be appended.\n   */\n  parentName: NonNullable<RouteRecordName>;\n\n  /**\n   * The route definition to be appended as a child route.\n   */\n  route: RouteRecordRaw;\n}\n\n/**\n * Defines extension points that plugins can hook into to extend Halo's functionality.\n * Extension points follow a naming convention: `<feature>:<action>:<operation>`.\n *\n * @remarks\n * Extension points are the primary mechanism for plugins to integrate with the Halo console and uc.\n * Each extension point represents a specific place where plugins can inject custom functionality.\n */\nexport interface ExtensionPoint {\n  /**\n   * Creates custom attachment selector providers.\n   * Allows plugins to provide alternative attachment selection interfaces.\n   *\n   * @returns An array of attachment select providers or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/attachment-selector-create\n   */\n  \"attachment:selector:create\"?: () =>\n    | AttachmentSelectProvider[]\n    | Promise<AttachmentSelectProvider[]>;\n\n  /**\n   * Creates custom editor providers.\n   * Allows plugins to register custom content editors.\n   *\n   * @returns An array of editor providers or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/editor-create\n   */\n  \"editor:create\"?: () => EditorProvider[] | Promise<EditorProvider[]>;\n\n  /**\n   * Creates tabs for the plugin's own settings page.\n   * Used to add configuration tabs within a plugin's detail view.\n   *\n   * @returns An array of plugin tabs or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/plugin-self-tabs-create\n   */\n  \"plugin:self:tabs:create\"?: () => PluginTab[] | Promise<PluginTab[]>;\n\n  /**\n   * Creates extensions for the default rich-text editor.\n   * Allows plugins to extend the editor with custom nodes, marks, or functionality.\n   *\n   * @returns An array of editor extensions or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/default-editor-extension-create\n   */\n  \"default:editor:extension:create\"?: () =>\n    | AnyExtension[]\n    | Promise<AnyExtension[]>;\n\n  /**\n   * Creates comment subject reference providers.\n   * Allows plugins to define custom content types that can receive comments.\n   *\n   * @returns An array of comment subject reference providers.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/comment-subject-ref-create\n   */\n  \"comment:subject-ref:create\"?: () => CommentSubjectRefProvider[];\n\n  /**\n   * Replaces the default comment editor.\n   * Allows plugins to provide a custom comment editing interface.\n   *\n   * @returns A comment editor provider or a promise resolving to it.\n   */\n  \"comment:editor:replace\"?: () =>\n    | CommentEditorProvider\n    | Promise<CommentEditorProvider>;\n\n  /**\n   * Replaces the default comment list item content display.\n   * Allows plugins to customize how comment content is rendered in lists.\n   *\n   * @returns A comment content provider or a promise resolving to it.\n   */\n  \"comment:list-item:content:replace\"?: () =>\n    | CommentContentProvider\n    | Promise<CommentContentProvider>;\n\n  /**\n   * Creates tabs for the backup management page.\n   * Allows plugins to add custom backup-related functionality.\n   *\n   * @returns An array of backup tabs or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/backup-tabs-create\n   */\n  \"backup:tabs:create\"?: () => BackupTab[] | Promise<BackupTab[]>;\n\n  /**\n   * Creates tabs for the plugin installation page.\n   * Allows plugins to add custom installation methods or configurations.\n   *\n   * @returns An array of plugin installation tabs or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/plugin-installation-tabs-create\n   */\n  \"plugin:installation:tabs:create\"?: () =>\n    | PluginInstallationTab[]\n    | Promise<PluginInstallationTab[]>;\n\n  /**\n   * Creates custom operations for post list items.\n   * Allows plugins to add action buttons or menu items to post list rows.\n   *\n   * @param post - A reactive reference to the post object.\n   * @returns An array of operation items for the post.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/post-list-item-operation-create\n   */\n  \"post:list-item:operation:create\"?: (\n    post: Ref<ListedPost>\n  ) => OperationItem<ListedPost>[];\n\n  /**\n   * Creates custom operations for single page list items.\n   * Allows plugins to add action buttons or menu items to single page list rows.\n   *\n   * @param singlePage - A reactive reference to the single page object.\n   * @returns An array of operation items for the single page.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/single-page-list-item-operation-create\n   */\n  \"single-page:list-item:operation:create\"?: (\n    singlePage: Ref<ListedSinglePage>\n  ) => OperationItem<ListedSinglePage>[];\n\n  /**\n   * Creates custom operations for comment list items.\n   * Allows plugins to add action buttons or menu items to comment list rows.\n   *\n   * @param comment - A reactive reference to the comment object.\n   * @returns An array of operation items for the comment.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/comment-list-item-operation-create\n   */\n  \"comment:list-item:operation:create\"?: (\n    comment: Ref<ListedComment>\n  ) => OperationItem<ListedComment>[];\n\n  /**\n   * Creates custom operations for reply list items.\n   * Allows plugins to add action buttons or menu items to reply list rows.\n   *\n   * @param reply - A reactive reference to the reply object.\n   * @returns An array of operation items for the reply.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/reply-list-item-operation-create\n   */\n  \"reply:list-item:operation:create\"?: (\n    reply: Ref<ListedReply>\n  ) => OperationItem<ListedReply>[];\n\n  /**\n   * Creates custom operations for plugin list items.\n   * Allows plugins to add action buttons or menu items to plugin list rows.\n   *\n   * @param plugin - A reactive reference to the plugin object.\n   * @returns An array of operation items for the plugin.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/plugin-list-item-operation-create\n   */\n  \"plugin:list-item:operation:create\"?: (\n    plugin: Ref<Plugin>\n  ) => OperationItem<Plugin>[];\n\n  /**\n   * Creates custom operations for backup list items.\n   * Allows plugins to add action buttons or menu items to backup list rows.\n   *\n   * @param backup - A reactive reference to the backup object.\n   * @returns An array of operation items for the backup.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/backup-list-item-operation-create\n   */\n  \"backup:list-item:operation:create\"?: (\n    backup: Ref<Backup>\n  ) => OperationItem<Backup>[];\n\n  /**\n   * Creates custom operations for attachment list items.\n   * Allows plugins to add action buttons or menu items to attachment list rows.\n   *\n   * @param attachment - A reactive reference to the attachment object.\n   * @returns An array of operation items for the attachment.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/attachment-list-item-operation-create\n   */\n  \"attachment:list-item:operation:create\"?: (\n    attachment: Ref<Attachment>\n  ) => OperationItem<Attachment>[];\n\n  /**\n   * Creates custom fields for plugin list items.\n   * Allows plugins to add custom columns or metadata fields to the plugin list display.\n   *\n   * @param plugin - A reactive reference to the plugin object.\n   * @returns An array of entity field items for the plugin.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/plugin-list-item-field-create\n   */\n  \"plugin:list-item:field:create\"?: (plugin: Ref<Plugin>) => EntityFieldItem[];\n\n  /**\n   * Creates custom fields for post list items.\n   * Allows plugins to add custom columns or metadata fields to the post list display.\n   *\n   * @param post - A reactive reference to the post object.\n   * @returns An array of entity field items for the post.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/post-list-item-field-create\n   */\n  \"post:list-item:field:create\"?: (post: Ref<ListedPost>) => EntityFieldItem[];\n\n  /**\n   * Creates custom fields for single page list items.\n   * Allows plugins to add custom columns or metadata fields to the single page list display.\n   *\n   * @param singlePage - A reactive reference to the single page object.\n   * @returns An array of entity field items for the single page.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/single-page-list-item-field-create\n   */\n  \"single-page:list-item:field:create\"?: (\n    singlePage: Ref<ListedSinglePage>\n  ) => EntityFieldItem[];\n\n  /**\n   * Creates tabs for the theme list page.\n   * Allows plugins to add custom theme management or configuration sections.\n   *\n   * @returns An array of theme list tabs or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/theme-list-tabs-create\n   */\n  \"theme:list:tabs:create\"?: () => ThemeListTab[] | Promise<ThemeListTab[]>;\n\n  /**\n   * Creates custom operations for theme list items.\n   * Allows plugins to add action buttons or menu items to theme list rows.\n   *\n   * @param theme - A reactive reference to the theme object.\n   * @returns An array of operation items for the theme.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/theme-list-item-operation-create\n   */\n  \"theme:list-item:operation:create\"?: (\n    theme: Ref<Theme>\n  ) => OperationItem<Theme>[];\n\n  /**\n   * Creates tabs for the user detail page in the console.\n   * Allows plugins to add custom user management sections.\n   *\n   * @returns An array of user tabs or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/user-detail-tabs-create\n   */\n  \"user:detail:tabs:create\"?: () => UserTab[] | Promise<UserTab[]>;\n\n  /**\n   * Creates tabs for the user profile page in the user center.\n   * Allows plugins to extend user profile settings and information.\n   *\n   * @returns An array of user profile tabs or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/uc-user-profile-tabs-create\n   */\n  \"uc:user:profile:tabs:create\"?: () =>\n    | UserProfileTab[]\n    | Promise<UserProfileTab[]>;\n\n  /**\n   * Creates custom dashboard widgets for the console.\n   * Allows plugins to add informational or interactive widgets to the main dashboard.\n   *\n   * @returns An array of dashboard widget definitions or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/dashboard-widgets#consoledashboardwidgets\n   */\n  \"console:dashboard:widgets:create\"?: () =>\n    | DashboardWidgetDefinition[]\n    | Promise<DashboardWidgetDefinition[]>;\n\n  /**\n   * Creates quick action items for internal dashboard widgets.\n   * Allows plugins to add quick access buttons or shortcuts to dashboard widgets.\n   *\n   * @returns An array of dashboard widget quick action items or a promise resolving to them.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui/dashboard-widgets#consoledashboardwidgetsinternalquick-actionitem\n   */\n  \"console:dashboard:widgets:internal:quick-action:item:create\"?: () =>\n    | DashboardWidgetQuickActionItem[]\n    | Promise<DashboardWidgetQuickActionItem[]>;\n}\n\n/**\n * Defines the structure of a UI plugin module for the Halo console and user center.\n *\n * @see https://docs.halo.run/developer-guide/plugin/basics/ui/entry\n */\nexport interface PluginModule {\n  /**\n   * Components that will be globally registered when the plugin is activated.\n   * These components can be used throughout the Halo console without explicit imports.\n   *\n   * @remarks\n   * The key is the component name (in kebab-case is recommended),\n   * and the value is the Vue component definition.\n   */\n  components?: Record<string, Component>;\n\n  /**\n   * Console routes that will be registered when the plugin is activated.\n   * Can be either standalone routes or routes that append to existing parent routes.\n   *\n   * @remarks\n   * - Use `RouteRecordRaw[]` for standalone routes\n   * - Use `RouteRecordAppend[]` to append routes to existing parent routes\n   * @see https://docs.halo.run/developer-guide/plugin/api-reference/ui/route\n   */\n  routes?: RouteRecordRaw[] | RouteRecordAppend[];\n\n  /**\n   * User center routes that will be registered when the plugin is activated.\n   * These routes are specifically for the user-facing area (UC).\n   *\n   * @remarks\n   * - Use `RouteRecordRaw[]` for standalone routes\n   * - Use `RouteRecordAppend[]` to append routes to existing parent routes\n   * @see https://docs.halo.run/developer-guide/plugin/api-reference/ui/route\n   */\n  ucRoutes?: RouteRecordRaw[] | RouteRecordAppend[];\n\n  /**\n   * Extension points that the plugin hooks into.\n   * This is where plugins register their custom functionality to extend Halo.\n   *\n   * @remarks\n   * Each extension point can provide custom functionality that integrates\n   * with specific parts of the Halo console.\n   * @see https://docs.halo.run/developer-guide/plugin/extension-points/ui\n   */\n  extensionPoints?: ExtensionPoint;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/plugin/types/user-tab.ts",
    "content": "import type { Component, Raw } from \"vue\";\n\n/**\n * Defines a custom tab for the user detail page in the console.\n *\n * @remarks\n * User tabs allow plugins to extend user management interfaces with custom\n * functionality such as activity logs, custom user metadata, integration\n * settings, or additional user management tools.\n *\n * @example\n * ```typescript\n * const activityTab: UserTab = {\n *   id: 'activity-log',\n *   label: 'Activity Log',\n *   component: markRaw(ActivityLogComponent),\n *   priority: 10,\n *   permissions: ['system:users:view-activity']\n * };\n * ```\n */\nexport interface UserTab {\n  /**\n   * Unique identifier for the user tab.\n   * Should follow a namespaced pattern to avoid conflicts (e.g., 'plugin-name:tab-id').\n   */\n  id: string;\n\n  /**\n   * Display label for the tab.\n   * This text will be shown in the tab navigation.\n   */\n  label: string;\n\n  /**\n   * The Vue component that renders the tab content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component typically receives the user object as a prop and should\n   * implement user-related functionality or display user-specific information.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional array of permission identifiers required to view this tab.\n   * The tab will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n\n  /**\n   * Priority for ordering multiple user tabs.\n   * Higher priority tabs appear first (leftmost) in the tab navigation.\n   */\n  priority: number;\n}\n\n/**\n * Defines a custom tab for the user profile page in the user center.\n *\n * @remarks\n * User profile tabs allow plugins to extend the user-facing profile interface\n * with custom sections such as social connections, preferences, achievements,\n * subscription management, or personalization settings.\n *\n * @example\n * ```typescript\n * const preferencesTab: UserProfileTab = {\n *   id: 'preferences',\n *   label: 'Preferences',\n *   component: markRaw(PreferencesComponent),\n *   priority: 15\n * };\n * ```\n */\nexport interface UserProfileTab {\n  /**\n   * Unique identifier for the user profile tab.\n   * Should follow a namespaced pattern to avoid conflicts (e.g., 'plugin-name:tab-id').\n   */\n  id: string;\n\n  /**\n   * Display label for the tab.\n   * This text will be shown in the tab navigation.\n   */\n  label: string;\n\n  /**\n   * The Vue component that renders the tab content.\n   * Must be wrapped with `markRaw` to prevent Vue from making it reactive.\n   *\n   * @remarks\n   * The component should provide user-facing functionality that allows users\n   * to view or modify their own profile information or settings.\n   */\n  component: Raw<Component>;\n\n  /**\n   * Optional array of permission identifiers required to view this tab.\n   * The tab will only be visible to users with at least one of these permissions.\n   */\n  permissions?: string[];\n\n  /**\n   * Priority for ordering multiple user profile tabs.\n   * Higher priority tabs appear first (leftmost) in the tab navigation.\n   */\n  priority: number;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/stores/index.ts",
    "content": "import {\n  axiosInstance,\n  consoleApiClient,\n  type DetailedUser,\n} from \"@halo-dev/api-client\";\nimport { defineStore } from \"pinia\";\nimport { ref } from \"vue\";\nimport type { GlobalInfo } from \"./types\";\n\n/**\n * Collection of Pinia stores for shared application state.\n *\n * @remarks\n * These stores provide centralized state management for common data\n * that needs to be accessed across multiple components and plugins.\n */\nexport const stores = {\n  /**\n   * Store for managing the current authenticated user's information.\n   *\n   * @remarks\n   * This store provides access to the current user's details and authentication state.\n   * It includes helper methods to fetch the latest user information from the server.\n   *\n   * @example\n   * ```typescript\n   * import { stores } from \"@halo-dev/ui-shared\";\n   *\n   * const userStore = stores.currentUser();\n   *\n   * // Fetch current user info\n   * await userStore.fetchCurrentUser();\n   *\n   * // Access user data\n   * console.log(userStore.currentUser?.user.metadata.name);\n   * console.log(userStore.isAnonymous); // Check if user is anonymous\n   * ```\n   */\n  currentUser: defineStore(\"currentUser\", () => {\n    /**\n     * The current authenticated user's detailed information.\n     * Will be `undefined` until `fetchCurrentUser` is called.\n     */\n    const currentUser = ref<DetailedUser>();\n\n    /**\n     * Indicates whether the current user is anonymous (not authenticated).\n     * `true` if the user is \"anonymousUser\", `false` otherwise.\n     */\n    const isAnonymous = ref(false);\n\n    /**\n     * Fetches the current user's information from the server.\n     * Updates both `currentUser` and `isAnonymous` reactive references.\n     *\n     * @throws Will throw an error if the API request fails or user is not authenticated.\n     */\n    async function fetchCurrentUser() {\n      const { data } = await consoleApiClient.user.getCurrentUserDetail();\n      currentUser.value = data;\n      isAnonymous.value = data.user.metadata.name === \"anonymousUser\";\n    }\n\n    return { currentUser, isAnonymous, fetchCurrentUser };\n  }),\n\n  /**\n   * Store for managing global system information and configuration.\n   *\n   * @remarks\n   * This store provides access to global system settings, configuration,\n   * and metadata that are shared across the entire application.\n   *\n   * @example\n   * ```typescript\n   * import { stores } from \"@halo-dev/ui-shared\";\n   *\n   * const globalInfoStore = stores.globalInfo();\n   *\n   * // Fetch global info\n   * await globalInfoStore.fetchGlobalInfo();\n   *\n   * // Access global settings\n   * console.log(globalInfoStore.globalInfo?.externalUrl);\n   * console.log(globalInfoStore.globalInfo?.siteTitle);\n   * console.log(globalInfoStore.globalInfo?.allowRegistration);\n   * ```\n   */\n  globalInfo: defineStore(\"global-info\", () => {\n    /**\n     * The global system information and configuration.\n     * Will be `undefined` until `fetchGlobalInfo` is called.\n     */\n    const globalInfo = ref<GlobalInfo>();\n\n    /**\n     * Fetches the global system information from the actuator endpoint.\n     * Updates the `globalInfo` reactive reference with the latest data.\n     *\n     * @throws Will throw an error if the API request fails.\n     */\n    async function fetchGlobalInfo() {\n      const { data } = await axiosInstance.get<GlobalInfo>(\n        `/actuator/globalinfo`,\n        {\n          withCredentials: true,\n        }\n      );\n      globalInfo.value = data;\n    }\n\n    return { globalInfo, fetchGlobalInfo };\n  }),\n};\n\nexport * from \"./types\";\n"
  },
  {
    "path": "ui/packages/shared/src/stores/types/actuator.ts",
    "content": "import type { ModeType } from \"./slug\";\n\nexport interface GlobalInfo {\n  externalUrl: string;\n  timeZone: string;\n  locale: string;\n  allowComments: boolean;\n  allowAnonymousComments: boolean;\n  allowRegistration: boolean;\n  favicon?: string;\n  postSlugGenerationStrategy: ModeType;\n  mustVerifyEmailOnRegistration: boolean;\n  siteTitle: string;\n}\n\nexport interface Info {\n  git?: Git;\n  build?: Build;\n  java: Java;\n  os: Os;\n  database: Database;\n}\n\nexport interface Database {\n  name: string;\n  version: string;\n}\n\nexport interface Commit {\n  id: string;\n  time: Date;\n}\n\nexport interface Git {\n  branch: string;\n  commit: Commit;\n}\n\nexport interface Build {\n  artifact: string;\n  name: string;\n  time: Date;\n  version: string;\n  group: string;\n}\n\nexport interface Vendor {\n  name: string;\n  version: string;\n}\n\nexport interface Runtime {\n  name: string;\n  version: string;\n}\n\nexport interface Jvm {\n  name: string;\n  vendor: string;\n  version: string;\n}\n\nexport interface Java {\n  version: string;\n  vendor: Vendor;\n  runtime: Runtime;\n  jvm: Jvm;\n}\n\nexport interface Os {\n  name: string;\n  version: string;\n  arch: string;\n}\n\nexport interface Tag {\n  key: string;\n  value: string;\n}\n\nexport interface StartupStep {\n  name: string;\n  id: number;\n  tags: Tag[];\n  parentId?: number;\n}\n\nexport interface Event {\n  endTime: Date;\n  duration: string;\n  startTime: Date;\n  startupStep: StartupStep;\n}\n\nexport interface Timeline {\n  startTime: Date;\n  events: Event[];\n}\n\nexport interface Startup {\n  springBootVersion: string;\n  timeline: Timeline;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/stores/types/index.ts",
    "content": "export * from \"./actuator\";\nexport * from \"./slug\";\n"
  },
  {
    "path": "ui/packages/shared/src/stores/types/slug.ts",
    "content": "export type ModeType = \"UUID\" | \"shortUUID\" | \"timestamp\" | \"generateByTitle\";\nexport enum FormType {\n  TAG = \"Tag\",\n  CATEGORY = \"Category\",\n  POST = \"Post\",\n  SINGLE_PAGE = \"SinglePage\",\n}\n"
  },
  {
    "path": "ui/packages/shared/src/types/index.ts",
    "content": "export * from \"./menus\";\n"
  },
  {
    "path": "ui/packages/shared/src/types/menus.ts",
    "content": "import type { Component } from \"vue\";\n\nexport type CoreMenuGroupId =\n  | \"dashboard\"\n  | \"content\"\n  | \"interface\"\n  | \"system\"\n  | \"tool\";\n\nexport interface MenuGroupType {\n  id: CoreMenuGroupId | string;\n  name?: string;\n  priority: number;\n  items?: MenuItemType[];\n}\n\nexport interface MenuItemType {\n  name: string;\n  path: string;\n  mobile?: boolean;\n  icon?: Component;\n  meta?: Record<string, unknown>;\n  children?: MenuItemType[];\n}\n"
  },
  {
    "path": "ui/packages/shared/src/utils/attachment.ts",
    "content": "import type { GetThumbnailByUriSizeEnum } from \"@halo-dev/api-client\";\nimport type { AttachmentLike, AttachmentSimple } from \"@/plugin\";\n\n/**\n * Mapping of thumbnail size enums to their corresponding widths in pixels\n *\n * @remarks\n * - XL: 1600px - Extra large thumbnails\n * - L: 1200px - Large thumbnails\n * - M: 800px - Medium thumbnails\n * - S: 400px - Small thumbnails\n */\nexport const THUMBNAIL_WIDTH_MAP: Record<GetThumbnailByUriSizeEnum, number> = {\n  XL: 1600,\n  L: 1200,\n  M: 800,\n  S: 400,\n};\n\nexport class AttachmentUtils {\n  /**\n   * Generates a thumbnail URL for the given image URL with the specified size\n   *\n   * @param url - The original image URL (can be absolute, relative, or external)\n   * @param size - The desired thumbnail size (XL, L, M, or S)\n   * @returns The thumbnail URL with width parameter, or original URL if size is invalid\n   *\n   * @remarks\n   * This method handles three scenarios:\n   * 1. If URL starts with current origin: Appends `?width={size}` query parameter\n   * 2. If URL is a relative path (starts with \"/\"): Appends `?width={size}` query parameter\n   * 3. If URL is external: Routes through Halo's thumbnail API endpoint\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   *\n   * // Local image\n   * utils.attachment.getThumbnailUrl(\"/uploads/image.jpg\", \"M\");\n   * // Returns: \"/uploads/image.jpg?width=800\"\n   *\n   * // Same origin image\n   * utils.attachment.getThumbnailUrl(\"https://example.com/image.jpg\", \"S\");\n   * // Returns: \"https://example.com/image.jpg?width=400\" (if current origin is example.com)\n   *\n   * // External image\n   * utils.attachment.getThumbnailUrl(\"https://external.com/image.jpg\", \"L\");\n   * // Returns: \"/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fexternal.com%2Fimage.jpg&width=1200\"\n   * ```\n   */\n  getThumbnailUrl(url: string, size: GetThumbnailByUriSizeEnum) {\n    const { origin } = location;\n\n    const width = THUMBNAIL_WIDTH_MAP[size];\n\n    if (!width) {\n      return url;\n    }\n\n    if (url.startsWith(\"data:\")) {\n      return url;\n    }\n\n    if (url.startsWith(origin) || url.startsWith(\"/\")) {\n      return `${url}?width=${width}`;\n    }\n\n    return `/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=${encodeURIComponent(\n      url\n    )}&width=${width}`;\n  }\n\n  /**\n   * Extracts the URL from an attachment-like object\n   *\n   * @param attachment - The attachment object (can be a string URL, Attachment object, or AttachmentSimple)\n   * @returns The URL string extracted from the attachment\n   * @throws {Error} When the attachment type is invalid or unrecognized\n   *\n   * @remarks\n   * This method handles three types of attachments:\n   * 1. String: Returns the string directly as URL\n   * 2. Attachment object (with \"spec\"): Returns the permalink from status\n   * 3. AttachmentSimple (with \"url\"): Returns the url property\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   *\n   * // String URL\n   * utils.attachment.getUrl(\"https://example.com/image.jpg\");\n   * // Returns: \"https://example.com/image.jpg\"\n   *\n   * // Attachment object\n   * utils.attachment.getUrl(attachmentObject);\n   * // Returns: attachmentObject.status?.permalink\n   *\n   * // AttachmentSimple object\n   * utils.attachment.getUrl({ url: \"https://example.com/image.jpg\" });\n   * // Returns: \"https://example.com/image.jpg\"\n   * ```\n   */\n  getUrl(attachment: AttachmentLike) {\n    if (typeof attachment === \"string\") {\n      return attachment;\n    }\n    if (\"spec\" in attachment) {\n      return attachment.status?.permalink;\n    }\n    if (\"url\" in attachment) {\n      return attachment.url;\n    }\n    throw new Error(\"Invalid attachment\");\n  }\n\n  /**\n   * Converts an attachment-like object to a simplified attachment format\n   *\n   * @param attachment - The attachment object to convert (can be a string URL, Attachment object, or AttachmentSimple)\n   * @returns A simplified attachment object with url, alt, and mediaType properties, or undefined\n   * @throws {Error} When the attachment type is invalid or unrecognized\n   *\n   * @remarks\n   * This method normalizes different attachment formats into a consistent AttachmentSimple structure:\n   * 1. String: Converts to object with only url property\n   * 2. Attachment object (with \"spec\"): Extracts permalink, displayName, and mediaType\n   * 3. AttachmentSimple (with \"url\"): Returns as-is since it's already in the correct format\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   *\n   * // String URL\n   * utils.attachment.convertToSimple(\"https://example.com/image.jpg\");\n   * // Returns: { url: \"https://example.com/image.jpg\" }\n   *\n   * // Attachment object\n   * utils.attachment.convertToSimple(attachmentObject);\n   * // Returns: {\n   * //   url: attachmentObject.status?.permalink || \"\",\n   * //   alt: attachmentObject.spec.displayName,\n   * //   mediaType: attachmentObject.spec.mediaType\n   * // }\n   *\n   * // AttachmentSimple object\n   * utils.attachment.convertToSimple({ url: \"https://example.com/image.jpg\", alt: \"Image\" });\n   * // Returns: { url: \"https://example.com/image.jpg\", alt: \"Image\" }\n   * ```\n   */\n  convertToSimple(attachment: AttachmentLike): AttachmentSimple | undefined {\n    if (typeof attachment === \"string\") {\n      return {\n        url: attachment,\n      };\n    }\n    if (\"spec\" in attachment) {\n      return {\n        url: attachment.status?.permalink || \"\",\n        alt: attachment.spec.displayName,\n        mediaType: attachment.spec.mediaType,\n      };\n    }\n    if (\"url\" in attachment) {\n      return attachment;\n    }\n    throw new Error(\"Invalid attachment\");\n  }\n}\n"
  },
  {
    "path": "ui/packages/shared/src/utils/date.ts",
    "content": "import _dayjs from \"dayjs\";\nimport \"dayjs/locale/en\";\nimport \"dayjs/locale/zh-cn\";\nimport \"dayjs/locale/zh-tw\";\nimport relativeTime from \"dayjs/plugin/relativeTime\";\nimport timezone from \"dayjs/plugin/timezone\";\nimport utc from \"dayjs/plugin/utc\";\n\n_dayjs.extend(timezone);\n_dayjs.extend(utc);\n_dayjs.extend(relativeTime);\n\nconst DEFAULT_FORMAT = \"YYYY-MM-DD HH:mm\";\n\nconst locales: Record<string, string> = {\n  en: \"en\",\n  zh: \"zh-cn\",\n  \"en-US\": \"en\",\n  \"zh-CN\": \"zh-cn\",\n  \"zh-TW\": \"zh-tw\",\n};\n\nexport class DateUtils {\n  readonly dayjs: typeof _dayjs;\n\n  constructor() {\n    this.dayjs = _dayjs;\n  }\n\n  /**\n   * Formats a date to a string according to the specified format\n   *\n   * @param date - The date to format (string, Date object, or null/undefined)\n   * @param format - The format string (defaults to \"YYYY-MM-DD HH:mm\")\n   * @returns The formatted date string, or empty string if date is null/undefined\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   * utils.date.format(new Date()); // \"2025-10-22 14:30\"\n   * utils.date.format(\"2025-10-22\", \"YYYY/MM/DD\"); // \"2025/10/22\"\n   * ```\n   */\n  format(date: string | Date | undefined | null, format?: string): string {\n    if (!date) {\n      return \"\";\n    }\n    return this.dayjs(date).format(format || DEFAULT_FORMAT);\n  }\n\n  /**\n   * Converts a date to ISO 8601 format string\n   *\n   * @param date - The date to convert (string, Date object, or null/undefined)\n   * @returns The ISO 8601 formatted date string, or empty string if date is null/undefined\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   * utils.date.toISOString(new Date(\"2025-10-22\")); // \"2025-10-22T00:00:00.000Z\"\n   * ```\n   */\n  toISOString(date: string | Date | undefined | null): string {\n    if (!date) {\n      return \"\";\n    }\n    return this.dayjs(date).utc(false).toISOString();\n  }\n\n  /**\n   * Converts a date to HTML5 datetime-local input format\n   *\n   * @param date - The date to convert (string, Date object, or null/undefined)\n   * @returns The datetime-local formatted string (YYYY-MM-DDTHH:mm), or empty string if date is null/undefined\n   *\n   * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#the_y10k_problem_often_client-side\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   * utils.date.toDatetimeLocal(new Date(\"2025-10-22 14:30\")); // \"2025-10-22T14:30\"\n   * ```\n   */\n  toDatetimeLocal(date: string | Date | undefined | null): string {\n    if (!date) {\n      return \"\";\n    }\n    // see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#the_y10k_problem_often_client-side\n    return this.dayjs(date).format(\"YYYY-MM-DDTHH:mm\");\n  }\n\n  /**\n   * Gets the relative time from now to the specified date\n   *\n   * @param date - The target date (string, Date object, or null/undefined)\n   * @returns A human-readable relative time string (e.g., \"in 2 hours\", \"3 days ago\"), or undefined if date is null/undefined\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   * // Assuming now is 2025-10-22\n   * utils.date.timeAgo(\"2025-10-23\"); // \"in a day\"\n   * utils.date.timeAgo(\"2025-10-21\"); // \"a day ago\"\n   * utils.date.timeAgo(\"2025-11-22\"); // \"in a month\"\n   * ```\n   */\n  timeAgo(date: string | Date | undefined | null): string | undefined {\n    if (!date) {\n      return;\n    }\n\n    return this.dayjs().to(this.dayjs(date));\n  }\n\n  /**\n   * Sets the locale for date formatting\n   *\n   * @param locale - The locale code (e.g., \"en\", \"zh\", \"en-US\", \"zh-CN\", \"zh-TW\")\n   *\n   * @remarks\n   * Supported locales:\n   * - \"en\" or \"en-US\" → English\n   * - \"zh\" or \"zh-CN\" → Simplified Chinese\n   * - \"zh-TW\" → Traditional Chinese\n   *\n   * Defaults to English if the locale is not supported.\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   * utils.date.setLocale(\"zh-CN\");\n   * utils.date.timeAgo(\"2025-10-21\"); // \"1 天前\"\n   * utils.date.setLocale(\"en\");\n   * utils.date.timeAgo(\"2025-10-21\"); // \"a day ago\"\n   * ```\n   */\n  setLocale(locale: string): void {\n    this.dayjs.locale(locales[locale] || locales[\"en\"]);\n  }\n}\n\n// See https://github.com/iamkun/dayjs/issues/364\ndeclare module \"dayjs\" {\n  interface Dayjs {\n    fromNow(withoutSuffix?: boolean): string;\n    from(compared: _dayjs.ConfigType, withoutSuffix?: boolean): string;\n    toNow(withoutSuffix?: boolean): string;\n    to(compared: _dayjs.ConfigType, withoutSuffix?: boolean): string;\n  }\n}\n\ndeclare module \"dayjs\" {\n  interface Dayjs {\n    tz(timezone?: string, keepLocalTime?: boolean): _dayjs.Dayjs;\n    offsetName(type?: \"short\" | \"long\"): string | undefined;\n  }\n\n  interface DayjsTimezone {\n    (date?: _dayjs.ConfigType, timezone?: string): _dayjs.Dayjs;\n    (date: _dayjs.ConfigType, format: string, timezone?: string): _dayjs.Dayjs;\n    guess(): string;\n    setDefault(timezone?: string): void;\n  }\n}\n\ndeclare module \"dayjs\" {\n  interface Dayjs {\n    utc(keepLocalTime?: boolean): _dayjs.Dayjs;\n\n    local(): _dayjs.Dayjs;\n\n    isUTC(): boolean;\n\n    utcOffset(offset: number | string, keepLocalTime?: boolean): _dayjs.Dayjs;\n  }\n\n  export function utc(\n    config?: _dayjs.ConfigType,\n    format?: string,\n    strict?: boolean\n  ): _dayjs.Dayjs;\n}\n"
  },
  {
    "path": "ui/packages/shared/src/utils/id.ts",
    "content": "import { v7 as uuidv7 } from \"uuid\";\n\n/**\n * Utilities for generating unique identifiers that remain sortable over time.\n *\n * Relies on the `uuid` package for RFC 4122 compliant UUID generation.\n */\nexport class IdUtils {\n  /**\n   * Generates a RFC 4122 version 7 UUID string using the `uuid` package.\n   *\n   * UUID v7 keeps the order of creation roughly aligned with chronological order,\n   * making it a good fit for persistence stores or log entries that benefit from\n   * monotonic sorting.\n   *\n   * @returns A lowercase UUIDv7 string such as \"018f1c2e-4fcb-7d04-9f21-1a2b3c4d5e6f\".\n   */\n  uuid(): string {\n    return uuidv7();\n  }\n}\n"
  },
  {
    "path": "ui/packages/shared/src/utils/index.ts",
    "content": "import { AttachmentUtils } from \"./attachment\";\nimport { DateUtils } from \"./date\";\nimport { IdUtils } from \"./id\";\nimport { PermissionUtils } from \"./permission\";\n\nexport const utils = {\n  date: new DateUtils(),\n  attachment: new AttachmentUtils(),\n  permission: new PermissionUtils(),\n  id: new IdUtils(),\n};\n\nexport { THUMBNAIL_WIDTH_MAP } from \"./attachment\";\n"
  },
  {
    "path": "ui/packages/shared/src/utils/permission.ts",
    "content": "/**\n * Utility class for checking user permissions.\n */\nexport class PermissionUtils {\n  private userPermissions?: Array<string>;\n\n  /**\n   * Creates a new PermissionUtils instance.\n   * @param userPermissions - Array of permissions that the user has\n   */\n  constructor(userPermissions?: Array<string>) {\n    this.userPermissions = userPermissions;\n  }\n\n  /**\n   * Checks if the user has the required permissions.\n   *\n   * @param permissions - Array of permissions to check against user's permissions\n   * @param any - If true, returns true when ANY of the required permissions match.\n   *              If false, returns true only when ALL required permissions match.\n   *              Defaults to true.\n   * @returns true if the permission check passes, false otherwise\n   *\n   * @throws Error if user permissions are not set\n   *\n   * @example\n   * ```ts\n   * import { utils } from \"@halo-dev/ui-shared\"\n   *\n   * // Check if user has any of the permissions\n   * utils.permission.has(['core:posts:manage'], true);\n   *\n   * // Check if user has all of the permissions\n   * utils.permission.has(['core:posts:view', 'core:attachments:view'], false);\n   * ```\n   */\n  has(permissions: Array<string>, any: boolean = true): boolean {\n    if (!this.userPermissions) {\n      throw new Error(\"User permissions not set in PermissionUtils\");\n    }\n\n    // Super user with wildcard permission has all permissions\n    if (this.userPermissions.includes(\"*\")) {\n      return true;\n    }\n\n    // If no permissions are required, access is granted\n    if (!permissions.length) {\n      return true;\n    }\n\n    // If user has no permissions, access is denied\n    if (!this.userPermissions.length) {\n      return false;\n    }\n\n    if (any) {\n      // Any match: at least one of the required permissions must be present\n      return permissions.some((p) => this.userPermissions!.includes(p));\n    }\n\n    // All match: all required permissions must be present\n    return permissions.every((p) => this.userPermissions!.includes(p));\n  }\n\n  /**\n   * Retrieves the current user permissions.\n   * @returns Array of user permissions or undefined if not set\n   */\n  getUserPermissions(): Array<string> | undefined {\n    return this.userPermissions;\n  }\n\n  /**\n   * Updates the user permissions.\n   * @param userPermissions - Array of permissions to set for the user\n   */\n  setUserPermissions(userPermissions: Array<string>) {\n    this.userPermissions = userPermissions;\n  }\n}\n"
  },
  {
    "path": "ui/packages/shared/tsconfig.app.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"exclude\": [\"src/**/__tests__/*\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "ui/packages/shared/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "ui/packages/shared/tsconfig.node.json",
    "content": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"include\": [\"tsdown.config.ts\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "ui/packages/shared/tsdown.config.ts",
    "content": "import { fileURLToPath, URL } from \"node:url\";\nimport { defineConfig } from \"tsdown\";\n\nexport default defineConfig({\n  entry: [\"./src/index.ts\"],\n  format: [\"esm\", \"iife\"],\n  deps: {\n    neverBundle: [\"vue\", \"vue-router\", \"pinia\", \"@halo-dev/api-client\"],\n    alwaysBundle: [\"mitt\"],\n    onlyAllowBundle: false,\n  },\n  outputOptions: {\n    globals: {\n      vue: \"Vue\",\n      \"vue-router\": \"VueRouter\",\n      pinia: \"Pinia\",\n      \"@halo-dev/api-client\": \"HaloApiClient\",\n    },\n  },\n  platform: \"browser\",\n  globalName: \"HaloUiShared\",\n  tsconfig: \"./tsconfig.app.json\",\n  alias: {\n    \"@\": fileURLToPath(new URL(\"./src\", import.meta.url)),\n  },\n  minify: true,\n  exports: true,\n  dts: {\n    tsgo: true,\n  },\n});\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/README.md",
    "content": "# @halo-dev/ui-plugin-bundler-kit\n\nA frontend build toolkit for Halo plugin development, supporting both Vite and Rsbuild build systems.\n\n## Introduction\n\n`@halo-dev/ui-plugin-bundler-kit` is a frontend build configuration toolkit specifically designed for Halo plugin development. It provides pre-configured build settings to help developers quickly set up and build frontend interfaces for Halo plugins.\n\n### Key Features\n\n- 🚀 **Ready to Use** - Provides pre-configured Vite and Rsbuild build settings\n- 📦 **Multi-Build Tool Support** - Supports both Vite and Rsbuild\n- 🔧 **Flexible Configuration** - Supports custom build configurations\n- 🎯 **Halo Optimized** - External dependencies and global variables optimized for Halo plugin development\n- 📁 **Smart Output** - Automatically selects output directory based on environment\n\n## Installation\n\n```bash\n# Using npm\nnpm install @halo-dev/ui-plugin-bundler-kit\n\n# Using yarn\nyarn add @halo-dev/ui-plugin-bundler-kit\n\n# Using pnpm\npnpm add @halo-dev/ui-plugin-bundler-kit\n```\n\n### Additional Dependencies\n\n**For Vite users**, you need to install Vite:\n\n```bash\nnpm install vite\n```\n\n**For Rsbuild users**, you need to install Rsbuild:\n\n```bash\nnpm install @rsbuild/core\n```\n\n## Usage\n\n### Vite Configuration\n\nCreate or update `vite.config.ts` file in your project root:\n\n```typescript\nimport { viteConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\n\nexport default viteConfig({\n  vite: {\n    // Your custom Vite configuration\n    plugins: [\n      // Additional plugins (Vue plugin is already included)\n    ],\n    // Other configurations...\n  },\n});\n```\n\n> **Note**: Vue plugin is pre-configured, no need to add it manually.\n\n### Rsbuild Configuration\n\nCreate or update `rsbuild.config.ts` file in your project root:\n\n```typescript\nimport { rsbuildConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\n\nexport default rsbuildConfig({\n  rsbuild: {\n    // Your custom Rsbuild configuration\n    plugins: [\n      // Additional plugins (Vue plugin is already included)\n    ],\n    // Other configurations...\n  },\n});\n```\n\n> **Note**: Vue plugin is pre-configured, no need to add it manually.\n\n### Legacy Configuration (Deprecated)\n\n> ⚠️ **Note**: The `HaloUIPluginBundlerKit` function is deprecated. Please use `viteConfig` or `rsbuildConfig` instead.\n\n```typescript\nimport { HaloUIPluginBundlerKit } from \"@halo-dev/ui-plugin-bundler-kit\";\n\nexport default {\n  plugins: [\n    HaloUIPluginBundlerKit({\n      // Configuration options\n    }),\n  ],\n};\n```\n\n## Configuration Options\n\n### Vite Configuration Options\n\n```typescript\ninterface ViteUserConfig {\n  /**\n   * Halo plugin manifest file path\n   * @default \"../src/main/resources/plugin.yaml\"\n   */\n  manifestPath?: string;\n\n  /**\n   * Custom Vite configuration\n   */\n  vite: UserConfig | UserConfigFnObject;\n}\n```\n\n### Rsbuild Configuration Options\n\n```typescript\ninterface RsBuildUserConfig {\n  /**\n   * Halo plugin manifest file path\n   * @default \"../src/main/resources/plugin.yaml\"\n   */\n  manifestPath?: string;\n\n  /**\n   * Custom Rsbuild configuration\n   */\n  rsbuild: RsbuildConfig | ((env: ConfigParams) => RsbuildConfig);\n}\n```\n\n## Advanced Configuration Examples\n\n### Adding Path Aliases (Vite)\n\n```typescript\nimport { viteConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\nimport path from \"path\";\n\nexport default viteConfig({\n  vite: {\n    resolve: {\n      alias: {\n        \"@\": path.resolve(__dirname, \"src\"),\n        \"@components\": path.resolve(__dirname, \"src/components\"),\n      },\n    },\n  },\n});\n```\n\n### Adding Path Aliases (Rsbuild)\n\n```typescript\nimport { rsbuildConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\n\nexport default rsbuildConfig({\n  rsbuild: {\n    source: {\n      alias: {\n        \"@\": \"./src\",\n        \"@components\": \"./src/components\",\n      },\n    },\n  },\n});\n```\n\n### Adding Additional Vite Plugins\n\n```typescript\nimport { viteConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\nimport { defineConfig } from \"vite\";\nimport UnoCSS from \"unocss/vite\";\n\nexport default viteConfig({\n  vite: {\n    plugins: [\n      UnoCSS(), // Add UnoCSS plugin\n    ],\n  },\n});\n```\n\n### Adding Additional Rsbuild Plugins\n\n```typescript\nimport { rsbuildConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\nimport { pluginSass } from \"@rsbuild/plugin-sass\";\n\nexport default rsbuildConfig({\n  rsbuild: {\n    plugins: [\n      pluginSass(), // Add Sass plugin\n    ],\n  },\n});\n```\n\n### Custom Plugin Manifest Path\n\n```typescript\nimport { viteConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\n\nexport default viteConfig({\n  manifestPath: \"application/src/main/resources/plugin.yaml\", // Custom manifest file path\n  vite: {\n    // Other configurations...\n  },\n});\n```\n\n## Development Scripts\n\nRecommended scripts to add to your `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"vite dev --mode=development --watch\",\n    \"build\": \"vite build\"\n  }\n}\n```\n\nFor Rsbuild:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"rsbuild dev --env-mode=development --watch\",\n    \"build\": \"rsbuild build\"\n  }\n}\n```\n\n## Build Output\n\n> Relative to the root directory of the Halo plugin project\n\n- **Development**: `build/resources/main/console`\n- **Production**: `ui/build/dist`\n\n> **Note**: The production build output directory of `HaloUIPluginBundlerKit` is still `src/main/resources/console` to ensure compatibility.\n\n## Requirements\n\n- **Node.js**: ^18.0.0 || >=20.0.0\n- **Peer Dependencies**:\n  - `@rsbuild/core`: ^1.0.0 (when using Rsbuild)\n  - `@rsbuild/plugin-vue`: ^1.0.0 (when using Rsbuild)\n  - `@vitejs/plugin-vue`: ^4.0.0 || ^5.0.0 (when using Vite)\n  - `vite`: ^4.0.0 || ^5.0.0 || ^6.0.0 (when using Vite)\n\n## Vite vs Rsbuild\n\nBoth Vite and Rsbuild are excellent build tools, but they have different strengths depending on your use case:\n\n### When to Use Rsbuild\n\n**Recommended for large-scale plugins**\n\n- ✅ **Code Splitting Support** - Rsbuild provides excellent support for code splitting and lazy loading\n- ✅ **Better Performance** - Generally faster build times and smaller bundle sizes for complex applications\n- ✅ **Dynamic Imports** - Perfect for plugins with heavy frontend components\n\n**Example with dynamic imports:**\n\n```typescript\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport { defineAsyncComponent } from \"vue\";\nimport { VLoading } from \"@halo-dev/components\";\n\nexport default definePlugin({\n  routes: [\n    {\n      parentName: \"Root\",\n      route: {\n        path: \"demo\",\n        name: \"DemoPage\",\n        // Lazy load heavy components\n        component: defineAsyncComponent({\n          loader: () => import(\"./views/DemoPage.vue\"),\n          loadingComponent: VLoading,\n        }),\n      },\n    },\n  ],\n  extensionPoints: {},\n});\n```\n\n### When to Use Vite\n\n**Recommended for simple to medium-scale plugins**\n\n- ✅ **Vue Ecosystem Friendly** - Better integration with Vue ecosystem tools and plugins\n- ✅ **Rich Plugin Ecosystem** - Extensive collection of Vite plugins available\n- ✅ **Simple Configuration** - Easier to configure for straightforward use cases\n\n### Summary\n\n| Feature           | Vite         | Rsbuild      |\n| ----------------- | ------------ | ------------ |\n| Code Splitting    | ❌ Limited   | ✅ Excellent |\n| Vue Ecosystem     | ✅ Excellent | ✅ Good      |\n| Build Performance | ✅ Good      | ✅ Excellent |\n| Dev Experience    | ✅ Excellent | ✅ Excellent |\n| Plugin Ecosystem  | ✅ Rich      | ✅ Growing   |\n| Configuration     | ✅ Simple    | ⚖️ Moderate  |\n\n**Recommendation**: Use **Rsbuild** for complex plugins with large frontend codebases, and **Vite** for simpler plugins or when you need extensive Vue ecosystem integration.\n\n## License\n\nGPL-3.0\n\n## Contributing\n\nIssues and Pull Requests are welcome! Please check our [Contributing Guide](https://github.com/halo-dev/halo/blob/main/CONTRIBUTING.md) for more information.\n\n## Related Links\n\n- [Halo Website](https://www.halo.run/)\n- [Halo Documentation](https://docs.halo.run/)\n- [GitHub Repository](https://github.com/halo-dev/halo)\n- [Plugin Development Guide](https://docs.halo.run/category/ui)\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/package.json",
    "content": "{\n  \"name\": \"@halo-dev/ui-plugin-bundler-kit\",\n  \"version\": \"2.23.0\",\n  \"homepage\": \"https://github.com/halo-dev/halo/tree/main/ui/packages/ui-plugin-bundler-kit#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/halo-dev/halo/issues\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"@halo-dev\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/halo-dev/halo.git\",\n    \"directory\": \"ui/packages/ui-plugin-bundler-kit\"\n  },\n  \"type\": \"module\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": \"./dist/index.mjs\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"dev\": \"tsdown --watch\",\n    \"prepublishOnly\": \"pnpm run build\"\n  },\n  \"dependencies\": {\n    \"@halo-dev/api-client\": \"workspace:*\",\n    \"js-yaml\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"@types/js-yaml\": \"^4.0.9\"\n  },\n  \"peerDependencies\": {\n    \"@rsbuild/core\": \"^1.0.0 || ^2.0.0\",\n    \"@rsbuild/plugin-vue\": \"^1.0.0 || ^2.0.0\",\n    \"@vitejs/plugin-vue\": \"^5.0.0 || ^6.0.0\",\n    \"vite\": \"^6.0.0 || ^7.0.0 || ^8.0.0\"\n  },\n  \"engines\": {\n    \"node\": \"^18.0.0 || >=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/constants/build.ts",
    "content": "const DEFAULT_OUT_DIR_DEV = \"../build/resources/main/console\";\nconst DEFAULT_OUT_DIR_PROD = \"./build/dist\";\n\nexport { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD };\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/constants/externals.ts",
    "content": "const GLOBALS = {\n  vue: \"Vue\",\n  \"vue-router\": \"VueRouter\",\n  pinia: \"Pinia\",\n  \"@vueuse/core\": \"VueUse\",\n  \"@vueuse/components\": \"VueUse\",\n  \"@vueuse/router\": \"VueUse\",\n  \"@halo-dev/ui-shared\": \"HaloUiShared\",\n  \"@halo-dev/components\": \"HaloComponents\",\n  \"@halo-dev/api-client\": \"HaloApiClient\",\n  \"@halo-dev/richtext-editor\": \"RichTextEditor\",\n  axios: \"axios\",\n};\n\nconst EXTERNALS = Object.keys(GLOBALS) as string[];\n\nexport { EXTERNALS, GLOBALS };\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/constants/halo-plugin.ts",
    "content": "const DEFAULT_MANIFEST_PATH = \"../src/main/resources/plugin.yaml\";\n\nexport { DEFAULT_MANIFEST_PATH };\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/index.ts",
    "content": "export { HaloUIPluginBundlerKit } from \"./legacy\";\nexport { rsbuildConfig } from \"./rsbuild\";\nexport { viteConfig } from \"./vite\";\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/legacy.ts",
    "content": "import { Plugin } from \"vite\";\nimport { DEFAULT_OUT_DIR_DEV } from \"./constants/build\";\nimport { EXTERNALS, GLOBALS } from \"./constants/externals\";\nimport { DEFAULT_MANIFEST_PATH } from \"./constants/halo-plugin\";\nimport { getHaloPluginManifest } from \"./utils/halo-plugin\";\n\nconst LEGACY_OUT_DIR_PROD = \"../src/main/resources/console\";\n\ninterface HaloUIPluginBundlerKitOptions {\n  outDir?:\n    | string\n    | {\n        dev: string;\n        prod: string;\n      };\n  manifestPath?: string;\n}\n\n/**\n * @deprecated Use `viteConfig` or `rsbuildConfig` instead.\n */\nexport function HaloUIPluginBundlerKit(\n  options: HaloUIPluginBundlerKitOptions = {}\n): Plugin {\n  return {\n    name: \"halo-ui-plugin-bundler-kit\",\n    config(config, env) {\n      const isProduction = env.mode === \"production\";\n\n      let outDir = isProduction ? LEGACY_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV;\n\n      if (options.outDir) {\n        if (typeof options.outDir === \"string\") {\n          outDir = options.outDir;\n        } else {\n          outDir = isProduction ? options.outDir.prod : options.outDir.dev;\n        }\n      }\n\n      const manifestPath = options.manifestPath || DEFAULT_MANIFEST_PATH;\n\n      const manifest = getHaloPluginManifest(manifestPath);\n\n      return {\n        ...config,\n        define: {\n          \"process.env\": process.env,\n        },\n        build: {\n          outDir,\n          emptyOutDir: true,\n          lib: {\n            entry: \"src/index.ts\",\n            name: manifest.metadata.name,\n            formats: [\"iife\"],\n            fileName: () => \"main.js\",\n          },\n          rollupOptions: {\n            external: EXTERNALS,\n            output: {\n              globals: GLOBALS,\n              extend: true,\n            },\n          },\n        },\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/rsbuild.ts",
    "content": "import {\n  defineConfig,\n  mergeRsbuildConfig,\n  type ConfigParams,\n  type RsbuildConfig,\n  type RsbuildMode,\n} from \"@rsbuild/core\";\nimport { pluginVue } from \"@rsbuild/plugin-vue\";\nimport { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD } from \"./constants/build\";\nimport { GLOBALS } from \"./constants/externals\";\nimport { DEFAULT_MANIFEST_PATH } from \"./constants/halo-plugin\";\nimport { getHaloPluginManifest } from \"./utils/halo-plugin\";\n\nexport interface RsBuildUserConfig {\n  /**\n   * Halo plugin manifest path.\n   *\n   * @default \"../src/main/resources/plugin.yaml\"\n   */\n  manifestPath?: string;\n\n  /**\n   * Custom Rsbuild config.\n   */\n  rsbuild: RsbuildConfig | ((env: ConfigParams) => RsbuildConfig);\n}\n\nfunction createRsbuildPresetsConfig(manifestPath: string) {\n  const manifest = getHaloPluginManifest(manifestPath);\n\n  return defineConfig(({ envMode }) => {\n    const isProduction = envMode === \"production\";\n\n    const outDir = isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV;\n\n    return {\n      mode: (envMode as RsbuildMode) || \"production\",\n      plugins: [pluginVue()],\n      source: {\n        entry: {\n          main: \"./src/index.ts\",\n        },\n      },\n      dev: {\n        hmr: false,\n      },\n      performance: {\n        chunkSplit: {\n          strategy: \"custom\",\n        },\n      },\n      tools: {\n        rspack: {\n          optimization: {\n            splitChunks: {\n              chunks: \"async\",\n            },\n            moduleIds: \"named\",\n          },\n          experiments: {\n            rspackFuture: {\n              bundlerInfo: {\n                force: false,\n              },\n            },\n          },\n          module: {\n            parser: {\n              javascript: {\n                importMeta: false,\n              },\n            },\n          },\n          output: {\n            publicPath: `/plugins/${manifest.metadata.name}/assets/console/`,\n            library: {\n              type: \"window\",\n              export: \"default\",\n              name: manifest.metadata.name,\n            },\n            globalObject: \"window\",\n            iife: true,\n          },\n        },\n        htmlPlugin: false,\n      },\n      output: {\n        distPath: {\n          root: outDir,\n          js: \"\",\n          css: \"\",\n          jsAsync: \"chunks\",\n          cssAsync: \"chunks\",\n        },\n        cleanDistPath: true,\n        filename: {\n          css: (pathData) => {\n            if (pathData.chunk?.name === \"main\") {\n              return \"style.css\";\n            }\n            return \"[name].[contenthash:8].css\";\n          },\n          js: (pathData) => {\n            if (pathData.chunk?.name === \"main\") {\n              return \"main.js\";\n            }\n            return \"[name].[contenthash:8].js\";\n          },\n        },\n        externals: GLOBALS,\n      },\n    };\n  });\n}\n\n/**\n * Rsbuild config for Halo UI Plugin.\n *\n * @example\n * ```ts\n * import { rsbuildConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\n *\n * export default rsbuildConfig({\n *   rsbuild: {\n *     // your custom rsbuild config\n *   },\n * });\n * ```\n * @param config\n * @returns\n */\nexport function rsbuildConfig(\n  config?: RsBuildUserConfig\n): (env: ConfigParams) => RsbuildConfig {\n  const presetsConfigFn = createRsbuildPresetsConfig(\n    config?.manifestPath || DEFAULT_MANIFEST_PATH\n  );\n  return defineConfig((env) => {\n    const presetsConfig = presetsConfigFn(env);\n    const userConfig =\n      typeof config?.rsbuild === \"function\"\n        ? config.rsbuild(env)\n        : config?.rsbuild || {};\n    return mergeRsbuildConfig(presetsConfig, userConfig);\n  });\n}\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/utils/halo-plugin.ts",
    "content": "import fs from \"node:fs\";\nimport type { Plugin as HaloPlugin } from \"@halo-dev/api-client\";\nimport yaml from \"js-yaml\";\n\nexport function getHaloPluginManifest(manifestPath: string) {\n  const manifest = yaml.load(\n    fs.readFileSync(manifestPath, \"utf8\")\n  ) as HaloPlugin;\n\n  return manifest;\n}\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/src/vite.ts",
    "content": "import Vue from \"@vitejs/plugin-vue\";\nimport {\n  defineConfig,\n  mergeConfig,\n  UserConfig,\n  UserConfigFnObject,\n} from \"vite\";\nimport { DEFAULT_OUT_DIR_DEV, DEFAULT_OUT_DIR_PROD } from \"./constants/build\";\nimport { EXTERNALS, GLOBALS } from \"./constants/externals\";\nimport { DEFAULT_MANIFEST_PATH } from \"./constants/halo-plugin\";\nimport { getHaloPluginManifest } from \"./utils/halo-plugin\";\n\nexport interface ViteUserConfig {\n  /**\n   * Halo plugin manifest path.\n   *\n   * @default \"../src/main/resources/plugin.yaml\"\n   */\n  manifestPath?: string;\n\n  /**\n   * Custom Vite config.\n   */\n  vite: UserConfig | UserConfigFnObject;\n}\n\nfunction createVitePresetsConfig(manifestPath: string) {\n  const manifest = getHaloPluginManifest(manifestPath);\n\n  return defineConfig(({ mode }) => {\n    const isProduction = mode === \"production\";\n\n    return {\n      mode: mode || \"production\",\n      plugins: [Vue()],\n      define: { \"process.env.NODE_ENV\": \"'production'\" },\n      build: {\n        outDir: isProduction ? DEFAULT_OUT_DIR_PROD : DEFAULT_OUT_DIR_DEV,\n        emptyOutDir: true,\n        lib: {\n          entry: \"src/index.ts\",\n          name: manifest.metadata.name,\n          formats: [\"iife\"],\n          fileName: () => \"main.js\",\n          cssFileName: \"style\",\n        },\n        rollupOptions: {\n          external: EXTERNALS,\n          output: {\n            globals: GLOBALS,\n            extend: true,\n          },\n        },\n      },\n    };\n  });\n}\n\n/**\n * Vite config for Halo UI Plugin.\n *\n * @example\n * ```ts\n * import { viteConfig } from \"@halo-dev/ui-plugin-bundler-kit\";\n *\n * export default viteConfig({\n *   vite: {\n *     // your custom vite config\n *   },\n * });\n * ```\n */\nexport function viteConfig(config?: ViteUserConfig) {\n  const presetsConfigFn = createVitePresetsConfig(\n    config?.manifestPath || DEFAULT_MANIFEST_PATH\n  );\n  return defineConfig((env) => {\n    const presetsConfig = presetsConfigFn(env);\n    const userConfig =\n      typeof config?.vite === \"function\"\n        ? config.vite(env)\n        : config?.vite || {};\n    return mergeConfig(presetsConfig, userConfig);\n  });\n}\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/tsconfig.json",
    "content": "{\n  \"include\": [\"src\"],\n  \"exclude\": [\"**/*.spec.ts\"],\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"target\": \"ES2020\",\n    \"module\": \"ES2020\",\n    \"moduleResolution\": \"bundler\",\n    \"strict\": true,\n    \"declaration\": true,\n    \"sourceMap\": true,\n    \"noImplicitOverride\": true,\n    \"noUnusedLocals\": true,\n    \"esModuleInterop\": true,\n    \"baseUrl\": \".\",\n    \"resolveJsonModule\": true\n  }\n}\n"
  },
  {
    "path": "ui/packages/ui-plugin-bundler-kit/tsdown.config.ts",
    "content": "import { defineConfig } from \"tsdown\";\n\nexport default defineConfig({\n  entry: [\"./src/index.ts\"],\n  format: [\"esm\"],\n  dts: {\n    tsgo: true,\n  },\n  exports: true,\n});\n"
  },
  {
    "path": "ui/patches/@tiptap__extension-drag-handle@3.17.1.patch",
    "content": "diff --git a/dist/index.js b/dist/index.js\nindex 6a2900a3013fdc9c970a2633d511c1beae6c6e12..762359036767e1e0c7637ceea92df41e9b3cd17f 100644\n--- a/dist/index.js\n+++ b/dist/index.js\n@@ -4,13 +4,7 @@ import { Extension } from \"@tiptap/core\";\n // src/drag-handle-plugin.ts\n import { computePosition } from \"@floating-ui/dom\";\n import { isFirefox } from \"@tiptap/core\";\n-import { isChangeOrigin } from \"@tiptap/extension-collaboration\";\n import { Plugin, PluginKey } from \"@tiptap/pm/state\";\n-import {\n-  absolutePositionToRelativePosition,\n-  relativePositionToAbsolutePosition,\n-  ySyncPluginKey\n-} from \"@tiptap/y-tiptap\";\n \n // src/helpers/dragHandler.ts\n import { getSelectionRanges, NodeRangeSelection } from \"@tiptap/extension-node-range\";\n@@ -426,18 +420,10 @@ var getOuterNode = (doc, pos) => {\n \n // src/drag-handle-plugin.ts\n var getRelativePos = (state, absolutePos) => {\n-  const ystate = ySyncPluginKey.getState(state);\n-  if (!ystate) {\n-    return null;\n-  }\n-  return absolutePositionToRelativePosition(absolutePos, ystate.type, ystate.binding.mapping);\n+  return null;\n };\n var getAbsolutePos = (state, relativePos) => {\n-  const ystate = ySyncPluginKey.getState(state);\n-  if (!ystate) {\n-    return -1;\n-  }\n-  return relativePositionToAbsolutePosition(ystate.doc, ystate.type, relativePos, ystate.binding.mapping) || 0;\n+  return -1;\n };\n var getOuterDomNode = (view, domNode) => {\n   let tmpDomNode = domNode;\n@@ -565,17 +551,10 @@ var DragHandlePlugin = ({\n             return value;\n           }\n           if (tr.docChanged && currentNodePos !== -1 && element) {\n-            if (isChangeOrigin(tr)) {\n-              const newPos = getAbsolutePos(state, currentNodeRelPos);\n-              if (newPos !== currentNodePos) {\n-                currentNodePos = newPos;\n-              }\n-            } else {\n-              const newPos = tr.mapping.map(currentNodePos);\n-              if (newPos !== currentNodePos) {\n-                currentNodePos = newPos;\n-                currentNodeRelPos = getRelativePos(state, currentNodePos);\n-              }\n+            const newPos = tr.mapping.map(currentNodePos);\n+            if (newPos !== currentNodePos) {\n+              currentNodePos = newPos;\n+              currentNodeRelPos = getRelativePos(state, currentNodePos);\n             }\n           }\n           return value;\n"
  },
  {
    "path": "ui/pnpm-workspace.yaml",
    "content": "packages:\n  - \"packages/**\"\n"
  },
  {
    "path": "ui/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n    \"postcss-viewport-height-correction\": {},\n  },\n};\n"
  },
  {
    "path": "ui/scripts/apply_missing_translations.mjs",
    "content": "/**\n * Apply Missing Translations\n * -------------------------\n * This script applies translated entries from \"_missing_translations_[lang].json\" files\n * to their corresponding language files.\n *\n * For each missing translations file, it:\n * 1. Compares entries with the English base file\n * 2. Identifies which entries have been translated (values different from English)\n * 3. Merges only the translated entries into the main language file\n * 4. Updates the missing translations file to keep only untranslated entries\n *\n * Usage:\n * node scripts/apply_missing_translations.mjs\n *\n * Example output:\n * Processing: src/locales/_missing_translations_zh-TW.json for language: zh-TW\n * Updated src/locales/zh-TW.json with 15 translated entries.\n * Updated src/locales/_missing_translations_zh-TW.json with 10 remaining untranslated entries.\n *\n * This script is designed to be run repeatedly as you translate more entries in the\n * missing translations files. It will only apply entries that differ from the English version.\n */\n\nimport { existsSync } from \"fs\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst translationsDirPath = path.resolve(__dirname, \"../src/locales\");\nconst baseFile = path.join(translationsDirPath, \"en.json\");\n\nconst VERBOSE = true;\n\nasync function main() {\n  try {\n    const dirEntries = await fs.readdir(translationsDirPath, {\n      withFileTypes: true,\n    });\n\n    const missingFiles = dirEntries\n      .filter(\n        (entry) =>\n          entry.isFile() &&\n          entry.name.includes(\"_missing_translations_\") &&\n          entry.name.endsWith(\".json\")\n      )\n      .map((entry) => path.join(translationsDirPath, entry.name));\n\n    if (missingFiles.length === 0) {\n      console.log(\"No missing translation files found.\");\n      return;\n    }\n\n    const enTranslations = await loadJsonFile(baseFile);\n\n    for (const missingFile of missingFiles) {\n      const fileName = path.basename(missingFile, \".json\");\n      const langCode = fileName.replace(\"_missing_translations_\", \"\");\n      const targetFile = path.join(translationsDirPath, `${langCode}.json`);\n\n      console.log(`\\nProcessing: ${missingFile} for language: ${langCode}`);\n\n      if (!existsSync(targetFile)) {\n        console.log(`Target translation file ${targetFile} does not exist`);\n        continue;\n      }\n\n      try {\n        const missingTranslations = await loadJsonFile(missingFile);\n        const currentTranslations = await loadJsonFile(targetFile);\n\n        const translatedEntries = {};\n        const untranslatedEntries = {};\n        const stats = { added: 0, skipped: 0 };\n\n        const keys = Object.keys(missingTranslations);\n        console.log(`Found ${keys.length} keys in missing translations file.`);\n\n        for (const key of keys) {\n          const missingValue = missingTranslations[key];\n          const enValue = enTranslations[key];\n\n          if (\n            missingValue !== enValue &&\n            missingValue !== null &&\n            missingValue !== undefined\n          ) {\n            translatedEntries[key] = missingValue;\n            stats.added++;\n            if (VERBOSE) {\n              console.log(\n                `✓ TRANSLATED: ${key} = \"${missingValue}\" (EN: \"${enValue}\")`\n              );\n            }\n          } else {\n            untranslatedEntries[key] = missingValue;\n            stats.skipped++;\n            if (VERBOSE) {\n              console.log(\n                `✗ NOT TRANSLATED: ${key} = \"${missingValue}\" (same as EN: \"${enValue}\")`\n              );\n            }\n          }\n        }\n\n        if (stats.added > 0) {\n          const updatedTranslations = {\n            ...currentTranslations,\n            ...translatedEntries,\n          };\n\n          await saveJsonFile(updatedTranslations, targetFile);\n          console.log(\n            `Updated ${targetFile} with ${stats.added} translated entries.`\n          );\n\n          if (stats.skipped > 0) {\n            await saveJsonFile(untranslatedEntries, missingFile);\n            console.log(\n              `Updated ${missingFile} with ${stats.skipped} remaining untranslated entries.`\n            );\n          } else {\n            // Delete the missing file if all entries are translated\n            await fs.unlink(missingFile);\n            console.log(\n              `Deleted ${missingFile} as all entries are now translated.`\n            );\n          }\n        } else {\n          console.log(\n            `No translated entries found in ${missingFile}, files not updated.`\n          );\n        }\n\n        console.log(`\\nSummary for ${langCode}:`);\n        console.log(`- Added: ${stats.added} translated entries`);\n        console.log(`- Remaining: ${stats.skipped} untranslated entries`);\n      } catch (e) {\n        console.error(`Error processing ${missingFile}:`, e);\n      }\n    }\n  } catch (e) {\n    console.error(`Error:`, e);\n  }\n}\n\nasync function loadJsonFile(filePath) {\n  try {\n    const content = await fs.readFile(filePath, \"utf8\");\n    return JSON.parse(content) || {};\n  } catch (error) {\n    console.error(`Error loading file ${filePath}:`, error);\n    return {};\n  }\n}\n\nasync function saveJsonFile(data, filePath) {\n  try {\n    const jsonContent = JSON.stringify(data, null, 2);\n    await fs.writeFile(filePath, jsonContent, \"utf8\");\n    return true;\n  } catch (error) {\n    console.error(`Error saving file ${filePath}:`, error);\n    return false;\n  }\n}\n\nmain();\n"
  },
  {
    "path": "ui/scripts/find_missing_translations.mjs",
    "content": "/**\n * Find Missing Translations\n * -------------------------\n * This script identifies missing translations in language files by comparing them\n * with the English base file (en.json).\n *\n * For each language file, it:\n * 1. Compares it with the English base file\n * 2. Identifies keys that are missing in the target language\n * 3. Creates a \"_missing_translations_[lang].json\" file with those missing keys\n *\n * Usage:\n * node scripts/find_missing_translations.mjs\n *\n * Example output:\n * Generated src/locales/_missing_translations_zh-TW.json with 25 missing translations\n *\n * After running this script, you can translate the missing entries in the generated files,\n * then use apply_missing_translations.mjs to merge them into the main language files.\n */\n\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst translationsDirPath = path.resolve(__dirname, \"../src/locales\");\nconst baseFile = `${translationsDirPath}/en.json`;\n\nasync function main() {\n  try {\n    const baseTranslations = await loadJsonFile(baseFile);\n\n    const dirEntries = await fs.readdir(translationsDirPath, {\n      withFileTypes: true,\n    });\n\n    const translationFiles = dirEntries\n      .filter(\n        (entry) =>\n          entry.isFile() &&\n          entry.name.endsWith(\".json\") &&\n          entry.name !== \"en.json\" &&\n          !entry.name.includes(\"_missing_translations_\")\n      )\n      .map((entry) => path.join(translationsDirPath, entry.name));\n\n    for (const transFile of translationFiles) {\n      const langCode = path.basename(transFile, \".json\");\n\n      try {\n        const translations = await loadJsonFile(transFile);\n        const missing = findMissingTranslations(baseTranslations, translations);\n\n        if (Object.keys(missing).length > 0) {\n          const missingFile = `${translationsDirPath}/_missing_translations_${langCode}.json`;\n          await saveJsonFile(missing, missingFile);\n          console.log(\n            `Generated ${missingFile} with ${\n              Object.keys(missing).length\n            } missing translations`\n          );\n        } else {\n          console.log(`No missing translations in ${transFile}`);\n        }\n      } catch (e) {\n        console.log(`Error processing ${transFile}: ${e}`);\n      }\n    }\n  } catch (e) {\n    console.log(`Error: ${e}`);\n  }\n}\n\nasync function loadJsonFile(filePath) {\n  const content = await fs.readFile(filePath, \"utf8\");\n  return JSON.parse(content) || {};\n}\n\nasync function saveJsonFile(data, filePath) {\n  const jsonContent = JSON.stringify(data, null, 2);\n  await fs.writeFile(filePath, jsonContent, \"utf8\");\n}\n\nfunction findMissingTranslations(baseDict, compareDict) {\n  const missing = {};\n\n  for (const key of Object.keys(baseDict)) {\n    if (!Object.prototype.hasOwnProperty.call(compareDict, key)) {\n      missing[key] = baseDict[key];\n    }\n  }\n\n  return missing;\n}\n\nmain();\n"
  },
  {
    "path": "ui/scripts/fix_translations.mjs",
    "content": "/**\n * Fix Translations\n * -------------------------\n * This script removes translation keys that exist in language files but are not\n * present in the English base file (en.json).\n *\n * For each language file, it:\n * 1. Compares it with the English base file\n * 2. Identifies keys that exist in the language file but not in the English file\n * 3. Removes these extra keys from the language file\n *\n * Usage:\n * node scripts/fix_translations.mjs\n *\n * Example output:\n * Extra key found: core.common.outdatedKey\n * Removed 5 extra keys from src/locales/zh-TW.json\n *\n * This script helps maintain consistency across language files by ensuring they\n * only contain keys that are present in the English base file.\n */\n\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst translationsDirPath = path.resolve(__dirname, \"../src/locales\");\nconst baseFile = path.join(translationsDirPath, \"en.json\");\n\nasync function main() {\n  try {\n    const baseTranslations = await loadJsonFile(baseFile);\n\n    const dirEntries = await fs.readdir(translationsDirPath, {\n      withFileTypes: true,\n    });\n\n    const translationFiles = dirEntries\n      .filter(\n        (entry) =>\n          entry.isFile() &&\n          entry.name.endsWith(\".json\") &&\n          entry.name !== \"en.json\" &&\n          !entry.name.includes(\"_missing_translations_\")\n      )\n      .map((entry) => path.join(translationsDirPath, entry.name));\n\n    for (const transFile of translationFiles) {\n      try {\n        const translations = await loadJsonFile(transFile);\n\n        const extraKeysCount = removeExtraTranslations(\n          translations,\n          baseTranslations\n        );\n\n        if (extraKeysCount > 0) {\n          await saveJsonFile(translations, transFile);\n          console.log(`Removed ${extraKeysCount} extra keys from ${transFile}`);\n        } else {\n          console.log(`No extra keys found in ${transFile}`);\n        }\n      } catch (e) {\n        console.log(`Error processing ${transFile}: ${e}`);\n      }\n    }\n  } catch (e) {\n    console.log(`Error: ${e}`);\n  }\n}\n\nasync function loadJsonFile(filePath) {\n  const content = await fs.readFile(filePath, \"utf8\");\n  return JSON.parse(content) || {};\n}\n\nasync function saveJsonFile(data, filePath) {\n  const jsonContent = JSON.stringify(data, null, 2);\n  await fs.writeFile(filePath, jsonContent, \"utf8\");\n}\n\nfunction removeExtraTranslations(translations, baseTranslations) {\n  let extraKeysCount = 0;\n  const keysToDelete = [];\n\n  for (const key of Object.keys(translations)) {\n    if (!Object.prototype.hasOwnProperty.call(baseTranslations, key)) {\n      keysToDelete.push(key);\n      extraKeysCount++;\n      console.log(`Extra key found: ${key}`);\n    }\n  }\n\n  for (const key of keysToDelete) {\n    delete translations[key];\n  }\n\n  return extraKeysCount;\n}\n\nmain();\n"
  },
  {
    "path": "ui/src/components/alerts/H2WarningAlert.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VAlert } from \"@halo-dev/components\";\nimport type { Info } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport axios from \"axios\";\n\nconst { data: info } = useQuery<Info>({\n  queryKey: [\"system-info\"],\n  queryFn: async () => {\n    const { data } = await axios.get<Info>(`/actuator/info`, {\n      withCredentials: true,\n    });\n    return data;\n  },\n  retry: 0,\n});\n</script>\n<template>\n  <VAlert\n    v-if=\"info?.database.name.startsWith('H2')\"\n    class=\"mt-3\"\n    type=\"warning\"\n    :title=\"$t('core.components.h2_warning_alert.title')\"\n    :closable=\"false\"\n  >\n    <template #description>\n      {{ $t(\"core.components.h2_warning_alert.description\") }}\n    </template>\n  </VAlert>\n</template>\n"
  },
  {
    "path": "ui/src/components/attachment/AttachmentGridListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport AttachmentError from \"@console/modules/contents/attachments/components/AttachmentError.vue\";\nimport AttachmentLoading from \"@console/modules/contents/attachments/components/AttachmentLoading.vue\";\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport { IconCheckboxFill } from \"@halo-dev/components\";\nimport { UseImage } from \"@vueuse/components\";\nimport { isImage } from \"@/utils/image\";\nimport LazyVideo from \"../video/LazyVideo.vue\";\n\nwithDefaults(\n  defineProps<{\n    attachment: Attachment;\n    isSelected?: boolean;\n    isDisabled?: boolean;\n  }>(),\n  {\n    isSelected: false,\n    isDisabled: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"select\"): void;\n}>();\n</script>\n<template>\n  <div\n    class=\"group relative inline-flex cursor-pointer flex-col overflow-hidden rounded-base bg-white ring-1 ring-gray-100 transition-all hover:shadow\"\n    :class=\"{\n      '!ring-primary': isSelected,\n      '!ring-red-600': attachment.metadata.deletionTimestamp,\n      'pointer-events-none !cursor-not-allowed opacity-50': isDisabled,\n    }\"\n  >\n    <div class=\"aspect-h-8 aspect-w-10 block overflow-hidden bg-gray-100\">\n      <AttachmentFileTypeIcon\n        v-if=\"!attachment.status?.permalink\"\n        :file-name=\"attachment.spec.displayName\"\n      />\n      <UseImage\n        v-else-if=\"isImage(attachment.spec.mediaType)\"\n        :src=\"attachment.status?.thumbnails?.S || attachment.status.permalink\"\n      >\n        <template #loading>\n          <AttachmentLoading />\n        </template>\n        <template #error>\n          <AttachmentError />\n        </template>\n        <template #default>\n          <Transition appear name=\"fade\">\n            <img\n              :alt=\"attachment.spec.displayName\"\n              :src=\"\n                attachment.status?.thumbnails?.S || attachment.status.permalink\n              \"\n              class=\"pointer-events-none transform-gpu object-cover group-hover:opacity-75\"\n            />\n          </Transition>\n        </template>\n      </UseImage>\n      <LazyVideo\n        v-else-if=\"attachment?.spec.mediaType?.startsWith('video/')\"\n        :src=\"attachment.status?.permalink\"\n        classes=\"object-cover group-hover:opacity-75\"\n      >\n        <template #loading>\n          <AttachmentLoading />\n        </template>\n        <template #error>\n          <AttachmentError />\n        </template>\n      </LazyVideo>\n      <AttachmentFileTypeIcon v-else :file-name=\"attachment.spec.displayName\" />\n    </div>\n\n    <p\n      v-tooltip=\"attachment.spec.displayName\"\n      class=\"block cursor-pointer truncate px-2 py-1 text-center text-xs font-medium text-gray-700\"\n    >\n      {{ attachment.spec.displayName }}\n    </p>\n\n    <div\n      v-if=\"attachment.metadata.deletionTimestamp\"\n      class=\"absolute right-1 top-1 text-xs text-red-300\"\n    >\n      {{ $t(\"core.common.status.deleting\") }}...\n    </div>\n\n    <div\n      v-if=\"!attachment.metadata.deletionTimestamp\"\n      v-permission=\"['system:attachments:manage']\"\n      :class=\"{\n        '!flex': isSelected,\n      }\"\n      class=\"absolute left-0 top-0 hidden h-1/3 w-full cursor-pointer justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex\"\n    >\n      <slot name=\"actions\" />\n      <IconCheckboxFill\n        :class=\"{\n          '!text-primary': isSelected,\n        }\"\n        class=\"mr-1 mt-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary\"\n        @click.stop=\"emit('select')\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/attachment/AttachmentImagePreview.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  GetThumbnailByUriSizeEnum,\n  type Attachment,\n} from \"@halo-dev/api-client\";\nimport { VTabbar } from \"@halo-dev/components\";\nimport { THUMBNAIL_WIDTH_MAP } from \"@halo-dev/ui-shared\";\nimport { UseImage } from \"@vueuse/components\";\nimport { computed, ref, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    attachment?: Attachment;\n  }>(),\n  {\n    attachment: undefined,\n  }\n);\n\nconst { attachment } = toRefs(props);\n\nconst sizeOrder: Record<GetThumbnailByUriSizeEnum, number> = {\n  XL: 4,\n  L: 3,\n  M: 2,\n  S: 1,\n};\n\nconst items = computed(\n  (): { label: string; id: string; permalink?: string }[] => {\n    return [\n      {\n        label: t(\"core.attachment.common.text.original\"),\n        id: \"original\",\n        permalink: attachment.value?.status?.permalink,\n      },\n      ...Object.entries(attachment.value?.status?.thumbnails || {})\n        .sort(\n          ([sizeA], [sizeB]) =>\n            (sizeOrder[sizeB as GetThumbnailByUriSizeEnum] || 0) -\n            (sizeOrder[sizeA as GetThumbnailByUriSizeEnum] || 0)\n        )\n        .map(([size, permalink]) => ({\n          label: `${THUMBNAIL_WIDTH_MAP[size as GetThumbnailByUriSizeEnum]}w`,\n          id: size,\n          permalink,\n        })),\n    ];\n  }\n);\n\nconst activeId = ref(items.value?.[0]?.id);\n</script>\n<template>\n  <VTabbar\n    v-model:active-id=\"activeId\"\n    :items=\"\n      items.map((item) => ({\n        id: item.id,\n        label: item.label,\n      }))\n    \"\n    type=\"outline\"\n  ></VTabbar>\n  <div class=\"mt-3\">\n    <a\n      v-for=\"item in items\"\n      :key=\"item.permalink\"\n      class=\"block\"\n      target=\"_blank\"\n      :href=\"item.permalink\"\n    >\n      <span v-if=\"!item.permalink\" class=\"text-red-400\">\n        {{ $t(\"core.common.status.loading_error\") }}\n      </span>\n      <UseImage\n        v-else-if=\"item.id === activeId\"\n        v-tooltip=\"{\n          content: item.permalink,\n          placement: 'bottom',\n        }\"\n        :src=\"item.permalink\"\n      >\n        <template #loading>\n          <span class=\"text-gray-400\">\n            {{ $t(\"core.common.status.loading\") }}...\n          </span>\n        </template>\n        <template #error>\n          <span class=\"text-red-400\">\n            {{ $t(\"core.common.status.loading_error\") }}\n          </span>\n        </template>\n        <template #default>\n          <Transition appear name=\"fade\">\n            <img :src=\"item.permalink\" class=\"max-w-full rounded\" />\n          </Transition>\n        </template>\n      </UseImage>\n    </a>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/attachment/AttachmentPermalinkList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VTabbar } from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { useClipboard } from \"@vueuse/core\";\nimport { computed, ref, toRefs } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { matchMediaType } from \"@/utils/media-type\";\n\nconst props = withDefaults(\n  defineProps<{\n    attachment?: Attachment;\n  }>(),\n  {\n    attachment: undefined,\n  }\n);\n\nconst { attachment } = toRefs(props);\n\nconst { copy } = useClipboard({ legacy: true });\nconst { t } = useI18n();\nconst { globalInfo } = stores.globalInfo();\n\nconst activeId = ref<\"relative\" | \"absolute\">(\"relative\");\n\nconst mediaType = computed(() => {\n  return attachment.value?.spec.mediaType;\n});\n\nconst isImage = computed(() => {\n  return mediaType.value && matchMediaType(mediaType.value, \"image/*\");\n});\n\nconst isVideo = computed(() => {\n  return mediaType.value && matchMediaType(mediaType.value, \"video/*\");\n});\n\nconst isAudio = computed(() => {\n  return mediaType.value && matchMediaType(mediaType.value, \"audio/*\");\n});\n\nconst isLocalAttachment = computed(() => {\n  return props.attachment?.status?.permalink?.startsWith(\"/\");\n});\n\nconst permalink = computed(() => {\n  const { permalink: value } = props.attachment?.status || {};\n\n  if (!isLocalAttachment.value) {\n    return value;\n  }\n\n  if (activeId.value === \"relative\") {\n    return value;\n  }\n\n  return `${globalInfo?.externalUrl}${value}`;\n});\n\nconst htmlText = computed(() => {\n  const { displayName } = attachment.value?.spec || {};\n\n  if (isImage.value) {\n    return `<img src=\"${permalink.value}\" alt=\"${displayName}\" />`;\n  } else if (isVideo.value) {\n    return `<video src=\"${permalink.value}\"></video>`;\n  } else if (isAudio.value) {\n    return `<audio src=\"${permalink.value}\"></audio>`;\n  }\n  return `<a href=\"${permalink.value}\">${displayName}</a>`;\n});\n\nconst markdownText = computed(() => {\n  const { displayName } = attachment.value?.spec || {};\n  if (isImage.value) {\n    return `![${displayName}](${permalink.value})`;\n  }\n  return `[${displayName}](${permalink.value})`;\n});\n\nconst handleCopy = (format: \"markdown\" | \"html\" | \"url\") => {\n  if (format === \"url\") {\n    copy(permalink.value || \"\");\n  } else if (format === \"markdown\") {\n    copy(markdownText.value);\n  } else if (format === \"html\") {\n    copy(htmlText.value);\n  }\n\n  Toast.success(t(\"core.common.toast.copy_success\"));\n};\n\nconst formats = computed(\n  (): {\n    label: string;\n    key: \"url\" | \"html\" | \"markdown\";\n    value?: string;\n  }[] => {\n    return [\n      {\n        label: \"URL\",\n        key: \"url\",\n        value: permalink.value,\n      },\n      {\n        label: \"HTML\",\n        key: \"html\",\n        value: htmlText.value,\n      },\n      {\n        label: \"Markdown\",\n        key: \"markdown\",\n        value: markdownText.value,\n      },\n    ];\n  }\n);\n</script>\n\n<template>\n  <VTabbar\n    v-if=\"isLocalAttachment\"\n    v-model:active-id=\"activeId\"\n    :items=\"[\n      {\n        label: $t('core.attachment.permalink_list.relative'),\n        id: 'relative',\n      },\n      {\n        label: $t('core.attachment.permalink_list.absolute'),\n        id: 'absolute',\n      },\n    ]\"\n    type=\"outline\"\n  ></VTabbar>\n  <ul class=\"mt-3 flex flex-col space-y-2\">\n    <li v-for=\"format in formats\" :key=\"format.key\">\n      <div\n        class=\"flex w-full cursor-pointer items-center justify-between space-x-3 rounded border p-3 hover:border-primary\"\n      >\n        <div class=\"flex flex-1 flex-col space-y-2 text-xs text-gray-900\">\n          <span class=\"font-semibold\">\n            {{ format.label }}\n          </span>\n          <span class=\"break-all\">\n            {{ format.value }}\n          </span>\n        </div>\n        <div>\n          <VButton size=\"sm\" @click=\"handleCopy(format.key)\">\n            {{ $t(\"core.common.buttons.copy\") }}\n          </VButton>\n        </div>\n      </div>\n    </li>\n  </ul>\n</template>\n"
  },
  {
    "path": "ui/src/components/back-to-top/BackToTop.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconArrowUpLine } from \"@halo-dev/components\";\nimport { useEventListener } from \"@vueuse/core\";\nimport { ref } from \"vue\";\n\nconst visible = ref(false);\n\nfunction onWindowScroll() {\n  const isDown = window.scrollY > 300;\n  visible.value = isDown;\n}\n\nfunction handleScrollToTop() {\n  window.scrollTo({ top: 0, behavior: \"smooth\" });\n}\n\nuseEventListener(window, \"scroll\", onWindowScroll);\n</script>\n<template>\n  <button\n    class=\"fixed bottom-8 right-8 z-10 inline-flex size-10 items-center justify-center rounded-full bg-primary transition-all hover:bg-primary/80 hover:shadow active:scale-95 active:bg-primary\"\n    :class=\"{\n      'opacity-100': visible,\n      'opacity-0': !visible,\n    }\"\n    @click=\"handleScrollToTop\"\n  >\n    <IconArrowUpLine class=\"size-6 text-white\" />\n  </button>\n</template>\n"
  },
  {
    "path": "ui/src/components/base-app/BaseApp.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitConfig } from \"@formkit/core\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { useFavicon } from \"@vueuse/core\";\nimport type { OverlayScrollbars } from \"overlayscrollbars\";\nimport {\n  useOverlayScrollbars,\n  type UseOverlayScrollbarsParams,\n} from \"overlayscrollbars-vue\";\nimport { storeToRefs } from \"pinia\";\nimport { computed, inject, onMounted, provide, reactive } from \"vue\";\nimport { RouterView } from \"vue-router\";\nimport { useAppTitle } from \"@/composables/use-title\";\nimport { i18n } from \"@/locales\";\n\nuseAppTitle();\n\nconst { globalInfo } = storeToRefs(stores.globalInfo());\n\n// Favicon\nconst defaultFavicon = \"/ui-assets/favicon.ico\";\nconst favicon = computed(() => {\n  return globalInfo.value?.favicon || defaultFavicon;\n});\n\nuseFavicon(favicon);\n\n// body scroll\nconst body = document.querySelector(\"body\");\nconst reactiveParams = reactive<UseOverlayScrollbarsParams>({\n  options: {\n    scrollbars: {\n      autoHide: \"scroll\",\n      autoHideDelay: 600,\n    },\n  },\n  defer: true,\n});\nconst [initialize, instance] = useOverlayScrollbars(reactiveParams);\nonMounted(() => {\n  if (body) initialize({ target: body });\n});\n\nprovide<() => OverlayScrollbars | null>(\"bodyScrollInstance\", instance);\n\n// setup formkit locale\n// see https://formkit.com/essentials/internationalization\nconst formkitLocales = {\n  en: \"en\",\n  zh: \"zh\",\n  \"en-US\": \"en\",\n  \"zh-CN\": \"zh\",\n};\nconst formkitConfig = inject(Symbol.for(\"FormKitConfig\")) as FormKitConfig;\nformkitConfig.locale = formkitLocales[i18n.global.locale.value] || \"zh\";\n\n// Fix 100vh issue in ios devices\nfunction setViewportProperty(doc: HTMLElement) {\n  let prevClientHeight: number;\n  const customVar = \"--vh\";\n\n  function handleResize() {\n    const clientHeight = doc.clientHeight;\n    if (clientHeight === prevClientHeight) return;\n    requestAnimationFrame(function updateViewportHeight() {\n      doc.style.setProperty(customVar, clientHeight * 0.01 + \"px\");\n      prevClientHeight = clientHeight;\n    });\n  }\n\n  handleResize();\n  return handleResize;\n}\n\nwindow.addEventListener(\n  \"resize\",\n  setViewportProperty(document.documentElement)\n);\n</script>\n\n<template>\n  <RouterView />\n</template>\n\n<style lang=\"scss\">\nbody {\n  background: #eff4f9;\n}\n\nhtml,\nbody,\n#app {\n  min-height: 100vh;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n.v-popper__popper {\n  outline: none;\n}\n\n.v-popper--theme-tooltip {\n  pointer-events: none;\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/button/SubmitButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton } from \"@halo-dev/components\";\nimport { useMagicKeys } from \"@vueuse/core\";\nimport { computed, useAttrs, watchEffect } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { isMac } from \"@/utils/device\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    text?: string;\n  }>(),\n  {\n    text: \"提交\",\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"submit\"): void;\n}>();\n\nconst attrs = useAttrs();\n\nconst buttonText = computed(() => {\n  return t(\"core.components.submit_button.computed_text\", {\n    text: props.text,\n    shortcut: `${isMac ? \"⌘\" : \"Ctrl\"} + ↵`,\n  });\n});\n\nconst { Command_Enter, Ctrl_Enter } = useMagicKeys();\n\nwatchEffect(() => {\n  if (Command_Enter.value || Ctrl_Enter.value) {\n    emit(\"submit\");\n  }\n});\n</script>\n\n<template>\n  <VButton v-bind=\"attrs\" @click=\"emit('submit')\">\n    {{ buttonText }}\n  </VButton>\n</template>\n"
  },
  {
    "path": "ui/src/components/codemirror/Codemirror.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { LanguageSupport } from \"@codemirror/language\";\nimport { Compartment, EditorState } from \"@codemirror/state\";\nimport { EditorView } from \"@codemirror/view\";\nimport { basicSetup } from \"codemirror\";\nimport { onBeforeUnmount, onMounted, shallowRef, watch } from \"vue\";\nimport { presetLanguages, type CodemirrorProps } from \"./supports\";\n\nconst props = withDefaults(defineProps<CodemirrorProps>(), {\n  modelValue: \"\",\n  height: \"auto\",\n  language: \"yaml\",\n  extensions: () => [],\n});\n\nconst emit = defineEmits<{\n  (e: \"update:modelValue\", value: string): void;\n  (e: \"change\", value: string): void;\n}>();\n\nconst wrapper = shallowRef<HTMLDivElement>();\nconst cmState = shallowRef<EditorState>();\nconst cmView = shallowRef<EditorView>();\n\nconst themeCompartment = new Compartment();\nconst languageCompartment = new Compartment();\n\nconst createCustomTheme = (height: string) => {\n  return EditorView.theme({\n    \"&\": {\n      height,\n      width: \"100%\",\n    },\n  });\n};\n\nconst createCmEditor = () => {\n  let extensions = [\n    basicSetup,\n    EditorView.lineWrapping,\n    themeCompartment.of(createCustomTheme(props.height)),\n    languageCompartment.of([]),\n    EditorView.updateListener.of((viewUpdate) => {\n      if (viewUpdate.docChanged) {\n        const doc = viewUpdate.state.doc.toString();\n        emit(\"update:modelValue\", doc);\n        emit(\"change\", doc);\n      }\n    }),\n  ];\n\n  if (props.extensions) {\n    extensions = extensions.concat(props.extensions);\n  }\n\n  cmState.value = EditorState.create({\n    doc: props.modelValue,\n    extensions,\n  });\n\n  cmView.value = new EditorView({\n    state: cmState.value,\n    parent: wrapper.value,\n  });\n\n  loadLanguage();\n};\n\nasync function loadLanguage() {\n  let language: LanguageSupport;\n\n  if (!props.language) {\n    return;\n  }\n\n  if (typeof props.language === \"string\") {\n    const loader = presetLanguages[props.language];\n    if (!loader) {\n      throw new Error(`Language ${props.language} not found`);\n    }\n    language = await loader();\n  } else {\n    language = props.language;\n  }\n\n  cmView.value?.dispatch({\n    effects: languageCompartment.reconfigure(language),\n  });\n}\n\nonMounted(() => {\n  createCmEditor();\n});\n\n// Update the codemirror editor doc when the model value changes.\nwatch(\n  () => props.modelValue,\n  (newValue) => {\n    if (!cmView.value) {\n      return;\n    }\n\n    if (newValue !== cmView.value.state.doc.toString()) {\n      cmView.value.dispatch({\n        changes: {\n          from: 0,\n          to: cmView.value.state.doc.length,\n          insert: newValue,\n        },\n      });\n    }\n  }\n);\n\nwatch(\n  () => props.height,\n  (newHeight) => {\n    if (cmView.value) {\n      cmView.value.dispatch({\n        effects: themeCompartment.reconfigure(createCustomTheme(newHeight)),\n      });\n    }\n  }\n);\n\nwatch(\n  () => props.language,\n  () => {\n    loadLanguage();\n  }\n);\n\n// Destroy codemirror editor when component unmounts\nonBeforeUnmount(() => {\n  cmView.value?.destroy();\n});\n</script>\n<template>\n  <div ref=\"wrapper\" class=\"codemirror-wrapper contents\"></div>\n</template>\n"
  },
  {
    "path": "ui/src/components/codemirror/supports.ts",
    "content": "import type { LanguageSupport } from \"@codemirror/language\";\nimport type { EditorStateConfig } from \"@codemirror/state\";\n\nexport const presetLanguages = {\n  yaml: async (): Promise<LanguageSupport> => {\n    const { yaml } = await import(\"@codemirror/lang-yaml\");\n    return yaml();\n  },\n  html: async (): Promise<LanguageSupport> => {\n    const { html } = await import(\"@codemirror/lang-html\");\n    return html();\n  },\n  javascript: async (): Promise<LanguageSupport> => {\n    const { javascript } = await import(\"@codemirror/lang-javascript\");\n    return javascript({\n      jsx: true,\n      typescript: true,\n    });\n  },\n  css: async (): Promise<LanguageSupport> => {\n    const { css } = await import(\"@codemirror/lang-css\");\n    return css();\n  },\n  json: async (): Promise<LanguageSupport> => {\n    const { json } = await import(\"@codemirror/lang-json\");\n    return json();\n  },\n  markdown: async (): Promise<LanguageSupport> => {\n    const { markdown } = await import(\"@codemirror/lang-markdown\");\n    return markdown({\n      addKeymap: true,\n      completeHTMLTags: true,\n    });\n  },\n} satisfies Record<string, () => Promise<LanguageSupport>>;\n\nexport interface CodemirrorProps {\n  modelValue?: string;\n  height?: string;\n  language: keyof typeof presetLanguages;\n  extensions?: EditorStateConfig[\"extensions\"];\n}\n"
  },
  {
    "path": "ui/src/components/common/AppDownloadAlert.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VAlert } from \"@halo-dev/components\";\n</script>\n\n<template>\n  <VAlert :title=\"$t('core.common.text.tip')\" :closable=\"false\">\n    <template #description>\n      <p>{{ $t(\"core.components.app_download_alert.description\") }}</p>\n      <ul class=\"mt-1.5 list-inside list-decimal space-y-1\">\n        <i18n-t\n          keypath=\"core.components.app_download_alert.sources.app_store\"\n          tag=\"li\"\n        >\n          <template #url>\n            <a\n              href=\"https://www.halo.run/store/apps\"\n              target=\"_blank\"\n              class=\"underline-offset-2 hover:text-gray-900 hover:underline\"\n            >\n              https://www.halo.run/store/apps\n            </a>\n          </template>\n        </i18n-t>\n        <i18n-t\n          keypath=\"core.components.app_download_alert.sources.github\"\n          tag=\"li\"\n        >\n          <template #url>\n            <a\n              href=\"https://github.com/halo-sigs/awesome-halo\"\n              target=\"_blank\"\n              class=\"underline-offset-2 hover:text-gray-900 hover:underline\"\n            >\n              https://github.com/halo-sigs/awesome-halo\n            </a>\n          </template>\n        </i18n-t>\n      </ul>\n    </template>\n  </VAlert>\n</template>\n"
  },
  {
    "path": "ui/src/components/dropdown-selector/EditorProviderSelector.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconExchange,\n  VAvatar,\n  VDropdown,\n  VDropdownItem,\n} from \"@halo-dev/components\";\nimport type { EditorProvider } from \"@halo-dev/ui-shared\";\nimport { useEditorExtensionPoints } from \"@/composables/use-editor-extension-points\";\n\nwithDefaults(\n  defineProps<{\n    provider?: EditorProvider;\n    allowForcedSelect?: boolean;\n  }>(),\n  {\n    provider: undefined,\n    allowForcedSelect: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"select\", provider: EditorProvider): void;\n}>();\n\nconst { editorProviders, fetchEditorProviders } = useEditorExtensionPoints();\n\nfetchEditorProviders();\n</script>\n\n<template>\n  <VDropdown>\n    <div\n      class=\"group flex w-full cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100\"\n    >\n      <VAvatar v-if=\"provider?.logo\" :src=\"provider.logo\" size=\"xs\"></VAvatar>\n      <div\n        class=\"select-none whitespace-nowrap text-sm text-gray-600 group-hover:text-gray-900\"\n      >\n        {{ provider?.displayName }}\n      </div>\n      <IconExchange class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\" />\n    </div>\n    <template #popper>\n      <VDropdownItem\n        v-for=\"(editorProvider, index) in editorProviders\"\n        :key=\"index\"\n        v-tooltip=\"{\n          disabled:\n            allowForcedSelect ||\n            provider?.rawType.toLowerCase() ===\n              editorProvider.rawType.toLowerCase(),\n          content: $t(\n            'core.components.editor_provider_selector.tooltips.disallow'\n          ),\n        }\"\n        :selected=\"provider?.name === editorProvider.name\"\n        :disabled=\"\n          !allowForcedSelect &&\n          provider?.rawType.toLowerCase() !==\n            editorProvider.rawType.toLowerCase()\n        \"\n        @click=\"emit('select', editorProvider)\"\n      >\n        <template v-if=\"editorProvider.logo\" #prefix-icon>\n          <VAvatar :src=\"editorProvider.logo\" size=\"xs\"></VAvatar>\n        </template>\n        {{ editorProvider.displayName }}\n      </VDropdownItem>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/src/components/editor/DefaultEditor.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  GetThumbnailByUriSizeEnum,\n  type Attachment,\n} from \"@halo-dev/api-client\";\nimport {\n  IconCalendar,\n  IconCharacterRecognition,\n  IconExchange,\n  IconFolder,\n  IconImageAddLine,\n  IconLink,\n  IconUserFollow,\n  Toast,\n  VButton,\n  VDropdown,\n  VDropdownItem,\n  VLoading,\n  VTabItem,\n  VTabs,\n} from \"@halo-dev/components\";\nimport {\n  convertToMediaContents,\n  DecorationSet,\n  Editor,\n  Extension,\n  ExtensionHeading,\n  ExtensionsKit,\n  Plugin,\n  PluginKey,\n  RichTextEditor,\n  ToolbarItem,\n  ToolboxItem,\n  VueEditor,\n  type Extensions,\n} from \"@halo-dev/richtext-editor\";\nimport { utils, type AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { useDebounceFn, useFileDialog, useLocalStorage } from \"@vueuse/core\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport {\n  inject,\n  markRaw,\n  nextTick,\n  onBeforeUnmount,\n  onMounted,\n  ref,\n  shallowRef,\n  watch,\n  type ComputedRef,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport LucideHeading1 from \"~icons/lucide/heading-1\";\nimport LucideHeading2 from \"~icons/lucide/heading-2\";\nimport LucideHeading3 from \"~icons/lucide/heading-3\";\nimport LucideHeading4 from \"~icons/lucide/heading-4\";\nimport LucideHeading5 from \"~icons/lucide/heading-5\";\nimport LucideHeading6 from \"~icons/lucide/heading-6\";\nimport MingcuteFoldVerticalLine from \"~icons/mingcute/fold-vertical-line\";\nimport MingcuteLayoutRightLine from \"~icons/mingcute/layout-right-line\";\nimport MingcuteUnfoldVerticalLine from \"~icons/mingcute/unfold-vertical-line\";\nimport { i18n } from \"@/locales\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    title?: string;\n    raw?: string;\n    content?: string;\n    cover?: string;\n    uploadImage?: (\n      file: File,\n      options?: AxiosRequestConfig\n    ) => Promise<Attachment>;\n  }>(),\n  {\n    title: \"\",\n    raw: \"\",\n    content: \"\",\n    cover: undefined,\n    uploadImage: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:title\", value: string): void;\n  (event: \"update:raw\", value: string): void;\n  (event: \"update:content\", value: string): void;\n  (event: \"update\", value: string): void;\n  (event: \"update:cover\", value: string | undefined): void;\n}>();\n\nconst owner = inject<ComputedRef<string | undefined>>(\"owner\");\nconst publishTime = inject<ComputedRef<string | undefined>>(\"publishTime\");\nconst permalink = inject<ComputedRef<string | undefined>>(\"permalink\");\n\ninterface HeadingNode {\n  id: string;\n  level: number;\n  text: string;\n}\n\nconst headingIcons = {\n  1: markRaw(LucideHeading1),\n  2: markRaw(LucideHeading2),\n  3: markRaw(LucideHeading3),\n  4: markRaw(LucideHeading4),\n  5: markRaw(LucideHeading5),\n  6: markRaw(LucideHeading6),\n};\n\nconst headingNodes = ref<HeadingNode[]>();\nconst selectedHeadingNode = ref<HeadingNode>();\nconst extraActiveId = ref(\"toc\");\n\nconst editor = shallowRef<VueEditor>();\nconst editorTitleRef = ref();\n\nconst { pluginModules } = usePluginModuleStore();\n\nconst showSidebar = useLocalStorage(\"halo:editor:show-sidebar\", true);\n\n// Attachment Selector Modal\nconst attachmentSelectorModalVisible = ref(false);\nfunction onAttachmentSelect(attachments: AttachmentLike[]) {\n  const contents = convertToMediaContents(editor.value!, attachments);\n  if (contents.length) {\n    editor.value?.chain().focus().insertContent(contents).run();\n  }\n}\n\nconst customExtensions = [\n  Extension.create({\n    name: \"custom-attachment-extension\",\n    addOptions() {\n      // If user has no permission to view attachments, return\n      if (\n        !utils.permission.has([\n          \"system:attachments:manage\",\n          \"uc:attachments:manage\",\n        ])\n      ) {\n        return this;\n      }\n\n      return {\n        getToolboxItems({ editor }: { editor: Editor }) {\n          return [\n            {\n              priority: 0,\n              component: markRaw(ToolboxItem),\n              props: {\n                editor,\n                icon: markRaw(IconFolder),\n                title: i18n.global.t(\n                  \"core.components.default_editor.toolbox.attachment\"\n                ),\n                action: () => {\n                  attachmentSelectorModalVisible.value = true;\n                  return true;\n                },\n              },\n            },\n          ];\n        },\n      };\n    },\n  }),\n  Extension.create({\n    name: \"custom-sidebar-toggle-extension\",\n    addOptions() {\n      return {\n        getToolbarItems({ editor }: { editor: Editor }) {\n          return {\n            priority: 1000,\n            component: markRaw(ToolbarItem),\n            props: {\n              editor,\n              isActive: showSidebar.value,\n              icon: markRaw(MingcuteLayoutRightLine),\n              title: i18n.global.t(\n                \"core.components.default_editor.toolbox.show_hide_sidebar\"\n              ),\n              action: () => {\n                showSidebar.value = !showSidebar.value;\n              },\n            },\n          };\n        },\n      };\n    },\n  }),\n  Extension.create({\n    name: \"get-heading-id-extension\",\n    addProseMirrorPlugins() {\n      return [\n        new Plugin({\n          key: new PluginKey(\"get-heading-id\"),\n          props: {\n            decorations: (state) => {\n              const headings: HeadingNode[] = [];\n              const { doc } = state;\n              doc.descendants((node) => {\n                if (node.type.name === ExtensionHeading.name) {\n                  headings.push({\n                    level: node.attrs.level,\n                    text: node.textContent,\n                    id: node.attrs.id,\n                  });\n                }\n              });\n              headingNodes.value = headings;\n              if (!selectedHeadingNode.value) {\n                selectedHeadingNode.value = headings[0];\n              }\n              return DecorationSet.empty;\n            },\n          },\n        }),\n      ];\n    },\n  }),\n];\n\nconst isInitialized = ref(false);\n\nonMounted(async () => {\n  const extensionsFromPlugins: Extensions = [];\n\n  for (const pluginModule of pluginModules) {\n    const callbackFunction =\n      pluginModule?.extensionPoints?.[\"default:editor:extension:create\"];\n\n    if (typeof callbackFunction !== \"function\") {\n      continue;\n    }\n\n    const extensions = await callbackFunction();\n\n    extensionsFromPlugins.push(...extensions);\n  }\n\n  // debounce OnUpdate\n  const debounceOnUpdate = useDebounceFn(() => {\n    const html = editor.value?.getHTML() + \"\";\n    emit(\"update:raw\", html);\n    emit(\"update:content\", html);\n    emit(\"update\", html);\n  }, 250);\n\n  editor.value = new VueEditor({\n    content: props.raw,\n    extensions: [\n      ExtensionsKit.configure({\n        image: {\n          uploadImage: props.uploadImage,\n        },\n        gallery: {\n          uploadImage: props.uploadImage,\n        },\n        video: {\n          uploadVideo: props.uploadImage,\n        },\n        audio: {\n          uploadAudio: props.uploadImage,\n        },\n        placeholder: {\n          placeholder: t(\n            \"core.components.default_editor.extensions.placeholder.options.placeholder\"\n          ),\n        },\n        customExtensions: [...customExtensions, ...extensionsFromPlugins],\n      }),\n    ],\n    parseOptions: {\n      preserveWhitespace: true,\n    },\n    onUpdate: () => {\n      debounceOnUpdate();\n    },\n    onCreate() {\n      isInitialized.value = true;\n      nextTick(() => {\n        if (editor.value?.isEmpty && !props.title) {\n          editorTitleRef.value.focus();\n        } else {\n          editor.value?.commands.focus();\n        }\n      });\n    },\n  });\n});\n\nonBeforeUnmount(() => {\n  editor.value?.destroy();\n});\n\nconst handleSelectHeadingNode = (node: HeadingNode) => {\n  selectedHeadingNode.value = node;\n  document.getElementById(node.id)?.scrollIntoView({ behavior: \"smooth\" });\n};\n\nwatch(\n  () => props.raw,\n  () => {\n    if (props.raw !== editor.value?.getHTML()) {\n      editor.value?.commands.setContent(props.raw);\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\n// fixme: temporary solution\nconst currentLocale = i18n.global.locale.value as\n  | \"zh-CN\"\n  | \"en\"\n  | \"zh\"\n  | \"en-US\";\n\nfunction onTitleInput(event: Event) {\n  emit(\"update:title\", (event.target as HTMLInputElement).value);\n}\n\nfunction handleFocusEditor(event) {\n  if (event.isComposing) {\n    return;\n  }\n  editor.value?.commands.focus(\"start\");\n}\n\n// Cover\nconst coverSelectorModalVisible = ref(false);\n\nfunction onCoverSelect(attachments: AttachmentLike[]) {\n  const attachment = attachments[0];\n  if (!attachment) {\n    return;\n  }\n  emit(\"update:cover\", utils.attachment.getUrl(attachment));\n}\n\nconst { onChange: onCoverInputChange, open: openCoverInputDialog } =\n  useFileDialog({\n    accept: \"image/*\", // Set to accept only image files\n    multiple: false,\n  });\n\nconst uploadProgress = ref(0);\n\nonCoverInputChange((files) => {\n  const file = files?.[0];\n  if (!file) {\n    return;\n  }\n  props\n    .uploadImage?.(file, {\n      onUploadProgress: (progress) => {\n        uploadProgress.value = Math.round(\n          (progress.loaded * 100) / (progress.total || 1)\n        );\n      },\n    })\n    .then((attachment) => {\n      emit(\"update:cover\", attachment.status?.permalink);\n    })\n    .catch((e: Error) => {\n      Toast.error(\n        t(\"core.components.default_editor.cover.toast.upload_failed\", {\n          message: e.message,\n        })\n      );\n    })\n    .finally(() => {\n      uploadProgress.value = 0;\n    });\n});\n\nconst expandedCover = ref(false);\n</script>\n\n<template>\n  <VLoading v-if=\"!isInitialized\" />\n  <div v-else>\n    <AttachmentSelectorModal\n      v-if=\"attachmentSelectorModalVisible\"\n      @select=\"onAttachmentSelect\"\n      @close=\"attachmentSelectorModalVisible = false\"\n    />\n    <!-- For cover image -->\n    <AttachmentSelectorModal\n      v-if=\"coverSelectorModalVisible\"\n      :min=\"1\"\n      :max=\"1\"\n      :accepts=\"['image/*']\"\n      @select=\"onCoverSelect\"\n      @close=\"coverSelectorModalVisible = false\"\n    />\n    <RichTextEditor v-if=\"editor\" :editor=\"editor\" :locale=\"currentLocale\">\n      <template #content>\n        <div class=\"group\">\n          <div\n            v-if=\"cover || uploadProgress\"\n            class=\"group/cover aspect-w-16 overflow-hidden rounded-lg transition-all\"\n            :class=\"{\n              'aspect-h-9': expandedCover,\n              'aspect-h-4': !expandedCover,\n            }\"\n          >\n            <img\n              v-if=\"cover\"\n              :src=\"\n                utils.attachment.getThumbnailUrl(\n                  cover,\n                  GetThumbnailByUriSizeEnum.Xl\n                )\n              \"\n              class=\"size-full object-cover\"\n            />\n            <div\n              v-if=\"uploadProgress\"\n              class=\"flex flex-col items-center justify-center bg-black/50 text-white\"\n            >\n              <VLoading class=\"!py-3\" />\n              <span class=\"text-sm\">{{ uploadProgress }}%</span>\n            </div>\n\n            <div\n              class=\"top-0 flex h-12 items-center justify-end gap-2 bg-gradient-to-b from-gray-300 to-transparent px-2 opacity-0 transition-all group-hover/cover:opacity-100\"\n            >\n              <VButton size=\"sm\" ghost @click=\"expandedCover = !expandedCover\">\n                <template #icon>\n                  <MingcuteUnfoldVerticalLine v-if=\"!expandedCover\" />\n                  <MingcuteFoldVerticalLine v-else />\n                </template>\n                {{\n                  expandedCover\n                    ? $t(\"core.common.buttons.fold\")\n                    : $t(\"core.common.buttons.unfold\")\n                }}\n              </VButton>\n              <HasPermission\n                :permissions=\"[\n                  'system:attachments:view',\n                  'uc:attachments:manage',\n                ]\"\n              >\n                <VDropdown>\n                  <VButton type=\"secondary\" size=\"sm\">\n                    <template #icon>\n                      <IconExchange />\n                    </template>\n                    {{\n                      $t(\"core.components.default_editor.cover.options.change\")\n                    }}\n                  </VButton>\n                  <template #popper>\n                    <HasPermission :permissions=\"['uc:attachments:manage']\">\n                      <VDropdownItem @click=\"openCoverInputDialog()\">\n                        {{\n                          $t(\n                            \"core.components.default_editor.cover.options.upload\"\n                          )\n                        }}\n                      </VDropdownItem>\n                    </HasPermission>\n                    <VDropdownItem @click=\"coverSelectorModalVisible = true\">\n                      {{\n                        $t(\n                          \"core.components.default_editor.cover.options.attachment\"\n                        )\n                      }}\n                    </VDropdownItem>\n                    <VDropdownItem\n                      type=\"danger\"\n                      @click=\"emit('update:cover', undefined)\"\n                    >\n                      {{ $t(\"core.common.buttons.delete\") }}\n                    </VDropdownItem>\n                  </template>\n                </VDropdown>\n              </HasPermission>\n            </div>\n          </div>\n          <HasPermission\n            :permissions=\"['system:attachments:view', 'uc:attachments:manage']\"\n          >\n            <div\n              class=\"mt-2 opacity-0\"\n              :class=\"{\n                'group-hover:opacity-100': !cover,\n                'pointer-events-none': cover,\n              }\"\n            >\n              <VDropdown class=\"!inline-flex\">\n                <VButton size=\"xs\">\n                  <template #icon>\n                    <IconImageAddLine />\n                  </template>\n                  {{ $t(\"core.components.default_editor.cover.options.add\") }}\n                </VButton>\n                <template #popper>\n                  <HasPermission :permissions=\"['uc:attachments:manage']\">\n                    <VDropdownItem @click=\"openCoverInputDialog()\">\n                      {{\n                        $t(\n                          \"core.components.default_editor.cover.options.upload\"\n                        )\n                      }}\n                    </VDropdownItem>\n                  </HasPermission>\n                  <VDropdownItem @click=\"coverSelectorModalVisible = true\">\n                    {{\n                      $t(\n                        \"core.components.default_editor.cover.options.attachment\"\n                      )\n                    }}\n                  </VDropdownItem>\n                </template>\n              </VDropdown>\n            </div>\n          </HasPermission>\n          <input\n            ref=\"editorTitleRef\"\n            :value=\"title\"\n            type=\"text\"\n            :placeholder=\"\n              $t('core.components.default_editor.title_placeholder')\n            \"\n            class=\"w-full border-x-0 !border-b border-t-0 !border-solid !border-gray-100 p-0 !py-2 text-4xl font-semibold leading-none placeholder:text-gray-300\"\n            @input=\"onTitleInput\"\n            @keydown.enter=\"handleFocusEditor\"\n          />\n          <slot name=\"content\" />\n        </div>\n      </template>\n      <template v-if=\"showSidebar\" #extra>\n        <OverlayScrollbarsComponent\n          element=\"div\"\n          :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n          class=\"h-full border-l bg-white p-2\"\n          defer\n        >\n          <VTabs v-model:active-id=\"extraActiveId\" type=\"outline\">\n            <VTabItem\n              id=\"toc\"\n              :label=\"$t('core.components.default_editor.tabs.toc.title')\"\n            >\n              <ul v-if=\"headingNodes?.length\" class=\"space-y-1\">\n                <li\n                  v-for=\"(node, index) in headingNodes\"\n                  :key=\"index\"\n                  :class=\"[\n                    { 'bg-gray-100': node.id === selectedHeadingNode?.id },\n                  ]\"\n                  class=\"group cursor-pointer truncate rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900\"\n                  @click=\"handleSelectHeadingNode(node)\"\n                >\n                  <div\n                    :style=\"{\n                      paddingLeft: `${(node.level - 1) * 0.8}rem`,\n                    }\"\n                    class=\"flex items-center gap-2\"\n                  >\n                    <component\n                      :is=\"headingIcons[node.level]\"\n                      class=\"h-4 w-4 rounded-sm bg-gray-100 p-0.5 group-hover:bg-white\"\n                      :class=\"[\n                        {\n                          '!bg-white': node.id === selectedHeadingNode?.id,\n                        },\n                      ]\"\n                    />\n                    <span class=\"flex-1 truncate\">{{ node.text }}</span>\n                  </div>\n                </li>\n              </ul>\n              <div v-else class=\"flex flex-col items-center py-10\">\n                <span class=\"text-sm text-gray-600\">\n                  {{ $t(\"core.components.default_editor.tabs.toc.empty\") }}\n                </span>\n              </div>\n            </VTabItem>\n            <VTabItem\n              id=\"information\"\n              :label=\"$t('core.components.default_editor.tabs.detail.title')\"\n            >\n              <div class=\"flex flex-col gap-2\">\n                <div class=\"grid grid-cols-2 gap-2\">\n                  <div\n                    class=\"group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all\"\n                  >\n                    <div class=\"flex items-center justify-between\">\n                      <div\n                        class=\"text-sm text-gray-500 group-hover:text-gray-900\"\n                      >\n                        {{\n                          $t(\n                            \"core.components.default_editor.tabs.detail.fields.character_count\"\n                          )\n                        }}\n                      </div>\n                      <div class=\"rounded bg-gray-200 p-0.5\">\n                        <IconCharacterRecognition\n                          class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                        />\n                      </div>\n                    </div>\n                    <div class=\"text-base font-medium text-gray-900\">\n                      {{ editor.storage.characterCount.characters() }}\n                    </div>\n                  </div>\n                  <div\n                    class=\"group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all\"\n                  >\n                    <div class=\"flex items-center justify-between\">\n                      <div\n                        class=\"text-sm text-gray-500 group-hover:text-gray-900\"\n                      >\n                        {{\n                          $t(\n                            \"core.components.default_editor.tabs.detail.fields.word_count\"\n                          )\n                        }}\n                      </div>\n                      <div class=\"rounded bg-gray-200 p-0.5\">\n                        <IconCharacterRecognition\n                          class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                        />\n                      </div>\n                    </div>\n                    <div class=\"text-base font-medium text-gray-900\">\n                      {{ editor.storage.characterCount.words() }}\n                    </div>\n                  </div>\n                </div>\n\n                <div v-if=\"publishTime\" class=\"grid grid-cols-1 gap-2\">\n                  <div\n                    class=\"group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all\"\n                  >\n                    <div class=\"flex items-center justify-between\">\n                      <div\n                        class=\"text-sm text-gray-500 group-hover:text-gray-900\"\n                      >\n                        {{\n                          $t(\n                            \"core.components.default_editor.tabs.detail.fields.publish_time\"\n                          )\n                        }}\n                      </div>\n                      <div class=\"rounded bg-gray-200 p-0.5\">\n                        <IconCalendar\n                          class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                        />\n                      </div>\n                    </div>\n                    <div class=\"text-base font-medium text-gray-900\">\n                      {{\n                        utils.date.format(publishTime) ||\n                        $t(\n                          \"core.components.default_editor.tabs.detail.fields.draft\"\n                        )\n                      }}\n                    </div>\n                  </div>\n                </div>\n                <div v-if=\"owner\" class=\"grid grid-cols-1 gap-2\">\n                  <div\n                    class=\"group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all\"\n                  >\n                    <div class=\"flex items-center justify-between\">\n                      <div\n                        class=\"text-sm text-gray-500 group-hover:text-gray-900\"\n                      >\n                        {{\n                          $t(\n                            \"core.components.default_editor.tabs.detail.fields.owner\"\n                          )\n                        }}\n                      </div>\n                      <div class=\"rounded bg-gray-200 p-0.5\">\n                        <IconUserFollow\n                          class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                        />\n                      </div>\n                    </div>\n                    <div class=\"text-base font-medium text-gray-900\">\n                      {{ owner }}\n                    </div>\n                  </div>\n                </div>\n                <div v-if=\"permalink\" class=\"grid grid-cols-1 gap-2\">\n                  <div\n                    class=\"group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all\"\n                  >\n                    <div class=\"flex items-center justify-between\">\n                      <div\n                        class=\"text-sm text-gray-500 group-hover:text-gray-900\"\n                      >\n                        {{\n                          $t(\n                            \"core.components.default_editor.tabs.detail.fields.permalink\"\n                          )\n                        }}\n                      </div>\n                      <div class=\"rounded bg-gray-200 p-0.5\">\n                        <IconLink\n                          class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                        />\n                      </div>\n                    </div>\n                    <div>\n                      <a\n                        :href=\"permalink\"\n                        :title=\"permalink\"\n                        target=\"_blank\"\n                        class=\"text-sm text-gray-900 hover:text-blue-600\"\n                      >\n                        {{ permalink }}\n                      </a>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </VTabItem>\n          </VTabs>\n        </OverlayScrollbarsComponent>\n      </template>\n    </RichTextEditor>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/entity/EntityDropdownItems.vue",
    "content": "<script setup lang=\"ts\" generic=\"T\">\nimport { VDropdown } from \"@halo-dev/components\";\nimport { utils, type OperationItem } from \"@halo-dev/ui-shared\";\n\nconst props = withDefaults(\n  defineProps<{\n    dropdownItems: OperationItem<T>[];\n    item?: T;\n  }>(),\n  {\n    item: undefined,\n  }\n);\n\nfunction action(dropdownItem: OperationItem<T>) {\n  dropdownItem.action?.(props.item);\n}\n</script>\n\n<template>\n  <template v-for=\"(dropdownItem, index) in dropdownItems\">\n    <template\n      v-if=\"\n        !dropdownItem.hidden &&\n        utils.permission.has(dropdownItem.permissions || [])\n      \"\n    >\n      <VDropdown\n        v-if=\"dropdownItem.children?.length\"\n        :key=\"`dropdown-children-items-${index}`\"\n        :triggers=\"['click']\"\n      >\n        <component\n          :is=\"dropdownItem.component\"\n          v-bind=\"dropdownItem.props\"\n          @click=\"action(dropdownItem)\"\n        >\n          {{ dropdownItem.label }}\n        </component>\n        <template #popper>\n          <template v-for=\"(childItem, childIndex) in dropdownItem.children\">\n            <component\n              :is=\"childItem.component\"\n              v-if=\"\n                !childItem.hidden &&\n                utils.permission.has(childItem.permissions || [])\n              \"\n              v-bind=\"childItem.props\"\n              :key=\"`dropdown-child-item-${childIndex}`\"\n              @click=\"action(childItem)\"\n            >\n              {{ childItem.label }}\n            </component>\n          </template>\n        </template>\n      </VDropdown>\n      <component\n        :is=\"dropdownItem.component\"\n        v-else\n        v-bind=\"dropdownItem.props\"\n        :key=\"`dropdown-item-${index}`\"\n        @click=\"action(dropdownItem)\"\n      >\n        {{ dropdownItem.label }}\n      </component>\n    </template>\n  </template>\n</template>\n"
  },
  {
    "path": "ui/src/components/entity-fields/EntityFieldItems.vue",
    "content": "<script lang=\"ts\" setup>\nimport { utils, type EntityFieldItem } from \"@halo-dev/ui-shared\";\n\nwithDefaults(\n  defineProps<{\n    fields: EntityFieldItem[];\n  }>(),\n  {}\n);\n</script>\n\n<template>\n  <template\n    v-for=\"(field, index) in fields\"\n    :key=\"`${field.position}-${index}`\"\n  >\n    <component\n      :is=\"field.component\"\n      v-bind=\"field.props\"\n      v-if=\"!field.hidden && utils.permission.has(field.permissions || [])\"\n    />\n  </template>\n</template>\n"
  },
  {
    "path": "ui/src/components/entity-fields/StatusDotField.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  VEntityField,\n  VStatusDot,\n  type StatusDotState,\n} from \"@halo-dev/components\";\n\nwithDefaults(\n  defineProps<{\n    tooltip?: string;\n    state?: StatusDotState;\n    animate?: boolean;\n    text?: string;\n  }>(),\n  {\n    tooltip: undefined,\n    state: \"default\",\n    animate: false,\n    text: undefined,\n  }\n);\n</script>\n\n<template>\n  <VEntityField>\n    <template #description>\n      <VStatusDot\n        v-tooltip=\"tooltip\"\n        :state=\"state\"\n        :animate=\"animate\"\n        :text=\"text\"\n      />\n    </template>\n  </VEntityField>\n</template>\n"
  },
  {
    "path": "ui/src/components/filter/CategoryFilterDropdown.vue",
    "content": "<script lang=\"ts\" setup>\nimport { usePostCategory } from \"@console/modules/contents/posts/categories/composables/use-post-category\";\nimport type { Category } from \"@halo-dev/api-client\";\nimport {\n  IconArrowDown,\n  VDropdown,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n} from \"@halo-dev/components\";\nimport Fuse from \"fuse.js\";\nimport { computed, ref, watch } from \"vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    label: string;\n    modelValue?: string;\n  }>(),\n  {\n    modelValue: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:modelValue\", value?: string): void;\n}>();\n\nconst { categories } = usePostCategory();\n\nconst dropdown = ref();\n\nconst handleSelect = (category: Category) => {\n  if (category.metadata.name === props.modelValue) {\n    emit(\"update:modelValue\", undefined);\n  } else {\n    emit(\"update:modelValue\", category.metadata.name);\n  }\n\n  dropdown.value.hide();\n};\n\nfunction onDropdownShow() {\n  setTimeout(() => {\n    setFocus(\"categoryFilterDropdownInput\");\n  }, 200);\n}\n\n// search\nconst keyword = ref(\"\");\n\nlet fuse: Fuse<Category> | undefined = undefined;\n\nwatch(\n  () => categories.value,\n  () => {\n    fuse = new Fuse(categories.value || [], {\n      keys: [\"spec.displayName\", \"metadata.name\"],\n      useExtendedSearch: true,\n      threshold: 0.2,\n    });\n  },\n  {\n    immediate: true,\n  }\n);\n\nconst searchResults = computed(() => {\n  if (!fuse || !keyword.value) {\n    return categories.value;\n  }\n\n  return fuse?.search(keyword.value).map((item) => item.item);\n});\n\nconst selectedCategory = computed(() => {\n  return categories.value?.find(\n    (category) => category.metadata.name === props.modelValue\n  );\n});\n</script>\n\n<template>\n  <VDropdown\n    ref=\"dropdown\"\n    popper-class=\"[&_.v-popper\\_\\_inner]:!p-0\"\n    @show=\"onDropdownShow\"\n  >\n    <div\n      class=\"flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black\"\n      :class=\"{ 'font-semibold text-gray-700': modelValue !== undefined }\"\n    >\n      <span v-if=\"!selectedCategory\" class=\"mr-0.5\">\n        {{ label }}\n      </span>\n      <span v-else class=\"mr-0.5\">\n        {{ label }}：{{ selectedCategory.spec.displayName }}\n      </span>\n      <span>\n        <IconArrowDown />\n      </span>\n    </div>\n    <template #popper>\n      <div class=\"h-96 w-80\">\n        <div class=\"border-b border-b-gray-100 bg-white p-4\">\n          <FormKit\n            id=\"categoryFilterDropdownInput\"\n            v-model=\"keyword\"\n            :placeholder=\"$t('core.common.placeholder.search')\"\n            type=\"text\"\n          ></FormKit>\n        </div>\n        <div>\n          <VEntityContainer>\n            <VEntity\n              v-for=\"category in searchResults\"\n              :key=\"category.metadata.name\"\n              :is-selected=\"modelValue === category.metadata.name\"\n              @click=\"handleSelect(category)\"\n            >\n              <template #start>\n                <VEntityField\n                  :title=\"category.spec.displayName\"\n                  :description=\"category.status?.permalink\"\n                />\n              </template>\n              <template #end>\n                <VEntityField\n                  :description=\"\n                    $t('core.common.fields.post_count', {\n                      count: category.status?.postCount || 0,\n                    })\n                  \"\n                />\n              </template>\n            </VEntity>\n          </VEntityContainer>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/src/components/filter/FilterCleanButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconDeleteBin } from \"@halo-dev/components\";\n</script>\n\n<template>\n  <div\n    class=\"group inline-flex cursor-pointer items-center justify-center rounded-full bg-primary px-1 py-1 opacity-80 ring-1 ring-primary transition-all duration-300 hover:opacity-100 hover:shadow-sm hover:ring-opacity-50\"\n  >\n    <div class=\"h-4 w-4 rounded-full transition-all\">\n      <IconDeleteBin\n        class=\"h-full w-full text-gray-200 transition-all group-hover:text-white\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/filter/FilterDropdown.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconArrowDown, VDropdown, VDropdownItem } from \"@halo-dev/components\";\nimport { computed } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    items: {\n      label: string;\n      value?: string | boolean | number;\n    }[];\n    label: string;\n    modelValue?: string | boolean | number;\n  }>(),\n  {\n    modelValue: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (\n    event: \"update:modelValue\",\n    modelValue: string | boolean | number | undefined\n  ): void;\n}>();\n\nconst selectedItem = computed(() => {\n  return props.items.find((item) => item.value === props.modelValue);\n});\n\nfunction handleSelect(item: {\n  label: string;\n  value?: string | boolean | number;\n}) {\n  if (item.value === props.modelValue) {\n    emit(\"update:modelValue\", undefined);\n    return;\n  }\n  emit(\"update:modelValue\", item.value);\n}\n</script>\n\n<template>\n  <VDropdown>\n    <div\n      class=\"flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black\"\n      :class=\"{ 'font-semibold text-gray-700': modelValue !== undefined }\"\n    >\n      <span v-if=\"!selectedItem\" class=\"mr-0.5\">\n        {{ label }}\n      </span>\n      <span v-else class=\"mr-0.5\"> {{ label }}：{{ selectedItem.label }} </span>\n      <span>\n        <IconArrowDown />\n      </span>\n    </div>\n    <template #popper>\n      <VDropdownItem\n        v-for=\"(item, index) in items\"\n        :key=\"index\"\n        :selected=\"item.value === modelValue\"\n        @click=\"handleSelect(item)\"\n      >\n        {{ item.label }}\n      </VDropdownItem>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/src/components/filter/FilterTag.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconCloseCircle } from \"@halo-dev/components\";\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n</script>\n\n<template>\n  <div\n    class=\"group inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-primary px-2 py-1 ring-1 ring-primary transition-all duration-300 hover:shadow-sm hover:ring-opacity-50\"\n  >\n    <span v-if=\"$slots.default\" class=\"text-xs text-white transition-all\">\n      <slot />\n    </span>\n    <div\n      class=\"h-4 w-4 rounded-full ring-white transition-all group-hover:ring-1\"\n      @click=\"emit('close')\"\n    >\n      <IconCloseCircle\n        class=\"h-4 w-4 text-gray-200 transition-all group-hover:text-white\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/filter/TagFilterDropdown.vue",
    "content": "<script lang=\"ts\" setup>\nimport PostTag from \"@console/modules/contents/posts/tags/components/PostTag.vue\";\nimport { useAllPostTagsQuery } from \"@console/modules/contents/posts/tags/composables/use-post-tag\";\nimport type { Tag } from \"@halo-dev/api-client\";\nimport {\n  IconArrowDown,\n  VDropdown,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n} from \"@halo-dev/components\";\nimport Fuse from \"fuse.js\";\nimport { computed, ref, watch } from \"vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    label: string;\n    modelValue?: string;\n  }>(),\n  {\n    modelValue: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:modelValue\", value?: string): void;\n}>();\n\nconst { data: tags } = useAllPostTagsQuery();\n\nconst dropdown = ref();\n\nconst handleSelect = (tag: Tag) => {\n  if (tag.metadata.name === props.modelValue) {\n    emit(\"update:modelValue\", undefined);\n  } else {\n    emit(\"update:modelValue\", tag.metadata.name);\n  }\n\n  dropdown.value.hide();\n};\n\nfunction onDropdownShow() {\n  setTimeout(() => {\n    setFocus(\"tagFilterDropdownInput\");\n  }, 200);\n}\n\n// search\nconst keyword = ref(\"\");\n\nlet fuse: Fuse<Tag> | undefined = undefined;\n\nwatch(\n  () => tags.value,\n  () => {\n    fuse = new Fuse(tags.value || [], {\n      keys: [\"spec.displayName\", \"metadata.name\", \"spec.email\"],\n      useExtendedSearch: true,\n      threshold: 0.2,\n    });\n  },\n  {\n    immediate: true,\n  }\n);\n\nconst searchResults = computed(() => {\n  if (!fuse || !keyword.value) {\n    return tags.value;\n  }\n\n  return fuse?.search(keyword.value).map((item) => item.item);\n});\n\nconst selectedTag = computed(() => {\n  return tags.value?.find((tag) => tag.metadata.name === props.modelValue);\n});\n</script>\n\n<template>\n  <VDropdown\n    ref=\"dropdown\"\n    popper-class=\"[&_.v-popper\\_\\_inner]:!p-0\"\n    @show=\"onDropdownShow\"\n  >\n    <div\n      class=\"flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black\"\n      :class=\"{ 'font-semibold text-gray-700': modelValue !== undefined }\"\n    >\n      <span v-if=\"!selectedTag\" class=\"mr-0.5\">\n        {{ label }}\n      </span>\n      <span v-else class=\"mr-0.5\">\n        {{ label }}：{{ selectedTag.spec.displayName }}\n      </span>\n      <span>\n        <IconArrowDown />\n      </span>\n    </div>\n    <template #popper>\n      <div class=\"h-96 w-80\">\n        <div class=\"border-b border-b-gray-100 bg-white p-4\">\n          <FormKit\n            id=\"tagFilterDropdownInput\"\n            v-model=\"keyword\"\n            :placeholder=\"$t('core.common.placeholder.search')\"\n            type=\"text\"\n          ></FormKit>\n        </div>\n        <div>\n          <VEntityContainer>\n            <VEntity\n              v-for=\"tag in searchResults\"\n              :key=\"tag.metadata.name\"\n              :is-selected=\"modelValue === tag.metadata.name\"\n              @click=\"handleSelect(tag)\"\n            >\n              <template #start>\n                <VEntityField :description=\"tag.status?.permalink\">\n                  <template #title>\n                    <PostTag :tag=\"tag\" />\n                  </template>\n                </VEntityField>\n              </template>\n              <template #end>\n                <VEntityField\n                  :description=\"\n                    $t('core.common.fields.post_count', {\n                      count: tag.status?.postCount || 0,\n                    })\n                  \"\n                />\n              </template>\n            </VEntity>\n          </VEntityContainer>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/src/components/filter/UserFilterDropdown.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  consoleApiClient,\n  coreApiClient,\n  type User,\n} from \"@halo-dev/api-client\";\nimport {\n  IconArrowDown,\n  VAvatar,\n  VDropdown,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { refDebounced } from \"@vueuse/shared\";\nimport { ref, toRefs } from \"vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst props = withDefaults(\n  defineProps<{\n    label: string;\n    modelValue?: string;\n  }>(),\n  {\n    modelValue: undefined,\n  }\n);\n\nconst { modelValue } = toRefs(props);\n\nconst emit = defineEmits<{\n  (event: \"update:modelValue\", value?: string): void;\n}>();\n\nconst keyword = ref(\"\");\nconst debouncedKeyword = refDebounced(keyword, 300);\n\nconst { data: selectedUser } = useQuery({\n  queryKey: [\"core:users:by-name\", modelValue],\n  queryFn: async () => {\n    if (!modelValue.value) {\n      return null;\n    }\n\n    const { data } = await coreApiClient.user.getUser({\n      name: modelValue.value,\n    });\n\n    return data;\n  },\n  cacheTime: 0,\n});\n\nconst { data: users } = useQuery({\n  queryKey: [\"core:users\", debouncedKeyword],\n  queryFn: async () => {\n    const { data } = await consoleApiClient.user.listUsers({\n      fieldSelector: [`name!=anonymousUser`],\n      keyword: debouncedKeyword.value,\n      page: 1,\n      size: 30,\n    });\n\n    const pureUsers = data?.items?.map((user) => user.user);\n\n    if (!pureUsers?.length) {\n      return [selectedUser.value].filter(Boolean) as User[];\n    }\n\n    if (selectedUser.value) {\n      return [\n        selectedUser.value,\n        ...pureUsers.filter(\n          (user) => user.metadata.name !== selectedUser.value?.metadata.name\n        ),\n      ];\n    }\n\n    return pureUsers;\n  },\n  staleTime: 2000,\n});\n\nconst dropdown = ref();\n\nconst handleSelect = (user: User) => {\n  const { name } = user.metadata || {};\n\n  if (name === props.modelValue) {\n    emit(\"update:modelValue\", undefined);\n  } else {\n    emit(\"update:modelValue\", name);\n  }\n\n  dropdown.value.hide();\n};\n\nfunction onDropdownShow() {\n  setTimeout(() => {\n    setFocus(\"userFilterDropdownInput\");\n  }, 200);\n}\n</script>\n\n<template>\n  <VDropdown\n    ref=\"dropdown\"\n    popper-class=\"[&_.v-popper\\_\\_inner]:!p-0\"\n    @show=\"onDropdownShow\"\n  >\n    <div\n      class=\"flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black\"\n      :class=\"{ 'font-semibold text-gray-700': modelValue !== undefined }\"\n    >\n      <span v-if=\"!selectedUser\" class=\"mr-0.5\">\n        {{ label }}\n      </span>\n      <span v-else class=\"mr-0.5\">\n        {{ label }}：{{ selectedUser.spec.displayName }}\n      </span>\n      <span>\n        <IconArrowDown />\n      </span>\n    </div>\n    <template #popper>\n      <div class=\"h-96 w-80\">\n        <div class=\"border-b border-b-gray-100 bg-white p-4\">\n          <FormKit\n            id=\"userFilterDropdownInput\"\n            v-model=\"keyword\"\n            :placeholder=\"$t('core.common.placeholder.search')\"\n            type=\"text\"\n          ></FormKit>\n        </div>\n        <div>\n          <VEntityContainer>\n            <VEntity\n              v-for=\"user in users\"\n              :key=\"user.metadata.name\"\n              :is-selected=\"modelValue === user.metadata.name\"\n              @click=\"handleSelect(user)\"\n            >\n              <template #start>\n                <VEntityField>\n                  <template #description>\n                    <VAvatar\n                      :key=\"user.metadata.name\"\n                      :alt=\"user.spec.displayName\"\n                      :src=\"user.spec.avatar\"\n                      size=\"md\"\n                    ></VAvatar>\n                  </template>\n                </VEntityField>\n                <VEntityField\n                  :title=\"user.spec.displayName\"\n                  :description=\"user.metadata.name\"\n                />\n              </template>\n            </VEntity>\n          </VEntityContainer>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/src/components/form/AnnotationsForm.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  reset,\n  submitForm,\n  type FormKitNode,\n  type FormKitSchemaDefinition,\n  type FormKitSchemaNode,\n} from \"@formkit/core\";\nimport { getValidationMessages } from \"@formkit/validation\";\nimport type { AnnotationSetting } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { IconArrowRight } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { computed, nextTick, onMounted, ref, watch } from \"vue\";\n\nfunction keyValidationRule(node: FormKitNode) {\n  const validAnnotations = [\n    ...Object.keys(annotations.value),\n    ...customAnnotationsState.value.map((item) => item.key),\n  ];\n  const count = validAnnotations.filter((item) => item === node.value);\n  return count.length < 2;\n}\n\nconst props = withDefaults(\n  defineProps<{\n    group: string;\n    kind: string;\n    value?: {\n      [key: string]: string;\n    } | null;\n    formData?: unknown;\n  }>(),\n  {\n    value: null,\n    formData: undefined,\n  }\n);\n\nconst annotationSettings = ref<AnnotationSetting[]>([] as AnnotationSetting[]);\n\nconst handleFetchAnnotationSettings = async () => {\n  try {\n    const { data } =\n      await ucApiClient.core.annotationSetting.listAvailableAnnotationSettings({\n        targetRef: `${[props.group, props.kind].join(\"/\")}`,\n      });\n    annotationSettings.value = data;\n  } catch (error) {\n    console.error(\"Failed to fetch annotation settings\", error);\n  }\n};\n\nconst specFormId = `${utils.id.uuid()}-specForm`;\nconst customFormId = `${utils.id.uuid()}-customForm`;\nconst annotations = ref<{\n  [key: string]: string;\n}>({});\nconst customAnnotationsState = ref<{ key: string; value: string }[]>([]);\n\nconst customAnnotations = computed(() => {\n  return customAnnotationsState.value.reduce(\n    (acc, cur) => {\n      acc[cur.key] = cur.value;\n      return acc;\n    },\n    {} as { [key: string]: string }\n  );\n});\n\nconst handleProcessCustomAnnotations = () => {\n  let formSchemas: FormKitSchemaNode[] = [];\n\n  annotationSettings.value.forEach((annotationSetting) => {\n    formSchemas = formSchemas.concat(\n      annotationSetting.spec?.formSchema as FormKitSchemaNode[]\n    );\n  });\n\n  customAnnotationsState.value = Object.entries(props.value || {})\n    .map(([key, value]) => {\n      const fromThemeSpec = formSchemas.some((item) => {\n        if (typeof item === \"object\" && \"$formkit\" in item) {\n          return item.name === key;\n        }\n        return false;\n      });\n      if (!fromThemeSpec) {\n        return {\n          key,\n          value,\n        };\n      }\n      return {\n        key: \"\",\n        value: \"\",\n      };\n    })\n    .filter((item) => item.key)\n    .filter(\n      (item, index, self) => self.findIndex((t) => t.key === item.key) === index\n    );\n\n  annotations.value = formSchemas\n    .map((item) => {\n      if (typeof item === \"object\" && \"$formkit\" in item) {\n        if (props.value && item.name in props.value) {\n          return {\n            key: item.name,\n            value: props.value[item.name],\n          };\n        } else {\n          return {\n            key: item.name,\n            value: item.value,\n          };\n        }\n      }\n    })\n    .filter(Boolean)\n    .reduce(\n      (acc, cur) => {\n        if (cur) {\n          acc[cur.key] = cur.value;\n        }\n        return acc;\n      },\n      {} as { [key: string]: string }\n    );\n};\n\nonMounted(async () => {\n  annotations.value = cloneDeep(props.value) || {};\n  await handleFetchAnnotationSettings();\n  handleProcessCustomAnnotations();\n});\n\nwatch(\n  () => props.value,\n  (value) => {\n    reset(specFormId);\n    reset(customFormId);\n    annotations.value = cloneDeep(props.value) || {};\n    if (value) {\n      handleProcessCustomAnnotations();\n    }\n  }\n);\n\n// submit\nconst specFormInvalid = ref(true);\nconst customFormInvalid = ref(true);\n\nconst handleSubmit = async () => {\n  if (annotationSettings.value.length) {\n    submitForm(specFormId);\n  } else {\n    specFormInvalid.value = false;\n  }\n  submitForm(customFormId);\n  await nextTick();\n};\n\nconst onSpecFormSubmitCheck = async (node?: FormKitNode) => {\n  if (!node) {\n    return;\n  }\n  const validations = getValidationMessages(node);\n  specFormInvalid.value = validations.size > 0;\n};\n\nconst onCustomFormSubmitCheck = async (node?: FormKitNode) => {\n  if (!node) {\n    return;\n  }\n  const validations = getValidationMessages(node);\n  customFormInvalid.value = validations.size > 0;\n};\n\ndefineExpose({\n  handleSubmit,\n  specFormInvalid,\n  customFormInvalid,\n  annotations,\n  customAnnotations,\n});\n\nconst showCustomForm = ref(false);\n\nfunction onCustomFormToggle(e: Event) {\n  showCustomForm.value = (e.target as HTMLDetailsElement).open;\n}\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-3 divide-y divide-gray-100\">\n    <FormKit\n      v-if=\"annotations && annotationSettings.length > 0\"\n      :id=\"specFormId\"\n      v-model=\"annotations\"\n      type=\"form\"\n      :preserve=\"true\"\n      @submit-invalid=\"onSpecFormSubmitCheck\"\n      @submit=\"specFormInvalid = false\"\n    >\n      <template v-for=\"annotationSetting in annotationSettings\">\n        <FormKitSchema\n          v-if=\"annotationSetting.spec?.formSchema\"\n          :key=\"annotationSetting.metadata.name\"\n          :schema=\"\n            annotationSetting.spec?.formSchema as FormKitSchemaDefinition\n          \"\n          :data=\"{\n            formData,\n          }\"\n        />\n      </template>\n    </FormKit>\n\n    <!-- @vue-ignore -->\n    <details\n      :open=\"showCustomForm\"\n      class=\"flex cursor-pointer flex-col space-y-4 py-4 transition-all first:pt-0\"\n      @toggle=\"onCustomFormToggle\"\n    >\n      <summary class=\"group flex items-center justify-between\">\n        <div class=\"block text-sm font-medium text-gray-700\">\n          {{\n            $t(\n              showCustomForm\n                ? \"core.components.annotations_form.buttons.collapse\"\n                : \"core.components.annotations_form.buttons.expand\"\n            )\n          }}\n        </div>\n        <div\n          class=\"-mr-1 inline-flex items-center justify-center rounded-full p-1 group-hover:bg-gray-100\"\n          :class=\"{ 'bg-gray-100': showCustomForm }\"\n        >\n          <IconArrowRight\n            :class=\"{ 'rotate-90 !text-gray-900': showCustomForm }\"\n            class=\"text-gray-600 transition-all\"\n          />\n        </div>\n      </summary>\n\n      <FormKit\n        v-if=\"annotations\"\n        :id=\"customFormId\"\n        type=\"form\"\n        :preserve=\"true\"\n        :form-class=\"`${annotationSettings.length ? 'py-4' : ''}`\"\n        @submit-invalid=\"onCustomFormSubmitCheck\"\n        @submit=\"customFormInvalid = false\"\n      >\n        <FormKit\n          v-model=\"customAnnotationsState\"\n          type=\"array\"\n          :label=\"$t('core.components.annotations_form.custom_fields.label')\"\n          :item-labels=\"[{ type: 'text', label: '$value.key' }]\"\n        >\n          <FormKit\n            type=\"text\"\n            label=\"Key\"\n            name=\"key\"\n            validation=\"required:trim|keyValidationRule\"\n            :validation-rules=\"{ keyValidationRule }\"\n            :validation-messages=\"{\n              keyValidationRule: $t(\n                'core.components.annotations_form.custom_fields.validation'\n              ),\n            }\"\n          ></FormKit>\n          <FormKit type=\"text\" label=\"Value\" name=\"value\" value=\"\"></FormKit>\n        </FormKit>\n      </FormKit>\n    </details>\n  </div>\n</template>\n\n<style scoped>\ndetails > summary {\n  list-style: none;\n}\n\n/** Hide the default marker **/\ndetails > summary::-webkit-details-marker {\n  display: none;\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/global-search/GlobalSearchModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useThemeStore } from \"@console/stores/theme\";\nimport {\n  consoleApiClient,\n  coreApiClient,\n  paginate,\n  type Attachment,\n  type AttachmentV1alpha1ApiListAttachmentRequest,\n  type Category,\n  type CategoryV1alpha1ApiListCategoryRequest,\n  type Plugin,\n  type PluginV1alpha1ApiListPluginRequest,\n  type Post,\n  type PostV1alpha1ApiListPostRequest,\n  type SinglePage,\n  type SinglePageV1alpha1ApiListSinglePageRequest,\n  type Tag,\n  type TagV1alpha1ApiListTagRequest,\n  type User,\n  type UserV1alpha1ApiListUserRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  IconBookRead,\n  IconFolder,\n  IconLink,\n  IconPages,\n  IconPalette,\n  IconSettings,\n  IconUserSettings,\n  VModal,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useEventListener } from \"@vueuse/core\";\nimport Fuse from \"fuse.js\";\nimport { storeToRefs } from \"pinia\";\nimport { computed, markRaw, onMounted, ref, watch, type Component } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRoute, useRouter, type RouteLocationRaw } from \"vue-router\";\n\nconst router = useRouter();\nconst route = useRoute();\nconst { t } = useI18n();\n\nconst { activatedTheme } = storeToRefs(useThemeStore());\n\nconst emit = defineEmits<{\n  (e: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst globalSearchInput = ref<HTMLInputElement | null>(null);\nconst keyword = ref(\"\");\n\ninterface SearchableItem {\n  title: string;\n  icon: { component: Component } | { src: string };\n  group?: string;\n  route: RouteLocationRaw;\n}\n\nconst searchableItem: SearchableItem[] = [];\nconst selectedIndex = ref(0);\n\nconst fuse = new Fuse(searchableItem, {\n  keys: [\"title\", \"group\", \"route.path\", \"route.name\"],\n  useExtendedSearch: true,\n  threshold: 0.2,\n});\n\nconst searchResults = computed((): SearchableItem[] => {\n  return fuse\n    .search(keyword.value, {\n      limit: 20,\n    })\n    .map((result) => result.item);\n});\n\nconst handleBuildSearchIndex = () => {\n  fuse.remove(() => true);\n\n  const routes = router.getRoutes().filter((route) => {\n    return !!route.meta?.title && route.meta?.searchable;\n  });\n\n  routes.forEach((route) => {\n    fuse.add({\n      title: t(route.meta?.title as string),\n      icon: {\n        component: markRaw(IconLink),\n      },\n      group: t(\"core.components.global_search.groups.console\"),\n      route,\n    });\n  });\n\n  if (utils.permission.has([\"system:users:view\"])) {\n    paginate<UserV1alpha1ApiListUserRequest, User>(\n      (params) => coreApiClient.user.listUser(params),\n      {\n        labelSelector: [\"!halo.run/hidden-user\"],\n        size: 1000,\n      }\n    ).then((users) => {\n      for (const user of users) {\n        fuse.add({\n          title: user.spec.displayName,\n          icon: {\n            component: markRaw(IconUserSettings),\n          },\n          group: t(\"core.components.global_search.groups.user\"),\n          route: {\n            name: \"UserDetail\",\n            params: {\n              name: user.metadata.name,\n            },\n          },\n        });\n      }\n    });\n  }\n\n  if (utils.permission.has([\"system:plugins:view\"])) {\n    paginate<PluginV1alpha1ApiListPluginRequest, Plugin>(\n      (params) => coreApiClient.plugin.plugin.listPlugin(params),\n      {\n        size: 1000,\n      }\n    ).then((plugins) => {\n      for (const plugin of plugins) {\n        fuse.add({\n          title: plugin.spec.displayName as string,\n          icon: {\n            src: plugin.status?.logo as string,\n          },\n          group: t(\"core.components.global_search.groups.plugin\"),\n          route: {\n            name: \"PluginDetail\",\n            params: {\n              name: plugin.metadata.name,\n            },\n          },\n        });\n      }\n    });\n  }\n\n  if (utils.permission.has([\"system:posts:view\"])) {\n    paginate<PostV1alpha1ApiListPostRequest, Post>(\n      (params) => coreApiClient.content.post.listPost(params),\n      {\n        size: 1000,\n      }\n    ).then((posts) => {\n      for (const post of posts) {\n        fuse.add({\n          title: post.spec.title,\n          icon: {\n            component: markRaw(IconBookRead),\n          },\n          group: t(\"core.components.global_search.groups.post\"),\n          route: {\n            name: \"PostEditor\",\n            query: {\n              name: post.metadata.name,\n            },\n          },\n        });\n      }\n    });\n\n    paginate<CategoryV1alpha1ApiListCategoryRequest, Category>(\n      (params) => coreApiClient.content.category.listCategory(params),\n      {\n        size: 1000,\n        sort: [\"metadata.creationTimestamp,desc\"],\n      }\n    ).then((categories) => {\n      for (const category of categories) {\n        fuse.add({\n          title: category.spec.displayName,\n          icon: {\n            component: markRaw(IconBookRead),\n          },\n          group: t(\"core.components.global_search.groups.category\"),\n          route: {\n            name: \"Categories\",\n            query: {\n              name: category.metadata.name,\n            },\n          },\n        });\n      }\n    });\n\n    paginate<TagV1alpha1ApiListTagRequest, Tag>(\n      (params) => coreApiClient.content.tag.listTag(params),\n      {\n        size: 1000,\n        sort: [\"metadata.creationTimestamp,desc\"],\n      }\n    ).then((tags) => {\n      for (const tag of tags) {\n        fuse.add({\n          title: tag.spec.displayName,\n          icon: {\n            component: markRaw(IconBookRead),\n          },\n          group: t(\"core.components.global_search.groups.tag\"),\n          route: {\n            name: \"Tags\",\n            query: {\n              name: tag.metadata.name,\n            },\n          },\n        });\n      }\n    });\n  }\n\n  if (utils.permission.has([\"system:singlepages:view\"])) {\n    paginate<SinglePageV1alpha1ApiListSinglePageRequest, SinglePage>(\n      (params) => coreApiClient.content.singlePage.listSinglePage(params),\n      {\n        size: 1000,\n      }\n    ).then((singlePages) => {\n      for (const singlePage of singlePages) {\n        fuse.add({\n          title: singlePage.spec.title,\n          icon: {\n            component: markRaw(IconPages),\n          },\n          group: t(\"core.components.global_search.groups.page\"),\n          route: {\n            name: \"SinglePageEditor\",\n            query: {\n              name: singlePage.metadata.name,\n            },\n          },\n        });\n      }\n    });\n  }\n\n  if (utils.permission.has([\"system:attachments:view\"])) {\n    paginate<AttachmentV1alpha1ApiListAttachmentRequest, Attachment>(\n      (params) => coreApiClient.storage.attachment.listAttachment(params),\n      {\n        size: 1000,\n      }\n    ).then((attachments) => {\n      for (const attachment of attachments) {\n        fuse.add({\n          title: attachment.spec.displayName as string,\n          icon: {\n            component: markRaw(IconFolder),\n          },\n          group: t(\"core.components.global_search.groups.attachment\"),\n          route: {\n            name: \"Attachments\",\n            query: {\n              name: attachment.metadata.name,\n            },\n          },\n        });\n      }\n    });\n  }\n\n  if (\n    utils.permission.has(\n      [\"system:settings:view\", \"system:configmaps:view\"],\n      false\n    )\n  ) {\n    coreApiClient.setting.getSetting({ name: \"system\" }).then((response) => {\n      response.data.spec.forms.forEach((form) => {\n        fuse.add({\n          title: form.label as string,\n          icon: {\n            component: markRaw(IconSettings),\n          },\n          group: t(\"core.components.global_search.groups.setting\"),\n          route: {\n            name: \"SystemSetting\",\n            params: {\n              group: form.group,\n            },\n          },\n        });\n      });\n    });\n  }\n\n  if (utils.permission.has([\"system:themes:view\"])) {\n    consoleApiClient.theme.theme\n      .fetchThemeSetting({ name: \"-\" })\n      .then(({ data: themeSettings }) => {\n        themeSettings.spec.forms.forEach((form) => {\n          fuse.add({\n            title: [activatedTheme.value?.spec.displayName, form.label].join(\n              \" / \"\n            ),\n            icon: {\n              component: markRaw(IconPalette),\n            },\n            group: t(\"core.components.global_search.groups.theme_setting\"),\n            route: {\n              name: \"ThemeSetting\",\n              params: {\n                group: form.group,\n              },\n            },\n          });\n        });\n      });\n  }\n};\n\nconst handleKeydown = (e: KeyboardEvent) => {\n  const { key, ctrlKey } = e;\n\n  if (key === \"ArrowUp\" || (key === \"k\" && ctrlKey)) {\n    selectedIndex.value = Math.max(0, selectedIndex.value - 1);\n    e.preventDefault();\n  }\n\n  if (key === \"ArrowDown\" || (key === \"j\" && ctrlKey)) {\n    selectedIndex.value = Math.min(\n      searchResults.value.length - 1,\n      selectedIndex.value + 1\n    );\n    e.preventDefault();\n  }\n\n  if (key === \"Enter\") {\n    const searchResult = searchResults.value[selectedIndex.value];\n    handleRoute(searchResult);\n  }\n\n  if (key === \"Escape\") {\n    modal.value?.close();\n    e.preventDefault();\n  }\n};\n\nconst handleRoute = async (item: SearchableItem) => {\n  // if route has query params, need router.go(0)\n  if (typeof item.route !== \"string\" && \"query\" in item.route) {\n    if (\"name\" in item.route && route.name === item.route.name) {\n      await router.push(item.route);\n      router.go(0);\n      return;\n    }\n  }\n  router.push(item.route);\n  modal.value?.close();\n};\n\nwatch(\n  () => selectedIndex.value,\n  (index) => {\n    if (index > 0) {\n      document.getElementById(`search-item-${index}`)?.scrollIntoView();\n      return;\n    }\n    document.getElementById(\"search-input\")?.scrollIntoView();\n  }\n);\n\nwatch(\n  () => keyword.value,\n  () => {\n    selectedIndex.value = 0;\n  }\n);\n\nonMounted(() => {\n  handleBuildSearchIndex();\n\n  setTimeout(() => {\n    globalSearchInput.value?.focus();\n  }, 100);\n});\n\nuseEventListener(\"keydown\", handleKeydown);\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :mount-to-body=\"true\"\n    :width=\"650\"\n    :centered=\"false\"\n    :layer-closable=\"true\"\n    @close=\"emit('close')\"\n  >\n    <div id=\"search-input\" class=\"border-b border-gray-100 px-4 py-2.5\">\n      <input\n        ref=\"globalSearchInput\"\n        v-model=\"keyword\"\n        :placeholder=\"$t('core.components.global_search.placeholder')\"\n        class=\"w-full px-0 py-1 text-base outline-none\"\n        autocomplete=\"off\"\n        autocorrect=\"off\"\n        spellcheck=\"false\"\n      />\n    </div>\n    <div class=\"px-2 py-2.5\">\n      <div\n        v-if=\"!searchResults.length\"\n        class=\"flex items-center justify-center text-sm text-gray-500\"\n      >\n        <span>{{ $t(\"core.components.global_search.no_results\") }}</span>\n      </div>\n      <ul\n        v-if=\"searchResults.length > 0\"\n        class=\"box-border flex h-full w-full flex-col gap-1\"\n        role=\"list\"\n      >\n        <li\n          v-for=\"(item, itemIndex) in searchResults\"\n          :id=\"`search-item-${itemIndex}`\"\n          :key=\"itemIndex\"\n          @click=\"handleRoute(item)\"\n        >\n          <div\n            class=\"flex cursor-pointer items-center rounded-md px-2 py-2.5 hover:bg-gray-100\"\n            :class=\"{ 'bg-gray-100': selectedIndex === itemIndex }\"\n          >\n            <div class=\"inline-flex flex-1 items-center gap-3\">\n              <div class=\"h-5 w-5 rounded border p-0.5\">\n                <component\n                  :is=\"item.icon.component\"\n                  v-if=\"'component' in item.icon\"\n                  class=\"h-full w-full\"\n                />\n                <img\n                  v-if=\"'src' in item.icon\"\n                  :src=\"item.icon.src\"\n                  class=\"h-full w-full object-cover\"\n                />\n              </div>\n              <span class=\"text-sm font-medium\">{{ item.title }}</span>\n            </div>\n            <div class=\"flex-none flex-shrink-0 text-xs text-gray-500\">\n              {{ item.group }}\n            </div>\n          </div>\n        </li>\n      </ul>\n    </div>\n    <div class=\"border-t border-gray-100 px-4 py-2.5\">\n      <div class=\"flex items-center justify-end\">\n        <span class=\"mr-1 text-xs text-gray-600\">\n          {{ $t(\"core.components.global_search.buttons.select\") }}\n        </span>\n        <kbd\n          class=\"mr-1 w-5 rounded border p-0.5 text-center text-[10px] text-gray-600 shadow-sm\"\n        >\n          ↑\n        </kbd>\n        <kbd\n          class=\"mr-5 w-5 rounded border p-0.5 text-center text-[10px] text-gray-600 shadow-sm\"\n        >\n          ↓\n        </kbd>\n        <span class=\"mr-1 text-xs text-gray-600\">\n          {{ $t(\"core.common.buttons.confirm\") }}\n        </span>\n        <kbd\n          class=\"mr-5 rounded border p-0.5 text-[10px] text-gray-600 shadow-sm\"\n        >\n          Enter\n        </kbd>\n        <span class=\"mr-1 text-xs text-gray-600\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </span>\n        <kbd class=\"rounded border p-0.5 text-[10px] text-gray-600 shadow-sm\">\n          Esc\n        </kbd>\n      </div>\n    </div>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/src/components/icon/AttachmentFileTypeIcon.vue",
    "content": "<script lang=\"ts\" setup>\nimport { extname } from \"path-browserify\";\nimport { computed, defineAsyncComponent } from \"vue\";\n\nconst FileTypeIconsMap = {\n  // image\n  \".jpg\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-image\")\n  ),\n  \".jpeg\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-image\")\n  ),\n  \".png\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-image\")\n  ),\n  \".gif\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-image\")\n  ),\n  \".webp\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-image\")\n  ),\n  \".svg\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-image\")\n  ),\n\n  // document\n  \".docx\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-word\")\n  ),\n  \".pptx\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-powerpoint\")\n  ),\n  \".xlsx\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-excel\")\n  ),\n  \".psd\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-photoshop\")\n  ),\n  \".ai\": defineAsyncComponent(() => import(\"~icons/vscode-icons/file-type-ai\")),\n  \".txt\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-text\")\n  ),\n\n  // programming languages or frameworks\n  \".json\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-json\")\n  ),\n  \".html\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-html\")\n  ),\n  \".yaml\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-yaml\")\n  ),\n  \".xml\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-xml\")\n  ),\n  \".java\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-java\")\n  ),\n  \".jar\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-jar\")\n  ),\n  \".class\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-class\")\n  ),\n  \".js\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-js-official\")\n  ),\n  \".ts\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-typescript-official\")\n  ),\n  \".vue\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-vue\")\n  ),\n  \".go\": defineAsyncComponent(() => import(\"~icons/vscode-icons/file-type-go\")),\n  \".c\": defineAsyncComponent(() => import(\"~icons/vscode-icons/file-type-c\")),\n  \".cpp\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-cpp\")\n  ),\n  \".astro\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-astro\")\n  ),\n  \".bat\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-bat\")\n  ),\n  \".css\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-css\")\n  ),\n  \".db\": defineAsyncComponent(() => import(\"~icons/vscode-icons/file-type-db\")),\n  \".gradle\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-gradle\")\n  ),\n  \".md\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-markdown\")\n  ),\n  \".py\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-python\")\n  ),\n  \".sh\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-shell\")\n  ),\n  \".php\": defineAsyncComponent(\n    () => import(\"~icons/vscode-icons/file-type-php3\")\n  ),\n};\n\nconst props = withDefaults(\n  defineProps<{\n    fileName?: string;\n    displayExt?: boolean;\n    width?: number;\n    height?: number;\n  }>(),\n  {\n    fileName: undefined,\n    displayExt: true,\n    width: 10,\n    height: 10,\n  }\n);\n\nconst getExtname = computed(() => {\n  const ext = extname(props.fileName);\n  if (ext) return ext.toLowerCase();\n  return undefined;\n});\n\nconst getIcon = computed(() => {\n  const icon = FileTypeIconsMap[getExtname.value];\n  if (icon) return icon;\n  return defineAsyncComponent(() => import(\"~icons/vscode-icons/default-file\"));\n});\n\nconst iconClass = computed(() => {\n  return [`w-${props.width}`, `h-${props.height}`];\n});\n</script>\n<template>\n  <div class=\"flex h-full w-full flex-col items-center justify-center gap-1\">\n    <component :is=\"getIcon\" :class=\"iconClass\" />\n    <span\n      v-if=\"getExtname && displayExt\"\n      class=\"select-none font-sans text-xs text-gray-500\"\n    >\n      {{ getExtname }}\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/input/SearchInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport { getNode, reset } from \"@formkit/core\";\nimport { IconCloseCircle } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\n\nconst props = withDefaults(\n  defineProps<{\n    placeholder?: string;\n    modelValue: string;\n    sync?: boolean;\n  }>(),\n  {\n    placeholder: undefined,\n    sync: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:modelValue\", modelValue: string): void;\n}>();\n\nconst id = `search-input-${utils.id.uuid()}`;\n\nfunction handleReset() {\n  emit(\"update:modelValue\", \"\");\n  reset(id);\n}\n\nfunction onKeywordChange() {\n  const keywordNode = getNode(id);\n  if (keywordNode) {\n    emit(\"update:modelValue\", keywordNode._value as string);\n  }\n}\n\nfunction onInput(value?: string) {\n  if (props.sync) {\n    emit(\"update:modelValue\", value || \"\");\n  }\n}\n</script>\n\n<template>\n  <FormKit\n    :id=\"id\"\n    outer-class=\"!p-0 w-full sm:w-auto\"\n    :placeholder=\"placeholder || $t('core.common.placeholder.search')\"\n    type=\"text\"\n    name=\"keyword\"\n    :model-value=\"modelValue\"\n    ignore\n    @update:model-value=\"onInput\"\n    @keyup.enter=\"onKeywordChange\"\n  >\n    <template v-if=\"modelValue\" #suffix>\n      <div\n        class=\"group flex h-full cursor-pointer items-center bg-white px-2 transition-all hover:bg-gray-50\"\n        @click=\"handleReset\"\n      >\n        <IconCloseCircle\n          class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n        />\n      </div>\n    </template>\n  </FormKit>\n</template>\n"
  },
  {
    "path": "ui/src/components/menu/MenuLoading.vue",
    "content": "<script lang=\"ts\" setup></script>\n\n<template>\n  <div class=\"p-3\">\n    <ul v-for=\"y in 2\" :key=\"y\" class=\"mt-5 first:mt-0\">\n      <li\n        v-for=\"x in 5\"\n        :key=\"x\"\n        class=\"flex h-[36.8px] items-center rounded-base py-[0.4rem]\"\n      >\n        <div class=\"mr-3 size-5 animate-pulse rounded-lg bg-gray-200\"></div>\n        <div\n          class=\"h-5 animate-pulse rounded-lg bg-gray-200\"\n          :style=\"{\n            width: `${Math.random() * 50 + 10}%`,\n          }\"\n        ></div>\n      </li>\n    </ul>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/menu/RoutesMenu.tsx",
    "content": "import { VMenu, VMenuItem, VMenuLabel } from \"@halo-dev/components\";\nimport type { MenuGroupType, MenuItemType } from \"@halo-dev/ui-shared\";\nimport type { Component, PropType } from \"vue\";\nimport { computed, defineComponent } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { RouteLocationMatched } from \"vue-router\";\nimport { useRoute, useRouter } from \"vue-router\";\n\nconst RoutesMenu = defineComponent({\n  name: \"RoutesMenu\",\n  props: {\n    menus: {\n      type: Object as PropType<MenuGroupType[]>,\n      required: true,\n    },\n  },\n  emits: [\"select\"],\n  setup(props, { emit }) {\n    const route = useRoute();\n    const { push } = useRouter();\n\n    const openIds = computed(() => {\n      return route.matched.map((item: RouteLocationMatched) => item.path);\n    });\n\n    async function handleSelect(id: string) {\n      emit(\"select\", id);\n      await push(id);\n    }\n\n    function renderIcon(icon: Component | undefined) {\n      if (!icon) return undefined;\n\n      return <icon />;\n    }\n\n    const { t } = useI18n();\n\n    function renderItems(items: MenuItemType[] | undefined) {\n      return items?.map((item) => {\n        return (\n          <>\n            {item.children?.length ? (\n              <VMenuItem\n                key={item.path}\n                id={item.path}\n                title={t(item.name, item.name)}\n                v-slots={{\n                  icon: () => renderIcon(item.icon),\n                }}\n                onSelect={handleSelect}\n                active={openIds.value.includes(item.path)}\n              >\n                {renderItems(item.children)}\n              </VMenuItem>\n            ) : (\n              <VMenuItem\n                key={item.path}\n                id={item.path}\n                title={t(item.name, item.name)}\n                v-slots={{\n                  icon: () => renderIcon(item.icon),\n                }}\n                onSelect={handleSelect}\n                active={openIds.value.includes(item.path)}\n              />\n            )}\n          </>\n        );\n      });\n    }\n\n    return () => (\n      // @ts-ignore\n      <VMenu openIds={openIds.value}>\n        {props.menus?.map((menu: MenuGroupType) => {\n          return (\n            <>\n              {menu.name && <VMenuLabel>{t(menu.name, menu.name)}</VMenuLabel>}\n              {menu.items?.length && renderItems(menu.items)}\n            </>\n          );\n        })}\n      </VMenu>\n    );\n  },\n});\n\nexport { RoutesMenu };\n"
  },
  {
    "path": "ui/src/components/permission/HasPermission.vue",
    "content": "<script lang=\"ts\" setup>\nimport { utils } from \"@halo-dev/ui-shared\";\n\nwithDefaults(\n  defineProps<{\n    permissions: string[];\n    any?: boolean;\n  }>(),\n  {\n    any: true,\n  }\n);\n</script>\n\n<template>\n  <slot v-if=\"utils.permission.has(permissions, any)\" />\n</template>\n"
  },
  {
    "path": "ui/src/components/preview/UrlPreviewModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconComputer,\n  IconLink,\n  IconPhone,\n  IconTablet,\n  VLoading,\n  VModal,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport { computed, markRaw, ref, toRefs } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    title?: string;\n    url?: string;\n  }>(),\n  {\n    title: undefined,\n    url: \"\",\n  }\n);\n\nconst { url } = toRefs(props);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst mockDevices = [\n  {\n    id: \"desktop\",\n    icon: markRaw(IconComputer),\n  },\n  {\n    id: \"tablet\",\n    icon: markRaw(IconTablet),\n  },\n  {\n    id: \"phone\",\n    icon: markRaw(IconPhone),\n  },\n];\nconst deviceActiveId = ref(mockDevices[0].id);\n\nconst iframeClasses = computed(() => {\n  if (deviceActiveId.value === \"desktop\") {\n    return \"w-full h-full\";\n  }\n  if (deviceActiveId.value === \"tablet\") {\n    return \"w-2/3 h-2/3 ring-2 rounded ring-gray-300\";\n  }\n  return \"w-96 h-[50rem] ring-2 rounded ring-gray-300\";\n});\n</script>\n<template>\n  <VModal\n    :body-class=\"['!p-0']\"\n    fullscreen\n    :title=\"title\"\n    :layer-closable=\"true\"\n    @close=\"emit('close')\"\n  >\n    <template #center>\n      <!-- TODO: Reactor VTabbar component to support icon prop -->\n      <VTabbar\n        v-model:active-id=\"deviceActiveId\"\n        :items=\"mockDevices as any\"\n        type=\"outline\"\n      ></VTabbar>\n    </template>\n    <template #actions>\n      <slot name=\"actions\"></slot>\n      <span>\n        <a :href=\"url\" target=\"_blank\">\n          <IconLink />\n        </a>\n      </span>\n    </template>\n    <div class=\"flex h-full items-center justify-center\">\n      <VLoading v-if=\"!url\" />\n      <iframe\n        v-else\n        class=\"border-none transition-all duration-500\"\n        :class=\"iframeClasses\"\n        :src=\"url\"\n      ></iframe>\n    </div>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/src/components/sticky-block/StickyBlock.vue",
    "content": "<script setup lang=\"ts\">\nimport { useEventListener } from \"@vueuse/core\";\nimport { onMounted, ref } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    position?: \"top\" | \"bottom\";\n  }>(),\n  {\n    position: \"top\",\n  }\n);\n\nconst stickyBlock = ref<HTMLElement | null>(null);\nconst isSticky = ref(false);\n\nfunction computeSticky() {\n  if (!stickyBlock.value) return;\n  const rect = stickyBlock.value?.getBoundingClientRect();\n\n  if (props.position === \"top\") {\n    isSticky.value = rect.top <= 0;\n  } else {\n    isSticky.value = rect.bottom >= window.innerHeight;\n  }\n}\n\nonMounted(() => {\n  computeSticky();\n});\n\nuseEventListener(\"scroll\", computeSticky);\n</script>\n\n<template>\n  <div\n    ref=\"stickyBlock\"\n    :class=\"{ 'sticky-element': true, 'sticky-shadow': isSticky }\"\n  >\n    <slot />\n  </div>\n</template>\n\n<style>\n.sticky-element {\n  position: sticky;\n  bottom: 0;\n}\n\n.sticky-shadow {\n  box-shadow: 0px -5px 10px -5px rgba(0, 0, 0, 0.1);\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/upload/UppyUpload.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Toast } from \"@halo-dev/components\";\nimport type { Restrictions } from \"@uppy/core\";\nimport Uppy, { type SuccessResponse } from \"@uppy/core\";\nimport ImageEditor from \"@uppy/image-editor\";\nimport en_US from \"@uppy/locales/lib/en_US\";\nimport zh_CN from \"@uppy/locales/lib/zh_CN\";\nimport \"@uppy/core/dist/style.css\";\nimport \"@uppy/dashboard/dist/style.css\";\nimport zh_TW from \"@uppy/locales/lib/zh_TW\";\nimport \"@uppy/image-editor/dist/style.min.css\";\nimport { Dashboard } from \"@uppy/vue\";\nimport XHRUpload from \"@uppy/xhr-upload\";\nimport objectHash from \"object-hash\";\nimport { computed, h, onUnmounted, watchEffect } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport type { ProblemDetail } from \"@/setup/setupApiClient\";\nimport { createHTMLContentModal } from \"@/utils/modal\";\n\nconst props = withDefaults(\n  defineProps<{\n    restrictions?: Restrictions;\n    meta?: Record<string, unknown>;\n    autoProceed?: boolean;\n    allowedMetaFields?: string[];\n    endpoint: string;\n    name?: string;\n    note?: string;\n    method?: \"GET\" | \"POST\" | \"PUT\" | \"HEAD\" | \"get\" | \"post\" | \"put\" | \"head\";\n    disabled?: boolean;\n    width?: string;\n    height?: string;\n    doneButtonHandler?: () => void;\n  }>(),\n  {\n    restrictions: undefined,\n    meta: undefined,\n    autoProceed: false,\n    allowedMetaFields: undefined,\n    name: \"file\",\n    note: undefined,\n    method: \"post\",\n    disabled: false,\n    width: \"750px\",\n    height: \"550px\",\n    doneButtonHandler: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"uploaded\", response: SuccessResponse): void;\n  (event: \"error\", file, response): void;\n}>();\n\nconst locales = {\n  en: en_US,\n  zh: zh_CN,\n  \"en-US\": en_US,\n  \"zh-CN\": zh_CN,\n  \"zh-TW\": zh_TW,\n};\n\nconst uppy = computed(() => {\n  return new Uppy({\n    locale: locales[i18n.global.locale.value] || locales[\"zh-CN\"],\n    meta: props.meta,\n    restrictions: props.restrictions,\n    autoProceed: props.autoProceed,\n  })\n    .use(XHRUpload, {\n      endpoint: `${props.endpoint}`,\n      allowedMetaFields: props.allowedMetaFields,\n      withCredentials: true,\n      formData: true,\n      fieldName: props.name,\n      method: props.method,\n      limit: 5,\n      timeout: 0,\n      getResponseError: (responseText: string, response: unknown) => {\n        try {\n          const response = JSON.parse(responseText);\n          if (typeof response === \"object\" && response && response) {\n            const { title, detail } = (response || {}) as ProblemDetail;\n            const message = [title, detail].filter(Boolean).join(\": \");\n\n            if (message) {\n              Toast.error(message, { duration: 5000 });\n\n              return new Error(message);\n            }\n          }\n        } catch (_) {\n          const responseBody = response as XMLHttpRequest;\n          const { status, statusText } = responseBody;\n          const defaultMessage = [status, statusText].join(\": \");\n\n          // Catch error requests where the response is text/html,\n          // which usually comes from a reverse proxy or WAF\n          // fixme: Because there is no responseType in the response, we can only judge it in this way for now.\n          const parser = new DOMParser();\n          const doc = parser.parseFromString(\n            responseBody.response,\n            \"text/html\"\n          );\n\n          if (\n            Array.from(doc.body.childNodes).some((node) => node.nodeType === 1)\n          ) {\n            createHTMLContentModal({\n              uniqueId: objectHash(responseBody.response || \"\"),\n              title: responseBody.status.toString(),\n              width: 700,\n              height: \"calc(100vh - 20px)\",\n              centered: true,\n              content: h(\"iframe\", {\n                srcdoc: responseBody.response,\n                sandbox: \"\",\n                referrerpolicy: \"no-referrer\",\n                loading: \"lazy\",\n                style: {\n                  width: \"100%\",\n                  height: \"100%\",\n                },\n              }),\n            });\n\n            return new Error(defaultMessage);\n          }\n\n          Toast.error(defaultMessage, { duration: 5000 });\n          return new Error(defaultMessage);\n        }\n        return new Error(\"Internal Server Error\");\n      },\n    })\n    .use(ImageEditor, {\n      locale: {\n        strings: {\n          revert: i18n.global.t(\"core.components.uppy.image_editor.revert\"),\n          rotate: i18n.global.t(\"core.components.uppy.image_editor.rotate\"),\n          zoomIn: i18n.global.t(\"core.components.uppy.image_editor.zoom_in\"),\n          zoomOut: i18n.global.t(\"core.components.uppy.image_editor.zoom_out\"),\n          flipHorizontal: i18n.global.t(\n            \"core.components.uppy.image_editor.flip_horizontal\"\n          ),\n          aspectRatioSquare: i18n.global.t(\n            \"core.components.uppy.image_editor.aspect_ratio_square\"\n          ),\n          aspectRatioLandscape: i18n.global.t(\n            \"core.components.uppy.image_editor.aspect_ratio_landscape\"\n          ),\n          aspectRatioPortrait: i18n.global.t(\n            \"core.components.uppy.image_editor.aspect_ratio_portrait\"\n          ),\n        },\n      },\n    });\n});\n\nwatchEffect(() => {\n  if (uppy.value) {\n    uppy.value.on(\"upload-success\", (_, response: SuccessResponse) => {\n      emit(\"uploaded\", response);\n    });\n\n    uppy.value.on(\"upload-error\", (file, _, response) => {\n      emit(\"error\", file, response);\n    });\n  }\n});\n\nonUnmounted(() => {\n  uppy.value.close({ reason: \"unmount\" });\n});\n</script>\n\n<template>\n  <dashboard\n    class=\"w-full\"\n    :uppy=\"uppy\"\n    :props=\"{\n      theme: 'light',\n      disabled: disabled,\n      note: note,\n      width,\n      height,\n      doneButtonHandler: doneButtonHandler,\n    }\"\n  />\n</template>\n"
  },
  {
    "path": "ui/src/components/user/PostContributorList.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Contributor } from \"@halo-dev/api-client\";\nimport { VAvatar, VAvatarGroup } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { computed } from \"vue\";\nimport { useRouter } from \"vue-router\";\n\nconst props = withDefaults(\n  defineProps<{\n    allowViewUserDetail?: boolean;\n    owner?: Contributor;\n    contributors: Contributor[];\n  }>(),\n  {\n    allowViewUserDetail: true,\n    owner: undefined,\n  }\n);\n\nconst router = useRouter();\n\nconst contributorsWithoutOwner = computed(() =>\n  props.contributors.filter(\n    (contributor) => contributor.name !== props.owner?.name\n  )\n);\n\nfunction handleRouteToUserDetail(contributor: Contributor) {\n  if (\n    !utils.permission.has([\"system:users:view\"]) ||\n    !props.allowViewUserDetail\n  ) {\n    return;\n  }\n  router.push({\n    name: \"UserDetail\",\n    params: { name: contributor.name },\n  });\n}\n</script>\n\n<template>\n  <VAvatarGroup size=\"xs\" circle>\n    <VAvatar\n      v-if=\"owner\"\n      v-tooltip=\"owner?.displayName\"\n      :src=\"owner.avatar\"\n      :alt=\"owner.displayName\"\n      @click=\"handleRouteToUserDetail(owner)\"\n    />\n    <VAvatar\n      v-for=\"contributor in contributorsWithoutOwner\"\n      :key=\"contributor.name\"\n      v-tooltip=\"contributor.displayName\"\n      :src=\"contributor.avatar\"\n      :alt=\"contributor.displayName\"\n      @click=\"handleRouteToUserDetail(contributor)\"\n    ></VAvatar>\n  </VAvatarGroup>\n</template>\n"
  },
  {
    "path": "ui/src/components/user-avatar/UserAvatar.vue",
    "content": "<script lang=\"ts\" setup>\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconAddCircle,\n  IconRiPencilFill,\n  Toast,\n  VAvatar,\n  VButton,\n  VDropdown,\n  VDropdownItem,\n  VLoading,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { useFileDialog } from \"@vueuse/core\";\nimport { computed, defineAsyncComponent, ref, toRefs, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\n\nconst props = withDefaults(\n  defineProps<{\n    name?: string;\n    isCurrentUser?: boolean;\n  }>(),\n  {\n    name: \"-\",\n    isCurrentUser: false,\n  }\n);\n\nconst { isCurrentUser, name } = toRefs(props);\nconst { fetchCurrentUser } = stores.currentUser();\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst { data: avatar, isFetching } = useQuery({\n  queryKey: [\"user-avatar\", name, isCurrentUser],\n  queryFn: async () => {\n    const { data } = props.isCurrentUser\n      ? await consoleApiClient.user.getCurrentUserDetail()\n      : await consoleApiClient.user.getUserDetail({\n          name: props.name,\n        });\n\n    const annotations = data?.user.metadata.annotations;\n\n    // Check avatar has been updated. if not, we need retry.\n    if (\n      annotations?.[rbacAnnotations.AVATAR_ATTACHMENT_NAME] !==\n      annotations?.[rbacAnnotations.LAST_AVATAR_ATTACHMENT_NAME]\n    ) {\n      throw new Error(\"Avatar is not updated\");\n    }\n\n    return data.user.spec.avatar || \"\";\n  },\n  retry: 5,\n  retryDelay: 1000,\n});\n\nconst UserAvatarCropper = defineAsyncComponent(\n  () => import(\"./UserAvatarCropper.vue\")\n);\n\ninterface IUserAvatarCropperType extends Ref<\n  InstanceType<typeof UserAvatarCropper>\n> {\n  getCropperFile(): Promise<File>;\n}\n\nconst { open, reset, onChange } = useFileDialog({\n  accept: \".jpg, .jpeg, .png\",\n  multiple: false,\n});\n\nconst userAvatarCropper = ref<IUserAvatarCropperType>();\nconst visibleCropperModal = ref(false);\nconst originalFile = ref<File>() as Ref<File>;\n\nonChange((files) => {\n  if (!files) {\n    return;\n  }\n  if (files.length > 0) {\n    originalFile.value = files[0];\n    visibleCropperModal.value = true;\n  }\n});\n\nconst uploadSaving = ref(false);\nconst handleUploadAvatar = () => {\n  userAvatarCropper.value?.getCropperFile().then((file) => {\n    uploadSaving.value = true;\n\n    consoleApiClient.user\n      .uploadUserAvatar({\n        name: props.isCurrentUser ? \"-\" : props.name,\n        file: file,\n      })\n      .then(() => {\n        queryClient.invalidateQueries({ queryKey: [\"user-avatar\"] });\n        queryClient.invalidateQueries({ queryKey: [\"user-detail\"] });\n        if (props.isCurrentUser) {\n          fetchCurrentUser();\n        }\n        handleCloseCropperModal();\n      })\n      .catch(() => {\n        Toast.error(t(\"core.components.user_avatar.toast_upload_failed\"));\n      })\n      .finally(() => {\n        uploadSaving.value = false;\n      });\n  });\n};\n\nconst handleRemoveCurrentAvatar = () => {\n  Dialog.warning({\n    title: t(\"core.components.user_avatar.remove.title\"),\n    description: t(\"core.common.dialog.descriptions.cannot_be_recovered\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      consoleApiClient.user\n        .deleteUserAvatar({\n          name: props.isCurrentUser ? \"-\" : props.name,\n        })\n        .then(() => {\n          queryClient.invalidateQueries({ queryKey: [\"user-avatar\"] });\n          queryClient.invalidateQueries({ queryKey: [\"user-detail\"] });\n          if (props.isCurrentUser) {\n            fetchCurrentUser();\n          }\n        })\n        .catch(() => {\n          Toast.error(t(\"core.components.user_avatar.toast_remove_failed\"));\n        });\n    },\n  });\n};\n\nconst handleCloseCropperModal = () => {\n  visibleCropperModal.value = false;\n  reset();\n};\n\nconst changeUploadAvatar = () => {\n  reset();\n  open();\n};\n\nconst hasAvatar = computed(() => {\n  return !!avatar.value;\n});\n</script>\n\n<template>\n  <VLoading v-if=\"isFetching\" class=\"h-full w-full\" />\n  <div v-else class=\"group h-full w-full\">\n    <VAvatar\n      :src=\"avatar\"\n      :alt=\"props.name\"\n      circle\n      width=\"100%\"\n      height=\"100%\"\n      class=\"shadow-xl ring-4 ring-white\"\n    />\n    <VDropdown\n      v-if=\"utils.permission.has(['system:users:manage']) || isCurrentUser\"\n    >\n      <div\n        class=\"absolute left-0 right-0 top-0 hidden h-full w-full cursor-pointer items-center rounded-full border-0 bg-black/60 text-center transition-all duration-300 group-hover:flex\"\n        :class=\"{\n          '!flex': !hasAvatar,\n        }\"\n      >\n        <IconAddCircle\n          v-if=\"!hasAvatar\"\n          class=\"inline-block w-full self-center text-2xl text-white\"\n        />\n        <IconRiPencilFill\n          v-else\n          class=\"inline-block w-full self-center text-2xl text-white\"\n        />\n      </div>\n      <template #popper>\n        <VDropdownItem @click=\"open()\">\n          {{ $t(\"core.common.buttons.upload\") }}\n        </VDropdownItem>\n        <VDropdownItem v-if=\"hasAvatar\" @click=\"handleRemoveCurrentAvatar\">\n          {{ $t(\"core.common.buttons.delete\") }}\n        </VDropdownItem>\n      </template>\n    </VDropdown>\n  </div>\n\n  <VModal\n    :visible=\"visibleCropperModal\"\n    :width=\"1200\"\n    :title=\"$t('core.components.user_avatar.cropper_modal.title')\"\n    mount-to-body\n    @update:visible=\"handleCloseCropperModal\"\n  >\n    <UserAvatarCropper\n      ref=\"userAvatarCropper\"\n      :file=\"originalFile\"\n      @change-file=\"changeUploadAvatar\"\n    />\n    <template #footer>\n      <VSpace>\n        <VButton\n          v-if=\"visibleCropperModal\"\n          :loading=\"uploadSaving\"\n          type=\"secondary\"\n          @click=\"handleUploadAvatar\"\n        >\n          {{ $t(\"core.common.buttons.submit\") }}\n        </VButton>\n        <VButton @click=\"handleCloseCropperModal\">\n          {{ $t(\"core.common.buttons.cancel\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/src/components/user-avatar/UserAvatarCropper.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  IconArrowLeftRightLine,\n  IconArrowUpDownLine,\n  IconRefreshLine,\n  IconRiUpload2Fill,\n  IconZoomInLine,\n  IconZoomOutLine,\n} from \"@halo-dev/components\";\nimport Cropper from \"cropperjs\";\nimport \"cropperjs/dist/cropper.css\";\nimport type { Component } from \"vue\";\nimport { computed, markRaw, onMounted, ref, watch, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst { t } = useI18n();\n\nexport type ToolbarName =\n  | \"upload\"\n  | \"zoomIn\"\n  | \"zoomOut\"\n  | \"flipHorizontal\"\n  | \"flipVertical\"\n  | \"reset\";\n\nexport interface ToolbarItem {\n  name: ToolbarName;\n  title?: string;\n  icon: Component;\n  onClick: () => void;\n}\n\nconst props = withDefaults(\n  defineProps<{\n    file: File;\n    preview?: boolean;\n    cropperWidth?: number;\n    cropperHeight?: number;\n    cropperImageType?: \"png\" | \"jpeg\" | \"webp\" | \"jpg\";\n    toolbars?: boolean | ToolbarName[];\n  }>(),\n  {\n    preview: true,\n    cropperWidth: 200,\n    cropperHeight: 200,\n    cropperImageType: \"png\",\n    toolbars: true,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"changeFile\"): void;\n}>();\n\nconst cropper = ref<Cropper>();\nconst defaultToolbars: ToolbarItem[] = [\n  {\n    name: \"upload\",\n    icon: markRaw(IconRiUpload2Fill),\n    onClick: () => {\n      emit(\"changeFile\");\n    },\n    title: t(\"core.components.user_avatar.tooltips.upload\"),\n  },\n  {\n    name: \"zoomIn\",\n    icon: markRaw(IconZoomInLine),\n    onClick: () => {\n      cropper.value?.zoom(0.1);\n    },\n    title: t(\"core.components.user_avatar.tooltips.zoom_in\"),\n  },\n  {\n    name: \"zoomOut\",\n    icon: markRaw(IconZoomOutLine),\n    onClick: () => {\n      cropper.value?.zoom(-0.1);\n    },\n    title: t(\"core.components.user_avatar.tooltips.zoom_out\"),\n  },\n  {\n    name: \"flipHorizontal\",\n    icon: markRaw(IconArrowLeftRightLine),\n    onClick: () => {\n      cropper.value?.scaleX(-cropper.value?.getData().scaleX || -1);\n    },\n    title: t(\"core.components.user_avatar.tooltips.flip_horizontal\"),\n  },\n  {\n    name: \"flipVertical\",\n    icon: markRaw(IconArrowUpDownLine),\n    onClick: () => {\n      cropper.value?.scaleY(-cropper.value?.getData().scaleY || -1);\n    },\n    title: t(\"core.components.user_avatar.tooltips.flip_vertical\"),\n  },\n  {\n    name: \"reset\",\n    icon: markRaw(IconRefreshLine),\n    onClick: () => {\n      cropper.value?.reset();\n    },\n    title: t(\"core.components.user_avatar.tooltips.reset\"),\n  },\n];\nconst previewElement = ref<HTMLElement>();\nconst imageElement = ref<HTMLImageElement>() as Ref<HTMLImageElement>;\nconst toolbarItems = computed(() => {\n  if (props.toolbars === true) {\n    return defaultToolbars;\n  }\n  if (Array.isArray(props.toolbars) && props.toolbars.length > 0) {\n    return defaultToolbars.filter((item) =>\n      (props.toolbars as string[]).includes(item.name)\n    );\n  }\n  return [];\n});\n\nonMounted(() => {\n  cropper.value = new Cropper(imageElement.value, {\n    aspectRatio: props.cropperHeight / props.cropperWidth,\n    viewMode: 1,\n    dragMode: \"move\",\n    checkCrossOrigin: false,\n    cropBoxResizable: false,\n    center: false,\n    minCropBoxWidth: props.cropperWidth,\n    minCropBoxHeight: props.cropperHeight,\n    preview: previewElement.value?.querySelectorAll(\".preview\"),\n  });\n});\n\nconst imageUrl = ref<string>(\"\");\n\nconst renderImages = (file: File) => {\n  const reader = new FileReader();\n  reader.onload = function () {\n    imageUrl.value = reader.result as string;\n  };\n  reader.readAsDataURL(file);\n};\n\nconst getCropperFile = (): Promise<File> => {\n  return new Promise<File>((resolve, reject) => {\n    if (!cropper.value) {\n      reject();\n      return;\n    }\n    cropper.value\n      .getCroppedCanvas({\n        width: props.cropperWidth,\n        height: props.cropperHeight,\n      })\n      .toBlob((blob) => {\n        if (blob === null) {\n          reject();\n          return;\n        }\n        const fileName = props.file.name.replace(\n          /\\.[^/.]+$/,\n          `.${props.cropperImageType}`\n        );\n        const file = new File([blob], fileName, {\n          type: `image/${props.cropperImageType}`,\n        });\n        resolve(file);\n      });\n  });\n};\n\nwatch(\n  () => props.file,\n  (file) => {\n    if (file) {\n      if (file.type.indexOf(\"image\") === -1) {\n        return;\n      }\n      renderImages(file);\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nwatch(\n  () => imageUrl.value,\n  (imageUrl) => {\n    if (imageUrl) {\n      cropper.value?.replace(imageUrl);\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\ndefineExpose({\n  getCropperFile,\n});\n</script>\n<template>\n  <div class=\"flex flex-col-reverse sm:flex-row\">\n    <div\n      class=\"relative max-h-[500px] flex-auto overflow-hidden rounded-md sm:mr-4\"\n      :style=\"{ minHeight: `${cropperHeight}px` }\"\n    >\n      <img\n        ref=\"imageElement\"\n        alt=\"Uploaded Image\"\n        class=\"block h-full w-full object-cover\"\n      />\n      <div class=\"absolute bottom-3 left-1/2 -translate-x-1/2\">\n        <ul class=\"flex rounded-md bg-gray-100 p-0.5 opacity-75\">\n          <li\n            v-for=\"toolbar in toolbarItems\"\n            :key=\"toolbar.name\"\n            :title=\"toolbar.title\"\n          >\n            <button\n              type=\"button\"\n              class=\"p-2 hover:rounded-md hover:bg-gray-200\"\n              @click=\"toolbar.onClick\"\n            >\n              <component :is=\"toolbar.icon\" />\n            </button>\n          </li>\n        </ul>\n      </div>\n    </div>\n    <div\n      ref=\"previewElement\"\n      class=\"mb-4 flex justify-around sm:mb-0 sm:inline-block sm:justify-start\"\n    >\n      <div\n        class=\"preview overflow-hidden rounded-md border border-gray-300 shadow-md sm:mb-4\"\n        :style=\"{ width: `${cropperWidth}px`, height: `${cropperHeight}px` }\"\n      ></div>\n      <div class=\"flex flex-col justify-between sm:flex-row\">\n        <div\n          class=\"preview overflow-hidden rounded-full border border-gray-300\"\n          :style=\"{\n            width: `${cropperWidth * 0.5}px`,\n            height: `${cropperWidth * 0.5}px`,\n          }\"\n        ></div>\n        <div\n          class=\"preview overflow-hidden rounded-full border border-gray-300\"\n          :style=\"{\n            width: `${cropperWidth * 0.3}px`,\n            height: `${cropperWidth * 0.3}px`,\n          }\"\n        ></div>\n        <div\n          class=\"preview overflow-hidden rounded-full border border-gray-300\"\n          :style=\"{\n            width: `${cropperWidth * 0.2}px`,\n            height: `${cropperWidth * 0.2}px`,\n          }\"\n        ></div>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/components/video/LazyVideo.vue",
    "content": "<script lang=\"ts\" setup>\nimport { onMounted, ref } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    src?: string;\n    classes?: string | string[];\n  }>(),\n  {\n    src: undefined,\n    classes: \"\",\n  }\n);\n\nconst isLoading = ref(false);\nconst error = ref(false);\n\nconst loadVideo = async () => {\n  if (!props.src) {\n    throw new Error(\"src is required\");\n  }\n\n  const video = document.createElement(\"video\");\n  video.src = props.src;\n  return new Promise((resolve, reject) => {\n    video.onloadedmetadata = () => resolve(video);\n    video.onerror = (err) => reject(err);\n  });\n};\n\nonMounted(async () => {\n  isLoading.value = true;\n  try {\n    await loadVideo();\n  } catch (_) {\n    error.value = true;\n  } finally {\n    isLoading.value = false;\n  }\n  isLoading.value = false;\n});\n</script>\n<template>\n  <template v-if=\"isLoading\">\n    <slot name=\"loading\"> loading... </slot>\n  </template>\n  <template v-else-if=\"error\">\n    <slot name=\"error\"> error </slot>\n  </template>\n  <video v-else :src=\"src\" preload=\"metadata\" :class=\"classes\" />\n</template>\n"
  },
  {
    "path": "ui/src/composables/use-auto-save-content.ts",
    "content": "import { useTimeoutFn, useWindowFocus } from \"@vueuse/core\";\nimport { watch, type Ref } from \"vue\";\nimport { onBeforeRouteLeave } from \"vue-router\";\nimport type { ContentCache } from \"./use-content-cache\";\n\nexport function useAutoSaveContent(\n  currentCache: Ref<ContentCache | undefined>,\n  raw: Ref<string | undefined>,\n  callback: () => void\n) {\n  // TODO it may be necessary to know the latest version before saving, otherwise it will overwrite the latest version\n  const handleAutoSave = () => {\n    if (currentCache.value) {\n      callback();\n    }\n  };\n\n  onBeforeRouteLeave(() => {\n    handleAutoSave();\n  });\n\n  watch(useWindowFocus(), (newFocus) => {\n    if (!newFocus) {\n      handleAutoSave();\n    }\n  });\n\n  window.addEventListener(\"beforeunload\", () => {\n    handleAutoSave();\n  });\n\n  const { start, isPending, stop } = useTimeoutFn(() => {\n    handleAutoSave();\n  }, 20 * 1000);\n  watch(raw, () => {\n    if (isPending.value) {\n      stop();\n    }\n    start();\n  });\n}\n"
  },
  {
    "path": "ui/src/composables/use-content-cache.ts",
    "content": "import { Toast } from \"@halo-dev/components\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { debounce } from \"es-toolkit\";\nimport { computed, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nexport interface ContentCache {\n  name: string;\n  content?: string;\n  version: number;\n}\n\ninterface useContentCacheReturn {\n  handleResetCache: () => void;\n  handleSetContentCache: () => void;\n  handleClearCache: (name?: string) => void;\n  currentCache: Ref<ContentCache | undefined>;\n}\n\nexport function useContentCache(\n  key: string,\n  name: Ref<string | undefined>,\n  raw: Ref<string | undefined>,\n  contentVersion: Ref<number>\n): useContentCacheReturn {\n  const content_caches = useLocalStorage<ContentCache[]>(key, []);\n  const currentCache = computed<ContentCache | undefined>(() => {\n    if (content_caches.value.length > 0) {\n      if (name.value) {\n        return content_caches.value.find(\n          (c: ContentCache) => c.name === name.value\n        );\n      } else {\n        return content_caches.value.find((c: ContentCache) => c.name === \"\");\n      }\n    }\n    return undefined;\n  });\n\n  const { t } = useI18n();\n\n  const handleResetCache = () => {\n    let cache: ContentCache | undefined;\n    if (name.value) {\n      cache = content_caches.value.find(\n        (c: ContentCache) => c.name === name.value\n      );\n    } else {\n      cache = content_caches.value.find(\n        (c: ContentCache) => c.name === \"\" && c.content\n      );\n    }\n    if (!cache) {\n      return;\n    }\n    if (cache.version != contentVersion.value) {\n      // TODO save the local offline cached content as a historical version\n      handleClearCache(name.value);\n      return;\n    }\n    Toast.info(t(\"core.composables.content_cache.toast_recovered\"));\n    raw.value = cache.content;\n    handleClearCache(name.value);\n  };\n\n  const handleSetContentCache = debounce(() => {\n    let cache: ContentCache | undefined;\n    if (name.value) {\n      cache = content_caches.value.find(\n        (c: ContentCache) => c.name === name.value\n      );\n    } else {\n      cache = content_caches.value.find((c: ContentCache) => c.name === \"\");\n    }\n    if (cache) {\n      cache.content = raw?.value;\n    } else {\n      content_caches.value.push({\n        name: name.value || \"\",\n        content: raw?.value,\n        version: contentVersion.value,\n      });\n    }\n  }, 500);\n\n  const handleClearCache = (name?: string) => {\n    let index: number;\n    if (name) {\n      index = content_caches.value.findIndex(\n        (c: ContentCache) => c.name === name\n      );\n    } else {\n      index = content_caches.value.findIndex(\n        (c: ContentCache) => c.name === \"\"\n      );\n    }\n    if (index > -1) {\n      content_caches.value.splice(index, 1);\n    }\n  };\n\n  return {\n    handleClearCache,\n    handleResetCache,\n    handleSetContentCache,\n    currentCache,\n  };\n}\n"
  },
  {
    "path": "ui/src/composables/use-editor-extension-points.ts",
    "content": "import type { EditorProvider } from \"@halo-dev/ui-shared\";\nimport { markRaw, shallowRef, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport Logo from \"@/assets/logo-64x64.png?inline\";\nimport DefaultEditor from \"@/components/editor/DefaultEditor.vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\n\ninterface useEditorExtensionPointsReturn {\n  editorProviders: Ref<EditorProvider[]>;\n  fetchEditorProviders: () => Promise<void>;\n}\n\nexport function useEditorExtensionPoints(): useEditorExtensionPointsReturn {\n  // resolve plugin extension points\n  const { pluginModules } = usePluginModuleStore();\n  const { t } = useI18n();\n\n  const editorProviders = shallowRef<EditorProvider[]>([\n    {\n      name: \"default\",\n      displayName: t(\"core.plugin.extension_points.editor.providers.default\"),\n      component: markRaw(DefaultEditor),\n      rawType: \"HTML\",\n      logo: Logo,\n    },\n  ]);\n\n  async function fetchEditorProviders() {\n    for (const pluginModule of pluginModules) {\n      try {\n        const callbackFunction =\n          pluginModule?.extensionPoints?.[\"editor:create\"];\n\n        if (typeof callbackFunction !== \"function\") {\n          continue;\n        }\n\n        const pluginProviders = await callbackFunction();\n\n        editorProviders.value = [...editorProviders.value, ...pluginProviders];\n      } catch (error) {\n        console.error(`Error processing plugin module:`, pluginModule, error);\n      }\n    }\n  }\n\n  return {\n    editorProviders,\n    fetchEditorProviders,\n  };\n}\n"
  },
  {
    "path": "ui/src/composables/use-role.ts",
    "content": "import type { Role } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { Toast } from \"@halo-dev/components\";\nimport { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { resolveDeepDependencies } from \"@/utils/role\";\n\ninterface RoleTemplateGroup {\n  module: string | null | undefined;\n  roles: Role[];\n}\n\ninterface useRoleFormReturn {\n  formState: Ref<Role>;\n  isSubmitting: Ref<boolean>;\n  isUpdateMode: ComputedRef<boolean>;\n  handleCreateOrUpdate: () => Promise<void>;\n}\n\ninterface useRoleTemplateSelectionReturn {\n  selectedRoleTemplates: Ref<Set<string>>;\n  roleTemplateGroups: ComputedRef<RoleTemplateGroup[]>;\n  handleRoleTemplateSelect: (e: Event) => void;\n}\n\n/**\n * Create or update a role.\n *\n * @returns {useRoleFormReturn}\n */\nexport function useRoleForm(): useRoleFormReturn {\n  const { t } = useI18n();\n\n  const formState = ref<Role>({\n    apiVersion: \"v1alpha1\",\n    kind: \"Role\",\n    metadata: {\n      name: \"\",\n      generateName: \"role-\",\n      labels: {},\n      annotations: {\n        [rbacAnnotations.DEPENDENCIES]: \"\",\n        [rbacAnnotations.DISPLAY_NAME]: \"\",\n      },\n    },\n    rules: [],\n  });\n  const isSubmitting = ref(false);\n\n  const isUpdateMode = computed(() => {\n    return !!formState.value.metadata.creationTimestamp;\n  });\n\n  const handleCreateOrUpdate = async () => {\n    try {\n      isSubmitting.value = true;\n      if (isUpdateMode.value) {\n        const { data } = await coreApiClient.role.updateRole({\n          name: formState.value.metadata.name,\n          role: formState.value,\n        });\n\n        formState.value = data;\n      } else {\n        const { data } = await coreApiClient.role.createRole({\n          role: formState.value,\n        });\n\n        formState.value = data;\n      }\n\n      Toast.success(t(\"core.common.toast.save_success\"));\n    } catch (e) {\n      console.error(e);\n    } finally {\n      isSubmitting.value = false;\n    }\n  };\n\n  return {\n    formState,\n    isSubmitting,\n    isUpdateMode,\n    handleCreateOrUpdate,\n  };\n}\n\n/**\n * Logic for selecting role templates\n *\n * @returns {useRoleTemplateSelectionReturn}\n */\nexport function useRoleTemplateSelection(\n  roleTemplates: Ref<Role[] | undefined>\n): useRoleTemplateSelectionReturn {\n  const selectedRoleTemplates = ref<Set<string>>(new Set());\n\n  /**\n   * Grouping role templates by module <br />\n   * Example:\n   * ```json\n   * {\n   *   \"module\": \"Users Management\",\n   *   \"roles\": [\n   *     {\n   *       \"rules\": [\n   *         {\n   *           \"apiGroups\": [\n   *             \"\"\n   *           ],\n   *           \"resources\": [\n   *             \"users\"\n   *           ],\n   *           \"resourceNames\": [],\n   *           \"nonResourceURLs\": [],\n   *           \"verbs\": [\n   *             \"create\",\n   *             \"patch\",\n   *             \"update\",\n   *             \"delete\",\n   *             \"deletecollection\"\n   *           ]\n   *         }\n   *       ],\n   *       \"apiVersion\": \"v1alpha1\",\n   *       \"kind\": \"Role\",\n   *       \"metadata\": {\n   *         \"name\": \"role-template-manage-users\",\n   *         \"labels\": {\n   *           \"halo.run/role-template\": \"true\"\n   *         },\n   *         \"annotations\": {\n   *           \"rbac.authorization.halo.run/dependencies\": \"[ \\\"role-template-view-users\\\", \\\"role-template-change-password\\\" ]\\n\",\n   *           \"rbac.authorization.halo.run/module\": \"Users Management\",\n   *           \"rbac.authorization.halo.run/display-name\": \"User manage\",\n   *           \"rbac.authorization.halo.run/ui-permissions\": \"[\\\"system:users:manage\\\"]\\n\",\n   *           \"rbac.authorization.halo.run/dependency-rules\": \"[{\\\"apiGroups\\\":[\\\"\\\"],\\\"resources\\\":[\\\"users\\\"],\\\"resourceNames\\\":[],\\\"nonResourceURLs\\\":[],\\\"verbs\\\":[\\\"get\\\",\\\"list\\\"]},{\\\"apiGroups\\\":[\\\"api.halo.run\\\"],\\\"resources\\\":[\\\"users/password\\\"],\\\"resourceNames\\\":[],\\\"nonResourceURLs\\\":[],\\\"verbs\\\":[\\\"update\\\"]}]\",\n   *           \"rbac.authorization.halo.run/ui-permissions-aggregated\": \"[\\\"system:users:view\\\"]\"\n   *         },\n   *         \"version\": 9\n   *       }\n   *     },\n   *     {\n   *       \"rules\": [\n   *         {\n   *           \"apiGroups\": [\n   *             \"\"\n   *           ],\n   *           \"resources\": [\n   *             \"users\"\n   *           ],\n   *           \"resourceNames\": [],\n   *           \"nonResourceURLs\": [],\n   *           \"verbs\": [\n   *             \"get\",\n   *             \"list\"\n   *           ]\n   *         }\n   *       ],\n   *       \"apiVersion\": \"v1alpha1\",\n   *       \"kind\": \"Role\",\n   *       \"metadata\": {\n   *         \"name\": \"role-template-view-users\",\n   *         \"labels\": {\n   *           \"halo.run/role-template\": \"true\"\n   *         },\n   *         \"annotations\": {\n   *           \"rbac.authorization.halo.run/module\": \"Users Management\",\n   *           \"rbac.authorization.halo.run/display-name\": \"User View\",\n   *           \"rbac.authorization.halo.run/ui-permissions\": \"[\\\"system:users:view\\\"]\\n\",\n   *           \"rbac.authorization.halo.run/dependency-rules\": \"[]\",\n   *           \"rbac.authorization.halo.run/ui-permissions-aggregated\": \"[]\"\n   *         },\n   *         \"version\": 9\n   *       }\n   *     }\n   *   ]\n   * }\n   * ```\n   */\n  const roleTemplateGroups = computed<RoleTemplateGroup[]>(() => {\n    const groups: RoleTemplateGroup[] = [];\n    roleTemplates.value?.forEach((role) => {\n      const group = groups.find(\n        (group) =>\n          group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]\n      );\n      if (group) {\n        group.roles.push(role);\n      } else {\n        groups.push({\n          module: role.metadata.annotations?.[rbacAnnotations.MODULE],\n          roles: [role],\n        });\n      }\n    });\n    return groups;\n  });\n\n  const handleRoleTemplateSelect = async (e: Event) => {\n    const { checked, value } = e.target as HTMLInputElement;\n    if (!checked) {\n      return;\n    }\n    const role = roleTemplates.value?.find(\n      (role) => role.metadata.name === value\n    );\n\n    if (!role) {\n      return;\n    }\n\n    selectedRoleTemplates.value.add(role.metadata.name);\n    resolveDeepDependencies(role, roleTemplates.value || []).forEach((name) => {\n      selectedRoleTemplates.value.add(name);\n    });\n  };\n\n  return {\n    selectedRoleTemplates,\n    roleTemplateGroups,\n    handleRoleTemplateSelect,\n  };\n}\n"
  },
  {
    "path": "ui/src/composables/use-route-menu-generator.ts",
    "content": "import {\n  utils,\n  type MenuGroupType,\n  type MenuItemType,\n} from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { sortBy } from \"es-toolkit\";\nimport { ref, watch } from \"vue\";\nimport {\n  useRouter,\n  type RouteRecordNormalized,\n  type RouteRecordRaw,\n} from \"vue-router\";\n\nexport function useRouteMenuGenerator(menuGroups: MenuGroupType[]) {\n  const router = useRouter();\n\n  function flattenRoutes(route: RouteRecordNormalized | RouteRecordRaw) {\n    let routes: (RouteRecordNormalized | RouteRecordRaw)[] = [route];\n    if (route.children) {\n      route.children.forEach((child) => {\n        routes = routes.concat(flattenRoutes(child));\n      });\n    }\n    return routes;\n  }\n\n  async function isRouteValid(route?: RouteRecordNormalized) {\n    if (!route) return false;\n    const { meta } = route;\n    if (!meta?.menu) return false;\n\n    // If permissions doesn't exist or is empty\n    if (!meta.permissions) return true;\n\n    // Check if permissions is a function\n    if (typeof meta.permissions === \"function\") {\n      try {\n        return await meta.permissions(\n          utils.permission.getUserPermissions() || []\n        );\n      } catch (e) {\n        console.error(\n          `Error checking permissions for route ${String(route.name)}:`,\n          e\n        );\n        return false;\n      }\n    }\n\n    // Default behavior for array of permissions\n    return utils.permission.has(meta.permissions as string[], true);\n  }\n\n  const { data, isLoading: isDataLoading } = useQuery({\n    queryKey: [\"core:sidebar:menus\"],\n    queryFn: async () => {\n      const allRoutes = router.getRoutes();\n\n      // Filter routes based on permissions (async)\n      const validRoutePromises = allRoutes.map(async (route) => {\n        const isValid = await isRouteValid(route);\n        return isValid ? route : null;\n      });\n\n      // Wait for all permission checks to complete\n      const validRoutes = (await Promise.all(validRoutePromises)).filter(\n        Boolean\n      ) as RouteRecordNormalized[];\n\n      // Sort the valid routes\n      let currentRoutes = sortBy<RouteRecordNormalized>(validRoutes, [\n        (route: RouteRecordRaw) => !route.meta?.core,\n        (route: RouteRecordRaw) => route.meta?.menu?.priority || 0,\n      ]);\n\n      // Flatten and filter child routes\n      for (const route of currentRoutes) {\n        if (route.children.length) {\n          const routesMap = new Map(\n            currentRoutes.map((route) => [route.name, route])\n          );\n\n          const childRoutesPromises = route.children\n            .flatMap((child) => flattenRoutes(child))\n            .map(async (flattenedChild) => {\n              const validRoute = routesMap.get(flattenedChild.name);\n              if (validRoute && (await isRouteValid(validRoute))) {\n                return validRoute;\n              }\n              return null;\n            });\n\n          // Wait for all child permission checks to complete\n          const flattenedAndValidChildren = (\n            await Promise.all(childRoutesPromises)\n          ).filter(Boolean) as RouteRecordNormalized[]; // filters out falsy values\n\n          // Sorting the routes\n          // @ts-ignore children must be RouteRecordRaw[], but it is RouteRecordNormalized[]\n          route.children = sortBy(flattenedAndValidChildren, [\n            (route) => !route?.meta?.core,\n            (route) => route?.meta?.menu?.priority || 0,\n          ]);\n        }\n      }\n\n      // Remove duplicate routes\n      const allChildren = currentRoutes.flatMap((route) => route.children);\n\n      currentRoutes = currentRoutes.filter(\n        (route) => !allChildren.find((child) => child.name === route.name)\n      );\n\n      // group by menu.group\n      const groupedMenus = currentRoutes.reduce((acc, route) => {\n        const { menu } = route.meta;\n        if (!menu) {\n          return acc;\n        }\n        const group = acc.find((item) => item.id === menu.group);\n        const childRoute = route.children;\n\n        const menuChildren: MenuItemType[] = childRoute\n          .map((child) => {\n            if (!child.meta?.menu) return;\n            return {\n              name: child.meta.menu.name,\n              path: child.path,\n              icon: child.meta.menu.icon,\n              mobile: child.meta.menu.mobile,\n            };\n          })\n          .filter(Boolean) as MenuItemType[];\n\n        if (group) {\n          group.items?.push({\n            name: menu.name,\n            path: route.path,\n            icon: menu.icon,\n            mobile: menu.mobile,\n            children: menuChildren,\n          });\n        } else {\n          const menuGroup = menuGroups.find((item) => item.id === menu.group);\n          let name = \"\";\n          if (!menuGroup) {\n            name = menu.group || \"\";\n          } else if (menuGroup.name) {\n            name = menuGroup.name;\n          }\n          acc.push({\n            id: menuGroup?.id || menu.group || \"\",\n            name: name,\n            priority: menuGroup?.priority || 0,\n            items: [\n              {\n                name: menu.name,\n                path: route.path,\n                icon: menu.icon,\n                mobile: menu.mobile,\n                children: menuChildren,\n              },\n            ],\n          });\n        }\n        return acc;\n      }, [] as MenuGroupType[]);\n\n      // sort by menu.priority\n      const menus = sortBy(groupedMenus, [\n        (menu: MenuGroupType) => {\n          return menuGroups.findIndex((item) => item.id === menu.id) < 0;\n        },\n        (menu: MenuGroupType) => menu.priority || 0,\n      ]);\n\n      const minimenus = menus\n        .reduce((acc, group) => {\n          if (group?.items) {\n            acc.push(...group.items);\n          }\n          return acc;\n        }, [] as MenuItemType[])\n        .filter((item) => item.mobile);\n\n      return {\n        menus,\n        minimenus,\n      };\n    },\n  });\n\n  const isLoading = ref(false);\n\n  // Make loading more user-friendly\n  watch(\n    () => isDataLoading.value,\n    (value) => {\n      let delayLoadingTimer: ReturnType<typeof setTimeout> | undefined;\n      if (value) {\n        delayLoadingTimer = setTimeout(() => {\n          isLoading.value = isDataLoading.value;\n        }, 200);\n      } else {\n        clearTimeout(delayLoadingTimer);\n        isLoading.value = false;\n      }\n    },\n    {\n      immediate: true,\n    }\n  );\n\n  return {\n    data,\n    isLoading,\n  };\n}\n"
  },
  {
    "path": "ui/src/composables/use-session-keep-alive.ts",
    "content": "import { stores } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed } from \"vue\";\n\nexport function useSessionKeepAlive() {\n  const { isAnonymous } = stores.currentUser();\n\n  useQuery({\n    queryKey: [\"health\", \"keep-session-alive\"],\n    queryFn: () => fetch(\"/actuator/health\"),\n    refetchInterval: 1000 * 60 * 5, // 5 minutes\n    refetchIntervalInBackground: true,\n    refetchOnWindowFocus: true,\n    enabled: computed(() => !isAnonymous),\n  });\n}\n"
  },
  {
    "path": "ui/src/composables/use-title.ts",
    "content": "import { stores } from \"@halo-dev/ui-shared\";\nimport { useTitle } from \"@vueuse/core\";\nimport { storeToRefs } from \"pinia\";\nimport { computed, type Ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRoute } from \"vue-router\";\nimport { AppName } from \"@/constants/app\";\n\nexport function useAppTitle(baseTitle?: Ref<string>) {\n  const { globalInfo } = storeToRefs(stores.globalInfo());\n\n  const { t } = useI18n();\n  const route = useRoute();\n\n  useTitle(\n    computed(() => {\n      const { title: routeTitle } = route.meta;\n      const siteTitle = globalInfo.value?.siteTitle || AppName;\n      return [t(baseTitle?.value || routeTitle || \"\"), siteTitle]\n        .filter(Boolean)\n        .join(\" - \");\n    })\n  );\n}\n"
  },
  {
    "path": "ui/src/constants/annotations.ts",
    "content": "// plugin\nexport enum pluginAnnotations {\n  DISPLAY_NAME = \"plugin.halo.run/display-name\",\n}\n\n// rbac\nexport enum rbacAnnotations {\n  MODULE = \"rbac.authorization.halo.run/module\",\n  ROLE_NAMES = \"rbac.authorization.halo.run/role-names\",\n  DISPLAY_NAME = \"rbac.authorization.halo.run/display-name\",\n  DEPENDENCIES = \"rbac.authorization.halo.run/dependencies\",\n  AVATAR_ATTACHMENT_NAME = \"halo.run/avatar-attachment-name\",\n  LAST_AVATAR_ATTACHMENT_NAME = \"halo.run/last-avatar-attachment-name\",\n  DISALLOW_ACCESS_CONSOLE = \"rbac.authorization.halo.run/disallow-access-console\",\n}\n\n// content\n\nexport enum contentAnnotations {\n  PREFERRED_EDITOR = \"content.halo.run/preferred-editor\",\n  PATCHED_CONTENT = \"content.halo.run/patched-content\",\n  PATCHED_RAW = \"content.halo.run/patched-raw\",\n  CONTENT_JSON = \"content.halo.run/content-json\",\n  SCHEDULED_PUBLISH_AT = \"content.halo.run/scheduled-publish-at\",\n}\n\n// pat\nexport enum patAnnotations {\n  ACCESS_TOKEN = \"security.halo.run/access-token\",\n}\n\n// Secret\nexport enum secretAnnotations {\n  DESCRIPTION = \"secret.halo.run/description\",\n}\n"
  },
  {
    "path": "ui/src/constants/app.ts",
    "content": "export const AppName = \"Halo\";\n"
  },
  {
    "path": "ui/src/constants/constants.ts",
    "content": "export const SUPER_ROLE_NAME = \"super-role\";\n"
  },
  {
    "path": "ui/src/constants/error-types.ts",
    "content": "export const ERROR_MFA_REQUIRED_TYPE = \"https://halo.run/probs/2fa-required\";\n"
  },
  {
    "path": "ui/src/constants/finalizers.ts",
    "content": "export const SYSTEM_PROTECTION = \"system-protection\";\n"
  },
  {
    "path": "ui/src/constants/labels.ts",
    "content": "// plugin\nexport enum pluginLabels {\n  NAME = \"plugin.halo.run/plugin-name\",\n  SYSTEM_RESERVED = \"plugin.halo.run/system-reserved\",\n}\n\n// role\nexport enum roleLabels {\n  TEMPLATE = \"halo.run/role-template\",\n  HIDDEN = \"halo.run/hidden\",\n  SYSTEM_RESERVED = \"rbac.authorization.halo.run/system-reserved\",\n}\n\n// post\nexport enum postLabels {\n  DELETED = \"content.halo.run/deleted\",\n  PUBLISHED = \"content.halo.run/published\",\n  OWNER = \"content.halo.run/owner\",\n  VISIBLE = \"content.halo.run/visible\",\n  PHASE = \"content.halo.run/phase\",\n  SCHEDULING_PUBLISH = \"content.halo.run/scheduling-publish\",\n}\n\n// singlePage\nexport enum singlePageLabels {\n  DELETED = \"content.halo.run/deleted\",\n  PUBLISHED = \"content.halo.run/published\",\n  OWNER = \"content.halo.run/owner\",\n  VISIBLE = \"content.halo.run/visible\",\n  PHASE = \"content.halo.run/phase\",\n}\n\n// attachment\nexport enum attachmentPolicyLabels {\n  // Used for ui display only\n  HIDDEN = \"storage.halo.run/policy-hidden-in-upload-ui\",\n  HIDDEN_WITH_JSON_PATCH = \"storage.halo.run~1policy-hidden-in-upload-ui\",\n  PRIORITY = \"storage.halo.run/policy-priority-in-upload-ui\",\n  PRIORITY_WITH_JSON_PATCH = \"storage.halo.run~1policy-priority-in-upload-ui\",\n}\n"
  },
  {
    "path": "ui/src/constants/regex.ts",
    "content": "export const PASSWORD_REGEX = /^[A-Za-z0-9!@#$%^&*.?]+$/;\n"
  },
  {
    "path": "ui/src/formkit/formkit.config.ts",
    "content": "import { createAutoHeightTextareaPlugin } from \"@formkit/addons\";\nimport { en, zh } from \"@formkit/i18n\";\nimport { group as nativeGroup, select as nativeSelect } from \"@formkit/inputs\";\nimport { generateClasses } from \"@formkit/themes\";\nimport type { DefaultConfigOptions } from \"@formkit/vue\";\nimport { array } from \"./inputs/array\";\nimport { attachment } from \"./inputs/attachment\";\nimport { attachmentGroupSelect } from \"./inputs/attachment-group-select\";\nimport { attachmentInput } from \"./inputs/attachment-input\";\nimport { attachmentPolicySelect } from \"./inputs/attachment-policy-select\";\nimport { categoryCheckbox } from \"./inputs/category-checkbox\";\nimport { categorySelect } from \"./inputs/category-select\";\nimport { code } from \"./inputs/code\";\nimport { color } from \"./inputs/color\";\nimport { form } from \"./inputs/form\";\nimport { group } from \"./inputs/group\";\nimport { iconify } from \"./inputs/iconify\";\nimport { list } from \"./inputs/list\";\nimport { menuCheckbox } from \"./inputs/menu-checkbox\";\nimport { menuItemSelect } from \"./inputs/menu-item-select\";\nimport { menuRadio } from \"./inputs/menu-radio\";\nimport { menuSelect } from \"./inputs/menu-select\";\nimport { password } from \"./inputs/password\";\nimport { postSelect } from \"./inputs/post-select\";\nimport { repeater } from \"./inputs/repeater\";\nimport { roleSelect } from \"./inputs/role-select\";\nimport { secret } from \"./inputs/secret\";\nimport { select } from \"./inputs/select\";\nimport { singlePageSelect } from \"./inputs/singlePage-select\";\nimport { switchInput } from \"./inputs/switch\";\nimport { tagCheckbox } from \"./inputs/tag-checkbox\";\nimport { tagSelect } from \"./inputs/tag-select\";\nimport { toggle } from \"./inputs/toggle\";\nimport { userSelect } from \"./inputs/user-select\";\nimport { verificationForm } from \"./inputs/verify-form\";\nimport autoScrollToErrors from \"./plugins/auto-scroll-to-errors\";\nimport passwordPreventAutocomplete from \"./plugins/password-prevent-autocomplete\";\nimport radioAlt from \"./plugins/radio-alt\";\nimport requiredAsterisk from \"./plugins/required-asterisk\";\nimport stopImplicitSubmission from \"./plugins/stop-implicit-submission\";\nimport theme from \"./theme\";\n\nconst config: DefaultConfigOptions = {\n  config: {\n    classes: generateClasses(theme),\n  },\n  plugins: [\n    radioAlt,\n    stopImplicitSubmission,\n    passwordPreventAutocomplete,\n    requiredAsterisk,\n    autoScrollToErrors,\n    createAutoHeightTextareaPlugin(),\n  ],\n  inputs: {\n    attachmentInput,\n    attachmentGroupSelect,\n    attachmentPolicySelect,\n    categoryCheckbox,\n    categorySelect,\n    code,\n    form,\n    group,\n    list,\n    menuCheckbox,\n    menuItemSelect,\n    menuRadio,\n    menuSelect,\n    nativeGroup,\n    password,\n    postSelect,\n    repeater,\n    roleSelect,\n    secret,\n    singlePageSelect,\n    tagCheckbox,\n    tagSelect,\n    verificationForm,\n    userSelect,\n    nativeSelect,\n    select,\n    array,\n    color,\n    iconify,\n    attachment,\n    switch: switchInput,\n    toggle,\n  },\n  locales: { zh, en },\n  locale: \"zh\",\n};\n\nexport default config;\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/ArrayFormModal.vue",
    "content": "<script setup lang=\"ts\">\nimport { getNode, type FormKitNode } from \"@formkit/core\";\nimport { VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { computed, onMounted, ref } from \"vue\";\nimport type { ArrayValue } from \"./ArrayInput.vue\";\n\nconst { node, itemValue, currentEditIndex } = defineProps<{\n  node: FormKitNode;\n  itemValue: Record<string, unknown>;\n  currentEditIndex: number;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst formKitNode = ref<FormKitNode>();\nconst isSubmitting = ref(false);\nconst formId = `array-item-form-${utils.id.uuid()}`;\n\nonMounted(() => {\n  formKitNode.value = getNode(formId);\n});\n\nconst formKitNodeValid = computed(() => {\n  return formKitNode.value?.context?.state.valid ?? true;\n});\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst handleSave = (data: Record<string, unknown>) => {\n  if (!formKitNodeValid.value) {\n    return;\n  }\n  const oldNodeValue = (node._value as ArrayValue) || [];\n  if (currentEditIndex === -1) {\n    node.input(oldNodeValue.concat(data), false);\n    modal.value?.close();\n    return;\n  }\n\n  if (currentEditIndex >= oldNodeValue.length) {\n    throw new Error(\"Current edit index is out of range\");\n  }\n\n  oldNodeValue[currentEditIndex] = data;\n  node.input(oldNodeValue, false);\n  modal.value?.close();\n};\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"$t('core.formkit.array.item_modal.title')\"\n    :width=\"700\"\n    mount-to-body\n    layer-closable\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      :id=\"formId\"\n      type=\"form\"\n      :ignore=\"true\"\n      :actions=\"false\"\n      :model-value=\"itemValue\"\n      @submit=\"handleSave\"\n    >\n      <component\n        :is=\"node.context?.slots.default\"\n        v-bind=\"{\n          value: itemValue,\n        }\"\n      />\n    </FormKit>\n\n    <template #footer>\n      <VSpace>\n        <VButton\n          type=\"secondary\"\n          :loading=\"isSubmitting\"\n          @click=\"() => formKitNode?.submit()\"\n        >\n          {{ $t(\"core.common.buttons.submit\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/ArrayInput.vue",
    "content": "<script setup lang=\"ts\">\nimport { getNode, type FormKitNode, type FormKitProps } from \"@formkit/core\";\nimport { undefine } from \"@formkit/utils\";\nimport { IconClose, VButton } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { isNil, isNotNil } from \"es-toolkit\";\nimport { cloneDeepWith, get } from \"es-toolkit/compat\";\nimport objectHash from \"object-hash\";\nimport { onMounted, ref, toRaw, watch } from \"vue\";\nimport { VueDraggable } from \"vue-draggable-plus\";\nimport MingcuteDotsLine from \"~icons/mingcute/dots-line\";\nimport type { ArrayItemLabel, ArrayItemLabelType } from \".\";\nimport type { IconifyFormat, IconifyValue } from \"../iconify/types\";\nimport ArrayFormModal from \"./ArrayFormModal.vue\";\nimport ColorLabel from \"./labels/ColorLabel.vue\";\nimport IconifyLabel from \"./labels/IconifyLabel.vue\";\nimport { renderItemLabelValue } from \"./renderers\";\n\nconst formKitChildrenId = `formkit-children-${utils.id.uuid()}`;\n\nexport type ArrayProps = {\n  removeControl?: boolean;\n  addButton?: boolean;\n  addLabel?: boolean;\n  addAttrs?: Record<string, unknown>;\n  min?: number;\n  max?: number;\n  itemLabels: ArrayItemLabelType[];\n};\n\nexport type ArrayValue = Record<string, unknown>[];\n\nconst props = defineProps<{\n  node: FormKitNode<ArrayValue>;\n}>();\n\nconst arrayValue = ref<ArrayValue>(props.node.value);\nconst hiddenChildrenFormKit = ref<FormKitNode<unknown>>();\nconst arrayModal = ref<boolean>(false);\nconst nodeProps = ref<Partial<FormKitProps<ArrayProps>>>(props.node.props);\n\ntype FnType = (index: number) => Record<string, unknown>;\n\nfunction createValue(num: number, fn: FnType): ArrayValue {\n  return new Array(num).fill(\"\").map((_, index) => fn(index));\n}\n\nfunction arrayFeature(node: FormKitNode<ArrayValue>) {\n  const initProps = node.props as Partial<FormKitProps<ArrayProps>>;\n  node.props.removeControl = initProps.removeControl ?? true;\n  node.props.addButton = initProps.addButton ?? true;\n  node.props.addAttrs = initProps.addAttrs ?? {};\n  node.props.min = initProps.min ? Number(initProps.min) : 0;\n  node.props.max = initProps.max ? Number(initProps.max) : 1 / 0;\n  if (node.props.min > node.props.max) {\n    throw Error(\"Array: min must be less than max\");\n  }\n\n  if (\"disabled\" in initProps) {\n    node.props.disabled = undefine(initProps.disabled);\n  }\n\n  nodeProps.value = {\n    ...initProps,\n  };\n\n  if (Array.isArray(node.value)) {\n    if (node.value.length < node.props.min) {\n      const value = createValue(node.props.min - node.value.length, () => ({}));\n      node.input(node.value.concat(value), false);\n    } else {\n      if (node.value.length > node.props.max) {\n        node.input(node.value.slice(0, node.props.max), false);\n      }\n    }\n  } else {\n    node.input(\n      createValue(node.props.min, () => ({})),\n      false\n    );\n  }\n\n  node.on(\"input\", ({ payload }) => {\n    arrayValue.value = toRaw(payload);\n  });\n}\n\nonMounted(async () => {\n  const node = props.node;\n  node._c.sync = true;\n  arrayFeature(node);\n  hiddenChildrenFormKit.value = getNode(formKitChildrenId);\n  await updateFormattedLabels();\n});\n\ntype FormattedItemLabel =\n  | {\n      type: Exclude<ArrayItemLabelType, \"iconify\">;\n      value: string;\n    }\n  | {\n      type: \"iconify\";\n      value: string | IconifyValue;\n      format: IconifyFormat;\n      valueOnly?: boolean;\n    };\n\nconst parseItemLabel = async (\n  itemLabel: ArrayItemLabel,\n  item: Record<string, unknown>\n): Promise<FormattedItemLabel[] | undefined> => {\n  if (!itemLabel.label) {\n    return;\n  }\n\n  if (itemLabel.label.startsWith(\"$value.\")) {\n    const path = itemLabel.label.split(\"$value.\")[1];\n    const value = get(item, path);\n    const node = hiddenChildrenFormKit.value?.at(path);\n\n    if (!node) {\n      return [\n        {\n          type: itemLabel.type,\n          value: String(value ?? \"\"),\n        } as FormattedItemLabel,\n      ];\n    }\n    const renderedValue = await renderItemLabelValue(node, value);\n    const castRenderedValueArray = Array.isArray(renderedValue)\n      ? renderedValue\n      : [renderedValue];\n    if (castRenderedValueArray.length === 0) {\n      return [\n        {\n          type: itemLabel.type,\n          value: String(value ?? \"\"),\n        } as FormattedItemLabel,\n      ];\n    }\n    return castRenderedValueArray.map((renderedValue) => {\n      return {\n        type: itemLabel.type,\n        value: isNil(renderedValue.value) ? value : renderedValue.value,\n        ...renderedValue,\n      } as FormattedItemLabel;\n    });\n  }\n};\n\nconst formatItemLabel = async (\n  item: Record<string, unknown>\n): Promise<FormattedItemLabel[]> => {\n  const defaultItemLabel = Object.keys(item).map((key) => {\n    return {\n      type: \"text\" as ArrayItemLabelType,\n      label: `$value.${key}`,\n    };\n  });\n  const itemLabels = props.node.props.itemLabels ?? defaultItemLabel;\n  if (itemLabels.length > 0) {\n    const results = await Promise.all<FormattedItemLabel[][]>(\n      itemLabels.map((label: ArrayItemLabel) => parseItemLabel(label, item))\n    ).then((results) => results.flat());\n\n    return results.filter(isNotNil);\n  }\n  return [];\n};\n\nconst itemValue = ref<Record<string, unknown>>({});\nconst currentEditIndex = ref<number>(-1);\n\nconst formattedItemLabels = ref<Map<string, FormattedItemLabel[]>>(new Map());\n\nconst updateFormattedLabels = async () => {\n  const newLabelsMap = new Map();\n\n  for (let index = 0; index < arrayValue.value.length; index++) {\n    const item = arrayValue.value[index];\n    const key = generateKey(item, index);\n    const labels = await formatItemLabel(item);\n    newLabelsMap.set(key, labels);\n  }\n\n  formattedItemLabels.value = newLabelsMap;\n};\n\nwatch(\n  () => arrayValue.value,\n  async () => {\n    await updateFormattedLabels();\n  },\n  { deep: true, immediate: false }\n);\n\nconst handleOpenArrayModal = (\n  item?: Record<string, unknown>,\n  index?: number\n) => {\n  itemValue.value = item ? cloneDeepWith(item) : {};\n  currentEditIndex.value = index ?? -1;\n  arrayModal.value = true;\n};\n\nconst handleCloseArrayModal = () => {\n  currentEditIndex.value = -1;\n  arrayModal.value = false;\n  arrayValue.value = [...props.node.value];\n};\n\nconst generateKey = (item: Record<string, unknown>, index: number) => {\n  return `${objectHash(item)}-${index}`;\n};\n\nconst handleDragUpdate = () => {\n  props.node.input(arrayValue.value, false);\n};\n\nconst handleRemoveItem = (index: number) => {\n  if (arrayValue.value.length <= nodeProps.value.min) {\n    return;\n  }\n  arrayValue.value.splice(index, 1);\n  props.node.input(arrayValue.value, false);\n};\n</script>\n<template>\n  <div class=\"mt-4 w-full space-y-2 sm:max-w-lg\">\n    <VueDraggable\n      v-if=\"arrayValue.length > 0\"\n      v-model=\"arrayValue\"\n      :animation=\"150\"\n      class=\"flex flex-col gap-1\"\n      handle=\".drag-handle\"\n      @update=\"handleDragUpdate\"\n    >\n      <div\n        v-for=\"(item, index) in arrayValue\"\n        :key=\"generateKey(item, index)\"\n        class=\"group/item flex h-12 items-center gap-2.5 rounded-md border bg-gray-50 px-2 transition-colors hover:bg-gray-100 active:bg-gray-200\"\n        @click=\"handleOpenArrayModal(item, index)\"\n      >\n        <MingcuteDotsLine class=\"drag-handle size-4.5 flex-none cursor-move\" />\n        <div\n          class=\"line-clamp-1 flex min-w-0 flex-1 shrink cursor-pointer items-center gap-2 whitespace-nowrap text-sm text-gray-900\"\n        >\n          <template\n            v-for=\"(itemLabel, labelIndex) in formattedItemLabels.get(\n              generateKey(item, index)\n            ) || []\"\n            :key=\"labelIndex\"\n          >\n            <a\n              v-if=\"itemLabel.type === 'image'\"\n              :href=\"itemLabel.value\"\n              target=\"_blank\"\n              class=\"block aspect-1 size-8 flex-none\"\n              @click.stop\n            >\n              <img\n                v-tooltip=\"\n                  $t('core.formkit.array.image_tooltip', {\n                    value: itemLabel.value,\n                  })\n                \"\n                :src=\"utils.attachment.getThumbnailUrl(itemLabel.value, 'S')\"\n                class=\"size-full object-cover\"\n              />\n            </a>\n            <span v-if=\"itemLabel.type === 'text'\">\n              {{ itemLabel.value }}\n            </span>\n            <ColorLabel\n              v-if=\"itemLabel.type === 'color'\"\n              :item-label=\"itemLabel\"\n            />\n            <IconifyLabel\n              v-if=\"itemLabel.type === 'iconify'\"\n              :item-label=\"itemLabel\"\n            />\n          </template>\n        </div>\n        <IconClose\n          v-if=\"nodeProps.removeControl\"\n          :disabled=\"arrayValue.length <= nodeProps.min\"\n          class=\"size-4.5 flex-none cursor-pointer text-gray-500 opacity-0 transition-opacity hover:text-gray-900 group-hover/item:opacity-100\"\n          :class=\"{\n            'pointer-events-none cursor-not-allowed opacity-50 hover:text-gray-500 group-hover/item:opacity-50':\n              arrayValue.length <= nodeProps.min,\n          }\"\n          @click.stop=\"handleRemoveItem(index)\"\n        />\n      </div>\n    </VueDraggable>\n    <div v-else class=\"text-sm text-gray-500\">\n      {{ nodeProps.emptyText ?? $t(\"core.formkit.array.empty_text\") }}\n    </div>\n    <VButton\n      v-if=\"nodeProps.addButton\"\n      v-bind=\"nodeProps.addAttrs\"\n      :disabled=\"arrayValue.length >= nodeProps.max\"\n      size=\"sm\"\n      type=\"default\"\n      @click=\"handleOpenArrayModal()\"\n    >\n      {{ nodeProps.addLabel ?? $t(\"core.common.buttons.add\") }}\n    </VButton>\n  </div>\n\n  <FormKit v-show=\"false\" :id=\"formKitChildrenId\" type=\"group\" ignore=\"true\">\n    <component :is=\"node.context?.slots.default\" />\n  </FormKit>\n\n  <ArrayFormModal\n    v-if=\"arrayModal\"\n    :node=\"node\"\n    :item-value=\"itemValue\"\n    :current-edit-index=\"currentEditIndex\"\n    @close=\"handleCloseArrayModal\"\n  />\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/index.ts",
    "content": "import type {\n  FormKitFrameworkContext,\n  FormKitTypeDefinition,\n} from \"@formkit/core\";\nimport {\n  help,\n  label,\n  message,\n  messages,\n  outer,\n  wrapper,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport { defineAsyncComponent } from \"vue\";\nimport { arraySection } from \"./sections/index\";\n/**\n * Input definition for a array input.\n * @public\n */\nexport const array: FormKitTypeDefinition = {\n  /**\n   * The actual schema of the input, or a function that returns the schema.\n   */\n  schema: outer(\n    wrapper(label(\"$label\"), help(\"$help\"), arraySection()),\n    messages(message(\"$message.value\"))\n  ),\n\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"list\",\n  /**\n   * An array of extra props to accept for this input.\n   */\n  props: [\n    \"min\",\n    \"max\",\n    \"removeControl\",\n    \"addLabel\",\n    \"addButton\",\n    \"itemLabels\",\n    \"emptyText\",\n  ],\n  /**\n   * Additional features that make this input work.\n   */\n  library: {\n    ArrayInput: defineAsyncComponent(() => import(\"./ArrayInput.vue\")),\n  },\n};\n\nexport type ArrayItemLabelType = \"image\" | \"text\" | \"iconify\" | \"color\";\n\nexport type ArrayItemLabel = {\n  type: ArrayItemLabelType;\n  label: string;\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    array: {\n      type: \"array\";\n      value?: Record<string, unknown>[];\n      itemLabels?: {\n        type: ArrayItemLabelType;\n        label: string;\n      }[];\n    };\n  }\n\n  export interface FormKitInputSlots<Props extends FormKitInputs<Props>> {\n    array: {\n      default: FormKitFrameworkContext<Record<string, unknown>>;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/labels/ColorLabel.vue",
    "content": "<script lang=\"ts\" setup>\nimport { TinyColor } from \"@ctrl/tinycolor\";\nimport { computed } from \"vue\";\nimport type { ArrayItemLabelType } from \"..\";\n\nconst props = defineProps<{\n  itemLabel: {\n    type: ArrayItemLabelType;\n    value: string;\n  };\n}>();\n\nconst colorValue = computed(() => {\n  const value = props.itemLabel.value;\n  if (!value) {\n    return null;\n  }\n\n  // Validate and sanitize color value using TinyColor\n  const color = new TinyColor(value);\n  if (!color.isValid) {\n    return null;\n  }\n\n  // Return the validated color in hex format to prevent CSS injection\n  return color.toHexString();\n});\n</script>\n<template>\n  <div\n    v-if=\"colorValue\"\n    class=\"size-4 rounded-full border border-gray-200\"\n    :style=\"{\n      backgroundColor: colorValue,\n    }\"\n  ></div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/labels/IconifyLabel.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Icon } from \"@iconify/vue\";\nimport { computed } from \"vue\";\nimport type { ArrayItemLabelType } from \"..\";\nimport type { IconifyFormat, IconifyValue } from \"../../iconify/types\";\n\nconst props = defineProps<{\n  itemLabel: {\n    type: ArrayItemLabelType;\n    value: string | IconifyValue;\n    format: IconifyFormat;\n    valueOnly?: boolean;\n  };\n}>();\n\nconst value = computed(() => {\n  if (props.itemLabel.valueOnly) {\n    return props.itemLabel.value as string;\n  }\n  return (props.itemLabel.value as IconifyValue)?.value;\n});\n</script>\n<template>\n  <div class=\"inline-flex items-center [&>*]:size-4\">\n    <img\n      v-if=\"['url', 'dataurl'].includes(itemLabel.format)\"\n      :src=\"value\"\n      class=\"max-w-none\"\n    />\n    <Icon v-else-if=\"itemLabel.format === 'name'\" :icon=\"value\" />\n    <div\n      v-else\n      class=\"inline-flex items-center justify-center\"\n      v-html=\"value\"\n    ></div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/attachment.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport type { LabelValueResult } from \"./types\";\n\nexport async function renderAttachmentLabelValue({\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): Promise<LabelValueResult | LabelValueResult[]> {\n  const castValueArray = Array.isArray(value) ? value : [value];\n  return castValueArray.map((v) => {\n    return {\n      value: v,\n    };\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/category-select.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { findOptions } from \"./helpers/findOption\";\nimport type { LabelValueResult } from \"./types\";\n\n/**\n * Render category select label value\n *\n */\nexport async function renderCategorySelectLabelValue({\n  node,\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): Promise<LabelValueResult> {\n  const options = node.context?.options;\n  if (!options || options.length === 0) {\n    return {\n      value,\n    };\n  }\n\n  if (typeof value === \"string\") {\n    const categorySelectSelectedOption = findOptions(options, value);\n    if (categorySelectSelectedOption) {\n      return {\n        value: categorySelectSelectedOption.label,\n      };\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return {\n      value: value.map((v) => {\n        const categorySelectSelectedOption = findOptions(options, v);\n        return categorySelectSelectedOption?.label;\n      }),\n    };\n  }\n\n  return {\n    value,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/checkbox.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { findOptions } from \"./helpers/findOption\";\nimport type { LabelValueResult } from \"./types\";\n\n/**\n * Render checkbox label value\n *\n */\nexport async function renderCheckboxLabelValue({\n  node,\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): Promise<LabelValueResult> {\n  const options = node.context?.attrs.options;\n  if (!options || options.length === 0) {\n    return {\n      value,\n    };\n  }\n\n  if (typeof value === \"string\") {\n    const checkboxSelectedOption = findOptions(options, value);\n    if (checkboxSelectedOption) {\n      return {\n        value: checkboxSelectedOption.label,\n      };\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return {\n      value: value.map((v) => {\n        const checkboxSelectedOption = findOptions(options, v);\n        return checkboxSelectedOption?.label;\n      }),\n    };\n  }\n\n  return {\n    value,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/helpers/findOption.ts",
    "content": "/**\n * From options to find\n *\n * The options can be provided in 3 ways:\n * An array of strings, example: ['teddy', 'bear']\n * An object of value/label pairs, example: [{ teddy: 'Bear', bear: 'Teddy' }]\n * An array of objects with label and value properties, example: [{ label: 'Shawshank redemption', value: 'shawshank' }]\n */\nexport function findOptions(\n  options: unknown[] | Record<string, string>,\n  value: unknown\n):\n  | {\n      label: string;\n      value: unknown;\n      group?: unknown;\n    }\n  | undefined {\n  if (!options || options.length === 0) {\n    return;\n  }\n\n  if (options instanceof Array) {\n    return findOptionsInArray(options, value);\n  }\n\n  if (options instanceof Object) {\n    return findOptionsInRecord(options, value);\n  }\n}\n\nexport function findOptionsInRecord(\n  options: Record<string, string>,\n  value: unknown\n):\n  | {\n      label: string;\n      value: unknown;\n      group?: unknown;\n    }\n  | undefined {\n  for (const [optionKey, optionValue] of Object.entries(options)) {\n    if (optionKey === value) {\n      return {\n        label: optionKey,\n        value: optionValue,\n      };\n    }\n  }\n}\nexport function findOptionsInArray(\n  options: unknown[],\n  value: unknown\n):\n  | {\n      label: string;\n      value: unknown;\n      group?: unknown;\n    }\n  | undefined {\n  for (const option of options) {\n    if (!option) {\n      continue;\n    }\n\n    // string option\n    // example: ['teddy', 'bear']\n    if (typeof option === \"string\") {\n      if (option === value) {\n        return {\n          label: option,\n          value: option,\n        };\n      }\n    }\n\n    // objects with label and value properties\n    // example: [{ label: 'Shawshank redemption', value: 'shawshank' }]\n    if (typeof option === \"object\") {\n      if (\"value\" in option && \"label\" in option) {\n        if (option.value === value) {\n          return {\n            label: option.label as string,\n            value: option.value,\n          };\n        }\n      }\n\n      // group option\n      // example: [{ group: 'Movies', options: [{ label: 'Shawshank redemption', value: 'shawshank' }] }]\n      if (\"group\" in option && \"options\" in option) {\n        const options = option.options as unknown[] | Record<string, string>;\n        const groupOptions = findOptions(options, value);\n        if (groupOptions) {\n          return {\n            label: groupOptions.label,\n            value: groupOptions.value,\n            group: option.group,\n          };\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/iconify.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport type { LabelValueResult } from \"./types\";\n\n/**\n * Render iconify type label value\n */\nexport function renderIconifyLabelValue({\n  node,\n}: {\n  node: FormKitNode<unknown>;\n}): LabelValueResult {\n  const format = node.props.format;\n  const valueOnly = node.props.valueOnly;\n  return {\n    format,\n    valueOnly,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/index.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { isNil } from \"es-toolkit\";\nimport { renderAttachmentLabelValue } from \"./attachment\";\nimport { renderCategorySelectLabelValue } from \"./category-select\";\nimport { renderCheckboxLabelValue } from \"./checkbox\";\nimport { renderIconifyLabelValue } from \"./iconify\";\nimport { renderNativeSelectLabelValue } from \"./native-select\";\nimport { renderRadioLabelValue } from \"./radio\";\nimport { renderSelectLabelValue } from \"./select\";\nimport { renderTagSelectLabelValue } from \"./tag-select\";\nimport { renderToggleLabelValue } from \"./toggle\";\nimport type { LabelValueRenderer, LabelValueResult } from \"./types\";\n\n/**\n * Default renderer for unknown types\n */\nconst defaultRenderer: LabelValueRenderer = ({\n  value,\n}: {\n  value: unknown;\n}): LabelValueResult => {\n  return isNil(value) ? { value: \"\" } : { value: String(value) };\n};\n\n/**\n * Renderer registry\n */\nconst rendererRegistry = new Map<string, LabelValueRenderer>([\n  [\"select\", renderSelectLabelValue],\n  [\"nativeSelect\", renderNativeSelectLabelValue],\n  [\"iconify\", renderIconifyLabelValue],\n  [\"checkbox\", renderCheckboxLabelValue],\n  [\"radio\", renderRadioLabelValue],\n  [\"toggle\", renderToggleLabelValue],\n  [\"tagSelect\", renderTagSelectLabelValue],\n  [\"categorySelect\", renderCategorySelectLabelValue],\n  [\"attachment\", renderAttachmentLabelValue],\n]);\n\n/**\n * Register a custom renderer for a specific node type\n */\nexport function registerRenderer(\n  nodeType: string,\n  renderer: LabelValueRenderer\n): void {\n  rendererRegistry.set(nodeType, renderer);\n}\n\n/**\n * Render item label value based on node type\n */\nexport async function renderItemLabelValue(\n  node: FormKitNode<unknown>,\n  value: unknown\n): Promise<LabelValueResult | LabelValueResult[]> {\n  const originalType = node.props.originalType;\n  const renderer = rendererRegistry.get(originalType);\n  if (renderer) {\n    return await renderer({ node, value });\n  }\n  const nodeType = node.props.type as string;\n  const typeNodeRenderer = rendererRegistry.get(nodeType) || defaultRenderer;\n  return await typeNodeRenderer({ node, value });\n}\n\nexport type { LabelValueRenderer, LabelValueResult };\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/native-select.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { findOptions } from \"./helpers/findOption.js\";\nimport type { LabelValueResult } from \"./types.js\";\n\n/**\n * Render native select type label value\n */\nexport function renderNativeSelectLabelValue({\n  node,\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): LabelValueResult {\n  const options = node.context?.options as unknown[] | Record<string, string>;\n\n  if (typeof value === \"string\") {\n    const selectedOption = findOptions(options, value);\n    if (selectedOption) {\n      return {\n        value: selectedOption.group\n          ? `${selectedOption.group}/${selectedOption.label}`\n          : selectedOption.label,\n      };\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return {\n      value: value.map((v) => {\n        const selectedOption = findOptions(options, v);\n        return selectedOption?.label;\n      }),\n    };\n  }\n\n  return {\n    value,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/radio.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { findOptions } from \"./helpers/findOption\";\nimport type { LabelValueResult } from \"./types\";\n\n/**\n * Render radio label value\n *\n */\nexport async function renderRadioLabelValue({\n  node,\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): Promise<LabelValueResult> {\n  const options = node.context?.attrs.options;\n  if (!options || options.length === 0) {\n    return {\n      value,\n    };\n  }\n  const radioSelectedOption = findOptions(options, value);\n  if (radioSelectedOption) {\n    return {\n      value: radioSelectedOption.label,\n    };\n  }\n  return {\n    value,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/select.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { axiosInstance } from \"@halo-dev/api-client\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { get, has } from \"es-toolkit/compat\";\nimport type { PropertyPath } from \"lodash-es\";\nimport { findOptions } from \"./helpers/findOption\";\nimport type { LabelValueResult } from \"./types\";\n\nconst formatOptionsData = (\n  items: Array<object>,\n  labelField: PropertyPath,\n  valueField: PropertyPath\n): Array<{ label: string; value: unknown }> | undefined => {\n  if (!items) {\n    console.warn(\n      \"Select options: data items are empty, please check the itemsField.\"\n    );\n    return [];\n  }\n  return items.map((item) => {\n    if (!has(item, labelField as PropertyPath)) {\n      console.error(\n        `labelField: ${labelField?.toString()} not found in response data items.`\n      );\n      return { label: \"\", value: \"\" };\n    }\n    if (!has(item, valueField as PropertyPath)) {\n      console.error(\n        `valueField: ${valueField?.toString()} not found in response data items.`\n      );\n      return { label: \"\", value: \"\" };\n    }\n    return {\n      label: get(item, labelField),\n      value: get(item, valueField),\n    };\n  });\n};\n\n/**\n * Parse action remote select response data\n */\nconst parseActionRemoteSelectResponse = (\n  node: FormKitNode<unknown>,\n  data: unknown\n): Array<{ label: string; value: unknown }> | undefined => {\n  const { requestOption } = node.props;\n  const { parseData } = requestOption;\n  if (parseData) {\n    return parseData(data);\n  }\n  const { labelField, valueField, itemsField } = requestOption;\n  if (!has(data, itemsField as PropertyPath)) {\n    console.error(\n      `itemsField: ${itemsField?.toString()} not found in response data.`\n    );\n    return;\n  }\n  const items = get(data, itemsField as PropertyPath);\n  return formatOptionsData(\n    items,\n    labelField as PropertyPath,\n    valueField as PropertyPath\n  );\n};\n\n/**\n * Fetch remote mapped options through action property\n */\nconst fetchRemoteMappedOptions = async (\n  node: FormKitNode<unknown>,\n  unmappedSelectValues: unknown[]\n): Promise<Array<{ label: string; value: unknown }> | undefined> => {\n  const { requestOption, action } = node.props;\n  if (!requestOption || !action) {\n    return;\n  }\n  const requestConfig: AxiosRequestConfig = {\n    method: requestOption?.method || \"GET\",\n    url: action,\n  };\n  if (requestConfig.method === \"GET\") {\n    requestConfig.params = {\n      fieldSelector: `${requestOption?.fieldSelectorKey?.toString()}=(${unmappedSelectValues.join(\n        \",\"\n      )})`,\n    };\n  } else {\n    requestConfig.data = {\n      fieldSelector: `${requestOption?.fieldSelectorKey?.toString()}=(${unmappedSelectValues.join(\n        \",\"\n      )})`,\n    };\n  }\n  const response = await axiosInstance.request(requestConfig);\n  const { data } = response;\n  return parseActionRemoteSelectResponse(node, data);\n};\n\n/**\n * Find the selected option by remote data fetching\n */\nasync function findSelectedOptionByRemote(\n  node: FormKitNode<unknown>,\n  value: unknown\n): Promise<\n  | {\n      label: string;\n      value: unknown;\n      group?: unknown;\n    }\n  | undefined\n> {\n  // select remote option\n  if (\"remote\" in node.props && node.props.remote) {\n    const { search, findOptionsByValues } = node.props.remoteOption;\n    if (findOptionsByValues) {\n      const options = await findOptionsByValues([value]);\n      if (options.length > 0) {\n        return {\n          label: options[0].label,\n          value: options[0].value,\n        };\n      }\n    }\n    if (search) {\n      const options = await search({\n        keyword: \"\",\n        page: 1,\n        size: 20,\n      });\n      if (options.length > 0) {\n        const selectedOption = findOptions(options, value);\n        if (selectedOption) {\n          return selectedOption;\n        }\n      }\n    }\n  }\n\n  // select action option\n  if (\"action\" in node.props && node.props.action) {\n    const mappedOptions = await fetchRemoteMappedOptions(node, [value]);\n    if (mappedOptions) {\n      const selectedOption = findOptions(mappedOptions, value);\n      if (selectedOption) {\n        return selectedOption;\n      }\n    }\n  }\n}\n\n/**\n * Render select label value\n *\n * Select has multiple ways to get options, such as:\n * - Get options through the `remote` property\n * - Get options through the `action` property\n * - Get options through the `options` property\n */\nexport async function renderSelectLabelValue({\n  node,\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): Promise<LabelValueResult> {\n  const remoteSelectedOption = await findSelectedOptionByRemote(node, value);\n  if (remoteSelectedOption) {\n    return {\n      value: remoteSelectedOption.label,\n    };\n  }\n  const options = node.context?.attrs.options;\n  if (typeof value === \"string\") {\n    const selectedOption = findOptions(options, value);\n    if (selectedOption) {\n      return {\n        value: selectedOption.label,\n      };\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return {\n      value: value.map((v) => {\n        const selectedOption = findOptions(options, v);\n        return selectedOption?.label;\n      }),\n    };\n  }\n\n  return {\n    value,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/tag-select.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { findOptions } from \"./helpers/findOption\";\nimport type { LabelValueResult } from \"./types\";\n\n/**\n * Render tag select label value\n *\n */\nexport async function renderTagSelectLabelValue({\n  node,\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): Promise<LabelValueResult> {\n  const options = node.context?.options;\n  if (!options || options.length === 0) {\n    return {\n      value,\n    };\n  }\n\n  if (typeof value === \"string\") {\n    const tagSelectSelectedOption = findOptions(options, value);\n    if (tagSelectSelectedOption) {\n      return {\n        value: tagSelectSelectedOption.label,\n      };\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return {\n      value: value.map((v) => {\n        const tagSelectSelectedOption = findOptions(options, v);\n        return tagSelectSelectedOption?.label;\n      }),\n    };\n  }\n\n  return {\n    value,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/toggle.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { findOptions } from \"./helpers/findOption\";\nimport type { LabelValueResult } from \"./types\";\n\nexport async function renderToggleLabelValue({\n  node,\n  value,\n}: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}): Promise<LabelValueResult> {\n  const options = node.context?.options;\n  if (!options || options.length === 0) {\n    return {\n      value,\n    };\n  }\n  if (typeof value === \"string\") {\n    const toggleSelectedOption = findOptions(options, value);\n    if (toggleSelectedOption) {\n      return {\n        value: toggleSelectedOption.label,\n      };\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return {\n      value: value.map((v) => {\n        const toggleSelectedOption = findOptions(options, v);\n        return toggleSelectedOption?.label;\n      }),\n    };\n  }\n\n  return {\n    value,\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/renderers/types.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\n\nexport type LabelValueResult = { value?: unknown } & Record<string, unknown>;\n\nexport type LabelValueRenderer = (params: {\n  node: FormKitNode<unknown>;\n  value: unknown;\n}) =>\n  | Promise<LabelValueResult | LabelValueResult[]>\n  | LabelValueResult\n  | LabelValueResult[];\n"
  },
  {
    "path": "ui/src/formkit/inputs/array/sections/index.ts",
    "content": "import { createRepeaterSection } from \"../../repeater/repeaterSection\";\n\nconst repeaterSection = createRepeaterSection();\n\nexport const arraySection = repeaterSection(\"arraySection\", () => ({\n  $cmp: \"ArrayInput\",\n  props: {\n    node: \"$node\",\n  },\n}));\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment/AttachmentDropdownItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdownItem } from \"@halo-dev/components\";\nimport type { AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { ref } from \"vue\";\n\ndefineProps<{\n  multiple: boolean;\n  accepts: string[];\n}>();\n\nconst emit = defineEmits<{\n  (event: \"selected\", attachments: AttachmentLike[]): void;\n}>();\n\nconst selectorModalVisible = ref(false);\n\nfunction onAttachmentsSelect(attachments: AttachmentLike[]) {\n  if (!attachments.length) {\n    return;\n  }\n  emit(\"selected\", attachments);\n}\n</script>\n<template>\n  <VDropdownItem @click=\"selectorModalVisible = true\">\n    {{ $t(\"core.formkit.attachment.operations.select\") }}\n  </VDropdownItem>\n\n  <AttachmentSelectorModal\n    v-if=\"selectorModalVisible\"\n    :max=\"multiple ? undefined : 1\"\n    :accepts=\"accepts\"\n    @select=\"onAttachmentsSelect\"\n    @close=\"selectorModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment/AttachmentInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport {\n  IconAddCircle,\n  VDropdown,\n  VDropdownDivider,\n  VDropdownItem,\n} from \"@halo-dev/components\";\nimport { utils, type AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { computed, useTemplateRef, type PropType } from \"vue\";\nimport { useDraggable } from \"vue-draggable-plus\";\nimport MingcuteMore2Line from \"~icons/mingcute/more-2-line\";\nimport AttachmentDropdownItem from \"./AttachmentDropdownItem.vue\";\nimport AttachmentPreview from \"./AttachmentPreview.vue\";\nimport CustomLinkDropdownItem from \"./CustomLinkDropdownItem.vue\";\nimport UploadDropdownItem from \"./UploadDropdownItem.vue\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst multiple = computed(() => props.context.multiple as boolean);\nconst width = computed(() => props.context.width as string);\nconst aspectRatio = computed(() => props.context.aspectRatio as string);\nconst accepts = computed(() => {\n  return props.context.accepts as string[];\n});\n\nconst currentValue = computed({\n  get() {\n    if (!props.context._value) {\n      return [];\n    }\n    if (multiple.value) {\n      return props.context._value as string[];\n    }\n    return [props.context._value as string];\n  },\n  set(value: string[]) {\n    props.context.node.input(value);\n  },\n});\n\nfunction onAttachmentsSelect(attachments: AttachmentLike[]) {\n  if (!attachments.length) {\n    return;\n  }\n  if (multiple.value) {\n    props.context.node.input([\n      ...currentValue.value,\n      ...attachments\n        .map((attachment) => utils.attachment.getUrl(attachment))\n        .filter(Boolean),\n    ]);\n  } else {\n    props.context.node.input(utils.attachment.getUrl(attachments[0]));\n  }\n}\n\nfunction onAttachmentReplace(index: number, attachments: AttachmentLike[]) {\n  if (!attachments.length) {\n    return;\n  }\n  if (multiple.value) {\n    const newAttachments = [...currentValue.value];\n\n    const url = utils.attachment.getUrl(attachments[0]);\n    if (!url) {\n      return;\n    }\n\n    newAttachments[index] = url;\n    props.context.node.input(newAttachments);\n  } else {\n    props.context.node.input(utils.attachment.getUrl(attachments[0]));\n  }\n}\n\nconst handleRemove = (index: number) => {\n  if (multiple.value) {\n    props.context.node.input(currentValue.value.filter((_, i) => i !== index));\n  } else {\n    props.context.node.input(\"\");\n  }\n};\n\nfunction onCustomLinkSubmit(url: string) {\n  if (multiple.value) {\n    props.context.node.input([...currentValue.value, url]);\n  } else {\n    props.context.node.input(url);\n  }\n}\n\nfunction onLinkReplace(index: number, url: string) {\n  if (multiple.value) {\n    const newAttachments = [...currentValue.value];\n    newAttachments[index] = url;\n    props.context.node.input(newAttachments);\n  } else {\n    props.context.node.input(url);\n  }\n}\n\n// Drag\nconst container = useTemplateRef<HTMLDivElement>(\"container\");\n\nuseDraggable(container, currentValue, {\n  disabled: !multiple.value,\n  draggable: \"[data-draggable='true']\",\n});\n\n// Permission\nconst canUploadAttachment = computed(() => {\n  return utils.permission.has([\n    \"system:attachments:manage\",\n    \"uc:attachments:manage\",\n  ]);\n});\n\nconst canViewAttachment = computed(() => {\n  return utils.permission.has([\n    \"system:attachments:view\",\n    \"uc:attachments:manage\",\n  ]);\n});\n</script>\n<template>\n  <div ref=\"container\" class=\"inline-flex w-full flex-wrap gap-2\">\n    <div\n      v-for=\"(item, index) in currentValue\"\n      :key=\"item\"\n      data-draggable=\"true\"\n      class=\"group/attachment-item relative overflow-hidden rounded-lg border bg-white\"\n      :style=\"{ width: width, aspectRatio: aspectRatio }\"\n    >\n      <AttachmentPreview :url=\"item\" />\n\n      <!-- @vue-ignore -->\n      <VDropdown\n        class=\"absolute right-1 top-1 inline-flex\"\n        :dispose-timeout=\"null\"\n      >\n        <template #default=\"{ shown }\">\n          <button\n            type=\"button\"\n            class=\"inline-flex size-5 items-center justify-center rounded transition-all\"\n            :class=\"{\n              'bg-primary opacity-100': shown,\n              'bg-primary/80 opacity-0 hover:bg-primary active:bg-primary/80 group-hover/attachment-item:opacity-100':\n                !shown,\n            }\"\n          >\n            <MingcuteMore2Line class=\"size-4 text-white\" />\n          </button>\n        </template>\n        <template #popper>\n          <UploadDropdownItem\n            v-if=\"canUploadAttachment\"\n            :multiple=\"false\"\n            :accepts=\"accepts\"\n            @selected=\"(attachments) => onAttachmentReplace(index, attachments)\"\n          />\n          <AttachmentDropdownItem\n            v-if=\"canViewAttachment\"\n            :multiple=\"false\"\n            :accepts=\"accepts\"\n            @selected=\"(attachments) => onAttachmentReplace(index, attachments)\"\n          />\n          <CustomLinkDropdownItem\n            :url=\"item\"\n            @submit=\"(url) => onLinkReplace(index, url)\"\n          />\n          <VDropdownDivider />\n          <VDropdownItem type=\"danger\" @click=\"handleRemove(index)\">\n            {{ $t(\"core.common.buttons.remove\") }}\n          </VDropdownItem>\n        </template>\n      </VDropdown>\n    </div>\n\n    <!-- @vue-ignore -->\n    <VDropdown\n      v-if=\"multiple || currentValue.length === 0\"\n      class=\"inline-flex\"\n      :style=\"{ width: width, aspectRatio: aspectRatio }\"\n      :dispose-timeout=\"null\"\n    >\n      <button\n        type=\"button\"\n        class=\"group/trigger inline-flex size-full items-center justify-center rounded-lg border border-dashed transition-colors hover:border-primary\"\n      >\n        <IconAddCircle\n          class=\"text-gray-600 transition-colors group-hover/trigger:text-primary\"\n        />\n      </button>\n      <template #popper>\n        <UploadDropdownItem\n          v-if=\"canUploadAttachment\"\n          :multiple=\"multiple\"\n          :accepts=\"accepts\"\n          @selected=\"onAttachmentsSelect\"\n        />\n        <AttachmentDropdownItem\n          v-if=\"canViewAttachment\"\n          :multiple=\"multiple\"\n          :accepts=\"accepts\"\n          @selected=\"onAttachmentsSelect\"\n        />\n        <CustomLinkDropdownItem @submit=\"onCustomLinkSubmit\" />\n      </template>\n    </VDropdown>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment/AttachmentPreview.vue",
    "content": "<script lang=\"ts\" setup>\nimport { GetThumbnailByUriSizeEnum } from \"@halo-dev/api-client\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport mime from \"mime/lite\";\nimport { computed } from \"vue\";\nimport LazyVideo from \"@/components/video/LazyVideo.vue\";\nimport { isImage } from \"@/utils/image\";\n\nconst props = defineProps<{\n  url: string;\n}>();\n\nconst mediaType = computed(() => {\n  return mime.getType(props.url);\n});\n</script>\n\n<template>\n  <img\n    v-if=\"isImage(mediaType)\"\n    :src=\"utils.attachment.getThumbnailUrl(url, GetThumbnailByUriSizeEnum.S)\"\n    class=\"size-full object-cover\"\n  />\n  <LazyVideo\n    v-else-if=\"mediaType?.startsWith('video/')\"\n    classes=\"size-full object-cover\"\n    :src=\"url\"\n  />\n  <AttachmentFileTypeIcon v-else :file-name=\"url\" />\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment/CustomLinkDropdownItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton, VDropdown, VDropdownItem } from \"@halo-dev/components\";\nimport { useTemplateRef } from \"vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\ndefineProps<{\n  url?: string;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"submit\", url: string): void;\n}>();\n\nconst dropdown = useTemplateRef<InstanceType<typeof VDropdown>>(\"dropdown\");\n\nfunction onSubmit({ value }: { value: string }) {\n  emit(\"submit\", value.trim());\n  dropdown.value?.hide();\n}\n\nfunction onDropdownShown() {\n  setTimeout(() => {\n    setFocus(\"url\");\n  }, 100);\n}\n</script>\n<template>\n  <VDropdown ref=\"dropdown\" @show=\"onDropdownShown\">\n    <VDropdownItem>\n      {{ $t(\"core.formkit.attachment.operations.input\") }}\n    </VDropdownItem>\n    <template #popper>\n      <div class=\"w-96\">\n        <FormKit\n          id=\"custom-link-form\"\n          type=\"form\"\n          ignore\n          name=\"custom-link-form\"\n          @submit=\"onSubmit\"\n        >\n          <FormKit\n            id=\"url\"\n            type=\"text\"\n            :model-value=\"url\"\n            name=\"value\"\n            validation=\"required\"\n            validation-label=\"URL\"\n          />\n        </FormKit>\n        <div class=\"mt-4\">\n          <VButton\n            type=\"secondary\"\n            @click=\"$formkit.submit('custom-link-form')\"\n          >\n            {{ $t(\"core.common.buttons.save\") }}\n          </VButton>\n        </div>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment/UploadDropdownItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  consoleApiClient,\n  ucApiClient,\n  type Attachment,\n} from \"@halo-dev/api-client\";\nimport { VDropdownItem } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useFileDialog } from \"@vueuse/core\";\n\nconst props = defineProps<{\n  multiple: boolean;\n  accepts: string[];\n}>();\n\nconst emit = defineEmits<{\n  (event: \"selected\", attachments: Attachment[]): void;\n}>();\n\nconst { onChange: onFileInputChange, open: openFileInputDialog } =\n  useFileDialog({\n    accept: props.accepts.join(\", \"),\n    multiple: props.multiple,\n  });\n\nonFileInputChange(async (files) => {\n  if (!files?.length) {\n    return;\n  }\n\n  const attachments: Attachment[] = [];\n  for (const file of files) {\n    if (utils.permission.has([\"system:attachments:manage\"])) {\n      const { data } =\n        await consoleApiClient.storage.attachment.uploadAttachmentForConsole({\n          file: file,\n        });\n      attachments.push(data);\n    } else if (utils.permission.has([\"uc:attachments:manage\"])) {\n      const { data } =\n        await ucApiClient.storage.attachment.uploadAttachmentForUc({\n          file: file,\n        });\n      attachments.push(data);\n    } else {\n      throw new Error(\"No permission to upload attachment\");\n    }\n  }\n\n  emit(\"selected\", attachments);\n});\n</script>\n<template>\n  <VDropdownItem @click=\"openFileInputDialog()\">\n    {{ $t(\"core.common.buttons.upload\") }}\n  </VDropdownItem>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment/feature.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { undefine } from \"@formkit/utils\";\n\nexport default function attachmentFeature(node: FormKitNode): void {\n  node.on(\"created\", () => {\n    node.props.multiple = undefine(node.props.multiple);\n    node.props.width = node.props.width ?? \"5rem\";\n    node.props.aspectRatio = node.props.aspectRatio ?? \"1/1\";\n    node.props.accepts = node.props.accepts ?? [\"*\"];\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment/index.ts",
    "content": "import { initialValue, type FormKitInputs } from \"@formkit/inputs\";\nimport { createInput } from \"@formkit/vue\";\nimport { defineAsyncComponent } from \"vue\";\nimport attachmentFeature from \"./feature\";\n\nexport const attachment = createInput(\n  defineAsyncComponent(() => import(\"./AttachmentInput.vue\")),\n  {\n    type: \"input\",\n    props: [\"multiple\", \"width\", \"aspectRatio\", \"accepts\"],\n    features: [initialValue, attachmentFeature],\n  }\n);\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    attachment: {\n      type: \"attachment\";\n      value?: string | string[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment-group-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport type { FormKitInputs } from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type Group,\n  type GroupV1alpha1ApiListGroupRequest,\n} from \"@halo-dev/api-client\";\nimport { select } from \"./select\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const groups = await paginate<GroupV1alpha1ApiListGroupRequest, Group>(\n      (params) => coreApiClient.storage.group.listGroup(params),\n      {\n        labelSelector: [\"!halo.run/hidden\"],\n        sort: [\"metadata.creationTimestamp,desc\"],\n      }\n    );\n\n    if (node.context) {\n      node.context.attrs.options = groups.map((group) => {\n        return {\n          value: group.metadata.name,\n          label: group.spec.displayName,\n        };\n      });\n    }\n  });\n}\n\nexport const attachmentGroupSelect: FormKitTypeDefinition = {\n  ...select,\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    attachmentGroupSelect: {\n      type: \"attachmentGroupSelect\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment-input/AttachmentInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { IconFolder } from \"@halo-dev/components\";\nimport { utils, type AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { ref, type PropType } from \"vue\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst attachmentSelectorModalVisible = ref(false);\n\nconst onInput = (e: Event) => {\n  props.context.handlers.DOMInput(e);\n};\n\nconst onAttachmentSelect = (attachments: AttachmentLike[]) => {\n  if (!attachments.length) {\n    return;\n  }\n  const attachment = attachments[0];\n  props.context.node.input(utils.attachment.getUrl(attachment));\n};\n</script>\n\n<template>\n  <input\n    :id=\"context.id\"\n    :value=\"context._value\"\n    :class=\"context.classes.input\"\n    :name=\"context.node.name\"\n    v-bind=\"context.attrs\"\n    type=\"text\"\n    @blur=\"context.handlers.blur()\"\n    @input=\"onInput\"\n  />\n\n  <HasPermission\n    :permissions=\"['uc:attachments:manage', 'system:attachments:view']\"\n  >\n    <div\n      class=\"group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100\"\n      @click=\"attachmentSelectorModalVisible = true\"\n    >\n      <IconFolder class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\" />\n    </div>\n  </HasPermission>\n\n  <AttachmentSelectorModal\n    v-if=\"attachmentSelectorModalVisible\"\n    :accepts=\"context.accepts as string[]\"\n    :min=\"1\"\n    :max=\"1\"\n    @select=\"onAttachmentSelect\"\n    @close=\"attachmentSelectorModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment-input/index.ts",
    "content": "import { initialValue, type FormKitInputs } from \"@formkit/inputs\";\nimport { createInput } from \"@formkit/vue\";\nimport AttachmentInput from \"./AttachmentInput.vue\";\n\nexport const attachmentInput = createInput(AttachmentInput, {\n  type: \"input\",\n  props: [\"accepts\"],\n  forceTypeProp: \"text\",\n  features: [initialValue],\n});\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    attachmentInput: {\n      type: \"attachmentInput\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/attachment-policy-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport type { FormKitInputs } from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type Policy,\n  type PolicyV1alpha1ApiListPolicyRequest,\n} from \"@halo-dev/api-client\";\nimport { select } from \"./select\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const policies = await paginate<PolicyV1alpha1ApiListPolicyRequest, Policy>(\n      (params) => coreApiClient.storage.policy.listPolicy(params),\n      {\n        size: 1000,\n      }\n    );\n\n    if (node.context) {\n      node.context.attrs.options = policies.map((policy) => {\n        return {\n          value: policy.metadata.name,\n          label: policy.spec.displayName,\n        };\n      });\n    }\n  });\n}\n\nexport const attachmentPolicySelect: FormKitTypeDefinition = {\n  ...select,\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    attachmentPolicySelect: {\n      type: \"attachmentPolicySelect\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/category-checkbox.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  checkbox,\n  checkboxes,\n  defaultIcon,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type Category,\n  type CategoryV1alpha1ApiListCategoryRequest,\n} from \"@halo-dev/api-client\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const categories = await paginate<\n      CategoryV1alpha1ApiListCategoryRequest,\n      Category\n    >((params) => coreApiClient.content.category.listCategory(params), {\n      sort: [\"metadata.creationTimestamp,desc\"],\n    });\n\n    node.props.options = categories.map((category) => {\n      return {\n        value: category.metadata.name,\n        label: category.spec.displayName,\n      };\n    });\n  });\n}\n\nexport const categoryCheckbox: FormKitTypeDefinition = {\n  ...checkbox,\n  props: [\"onValue\", \"offValue\"],\n  forceTypeProp: \"checkbox\",\n  features: [\n    optionsHandler,\n    checkboxes,\n    defaultIcon(\"decorator\", \"checkboxDecorator\"),\n  ],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    categoryCheckbox: {\n      type: \"categoryCheckbox\";\n      value?: string[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/category-select/CategorySelect.vue",
    "content": "<script lang=\"ts\" setup>\nimport { usePostCategory } from \"@console/modules/contents/posts/categories/composables/use-post-category\";\nimport {\n  convertTreeToCategories,\n  type CategoryTreeNode,\n} from \"@console/modules/contents/posts/categories/utils\";\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport type { Category } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { IconArrowRight, VDropdown } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { onClickOutside, useResizeObserver } from \"@vueuse/core\";\nimport Fuse from \"fuse.js\";\nimport ShortUniqueId from \"short-unique-id\";\nimport { slugify } from \"transliteration\";\nimport {\n  computed,\n  provide,\n  ref,\n  useTemplateRef,\n  watch,\n  type PropType,\n  type Ref,\n} from \"vue\";\nimport CategoryListItem from \"./components/CategoryListItem.vue\";\nimport CategoryTag from \"./components/CategoryTag.vue\";\nimport SearchResultListItem from \"./components/SearchResultListItem.vue\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst multiple = computed(() => {\n  const { multiple } = props.context;\n  if (multiple === undefined) {\n    return false;\n  }\n  if (typeof multiple === \"boolean\") {\n    return multiple;\n  }\n  return multiple === \"true\";\n});\n\nconst { categories, categoriesTree, handleFetchCategories } = usePostCategory();\n\nprovide<Ref<CategoryTreeNode[]>>(\"categoriesTree\", categoriesTree);\n\nconst selectedCategory = ref<Category | CategoryTreeNode>();\n\nprovide<Ref<Category | CategoryTreeNode | undefined>>(\n  \"selectedCategory\",\n  selectedCategory\n);\n\nconst dropdownVisible = ref(false);\nconst text = ref(\"\");\nconst wrapperRef = useTemplateRef<HTMLElement>(\"wrapperRef\");\nconst popperRef = useTemplateRef<HTMLElement>(\"popperRef\");\n\n// resolve the issue of the dropdown position when the container size changes\n// https://github.com/Akryum/floating-vue/issues/977#issuecomment-1651898070\nuseResizeObserver(wrapperRef, () => {\n  window.dispatchEvent(new Event(\"resize\"));\n});\n\nonClickOutside(\n  wrapperRef,\n  () => {\n    dropdownVisible.value = false;\n  },\n  {\n    ignore: [popperRef],\n  }\n);\n\n// search\nlet fuse: Fuse<Category> | undefined = undefined;\n\nconst searchResults = computed(() => {\n  if (!text.value) {\n    return categories.value;\n  }\n  return fuse?.search(text.value).map((item) => item.item) || [];\n});\n\nwatch(\n  () => searchResults.value,\n  (value) => {\n    if (value?.length && text.value) {\n      selectedCategory.value = value[0];\n      scrollToSelected();\n    } else {\n      selectedCategory.value = undefined;\n    }\n  }\n);\n\nwatch(\n  () => categories.value,\n  () => {\n    fuse = new Fuse(categories.value || [], {\n      keys: [\"spec.displayName\", \"spec.slug\"],\n      useExtendedSearch: true,\n      threshold: 0.2,\n    });\n    if (props.context) {\n      // eslint-disable-next-line vue/no-mutating-props\n      props.context.options =\n        categories.value?.map((category) => {\n          return {\n            label: category.spec.displayName,\n            value: category.metadata.name,\n          };\n        }) || [];\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nconst selectedCategories = computed(() => {\n  if (multiple.value) {\n    const currentValue = props.context._value || [];\n    return currentValue\n      .map((categoryName): Category | undefined => {\n        return categories.value?.find(\n          (category) => category.metadata.name === categoryName\n        );\n      })\n      .filter(Boolean) as Category[];\n  }\n\n  const category = categories.value?.find(\n    (category) => category.metadata.name === props.context._value\n  );\n  return [category].filter(Boolean) as Category[];\n});\n\nconst isSelected = (category: CategoryTreeNode | Category) => {\n  if (multiple.value) {\n    return (props.context._value || []).includes(category.metadata.name);\n  }\n  return props.context._value === category.metadata.name;\n};\n\nprovide<(category: CategoryTreeNode | Category) => boolean>(\n  \"isSelected\",\n  isSelected\n);\n\nconst handleSelect = (category: CategoryTreeNode | Category) => {\n  if (multiple.value) {\n    const currentValue = props.context._value || [];\n    if (currentValue.includes(category.metadata.name)) {\n      props.context.node.input(\n        currentValue.filter((name: string) => name !== category.metadata.name)\n      );\n    } else {\n      props.context.node.input([...currentValue, category.metadata.name]);\n      text.value = \"\";\n    }\n    return;\n  }\n\n  props.context.node.input(\n    category.metadata.name === props.context._value\n      ? \"\"\n      : category.metadata.name\n  );\n};\n\nconst handleKeydown = (e: KeyboardEvent) => {\n  if (!searchResults.value) return;\n\n  if (e.key === \"ArrowDown\") {\n    e.preventDefault();\n\n    const categoryIndices = text.value\n      ? searchResults.value\n      : convertTreeToCategories(categoriesTree.value);\n\n    const index = categoryIndices.findIndex(\n      (category) =>\n        category.metadata.name === selectedCategory.value?.metadata.name\n    );\n\n    if (index < searchResults.value.length - 1) {\n      selectedCategory.value = categoryIndices[index + 1];\n    }\n    scrollToSelected();\n  }\n\n  if (e.key === \"ArrowUp\") {\n    e.preventDefault();\n\n    const categoryIndices = text.value\n      ? searchResults.value\n      : convertTreeToCategories(categoriesTree.value);\n\n    const index = categoryIndices.findIndex(\n      (category) =>\n        category.metadata.name === selectedCategory.value?.metadata.name\n    );\n    if (index > 0) {\n      selectedCategory.value = categoryIndices[index - 1];\n    } else {\n      selectedCategory.value = undefined;\n    }\n    scrollToSelected();\n  }\n\n  if (e.key === \"Enter\") {\n    if (!selectedCategory.value && text.value) {\n      handleCreateCategory();\n      return;\n    }\n\n    if (selectedCategory.value) {\n      handleSelect(selectedCategory.value);\n      text.value = \"\";\n      e.preventDefault();\n    }\n  }\n};\n\nconst scrollToSelected = () => {\n  const selectedNodeName = selectedCategory.value\n    ? selectedCategory.value?.metadata.name\n    : \"create\";\n  const selectedNode = document.getElementById(`category-${selectedNodeName}`);\n  if (selectedNode) {\n    selectedNode.scrollIntoView({\n      behavior: \"smooth\",\n      block: \"nearest\",\n      inline: \"start\",\n    });\n  }\n};\n\nconst uid = new ShortUniqueId();\n\nconst handleCreateCategory = async () => {\n  if (!utils.permission.has([\"system:posts:manage\"])) {\n    return;\n  }\n\n  let slug = slugify(text.value, { trim: true });\n\n  // Check if slug is unique, if not, add -1 to the slug\n  const { data: categoriesWithSameSlug } =\n    await coreApiClient.content.category.listCategory({\n      fieldSelector: [`spec.slug=${slug}`],\n      page: 1,\n      size: 1,\n    });\n\n  if (categoriesWithSameSlug.total) {\n    slug = `${slug}-${uid.randomUUID(8)}`;\n  }\n\n  const { data } = await coreApiClient.content.category.createCategory({\n    category: {\n      spec: {\n        displayName: text.value,\n        slug,\n        description: \"\",\n        cover: \"\",\n        template: \"\",\n        priority: categories.value?.length || 0 + 1,\n        children: [],\n      },\n      apiVersion: \"content.halo.run/v1alpha1\",\n      kind: \"Category\",\n      metadata: {\n        name: \"\",\n        generateName: \"category-\",\n      },\n    },\n  });\n\n  handleFetchCategories();\n  handleSelect(data);\n  text.value = \"\";\n};\n\n// update value immediately during IME composition\n// please see https://vuejs.org//guide/essentials/forms.html#text\nconst onTextInput = (e: Event) => {\n  text.value = (e.target as HTMLInputElement).value;\n};\n\n// delete last category when text input is empty\nconst handleDelete = () => {\n  if (!text.value) {\n    if (multiple.value) {\n      const selectedTagNames = (props.context._value as string[]) || [];\n      props.context.node.input(selectedTagNames.slice(0, -1));\n      return;\n    }\n    props.context.node.input(\"\");\n  }\n};\n</script>\n\n<template>\n  <VDropdown\n    :triggers=\"[]\"\n    :shown=\"dropdownVisible\"\n    auto-size\n    :auto-hide=\"false\"\n    container=\"body\"\n    :distance=\"10\"\n    class=\"w-full\"\n    popper-class=\"post-category-dropdown\"\n  >\n    <div ref=\"wrapperRef\" :class=\"context.classes['post-categories-wrapper']\">\n      <div :class=\"context.classes['post-categories']\">\n        <CategoryTag\n          v-for=\"(category, index) in selectedCategories\"\n          :key=\"index\"\n          :category=\"category\"\n          @select=\"handleSelect\"\n        />\n        <input\n          :value=\"text\"\n          :class=\"context.classes.input\"\n          type=\"text\"\n          @input=\"onTextInput\"\n          @focus=\"dropdownVisible = true\"\n          @keydown=\"handleKeydown\"\n          @keydown.delete=\"handleDelete\"\n        />\n      </div>\n\n      <div\n        :class=\"context.classes['post-categories-button']\"\n        @click=\"dropdownVisible = !dropdownVisible\"\n      >\n        <IconArrowRight class=\"rotate-90 text-gray-500 hover:text-gray-700\" />\n      </div>\n    </div>\n    <template #popper>\n      <div ref=\"popperRef\" :class=\"context.classes['dropdown-wrapper']\">\n        <ul class=\"p-1\">\n          <HasPermission\n            v-if=\"text.trim()\"\n            :permissions=\"['system:posts:manage']\"\n          >\n            <li\n              id=\"category-create\"\n              class=\"group flex cursor-pointer items-center justify-between rounded p-2\"\n              :class=\"{\n                'bg-gray-100': selectedCategory === undefined,\n              }\"\n              @click=\"handleCreateCategory\"\n            >\n              <span class=\"text-xs text-gray-700 group-hover:text-gray-900\">\n                {{\n                  $t(\"core.formkit.category_select.creation_label\", {\n                    text: text,\n                  })\n                }}\n              </span>\n            </li>\n          </HasPermission>\n\n          <template v-if=\"text\">\n            <SearchResultListItem\n              v-for=\"category in searchResults\"\n              :key=\"category.metadata.name\"\n              :category=\"category\"\n              @select=\"handleSelect\"\n            />\n          </template>\n          <template v-else>\n            <CategoryListItem\n              v-for=\"category in categoriesTree\"\n              :key=\"category.metadata.name\"\n              :category=\"category\"\n              @select=\"handleSelect\"\n            />\n          </template>\n        </ul>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n<style lang=\"scss\">\n.post-category-dropdown {\n  .v-popper__arrow-container {\n    display: none;\n  }\n  .v-popper__inner {\n    padding: 0 !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/src/formkit/inputs/category-select/components/CategoryListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { CategoryTreeNode } from \"@console/modules/contents/posts/categories/utils\";\nimport type { Category } from \"@halo-dev/api-client\";\nimport { IconCheckboxCircle } from \"@halo-dev/components\";\nimport { inject, ref, type Ref } from \"vue\";\n\nwithDefaults(\n  defineProps<{\n    category: CategoryTreeNode;\n  }>(),\n  {}\n);\n\nconst isSelected =\n  inject<(category: CategoryTreeNode) => boolean>(\"isSelected\");\nconst selectedCategory = inject<Ref<Category | CategoryTreeNode | undefined>>(\n  \"selectedCategory\",\n  ref(undefined)\n);\n\nconst emit = defineEmits<{\n  (event: \"select\", category: CategoryTreeNode): void;\n}>();\n\nconst onSelect = (childCategory: CategoryTreeNode) => {\n  emit(\"select\", childCategory);\n};\n</script>\n\n<template>\n  <li :id=\"`category-${category.metadata.name}`\">\n    <div\n      class=\"flex cursor-pointer items-center justify-between rounded p-2 hover:bg-gray-100\"\n      :class=\"{\n        'bg-gray-100':\n          selectedCategory?.metadata.name === category.metadata.name,\n      }\"\n      @click=\"emit('select', category)\"\n    >\n      <span\n        class=\"flex-1 truncate text-xs text-gray-700 group-hover:text-gray-900\"\n        :class=\"{\n          'text-gray-900':\n            isSelected?.(category) &&\n            selectedCategory?.metadata.name === category.metadata.name,\n        }\"\n      >\n        {{ category.spec.displayName }}\n      </span>\n\n      <IconCheckboxCircle\n        class=\"text-primary opacity-0\"\n        :class=\"{ 'opacity-100': isSelected?.(category) }\"\n      />\n    </div>\n\n    <ul\n      v-if=\"category.children.length > 0\"\n      class=\"my-2.5 ml-2.5 border-l pl-1.5\"\n    >\n      <CategoryListItem\n        v-for=\"(childCategory, index) in category.children\"\n        :key=\"index\"\n        :category=\"childCategory\"\n        @select=\"onSelect\"\n      />\n    </ul>\n  </li>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/category-select/components/CategoryTag.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  getCategoryPath,\n  type CategoryTreeNode,\n} from \"@console/modules/contents/posts/categories/utils\";\nimport type { Category } from \"@halo-dev/api-client\";\nimport { IconClose } from \"@halo-dev/components\";\nimport { computed, inject, ref, type Ref } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    category: Category;\n  }>(),\n  {}\n);\n\nconst emit = defineEmits<{\n  (event: \"select\", category: CategoryTreeNode | Category): void;\n}>();\n\nconst categoriesTree = inject<Ref<CategoryTreeNode[]>>(\n  \"categoriesTree\",\n  ref([])\n);\n\nconst label = computed(() => {\n  const categories = getCategoryPath(\n    categoriesTree.value,\n    props.category.metadata.name\n  );\n  return categories\n    ?.map((category: CategoryTreeNode) => category.spec.displayName)\n    .join(\" / \");\n});\n</script>\n\n<template>\n  <div class=\"inline-flex items-center p-1\">\n    <div\n      class=\"box-border inline-flex min-h-[1.25rem] items-center gap-1 rounded-full border border-solid border-[#d9d9d9] bg-white px-1 align-middle\"\n    >\n      <span class=\"flex-1 text-xs\">\n        {{ label }}\n      </span>\n      <IconClose\n        class=\"h-4 w-4 cursor-pointer text-gray-600 hover:text-gray-900\"\n        @click=\"emit('select', category)\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/category-select/components/SearchResultListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  getCategoryPath,\n  type CategoryTreeNode,\n} from \"@console/modules/contents/posts/categories/utils\";\nimport type { Category } from \"@halo-dev/api-client\";\nimport { IconCheckboxCircle } from \"@halo-dev/components\";\nimport { computed, inject, ref, type Ref } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    category: Category;\n  }>(),\n  {}\n);\n\nconst categoriesTree = inject<Ref<CategoryTreeNode[]>>(\n  \"categoriesTree\",\n  ref([])\n);\nconst selectedCategory = inject<Ref<Category | CategoryTreeNode | undefined>>(\n  \"selectedCategory\",\n  ref(undefined)\n);\n\nconst isSelected =\n  inject<(category: Category | CategoryTreeNode) => boolean>(\"isSelected\");\n\nconst emit = defineEmits<{\n  (event: \"select\", category: CategoryTreeNode | Category): void;\n}>();\n\nconst label = computed(() => {\n  const categories = getCategoryPath(\n    categoriesTree.value,\n    props.category.metadata.name\n  );\n  return categories\n    ?.map((category: CategoryTreeNode) => category.spec.displayName)\n    .join(\" / \");\n});\n</script>\n\n<template>\n  <li :id=\"`category-${category.metadata.name}`\">\n    <div\n      class=\"flex cursor-pointer items-center justify-between rounded p-2 hover:bg-gray-100\"\n      :class=\"{\n        'bg-gray-100':\n          selectedCategory?.metadata.name === category.metadata.name,\n      }\"\n      @click=\"emit('select', category)\"\n    >\n      <span\n        class=\"flex-1 truncate text-xs text-gray-700 group-hover:text-gray-900\"\n        :class=\"{\n          'text-gray-900':\n            isSelected?.(category) &&\n            selectedCategory?.metadata.name === category.metadata.name,\n        }\"\n      >\n        {{ label }}\n      </span>\n      <IconCheckboxCircle\n        class=\"h-5 w-5 text-primary opacity-0\"\n        :class=\"{ 'opacity-100': isSelected?.(category) }\"\n      />\n    </div>\n  </li>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/category-select/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  help,\n  icon,\n  inner,\n  label,\n  message,\n  messages,\n  outer,\n  prefix,\n  suffix,\n  wrapper,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport { defineAsyncComponent } from \"vue\";\nimport { CategorySelectSection } from \"./sections\";\n\nexport const categorySelect: FormKitTypeDefinition = {\n  schema: outer(\n    wrapper(\n      label(\"$label\"),\n      inner(\n        icon(\"prefix\"),\n        prefix(),\n        CategorySelectSection(),\n        suffix(),\n        icon(\"suffix\")\n      )\n    ),\n    help(\"$help\"),\n    messages(message(\"$message.value\"))\n  ),\n  type: \"input\",\n  props: [\"multiple\"],\n  library: {\n    CategorySelect: defineAsyncComponent(() => import(\"./CategorySelect.vue\")),\n  },\n  schemaMemoKey: \"custom-category-select\",\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    categorySelect: {\n      type: \"categorySelect\";\n      value?: string | string[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/category-select/sections/index.ts",
    "content": "import { createSection } from \"@formkit/inputs\";\n\nexport const CategorySelectSection = createSection(\n  \"CategorySelectSection\",\n  () => ({\n    $cmp: \"CategorySelect\",\n    props: {\n      context: \"$node.context\",\n    },\n  })\n);\n"
  },
  {
    "path": "ui/src/formkit/inputs/code/CodeInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { VButton, VPageHeader } from \"@halo-dev/components\";\nimport { useEventListener } from \"@vueuse/core\";\nimport { computed, ref, type PropType } from \"vue\";\nimport RiFullscreenLine from \"~icons/ri/fullscreen-line\";\nimport type { CodemirrorProps } from \"@/components/codemirror/supports\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst codeInputWrapperRef = ref();\n\nconst language = props.context.language as CodemirrorProps[\"language\"];\n\nconst onChange = (value: string) => {\n  props.context.node.input(value);\n};\n\nconst fullscreen = ref(false);\n\nconst height = computed(() => {\n  return fullscreen.value ? \"100%\" : (props.context.height as string);\n});\n\nuseEventListener(codeInputWrapperRef, \"keydown\", (e: KeyboardEvent) => {\n  if (e.key === \"Escape\" && fullscreen.value) {\n    fullscreen.value = false;\n  }\n});\n\nconst editorHeight = computed(() => {\n  if (fullscreen.value) {\n    // VPageHeader height is 3.5rem\n    return \"calc(100vh - 3.5rem)\";\n  }\n\n  return \"100%\";\n});\n</script>\n\n<template>\n  <Suspense>\n    <Teleport to=\"body\" :disabled=\"!fullscreen\">\n      <div\n        ref=\"codeInputWrapperRef\"\n        :style=\"{ height }\"\n        :class=\"{ '!fixed inset-0 z-[99999] !block bg-white': fullscreen }\"\n        class=\"group relative h-9 w-full\"\n      >\n        <VPageHeader v-if=\"fullscreen\" :title=\"context.label\" class=\"border-b\">\n          <template #actions>\n            <VButton @click=\"fullscreen = false\">\n              {{ $t(\"core.formkit.code.fullscreen.exit\") }}\n            </VButton>\n          </template>\n        </VPageHeader>\n\n        <VCodemirror\n          :model-value=\"props.context._value\"\n          v-bind=\"context.attrs\"\n          :height=\"editorHeight\"\n          :language=\"language\"\n          class=\"block w-full\"\n          @change=\"onChange\"\n        />\n\n        <button\n          v-if=\"!fullscreen\"\n          v-tooltip=\"$t('core.formkit.code.fullscreen.enter')\"\n          class=\"absolute bottom-2 right-2 inline-flex cursor-pointer items-center justify-center rounded-full bg-primary p-1.5 text-white opacity-0 transition-all hover:!opacity-90 hover:shadow group-hover:opacity-100\"\n          type=\"button\"\n          @click=\"fullscreen = true\"\n        >\n          <RiFullscreenLine class=\"text-xs\" />\n        </button>\n      </div>\n    </Teleport>\n\n    <template #fallback>\n      <span class=\"p-1 text-xs text-gray-400\">\n        {{ $t(\"core.common.status.loading\") }}...\n      </span>\n    </template>\n  </Suspense>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/code/index.ts",
    "content": "import type { FormKitInputs } from \"@formkit/inputs\";\nimport { createInput } from \"@formkit/vue\";\nimport CodeInput from \"./CodeInput.vue\";\n\nexport const code = createInput(CodeInput, {\n  type: \"input\",\n  props: [\"height\", \"language\"],\n  forceTypeProp: \"textarea\",\n});\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    code: {\n      type: \"code\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/color/ColorInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Sketch, type Payload } from \"@ckpack/vue-color\";\nimport { TinyColor } from \"@ctrl/tinycolor\";\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { IconClose, VButton, VDropdown } from \"@halo-dev/components\";\nimport Color from \"colorjs.io\";\nimport { computed, useTemplateRef, type PropType } from \"vue\";\nimport RiCodeSSlashLine from \"~icons/ri/code-s-slash-line\";\nimport type { ColorFormat } from \"./types\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst format = computed(() => props.context.format as ColorFormat);\n\nfunction onColorChange(color: Payload) {\n  props.context.node.input(formatPayload(color));\n}\n\nfunction formatPayload(color: Payload) {\n  switch (format.value) {\n    case \"rgb\":\n      return new TinyColor(color.rgba).toRgbString();\n    case \"hex8\":\n      return color.hex8;\n    case \"hsl\":\n      return new TinyColor(color.hsl).toHslString();\n    default:\n      return color.hex;\n  }\n}\n\nfunction formatColorByUnpredictableValue(value: string) {\n  const color = new TinyColor(value);\n  switch (format.value) {\n    case \"rgb\":\n      return color.toRgbString();\n    case \"hex8\":\n      return color.toHex8String();\n    case \"hsl\":\n      return color.toHslString();\n    default:\n      return color.toHexString();\n  }\n}\n\nconst isHighContrast = computed(() => {\n  const color = props.context._value;\n  if (!color) {\n    return false;\n  }\n  try {\n    const onWhite = Math.abs(Color.contrast(color, \"white\", \"APCA\"));\n    const onBlack = Math.abs(Color.contrast(color, \"black\", \"APCA\"));\n    return onWhite > onBlack;\n  } catch {\n    return false; // Default to low contrast on error\n  }\n});\n\nconst editFormDropdown =\n  useTemplateRef<InstanceType<typeof VDropdown>>(\"editFormDropdown\");\n\nfunction onEditFormSubmit({ value }: { value: string }) {\n  props.context.node.input(formatColorByUnpredictableValue(value));\n  editFormDropdown.value?.hide();\n}\n</script>\n\n<template>\n  <div class=\"group/color-input inline-flex items-center gap-2\">\n    <VDropdown class=\"inline-flex\" popper-class=\"[&_.v-popper\\_\\_inner]:!p-0\">\n      <button\n        type=\"button\"\n        aria-label=\"Choose color\"\n        class=\"inline-flex h-8 items-center justify-center rounded-lg bg-white px-2 transition-all hover:opacity-80 hover:shadow active:opacity-70\"\n        :style=\"{\n          backgroundColor: context._value,\n        }\"\n        :class=\"[\n          { 'text-white': isHighContrast },\n          { 'text-gray-900 ring-1 ring-gray-200': !isHighContrast },\n        ]\"\n      >\n        <span class=\"text-sm\">\n          {{ context._value || $t(\"core.formkit.color.placeholder\") }}\n        </span>\n      </button>\n      <template #popper>\n        <Sketch\n          :model-value=\"context._value || '#000'\"\n          @update:model-value=\"onColorChange\"\n        />\n      </template>\n    </VDropdown>\n\n    <div class=\"inline-flex items-center gap-1.5\">\n      <VDropdown ref=\"editFormDropdown\" class=\"inline-flex\">\n        <template #default=\"{ shown }\">\n          <button\n            v-tooltip=\"$t('core.formkit.color.operations.edit')\"\n            type=\"button\"\n            :aria-label=\"$t('core.formkit.color.operations.edit')\"\n            class=\"text-gray-500 opacity-0 transition-all hover:text-gray-900 group-hover/color-input:opacity-100\"\n            :class=\"{ '!text-gray-900 !opacity-100': shown }\"\n          >\n            <RiCodeSSlashLine />\n          </button>\n        </template>\n        <template #popper>\n          <div class=\"w-96\">\n            <FormKit\n              id=\"color-edit-form\"\n              type=\"form\"\n              ignore\n              name=\"color-edit-form\"\n              @submit=\"onEditFormSubmit\"\n            >\n              <FormKit type=\"text\" :model-value=\"context._value\" name=\"value\" />\n            </FormKit>\n            <div class=\"mt-4\">\n              <VButton\n                type=\"secondary\"\n                @click=\"$formkit.submit('color-edit-form')\"\n              >\n                {{ $t(\"core.common.buttons.save\") }}\n              </VButton>\n            </div>\n          </div>\n        </template>\n      </VDropdown>\n      <button\n        v-if=\"context._value\"\n        v-tooltip=\"$t('core.common.buttons.delete')\"\n        type=\"button\"\n        :aria-label=\"$t('core.common.buttons.delete')\"\n        class=\"text-gray-500 opacity-0 transition-all hover:text-gray-900 group-hover/color-input:opacity-100\"\n        @click=\"context.node.input(undefined)\"\n      >\n        <IconClose />\n      </button>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/color/index.ts",
    "content": "import { initialValue, type FormKitInputs } from \"@formkit/inputs\";\nimport { createInput } from \"@formkit/vue\";\nimport { defineAsyncComponent } from \"vue\";\n\nexport const color = createInput(\n  defineAsyncComponent(() => import(\"./ColorInput.vue\")),\n  {\n    type: \"input\",\n    props: [\"format\"],\n    features: [initialValue],\n  }\n);\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    color: {\n      type: \"color\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/color/types.ts",
    "content": "export type ColorFormat = \"rgb\" | \"hex\" | \"hex8\" | \"hsl\";\n"
  },
  {
    "path": "ui/src/formkit/inputs/form.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  disablesChildren,\n  formInput,\n  forms,\n  message,\n  messages,\n} from \"@formkit/inputs\";\n\n/**\n * Input definition for a form.\n * @public\n */\nexport const form: FormKitTypeDefinition = {\n  /**\n   * The actual schema of the input, or a function that returns the schema.\n   */\n  schema: formInput(\"$slots.default\", messages(message(\"$message.value\"))),\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"group\",\n  /**\n   * An array of extra props to accept for this input.\n   */\n  props: [\n    \"actions\",\n    \"submit\",\n    \"submitLabel\",\n    \"submitAttrs\",\n    \"submitBehavior\",\n    \"incompleteMessage\",\n  ],\n  forceTypeProp: \"form\",\n  /**\n   * Additional features that should be added to your input\n   */\n  features: [forms, disablesChildren],\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/group.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  disablesChildren,\n  fieldset,\n  help,\n  inner,\n  legend,\n  message,\n  messages,\n  outer,\n} from \"@formkit/inputs\";\n\nexport const group: FormKitTypeDefinition = {\n  schema: outer(\n    fieldset(\n      legend(\"$label\"),\n      help(\"$help\"),\n      inner(\"$slots.default\"),\n      messages(message(\"$message.value\"))\n    )\n  ),\n\n  type: \"group\",\n\n  props: [],\n\n  features: [disablesChildren],\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/Collections.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VLoading } from \"@halo-dev/components\";\nimport type { IconifyInfo } from \"@iconify/types\";\nimport { Icon } from \"@iconify/vue\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useVirtualizer } from \"@tanstack/vue-virtual\";\nimport { useFuse, type UseFuseOptions } from \"@vueuse/integrations/useFuse\";\nimport { computed, ref, useTemplateRef, watch } from \"vue\";\nimport { iconifyClient } from \"./api\";\n\nconst ROW_HEIGHT = 80;\nconst ROW_GAP = 8;\n\nconst modelValue = defineModel<string | undefined>();\n\nconst container = useTemplateRef<HTMLDivElement>(\"container\");\n\nconst { data, isLoading } = useQuery({\n  queryKey: [\"iconify:collections\"],\n  queryFn: () =>\n    iconifyClient\n      .get<Record<string, IconifyInfo>>(\"/collections\")\n      .then((res) => res.data),\n});\n\nconst collections = computed(() => {\n  return Object.entries(data.value || {})\n    .map(([key, value]) => ({\n      key,\n      value,\n    }))\n    .filter(({ value }) => value.hidden !== true);\n});\n\nconst keyword = ref<string>(\"\");\n\nconst fuseOptions: UseFuseOptions<{\n  key: string;\n  value: IconifyInfo;\n}> = {\n  matchAllWhenSearchEmpty: true,\n  fuseOptions: {\n    keys: [\"key\", \"value.name\"],\n  },\n};\n\nconst { results } = useFuse(keyword, collections, fuseOptions);\n\nconst rowVirtualizerOptions = computed(() => ({\n  count: results.value.length,\n  getScrollElement: () => container.value,\n  estimateSize: () => ROW_HEIGHT + ROW_GAP,\n  overscan: 5,\n}));\n\nconst rowVirtualizer = useVirtualizer(rowVirtualizerOptions);\n\nconst virtualRows = computed(() => rowVirtualizer.value.getVirtualItems());\nconst totalSize = computed(() => rowVirtualizer.value.getTotalSize());\n\nwatch(\n  () => results.value,\n  (value) => {\n    if (value.length) {\n      modelValue.value = value[0].item.key;\n    }\n    rowVirtualizer.value.scrollToIndex(0);\n  },\n  {\n    immediate: true,\n  }\n);\n</script>\n<template>\n  <div class=\"flex-none\">\n    <SearchInput\n      v-model=\"keyword\"\n      :placeholder=\"$t('core.formkit.iconify.collection_search_placeholder')\"\n      sync\n    />\n  </div>\n  <VLoading v-if=\"isLoading\" />\n  <div\n    v-else\n    ref=\"container\"\n    class=\"size-full min-h-0 flex-1 shrink overflow-auto\"\n  >\n    <div\n      :style=\"{\n        height: `${totalSize}px`,\n        width: '100%',\n        position: 'relative',\n      }\"\n    >\n      <div\n        v-for=\"virtualRow in virtualRows\"\n        :key=\"virtualRow.index\"\n        class=\"absolute left-0 flex w-full cursor-pointer items-center justify-between gap-2 rounded-base border p-2 transition-all\"\n        :class=\"{\n          'border-primary': modelValue === results[virtualRow.index].item.key,\n        }\"\n        :style=\"{\n          height: `${ROW_HEIGHT}px`,\n          transform: `translateY(${virtualRow.start}px)`,\n        }\"\n        @click=\"modelValue = results[virtualRow.index].item.key\"\n      >\n        <div>\n          <div class=\"line-clamp-1 text-sm\">\n            {{ results[virtualRow.index].item.value.name }}\n          </div>\n          <div class=\"line-clamp-1 text-xs text-gray-600\">\n            {{ results[virtualRow.index].item.value.license.title }}\n          </div>\n          <div class=\"text-xs text-gray-600\">\n            {{ results[virtualRow.index].item.value.total }}\n          </div>\n        </div>\n\n        <div class=\"hidden sm:grid sm:grid-cols-3 sm:gap-1\">\n          <Icon\n            v-for=\"sample in results[virtualRow.index].item.value.samples\"\n            :key=\"sample\"\n            :icon=\"`${results[virtualRow.index].item.key}:${sample}`\"\n          />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/CollectionsView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VLoading } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useFuse } from \"@vueuse/integrations/useFuse.mjs\";\nimport { flatten } from \"es-toolkit\";\nimport { values } from \"es-toolkit/compat\";\nimport { computed, ref } from \"vue\";\nimport { iconifyClient } from \"./api\";\nimport Collections from \"./Collections.vue\";\nimport Icons from \"./Icons.vue\";\nimport type { IconifyValue } from \"./types\";\n\nconst emit = defineEmits<{\n  (e: \"select\", icon: IconifyValue): void;\n}>();\n\nconst selectedCollection = ref<string>();\nconst keyword = ref<string>(\"\");\n\nconst { data, isLoading } = useQuery({\n  queryKey: [\"iconify:icons\", selectedCollection],\n  queryFn: () =>\n    iconifyClient\n      .get(`/collection`, {\n        params: {\n          prefix: selectedCollection.value,\n          chars: true,\n        },\n      })\n      .then((res) => res.data),\n  enabled: computed(() => !!selectedCollection.value),\n});\n\nconst icons = computed(() => {\n  return [\n    ...flatten(values(data.value?.categories)),\n    ...(data.value?.uncategorized || []),\n  ].map((icon) => `${selectedCollection.value}:${icon}`);\n});\n\nconst { results } = useFuse(keyword, icons, {\n  matchAllWhenSearchEmpty: true,\n});\n\nconst onSelect = (icon: IconifyValue) => {\n  emit(\"select\", icon);\n};\n</script>\n<template>\n  <div\n    class=\"flex h-[350px] divide-x rounded-base border border-gray-200 bg-white sm:h-[500px] sm:w-[700px]\"\n  >\n    <div\n      class=\"flex h-full w-28 flex-none flex-col gap-2 overflow-auto p-1.5 sm:w-56\"\n    >\n      <Collections v-model=\"selectedCollection\" />\n    </div>\n    <div class=\"flex h-full flex-1 flex-col gap-2 p-1.5\">\n      <SearchInput\n        v-model=\"keyword\"\n        :placeholder=\"$t('core.formkit.iconify.search_placeholder')\"\n        sync\n      />\n      <VLoading v-if=\"isLoading\" />\n      <Icons\n        v-else\n        :icons=\"results.map((result) => result.item)\"\n        @select=\"onSelect\"\n      />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/Icon.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VDropdown } from \"@halo-dev/components\";\nimport { Icon as IconifyIcon } from \"@iconify/vue\";\nimport { useTemplateRef } from \"vue\";\nimport IconConfirmPanel from \"./IconConfirmPanel.vue\";\nimport type { IconifyValue } from \"./types\";\n\ndefineProps<{\n  iconName: string;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"select\", icon: IconifyValue): void;\n}>();\n\nconst dropdown = useTemplateRef<InstanceType<typeof VDropdown>>(\"dropdown\");\n\nfunction onSelect(icon: IconifyValue) {\n  emit(\"select\", icon);\n  dropdown.value?.hide();\n}\n</script>\n<template>\n  <VDropdown ref=\"dropdown\" class=\"inline-flex\">\n    <button\n      v-tooltip=\"iconName\"\n      type=\"button\"\n      class=\"inline-flex size-full items-center justify-center rounded-lg hover:bg-gray-100 active:bg-gray-200\"\n      :aria-label=\"`Select icon: ${iconName}`\"\n    >\n      <IconifyIcon :icon=\"iconName\" class=\"text-2xl\" />\n    </button>\n    <template #popper>\n      <IconConfirmPanel :icon-name=\"iconName\" @select=\"onSelect\" />\n    </template>\n  </VDropdown>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/IconConfirmPanel.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Toast, VButton } from \"@halo-dev/components\";\nimport { Icon as IconifyIcon } from \"@iconify/vue\";\nimport { refDefault } from \"@vueuse/shared\";\nimport { AxiosError } from \"axios\";\nimport { computed, inject, onMounted, ref, type Ref } from \"vue\";\nimport { ICONIFY_BASE_URL, iconifyClient } from \"./api\";\nimport type { IconifyFormat, IconifySizing, IconifyValue } from \"./types\";\n\nconst props = defineProps<{\n  iconName: string;\n}>();\n\nconst emit = defineEmits<{\n  (e: \"select\", icon: IconifyValue): void;\n}>();\n\nconst format = inject<Ref<IconifyFormat>>(\"format\");\nconst sizing = inject<Ref<IconifySizing>>(\"sizing\");\nconst currentIconifyValue = inject<Ref<IconifyValue | undefined>>(\n  \"currentIconifyValue\"\n);\n\nconst color = ref<string>(\"\");\nconst width = refDefault<string | undefined>(\n  ref(),\n  sizing?.value.default || \"24\"\n);\n\nconst defaultSizeOptions = [\"12\", \"24\", \"48\", \"96\", \"1em\", \"1.2em\", \"2em\"];\n\nconst sizeOptions = computed(() => {\n  return Array.from(\n    new Set([\n      sizing?.value.default || \"24\",\n      ...(sizing?.value.presets || defaultSizeOptions),\n    ])\n  );\n});\n\nonMounted(() => {\n  if (currentIconifyValue?.value) {\n    color.value = currentIconifyValue.value.color || \"\";\n    width.value = currentIconifyValue.value.width;\n  }\n});\n\nconst isFetching = ref(false);\n\nasync function handleConfirm() {\n  if (!format?.value) {\n    return;\n  }\n\n  const iconifyValue: IconifyValue = {\n    name: props.iconName,\n    width: width.value,\n    color: color.value,\n    value: \"\",\n  };\n\n  const iconifyUrl = `${ICONIFY_BASE_URL}/${props.iconName}.svg?width=${width.value}&color=${encodeURIComponent(color.value!)}`;\n\n  if (format.value === \"name\") {\n    iconifyValue.value = props.iconName;\n    emit(\"select\", iconifyValue);\n    return;\n  }\n\n  if (format.value === \"url\") {\n    iconifyValue.value = iconifyUrl;\n    emit(\"select\", iconifyValue);\n    return;\n  }\n\n  try {\n    isFetching.value = true;\n    const { data } = await iconifyClient.get(iconifyUrl);\n\n    if (format.value === \"svg\") {\n      iconifyValue.value = data;\n    } else if (format.value === \"dataurl\") {\n      iconifyValue.value = `data:image/svg+xml,${encodeURIComponent(data)}`;\n    }\n    emit(\"select\", iconifyValue);\n  } catch (error) {\n    if (error instanceof AxiosError) {\n      Toast.error(error.message);\n    }\n  } finally {\n    isFetching.value = false;\n  }\n}\n</script>\n<template>\n  <div class=\"flex items-start gap-3\">\n    <div\n      class=\"inline-flex items-center justify-center rounded-base border p-2\"\n    >\n      <IconifyIcon\n        v-tooltip=\"iconName\"\n        :icon=\"iconName\"\n        :color=\"color\"\n        :width=\"width === 'none' ? undefined : width\"\n      />\n    </div>\n    <div>\n      <FormKit type=\"form\" ignore>\n        <FormKit\n          v-if=\"sizing?.enabled\"\n          v-model=\"width\"\n          type=\"select\"\n          name=\"size\"\n          :label=\"$t('core.formkit.iconify.option_size')\"\n          allow-create\n          searchable\n          ignore\n          :options=\"[\n            {\n              label: $t('core.common.text.none'),\n              value: '',\n            },\n            ...sizeOptions.map((size) => ({\n              label: size,\n              value: size,\n            })),\n          ]\"\n          clearable\n        ></FormKit>\n        <FormKit\n          v-model=\"color\"\n          type=\"color\"\n          name=\"color\"\n          :label=\"$t('core.formkit.iconify.option_color')\"\n          ignore\n        >\n        </FormKit>\n      </FormKit>\n      <div class=\"mt-5\">\n        <VButton\n          type=\"secondary\"\n          :loading=\"isFetching\"\n          @click=\"handleConfirm()\"\n        >\n          {{ $t(\"core.common.buttons.confirm\") }}\n        </VButton>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/IconifyInput.vue",
    "content": "<script setup lang=\"ts\">\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport {\n  IconClose,\n  VButton,\n  VDropdown,\n  type VDropdownPlacement,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { Icon } from \"@iconify/vue\";\nimport {\n  computed,\n  provide,\n  useTemplateRef,\n  type PropType,\n  type Ref,\n} from \"vue\";\nimport RiCodeSSlashLine from \"~icons/ri/code-s-slash-line\";\nimport IconifyPicker from \"./IconifyPicker.vue\";\nimport type { IconifyFormat, IconifySizing, IconifyValue } from \"./types\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst format = computed(() => props.context.format as IconifyFormat);\nconst valueOnly = computed(() => props.context.valueOnly as boolean);\nconst sizing = computed(() => props.context.sizing as IconifySizing);\n\nconst currentIconifyValue = computed(() => {\n  const value = props.context._value;\n\n  if (!value) {\n    return undefined;\n  }\n\n  if (valueOnly.value) {\n    return {\n      value: value as string,\n    };\n  }\n  return value as IconifyValue;\n});\n\nprovide<Ref<IconifyFormat>>(\"format\", format);\nprovide<Ref<IconifySizing>>(\"sizing\", sizing);\n\nprovide<Ref<IconifyValue | undefined>>(\n  \"currentIconifyValue\",\n  currentIconifyValue\n);\n\nconst popperPlacement = computed(\n  () => props.context.popperPlacement as VDropdownPlacement\n);\n\nconst dropdown = useTemplateRef<InstanceType<typeof VDropdown>>(\"dropdown\");\n\nconst onSelect = (icon: IconifyValue) => {\n  if (valueOnly.value) {\n    props.context.node.input(icon.value);\n  } else {\n    props.context.node.input(icon);\n  }\n  dropdown.value?.hide();\n};\n\nconst editFormDropdown =\n  useTemplateRef<InstanceType<typeof VDropdown>>(\"editFormDropdown\");\n\nfunction onEditFormSubmit({ value: iconValue }: { value: string }) {\n  if (valueOnly.value) {\n    props.context.node.input(iconValue);\n    editFormDropdown.value?.hide();\n    return;\n  }\n\n  const valueToUpdate: IconifyValue = {\n    ...currentIconifyValue.value,\n    value: iconValue,\n  };\n\n  if (format.value === \"name\") {\n    valueToUpdate.name = iconValue;\n  } else if (iconValue !== currentIconifyValue.value?.value) {\n    valueToUpdate.name = \"\";\n  }\n\n  props.context.node.input(valueToUpdate);\n  editFormDropdown.value?.hide();\n}\n\nconst editFormId = `icon-edit-form-${utils.id.uuid()}`;\n</script>\n\n<template>\n  <div class=\"group/iconify-input inline-flex items-center gap-2\">\n    <VDropdown\n      ref=\"dropdown\"\n      class=\"inline-flex\"\n      popper-class=\"w-full sm:w-auto\"\n      :placement=\"popperPlacement\"\n    >\n      <button\n        v-tooltip=\"$t('core.formkit.iconify.placeholder')\"\n        type=\"button\"\n        class=\"inline-flex h-9 items-center justify-center rounded-lg border bg-white px-2 transition-all hover:bg-gray-50 hover:shadow active:bg-gray-100\"\n        :aria-label=\"$t('core.formkit.iconify.placeholder')\"\n      >\n        <div\n          v-if=\"currentIconifyValue?.value\"\n          class=\"inline-flex size-full items-center justify-center [&>*]:h-5\"\n        >\n          <img\n            v-if=\"['url', 'dataurl'].includes(format)\"\n            :src=\"currentIconifyValue.value\"\n          />\n          <Icon\n            v-else-if=\"format === 'name'\"\n            :icon=\"currentIconifyValue.value\"\n          />\n          <div\n            v-else\n            class=\"inline-flex size-full items-center justify-center [&>*]:h-full\"\n            v-html=\"currentIconifyValue.value\"\n          ></div>\n        </div>\n        <div v-else class=\"text-sm text-gray-600\">\n          {{ $t(\"core.formkit.iconify.placeholder\") }}\n        </div>\n      </button>\n      <template #popper>\n        <IconifyPicker @select=\"onSelect\" />\n      </template>\n    </VDropdown>\n    <div class=\"inline-flex items-center gap-1.5\">\n      <!-- @vue-ignore -->\n      <VDropdown\n        ref=\"editFormDropdown\"\n        class=\"inline-flex\"\n        :dispose-timeout=\"null\"\n      >\n        <template #default=\"{ shown }\">\n          <button\n            v-tooltip=\"$t('core.formkit.iconify.operations.edit')\"\n            type=\"button\"\n            :aria-label=\"$t('core.formkit.iconify.operations.edit')\"\n            class=\"text-gray-500 opacity-0 transition-all hover:text-gray-900 group-hover/iconify-input:opacity-100\"\n            :class=\"{ '!text-gray-900 !opacity-100': shown }\"\n          >\n            <RiCodeSSlashLine />\n          </button>\n        </template>\n        <template #popper>\n          <div class=\"w-96\">\n            <FormKit\n              :id=\"editFormId\"\n              type=\"form\"\n              ignore\n              :name=\"editFormId\"\n              @submit=\"onEditFormSubmit\"\n            >\n              <FormKit\n                v-if=\"format === 'svg'\"\n                type=\"code\"\n                height=\"120px\"\n                :model-value=\"currentIconifyValue?.value\"\n                language=\"html\"\n                name=\"value\"\n              />\n              <FormKit\n                v-else-if=\"['dataurl', 'url'].includes(format)\"\n                type=\"attachmentInput\"\n                name=\"value\"\n                :model-value=\"currentIconifyValue?.value\"\n              ></FormKit>\n              <FormKit\n                v-else\n                type=\"text\"\n                name=\"value\"\n                :model-value=\"currentIconifyValue?.value\"\n              ></FormKit>\n            </FormKit>\n            <div class=\"mt-4\">\n              <VButton type=\"secondary\" @click=\"$formkit.submit(editFormId)\">\n                {{ $t(\"core.common.buttons.save\") }}\n              </VButton>\n            </div>\n          </div>\n        </template>\n      </VDropdown>\n      <button\n        v-if=\"currentIconifyValue\"\n        v-tooltip=\"$t('core.common.buttons.delete')\"\n        type=\"button\"\n        :aria-label=\"$t('core.common.buttons.delete')\"\n        class=\"text-gray-500 opacity-0 transition-all hover:text-gray-900 group-hover/iconify-input:opacity-100\"\n        @click=\"context.node.input(undefined)\"\n      >\n        <IconClose />\n      </button>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/IconifyPicker.vue",
    "content": "<script setup lang=\"ts\">\nimport { VTabItem, VTabs } from \"@halo-dev/components\";\nimport { inject, ref, type Ref } from \"vue\";\nimport CollectionsView from \"./CollectionsView.vue\";\nimport IconConfirmPanel from \"./IconConfirmPanel.vue\";\nimport SearchView from \"./SearchView.vue\";\nimport type { IconifyValue } from \"./types\";\n\nconst currentIconifyValue = inject<Ref<IconifyValue | undefined>>(\n  \"currentIconifyValue\"\n);\n\nconst emit = defineEmits<{\n  (e: \"select\", icon: IconifyValue): void;\n}>();\n\nconst activeTab = ref<\"collections\" | \"search\">(\"collections\");\n\nconst onSelect = (icon: IconifyValue) => {\n  emit(\"select\", icon);\n};\n</script>\n\n<template>\n  <div class=\"w-full\">\n    <VTabs v-model:active-id=\"activeTab\" type=\"outline\">\n      <VTabItem\n        id=\"collections\"\n        :label=\"$t('core.formkit.iconify.tabs.collections')\"\n      >\n        <CollectionsView @select=\"onSelect\" />\n      </VTabItem>\n      <VTabItem id=\"search\" :label=\"$t('core.formkit.iconify.tabs.search')\">\n        <SearchView @select=\"onSelect\" />\n      </VTabItem>\n      <VTabItem\n        v-if=\"currentIconifyValue?.name\"\n        id=\"custom\"\n        :label=\"$t('core.formkit.iconify.tabs.current')\"\n      >\n        <IconConfirmPanel\n          :icon-name=\"currentIconifyValue.name\"\n          @select=\"onSelect\"\n        />\n      </VTabItem>\n    </VTabs>\n\n    <div class=\"mt-2 flex justify-end text-xs text-gray-500\">\n      <i18n-t keypath=\"core.formkit.iconify.copyright\" tag=\"span\">\n        <template #url>\n          <a\n            href=\"https://iconify.design\"\n            class=\"text-gray-900 hover:underline\"\n            rel=\"noopener noreferrer\"\n            target=\"_blank\"\n          >\n            Iconify\n          </a>\n        </template>\n      </i18n-t>\n    </div>\n  </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/Icons.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useVirtualizer } from \"@tanstack/vue-virtual\";\nimport { chunk } from \"es-toolkit\";\nimport { computed, useTemplateRef, watch } from \"vue\";\nimport Icon from \"./Icon.vue\";\nimport type { IconifyValue } from \"./types\";\n\nconst COLUMNS = 12;\nconst ROW_HEIGHT = 36;\n\nconst props = defineProps<{\n  icons: string[];\n}>();\n\nconst emit = defineEmits<{\n  (e: \"select\", icon: IconifyValue): void;\n}>();\n\nconst container = useTemplateRef<HTMLDivElement>(\"container\");\n\nconst iconRows = computed(() => chunk(props.icons, COLUMNS));\n\nconst rowVirtualizerOptions = computed(() => ({\n  count: iconRows.value.length,\n  getScrollElement: () => container.value,\n  estimateSize: () => ROW_HEIGHT,\n  overscan: 5,\n}));\n\nconst rowVirtualizer = useVirtualizer(rowVirtualizerOptions);\n\nconst virtualRows = computed(() => rowVirtualizer.value.getVirtualItems());\nconst totalSize = computed(() => rowVirtualizer.value.getTotalSize());\n\nwatch(\n  () => props.icons,\n  () => {\n    rowVirtualizer.value.scrollToIndex(0);\n  }\n);\n\nfunction onSelect(icon: IconifyValue) {\n  emit(\"select\", icon);\n}\n</script>\n<template>\n  <div ref=\"container\" class=\"size-full flex-1 overflow-auto\">\n    <div\n      :style=\"{\n        height: `${totalSize}px`,\n        width: '100%',\n        position: 'relative',\n      }\"\n    >\n      <div\n        v-for=\"virtualRow in virtualRows\"\n        :key=\"virtualRow.index\"\n        class=\"absolute left-0 grid w-full grid-cols-12 gap-1\"\n        :style=\"{\n          height: `${virtualRow.size}px`,\n          transform: `translateY(${virtualRow.start}px)`,\n        }\"\n      >\n        <Icon\n          v-for=\"iconName in iconRows[virtualRow.index]\"\n          :key=\"iconName\"\n          :icon-name=\"iconName\"\n          @select=\"onSelect\"\n        >\n        </Icon>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/SearchView.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VLoading } from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { iconifyClient } from \"./api\";\nimport Icons from \"./Icons.vue\";\nimport type { IconifyValue } from \"./types\";\n\nconst emit = defineEmits<{\n  (e: \"select\", icon: IconifyValue): void;\n}>();\n\nconst keyword = ref<string>(\"\");\n\nconst { data, isLoading } = useQuery({\n  queryKey: [\"iconify:icons\", keyword],\n  queryFn: async () => {\n    if (!keyword.value) {\n      return [];\n    }\n    const response = await iconifyClient\n      .get<{ icons: string[] }>(`/search`, {\n        params: {\n          query: keyword.value,\n          limit: 999,\n        },\n      })\n      .then((res) => res.data);\n\n    return response.icons;\n  },\n});\n\nconst onSelect = (icon: IconifyValue) => {\n  emit(\"select\", icon);\n};\n</script>\n<template>\n  <div class=\"flex h-[350px] flex-col gap-2 bg-white sm:h-[500px] sm:w-[500px]\">\n    <div class=\"flex-none\">\n      <SearchInput v-model=\"keyword\" />\n    </div>\n    <div\n      v-if=\"!keyword\"\n      class=\"flex items-center justify-center py-2 text-sm text-gray-600\"\n    >\n      {{ $t(\"core.common.placeholder.search\") }}\n    </div>\n    <VLoading v-else-if=\"isLoading\" />\n    <div\n      v-else-if=\"!data?.length\"\n      class=\"flex items-center justify-center py-2 text-sm text-gray-600\"\n    >\n      {{ $t(\"core.formkit.iconify.no_results\") }}\n    </div>\n    <div\n      v-else\n      class=\"flex min-h-0 flex-1 shrink rounded-base border border-gray-200 p-1.5\"\n    >\n      <Icons :icons=\"data\" @select=\"onSelect\" />\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/api.ts",
    "content": "import axios from \"axios\";\n\nexport const ICONIFY_BASE_URL = \"https://api.iconify.design\";\n\nexport const iconifyClient = axios.create({\n  baseURL: ICONIFY_BASE_URL,\n});\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/feature.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { undefine } from \"@formkit/utils\";\n\n/**\n * Ensure that the content entered for iconify is correctly set, and use the default value when it is not set.\n * @param node\n */\nexport default function iconifyFeature(node: FormKitNode): void {\n  node.on(\"created\", () => {\n    node.props.format = node.props.format ?? \"svg\";\n    node.props.popperPlacement = node.props.popperPlacement ?? \"auto\";\n    node.props.valueOnly = undefine(node.props.valueOnly);\n    node.props.sizing = {\n      enabled: false,\n      ...node.props.sizing,\n    };\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/index.ts",
    "content": "import { initialValue, type FormKitInputs } from \"@formkit/inputs\";\nimport { createInput } from \"@formkit/vue\";\nimport { defineAsyncComponent } from \"vue\";\nimport iconifyFeature from \"./feature\";\nimport type { IconifyFormat, IconifySizing, IconifyValue } from \"./types\";\n\nexport const iconify = createInput(\n  defineAsyncComponent(() => import(\"./IconifyInput.vue\")),\n  {\n    type: \"input\",\n    props: [\"format\", \"popperPlacement\", \"valueOnly\", \"sizing\"],\n    features: [initialValue, iconifyFeature],\n  }\n);\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    iconify: {\n      type: \"iconify\";\n      value?: IconifyValue | string;\n      format: IconifyFormat;\n      valueOnly?: boolean;\n      sizing?: IconifySizing;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/iconify/types.ts",
    "content": "export type IconifyFormat = \"svg\" | \"dataurl\" | \"url\" | \"name\";\n\nexport interface IconifyValue {\n  value: string;\n  name?: string;\n  width?: string;\n  color?: string;\n}\n\nexport type IconifySizing = {\n  enabled?: boolean;\n  default?: string;\n  presets?: string[];\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/list/AddButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { IconAddCircle, VButton } from \"@halo-dev/components\";\nimport type { PropType } from \"vue\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n  disabled: {\n    type: Boolean,\n    required: false,\n  },\n  onClick: {\n    type: Function as PropType<() => void>,\n    required: true,\n  },\n});\n\nconst handleAppendClick = () => {\n  if (!props.disabled && props.onClick) {\n    props.onClick();\n  }\n};\n</script>\n\n<template>\n  <div :class=\"context.classes.add\" @click=\"handleAppendClick\">\n    <VButton :disabled=\"disabled\" type=\"secondary\">\n      <template #icon>\n        <IconAddCircle />\n      </template>\n      {{ context.addLabel || $t(\"core.common.buttons.add\") }}\n    </VButton>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/list/features/lists.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { undefine } from \"@formkit/utils\";\n\nexport const lists = function (node: FormKitNode) {\n  node._c.sync = true;\n  node.on(\"created\", listFeature.bind(null, node));\n};\n\nconst fn = (node: FormKitNode): object | string | boolean | number => {\n  switch (node.props.itemType.toLocaleLowerCase()) {\n    case \"object\":\n      return {};\n    case \"boolean\":\n      return false;\n    case \"number\":\n      return 0;\n    default:\n      return \"\";\n  }\n};\n\nfunction createValue(num: number, node: FormKitNode) {\n  return new Array(num).fill(\"\").map(() => fn(node));\n}\n\nfunction listFeature(node: FormKitNode) {\n  node.props.removeControl = node.props.removeControl ?? true;\n  node.props.upControl = node.props.upControl ?? true;\n  node.props.downControl = node.props.downControl ?? true;\n  node.props.insertControl = node.props.insertControl ?? true;\n  node.props.addButton = node.props.addButton ?? true;\n  node.props.addLabel = node.props.addLabel ?? false;\n  node.props.addAttrs = node.props.addAttrs ?? {};\n  node.props.min = node.props.min ? Number(node.props.min) : 0;\n  node.props.max = node.props.max ? Number(node.props.max) : Infinity;\n  node.props.itemType = node.props.itemType ?? \"string\";\n  if (node.props.min > node.props.max) {\n    throw Error(\"list: min must be less than max\");\n  }\n\n  if (\"disabled\" in node.props) {\n    node.props.disabled = undefine(node.props.disabled);\n  }\n\n  if (Array.isArray(node.value)) {\n    if (node.value.length < node.props.min) {\n      const value = createValue(node.props.min - node.value.length, node);\n      node.input(node.value.concat(value), false);\n    } else {\n      if (node.value.length > node.props.max) {\n        node.input(node.value.slice(0, node.props.max), false);\n      }\n    }\n  } else {\n    node.input(createValue(node.props.min, node), false);\n  }\n\n  if (node.context) {\n    const fns = node.context.fns;\n    fns.createShift = (index: number, offset: number) => () => {\n      const value = node._value as unknown[];\n      value.splice(index + offset, 0, value.splice(index, 1)[0]);\n      node.input(value, false);\n    };\n    fns.createInsert = (index: number) => () => {\n      const value = node._value as unknown[];\n      value.splice(index + 1, 0, fn(node));\n      node.input(value, false);\n    };\n    fns.createAppend = () => () => {\n      const value = node._value as unknown[];\n      value.push(fn(node));\n      node.input(value, false);\n    };\n    fns.createRemover = (index: number) => () => {\n      const value = node._value as unknown[];\n      value.splice(index, 1);\n      node.input(value, false);\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/list/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  $if,\n  disablesChildren,\n  fieldset,\n  help,\n  inner,\n  legend,\n  message,\n  messages,\n  outer,\n  prefix,\n  renamesRadios,\n  suffix,\n} from \"@formkit/inputs\";\nimport {\n  IconAddCircle,\n  IconArrowDownCircleLine,\n  IconArrowUpCircleLine,\n  IconCloseCircle,\n} from \"@halo-dev/components\";\nimport { i18n } from \"@/locales\";\nimport AddButton from \"./AddButton.vue\";\nimport { lists } from \"./features/lists\";\nimport {\n  addButton,\n  content,\n  controls,\n  down,\n  downControl,\n  downIcon,\n  empty,\n  insert,\n  insertControl,\n  insertIcon,\n  item,\n  items,\n  remove,\n  removeControl,\n  removeIcon,\n  up,\n  upControl,\n  upIcon,\n} from \"./sections\";\n\n/**\n * Input definition for a dynamic list input.\n * @public\n */\nexport const list: FormKitTypeDefinition = {\n  /**\n   * The actual schema of the input, or a function that returns the schema.\n   */\n  schema: outer(\n    fieldset(\n      legend(\"$label\"),\n      help(\"$help\"),\n      inner(\n        prefix(),\n        $if(\n          \"$value.length === 0\",\n          $if(\"$slots.empty\", empty()),\n          $if(\n            \"$slots.default\",\n            items(\n              item(\n                content(\"$slots.default\"),\n                controls(\n                  up(upControl(upIcon())),\n                  remove(removeControl(removeIcon())),\n                  insert(insertControl(insertIcon())),\n                  down(downControl(downIcon()))\n                )\n              )\n            ),\n            suffix()\n          )\n        ),\n        suffix(),\n        addButton(`$addLabel || (${i18n.global.t(\"core.common.buttons.add\")})`)\n      )\n    ),\n    messages(message(\"$message.value\"))\n  ),\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"list\",\n  /**\n   * An array of extra props to accept for this input.\n   */\n  props: [\n    \"min\",\n    \"max\",\n    \"upControl\",\n    \"downControl\",\n    \"removeControl\",\n    \"insertControl\",\n    \"addLabel\",\n    \"addButton\",\n    \"itemType\",\n  ],\n  /**\n   * Additional features that should be added to your input\n   */\n  features: [lists, disablesChildren, renamesRadios],\n\n  library: {\n    IconAddCircle,\n    IconCloseCircle,\n    IconArrowUpCircleLine,\n    IconArrowDownCircleLine,\n    AddButton,\n  },\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/list/listSection.ts",
    "content": "import {\n  isComponent,\n  isDOM,\n  type FormKitExtendableSchemaRoot,\n  type FormKitSchemaCondition,\n  type FormKitSchemaNode,\n  type FormKitSectionsSchema,\n} from \"@formkit/core\";\nimport {\n  extendSchema,\n  type FormKitSchemaExtendableSection,\n  type FormKitSection,\n} from \"@formkit/inputs\";\n\nexport function createListSection() {\n  return (\n    section: string,\n    el: string | null | (() => FormKitSchemaNode),\n    fragment = false\n  ) => {\n    return createSection(\n      section,\n      el,\n      fragment\n    ) as FormKitSection<FormKitSchemaExtendableSection>;\n  };\n}\n\nfunction createSection(\n  section: string,\n  el: string | null | (() => FormKitSchemaNode),\n  fragment = false\n): FormKitSection<\n  FormKitExtendableSchemaRoot | FormKitSchemaExtendableSection\n> {\n  return (\n    ...children: Array<\n      FormKitSchemaExtendableSection | string | FormKitSchemaCondition\n    >\n  ) => {\n    const extendable = (extensions: FormKitSectionsSchema) => {\n      const node = !el || typeof el === \"string\" ? { $el: el } : el();\n\n      if (\"string\" != typeof node) {\n        if (isDOM(node) || isComponent(node) || \"$formkit\" in node) {\n          if (children.length && !node.children) {\n            node.children = [\n              ...children.map((child) =>\n                typeof child === \"function\" ? child(extensions) : child\n              ),\n            ];\n          }\n          if (!node.meta) {\n            node.meta = { section };\n          }\n          if (isDOM(node)) {\n            node.attrs = {\n              class: `$classes.${section}`,\n              ...(node.attrs || {}),\n            };\n          }\n          if (\"$formkit\" in node) {\n            node.outerClass = `$classes.${section}`;\n          }\n        }\n      }\n      return {\n        if: `$slots.${section}`,\n        then: `$slots.${section}`,\n        else:\n          section in extensions\n            ? extendSchema(node as FormKitSchemaNode, extensions[section])\n            : node,\n      } as FormKitSchemaCondition;\n    };\n    extendable._s = section;\n    return fragment ? createRoot(extendable) : extendable;\n  };\n}\n\n/**\n *\n * Returns an extendable schema root node.\n *\n * @param rootSection - Creates the root node.\n *\n * @returns {@link @formkit/core#FormKitExtendableSchemaRoot | FormKitExtendableSchemaRoot}\n *\n * @internal\n */\nexport function createRoot(\n  rootSection: FormKitSchemaExtendableSection\n): FormKitExtendableSchemaRoot {\n  return (extensions: FormKitSectionsSchema) => {\n    return [rootSection(extensions)];\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/list/sections/index.ts",
    "content": "import { createListSection } from \"../listSection\";\n\nconst listSection = createListSection();\n\nexport const addButton = listSection(\"addButton\", () => ({\n  $cmp: \"AddButton\",\n  props: {\n    onClick: \"$fns.createAppend()\",\n    disabled: \"$value.length >= $max\",\n    context: \"$node.context\",\n  },\n  bind: \"$addAttrs\",\n  if: \"$addButton\",\n  type: \"button\",\n}));\n\nexport const content = listSection(\"content\", () => ({\n  $el: \"div\",\n  attrs: {\n    key: \"$item\",\n    index: \"$index\",\n    \"data-index\": \"$index\",\n  },\n}));\n\nexport const controlLabel = listSection(\"controlLabel\", \"span\");\n\nexport const controls = listSection(\"controls\", () => ({\n  $el: \"ul\",\n  if: \"$removeControl || $insertControl || $upControl || $downControl\",\n}));\n\nexport const down = listSection(\"down\", () => ({\n  $el: \"li\",\n  if: \"$downControl\",\n}));\n\nexport const downControl = listSection(\"downControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$index >= $value.length - 1\",\n    onClick: \"$fns.createShift($index, 1)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const downIcon = listSection(\"downIcon\", () => ({\n  $cmp: \"IconArrowDownCircleLine\",\n}));\n\nexport const empty = listSection(\"empty\", () => ({\n  $el: \"div\",\n}));\n\nexport const fieldset = listSection(\"fieldset\", () => ({\n  $el: \"fieldset\",\n  attrs: {\n    id: \"$id\",\n    disabled: \"$disabled\",\n  },\n}));\n\nexport const insert = listSection(\"insert\", () => ({\n  $el: \"li\",\n  if: \"$insertControl\",\n}));\n\nexport const insertControl = listSection(\"insertControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$value.length >= $max\",\n    onClick: \"$fns.createInsert($index)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const insertIcon = listSection(\"insertIcon\", () => ({\n  $cmp: \"IconAddCircle\",\n}));\n\nexport const item = listSection(\"item\", () => ({\n  $el: \"li\",\n  for: [\"item\", \"index\", \"$items\"],\n  attrs: {\n    role: \"listitem\",\n    key: \"$item\",\n    index: \"$index\",\n    \"data-index\": \"$index\",\n  },\n}));\n\nexport const items = listSection(\"items\", () => ({\n  $el: \"ul\",\n  attrs: {\n    role: \"list\",\n  },\n}));\n\nexport const remove = listSection(\"remove\", () => ({\n  $el: \"li\",\n  if: \"$removeControl\",\n}));\n\nexport const removeControl = listSection(\"removeControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$value.length <= $min\",\n    onClick: \"$fns.createRemover($index)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const removeIcon = listSection(\"removeIcon\", () => ({\n  $cmp: \"IconCloseCircle\",\n}));\n\nexport const up = listSection(\"up\", () => ({\n  $el: \"li\",\n  if: \"$upControl\",\n}));\n\nexport const upControl = listSection(\"upControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$index <= 0\",\n    onClick: \"$fns.createShift($index, -1)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const upIcon = listSection(\"upIcon\", () => ({\n  $cmp: \"IconArrowUpCircleLine\",\n}));\n"
  },
  {
    "path": "ui/src/formkit/inputs/menu-checkbox.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  checkbox,\n  checkboxes,\n  defaultIcon,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type Menu,\n  type MenuV1alpha1ApiListMenuRequest,\n} from \"@halo-dev/api-client\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const menus = await paginate<MenuV1alpha1ApiListMenuRequest, Menu>(\n      (params) => coreApiClient.menu.listMenu(params),\n      {\n        size: 1000,\n      }\n    );\n\n    node.props.options = menus.map((menu) => {\n      return {\n        value: menu.metadata.name,\n        label: menu.spec.displayName,\n      };\n    });\n  });\n}\n\nexport const menuCheckbox: FormKitTypeDefinition = {\n  ...checkbox,\n  props: [\"onValue\", \"offValue\"],\n  forceTypeProp: \"checkbox\",\n  features: [\n    optionsHandler,\n    checkboxes,\n    defaultIcon(\"decorator\", \"checkboxDecorator\"),\n  ],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    menuCheckbox: {\n      type: \"menuCheckbox\";\n      value?: string[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/menu-item-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport type { FormKitInputs } from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type MenuItem,\n  type MenuItemV1alpha1ApiListMenuItemRequest,\n} from \"@halo-dev/api-client\";\nimport { select } from \"./select\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const menuItems = await paginate<\n      MenuItemV1alpha1ApiListMenuItemRequest,\n      MenuItem\n    >((params) => coreApiClient.menuItem.listMenuItem(params), {\n      fieldSelector: [`name=(${node.props.menuItems.join(\",\")})`],\n    });\n\n    if (node.context) {\n      node.context.attrs.options = menuItems.map((menuItem) => {\n        return {\n          value: menuItem.metadata.name,\n          label: menuItem.status?.displayName,\n        };\n      });\n    }\n  });\n}\n\nexport const menuItemSelect: FormKitTypeDefinition = {\n  ...select,\n  props: [\"menuItems\", ...(select.props as string[])],\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    menuItemSelect: {\n      type: \"menuItemSelect\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/menu-radio.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  defaultIcon,\n  radio,\n  radios,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type Menu,\n  type MenuV1alpha1ApiListMenuRequest,\n} from \"@halo-dev/api-client\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const menus = await paginate<MenuV1alpha1ApiListMenuRequest, Menu>(\n      (params) => coreApiClient.menu.listMenu(params),\n      {\n        size: 1000,\n      }\n    );\n\n    node.props.options = menus.map((menu) => {\n      return {\n        value: menu.metadata.name,\n        label: menu.spec.displayName,\n      };\n    });\n  });\n}\n\nexport const menuRadio: FormKitTypeDefinition = {\n  ...radio,\n  props: [\"onValue\", \"offValue\"],\n  forceTypeProp: \"radio\",\n  features: [\n    optionsHandler,\n    radios,\n    defaultIcon(\"decorator\", \"radioDecorator\"),\n  ],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    menuRadio: {\n      type: \"menuRadio\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/menu-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  coreApiClient,\n  paginate,\n  type Menu,\n  type MenuV1alpha1ApiListMenuRequest,\n} from \"@halo-dev/api-client\";\nimport { select } from \"./select\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const menus = await paginate<MenuV1alpha1ApiListMenuRequest, Menu>(\n      (params) => coreApiClient.menu.listMenu(params),\n      {\n        size: 1000,\n      }\n    );\n\n    if (node.context) {\n      node.context.attrs.options = menus.map((menu) => {\n        return {\n          value: menu.metadata.name,\n          label: menu.spec.displayName,\n        };\n      });\n    }\n  });\n}\n\nexport const menuSelect: FormKitTypeDefinition = {\n  ...select,\n  props: [...(select.props as string[])],\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/password/RevealButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { IconEye, IconEyeOff } from \"@halo-dev/components\";\nimport type { PropType } from \"vue\";\nimport { toRefs } from \"vue\";\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst { context } = toRefs(props);\n\nfunction handleChange() {\n  context.value.node.props.type =\n    context.value.node.props.type === \"password\" ? \"text\" : \"password\";\n}\n</script>\n\n<template>\n  <div\n    class=\"group flex h-full cursor-pointer items-center px-3 transition-all\"\n    @click=\"handleChange\"\n  >\n    <IconEye\n      v-if=\"context.node.props.type !== 'password'\"\n      class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n    />\n    <IconEyeOff\n      v-else\n      class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/password/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  help,\n  icon,\n  inner,\n  label,\n  message,\n  messages,\n  outer,\n  prefix,\n  suffix,\n  textInput,\n  wrapper,\n} from \"@formkit/inputs\";\nimport { createSection } from \"@formkit/inputs\";\nimport RevealButton from \"./RevealButton.vue\";\n\nexport const RevealButtonSuffix = createSection(\"RevealButtonSuffix\", () => ({\n  $cmp: \"RevealButton\",\n  props: {\n    context: \"$node.context\",\n  },\n}));\n\n/**\n * Input definition for a text.\n * @public\n */\nexport const password: FormKitTypeDefinition = {\n  /**\n   * The actual schema of the input, or a function that returns the schema.\n   */\n  schema: outer(\n    wrapper(\n      label(\"$label\"),\n      inner(\n        icon(\"prefix\", \"label\"),\n        prefix(),\n        textInput(),\n        suffix(),\n        RevealButtonSuffix()\n      )\n    ),\n    help(\"$help\"),\n    messages(message(\"$message.value\"))\n  ),\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"input\",\n  /**\n   * The family of inputs this one belongs too. For example \"text\" and \"email\"\n   * are both part of the \"text\" family. This is primary used for styling.\n   */\n  family: \"text\",\n  /**\n   * An array of extra props to accept for this input.\n   */\n  props: [],\n  /**\n   * Forces node.props.type to be this explicit value.\n   */\n  forceTypeProp: \"password\",\n  /**\n   * Additional features that should be added to your input\n   */\n  features: [],\n  /**\n   * The key used to memoize the schema.\n   */\n  schemaMemoKey: \"92o49lnph2p\",\n  library: {\n    RevealButton,\n  },\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/post-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport type { FormKitInputs } from \"@formkit/inputs\";\nimport {\n  consoleApiClient,\n  paginate,\n  type ListedPost,\n  type PostV1alpha1ConsoleApiListPostsRequest,\n} from \"@halo-dev/api-client\";\nimport { postLabels } from \"@/constants/labels\";\nimport { select } from \"./select\";\n\nasync function search({ page, size, keyword }) {\n  const { data } = await consoleApiClient.content.post.listPosts({\n    page,\n    size,\n    keyword,\n    labelSelector: [\n      `${postLabels.DELETED}=false`,\n      `${postLabels.PUBLISHED}=true`,\n    ],\n  });\n\n  return {\n    options: data.items.map((post) => {\n      return {\n        value: post.post.metadata.name,\n        label: post.post.spec.title,\n      };\n    }),\n    total: data.total,\n    size: data.size,\n    page: data.page,\n  };\n}\n\nasync function findOptionsByValues(values: string[]) {\n  if (values.length === 0) {\n    return [];\n  }\n\n  const posts = await paginate<\n    PostV1alpha1ConsoleApiListPostsRequest,\n    ListedPost\n  >((params) => consoleApiClient.content.post.listPosts(params), {\n    fieldSelector: [`metadata.name=(${values.join(\",\")})`],\n    size: 1000,\n  });\n\n  return posts.map((post) => {\n    return {\n      value: post.post.metadata.name,\n      label: post.post.spec.title,\n    };\n  });\n}\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    node.props = {\n      ...node.props,\n      remote: true,\n      remoteOption: {\n        search,\n        findOptionsByValues,\n      },\n      searchable: true,\n    };\n  });\n}\n\nexport const postSelect: FormKitTypeDefinition = {\n  ...select,\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    postSelect: {\n      type: \"postSelect\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/repeater/AddButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { IconAddCircle, VButton } from \"@halo-dev/components\";\nimport type { PropType } from \"vue\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n  disabled: {\n    type: Boolean,\n    required: false,\n  },\n  onClick: {\n    type: Function as PropType<() => void>,\n    required: true,\n  },\n});\n\nconst handleAppendClick = () => {\n  if (!props.disabled && props.onClick) {\n    props.onClick();\n  }\n};\n</script>\n\n<template>\n  <div :class=\"context.classes.add\" @click=\"handleAppendClick\">\n    <VButton :disabled=\"disabled\" type=\"secondary\">\n      <template #icon>\n        <IconAddCircle />\n      </template>\n      {{ context.addLabel || $t(\"core.common.buttons.add\") }}\n    </VButton>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/repeater/features/repeats.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { undefine } from \"@formkit/utils\";\n\nexport const repeats = function (node: FormKitNode) {\n  node._c.sync = true;\n  node.on(\"created\", repeaterFeature.bind(null, node));\n};\n\ntype FnType = (index: number) => object;\n\nfunction createValue(num: number, fn: FnType) {\n  return new Array(num).fill(\"\").map((_, index) => fn(index));\n}\n\nfunction repeaterFeature(node: FormKitNode) {\n  node.props.removeControl = node.props.removeControl ?? true;\n  node.props.upControl = node.props.upControl ?? true;\n  node.props.downControl = node.props.downControl ?? true;\n  node.props.insertControl = node.props.insertControl ?? true;\n  node.props.addButton = node.props.addButton ?? true;\n  node.props.addLabel = node.props.addLabel ?? false;\n  node.props.addAttrs = node.props.addAttrs ?? {};\n  node.props.min = node.props.min ? Number(node.props.min) : 0;\n  node.props.max = node.props.max ? Number(node.props.max) : 1 / 0;\n  if (node.props.min > node.props.max) {\n    throw Error(\"Repeater: min must be less than max\");\n  }\n\n  if (\"disabled\" in node.props) {\n    node.props.disabled = undefine(node.props.disabled);\n  }\n\n  if (Array.isArray(node.value)) {\n    if (node.value.length < node.props.min) {\n      const value = createValue(node.props.min - node.value.length, () => ({}));\n      node.input(node.value.concat(value), false);\n    } else {\n      if (node.value.length > node.props.max) {\n        node.input(node.value.slice(0, node.props.max), false);\n      }\n    }\n  } else {\n    node.input(\n      createValue(node.props.min, () => ({})),\n      false\n    );\n  }\n\n  if (node.context) {\n    const fns = node.context.fns;\n    fns.createShift = (index: number, offset: number) => () => {\n      const value = node._value as unknown[];\n      value.splice(index + offset, 0, value.splice(index, 1)[0]);\n      node.input(value, false);\n    };\n    fns.createInsert = (index: number) => () => {\n      const value = node._value as unknown[];\n      value.splice(index + 1, 0, {});\n      node.input(value, false);\n    };\n    fns.createAppend = () => () => {\n      const value = node._value as unknown[];\n      value.push({});\n      node.input(value, false);\n    };\n    fns.createRemover = (index: number) => () => {\n      const value = node._value as unknown[];\n      value.splice(index, 1);\n      node.input(value, false);\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/repeater/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  $if,\n  fieldset,\n  help,\n  inner,\n  legend,\n  message,\n  messages,\n  outer,\n  prefix,\n  suffix,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport {\n  IconAddCircle,\n  IconArrowDownCircleLine,\n  IconArrowUpCircleLine,\n  IconCloseCircle,\n} from \"@halo-dev/components\";\nimport { i18n } from \"@/locales\";\nimport AddButton from \"./AddButton.vue\";\nimport { repeats } from \"./features/repeats\";\nimport {\n  addButton,\n  content,\n  controls,\n  down,\n  downControl,\n  downIcon,\n  empty,\n  group,\n  insert,\n  insertControl,\n  insertIcon,\n  item,\n  items,\n  remove,\n  removeControl,\n  removeIcon,\n  up,\n  upControl,\n  upIcon,\n} from \"./sections\";\n\n/**\n * Input definition for a repeater input.\n *\n * it is recommended to use array instead of repeater. {@link array}\n *\n * @public\n * @deprecated Use array instead.\n * @see array\n */\nexport const repeater: FormKitTypeDefinition = {\n  /**\n   * The actual schema of the input, or a function that returns the schema.\n   */\n  schema: outer(\n    fieldset(\n      legend(\"$label\"),\n      help(\"$help\"),\n\n      inner(\n        prefix(),\n        $if(\n          \"$value.length === 0\",\n          $if(\"$slots.empty\", empty()),\n          $if(\n            \"$slots.default\",\n            items(\n              item(\n                content(group(\"$slots.default\")),\n                controls(\n                  up(upControl(upIcon())),\n                  remove(removeControl(removeIcon())),\n                  insert(insertControl(insertIcon())),\n                  down(downControl(downIcon()))\n                )\n              )\n            ),\n            suffix()\n          )\n        ),\n        addButton(`$addLabel || (${i18n.global.t(\"core.common.buttons.add\")})`)\n      )\n    ),\n    messages(message(\"$message.value\"))\n  ),\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"list\",\n  /**\n   * An array of extra props to accept for this input.\n   */\n  props: [\n    \"min\",\n    \"max\",\n    \"upControl\",\n    \"downControl\",\n    \"removeControl\",\n    \"insertControl\",\n    \"addLabel\",\n    \"addButton\",\n  ],\n  /**\n   * Additional features that make this input work.\n   */\n  features: [repeats],\n  library: {\n    IconAddCircle,\n    IconCloseCircle,\n    IconArrowUpCircleLine,\n    IconArrowDownCircleLine,\n    AddButton,\n  },\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    repeater: {\n      type: \"repeater\";\n      value?: Record<string, unknown>[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/repeater/repeaterSection.ts",
    "content": "import {\n  isComponent,\n  isDOM,\n  type FormKitExtendableSchemaRoot,\n  type FormKitSchemaCondition,\n  type FormKitSchemaNode,\n  type FormKitSectionsSchema,\n} from \"@formkit/core\";\nimport {\n  extendSchema,\n  type FormKitSchemaExtendableSection,\n  type FormKitSection,\n} from \"@formkit/inputs\";\n\nexport function createRepeaterSection() {\n  return (\n    section: string,\n    el: string | null | (() => FormKitSchemaNode),\n    fragment = false\n  ) => {\n    return createSection(\n      section,\n      el,\n      fragment\n    ) as FormKitSection<FormKitSchemaExtendableSection>;\n  };\n}\n\nfunction createSection(\n  section: string,\n  el: string | null | (() => FormKitSchemaNode),\n  fragment = false\n): FormKitSection<\n  FormKitExtendableSchemaRoot | FormKitSchemaExtendableSection\n> {\n  return (\n    ...children: Array<\n      FormKitSchemaExtendableSection | string | FormKitSchemaCondition\n    >\n  ) => {\n    const extendable = (extensions: FormKitSectionsSchema) => {\n      const node = !el || typeof el === \"string\" ? { $el: el } : el();\n      if (\"string\" != typeof node) {\n        if (isDOM(node) || isComponent(node) || \"$formkit\" in node) {\n          if (children.length && !node.children) {\n            node.children = [\n              ...children.map((child) =>\n                typeof child === \"function\" ? child(extensions) : child\n              ),\n            ];\n          }\n          if (!node.meta) {\n            node.meta = { section };\n          }\n          if (isDOM(node)) {\n            node.attrs = {\n              class: `$classes.${section}`,\n              ...(node.attrs || {}),\n            };\n          }\n          if (\"$formkit\" in node) {\n            node.outerClass = `$classes.${section}`;\n          }\n        }\n      }\n      return {\n        if: `$slots.${section}`,\n        then: `$slots.${section}`,\n        else:\n          section in extensions\n            ? extendSchema(node as FormKitSchemaNode, extensions[section])\n            : node,\n      } as FormKitSchemaCondition;\n    };\n    extendable._s = section;\n    return fragment ? createRoot(extendable) : extendable;\n  };\n}\n\n/**\n *\n * Returns an extendable schema root node.\n *\n * @param rootSection - Creates the root node.\n *\n * @returns {@link @formkit/core#FormKitExtendableSchemaRoot | FormKitExtendableSchemaRoot}\n *\n * @internal\n */\nexport function createRoot(\n  rootSection: FormKitSchemaExtendableSection\n): FormKitExtendableSchemaRoot {\n  return (extensions: FormKitSectionsSchema) => {\n    return [rootSection(extensions)];\n  };\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/repeater/sections/index.ts",
    "content": "import { createRepeaterSection } from \"../repeaterSection\";\n\nconst repeaterSection = createRepeaterSection();\n\nexport const addButton = repeaterSection(\"addButton\", () => ({\n  $cmp: \"AddButton\",\n  props: {\n    onClick: \"$fns.createAppend()\",\n    disabled: \"$value.length >= $max\",\n    context: \"$node.context\",\n  },\n  bind: \"$addAttrs\",\n  if: \"$addButton\",\n  type: \"button\",\n}));\n\nexport const content = repeaterSection(\"content\", \"div\");\n\nexport const controlLabel = repeaterSection(\"controlLabel\", \"span\");\n\nexport const controls = repeaterSection(\"controls\", () => ({\n  $el: \"ul\",\n  if: \"$removeControl || $insertControl || $upControl || $downControl\",\n}));\n\nexport const down = repeaterSection(\"down\", () => ({\n  $el: \"li\",\n  if: \"$downControl\",\n}));\n\nexport const downControl = repeaterSection(\"downControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$index >= $value.length - 1\",\n    onClick: \"$fns.createShift($index, 1)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const downIcon = repeaterSection(\"downIcon\", () => ({\n  $cmp: \"IconArrowDownCircleLine\",\n}));\n\nexport const empty = repeaterSection(\"empty\", () => ({\n  $el: \"div\",\n}));\n\nexport const fieldset = repeaterSection(\"fieldset\", () => ({\n  $el: \"fieldset\",\n  attrs: {\n    id: \"$id\",\n    disabled: \"$disabled\",\n  },\n}));\n\nexport const group = repeaterSection(\"group\", () => ({\n  $formkit: \"nativeGroup\",\n  index: \"$index\",\n}));\n\nexport const insert = repeaterSection(\"insert\", () => ({\n  $el: \"li\",\n  if: \"$insertControl\",\n}));\n\nexport const insertControl = repeaterSection(\"insertControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$value.length >= $max\",\n    onClick: \"$fns.createInsert($index)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const insertIcon = repeaterSection(\"insertIcon\", () => ({\n  $cmp: \"IconAddCircle\",\n}));\n\nexport const item = repeaterSection(\"item\", () => ({\n  $el: \"li\",\n  for: [\"item\", \"index\", \"$items\"],\n  attrs: {\n    role: \"listitem\",\n    key: \"$item\",\n    \"data-index\": \"$index\",\n  },\n}));\n\nexport const items = repeaterSection(\"items\", () => ({\n  $el: \"ul\",\n  attrs: {\n    role: \"list\",\n  },\n}));\n\nexport const remove = repeaterSection(\"remove\", () => ({\n  $el: \"li\",\n  if: \"$removeControl\",\n}));\n\nexport const removeControl = repeaterSection(\"removeControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$value.length <= $min\",\n    onClick: \"$fns.createRemover($index)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const removeIcon = repeaterSection(\"removeIcon\", () => ({\n  $cmp: \"IconCloseCircle\",\n}));\n\nexport const up = repeaterSection(\"up\", () => ({\n  $el: \"li\",\n  if: \"$upControl\",\n}));\n\nexport const upControl = repeaterSection(\"upControl\", () => ({\n  $el: \"button\",\n  attrs: {\n    disabled: \"$index <= 0\",\n    onClick: \"$fns.createShift($index, -1)\",\n    type: \"button\",\n    class: `$classes.control`,\n  },\n}));\n\nexport const upIcon = repeaterSection(\"upIcon\", () => ({\n  $cmp: \"IconArrowUpCircleLine\",\n}));\n"
  },
  {
    "path": "ui/src/formkit/inputs/role-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport type { FormKitInputs } from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type Role,\n  type RoleV1alpha1ApiListRoleRequest,\n} from \"@halo-dev/api-client\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { roleLabels } from \"@/constants/labels\";\nimport { select } from \"./select\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const roles = await paginate<RoleV1alpha1ApiListRoleRequest, Role>(\n      (params) => coreApiClient.role.listRole(params),\n      {\n        size: 1000,\n        labelSelector: [`!${roleLabels.TEMPLATE}`],\n      }\n    );\n\n    const options = [\n      ...roles.map((role) => {\n        return {\n          label:\n            role.metadata?.annotations?.[rbacAnnotations.DISPLAY_NAME] ||\n            role.metadata.name,\n          value: role.metadata?.name,\n        };\n      }),\n    ];\n    if (node.context) {\n      node.context.attrs.options = options;\n    }\n  });\n}\n\nexport const roleSelect: FormKitTypeDefinition = {\n  ...select,\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    roleSelect: {\n      type: \"roleSelect\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/SecretSelect.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  IconClose,\n  IconExchange,\n  IconInformation,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { computed, ref, type PropType } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport MingcuteFileSecurityLine from \"~icons/mingcute/file-security-line\";\nimport RiEditBoxLine from \"~icons/ri/edit-box-line\";\nimport { secretAnnotations } from \"@/constants/annotations\";\nimport SecretCreationModal from \"./components/SecretCreationModal.vue\";\nimport SecretEditModal from \"./components/SecretEditModal.vue\";\nimport SecretListModal from \"./components/SecretListModal.vue\";\nimport type { RequiredKey } from \"./types\";\n\nconst { t } = useI18n();\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst currentValue = computed(() => props.context._value);\nconst requiredKeys = computed(\n  () => props.context.requiredKeys as RequiredKey[]\n);\n\nconst hasPermission = utils.permission.has([\"*\"]);\n\nconst { data, isLoading, refetch } = useQuery({\n  queryKey: [\"core:formkit:inputs:secret\", currentValue, hasPermission],\n  queryFn: async () => {\n    if (!currentValue.value || !hasPermission) {\n      return null;\n    }\n\n    const { data } = await coreApiClient.secret.getSecret({\n      name: currentValue.value as string,\n    });\n\n    return data;\n  },\n});\n\nconst title = computed(() => {\n  if (isLoading.value) {\n    return t(\"core.common.status.loading\");\n  }\n\n  if (!data.value) {\n    return;\n  }\n\n  return (\n    data.value?.metadata.annotations?.[secretAnnotations.DESCRIPTION] ||\n    data.value?.metadata.name\n  );\n});\n\nconst keys = computed(() => {\n  return Object.keys(data.value?.stringData || {});\n});\n\nconst description = computed(() => {\n  if (isLoading.value) {\n    return \"--\";\n  }\n\n  if (!hasPermission) {\n    return t(\"core.formkit.secret.no_permission\");\n  }\n\n  if (!data.value) {\n    return t(\"core.formkit.secret.no_selected\");\n  }\n\n  if (keys.value.length > 0) {\n    return t(\"core.formkit.secret.includes_keys\", {\n      keys: keys.value.join(\", \"),\n    });\n  }\n\n  return t(\"core.formkit.secret.no_fields\");\n});\n\n// Select\nconst listModalVisible = ref(false);\n\nfunction onSelect(secretName: string) {\n  props.context.node.input(secretName);\n  listModalVisible.value = false;\n}\n\nfunction onListModalClose() {\n  listModalVisible.value = false;\n  refetch();\n}\n\n// Create\nconst creationModalVisible = ref(false);\n\nfunction onCreated(secretName: string) {\n  props.context.node.input(secretName);\n  creationModalVisible.value = false;\n}\n\n// Edit\nconst editModalVisible = ref(false);\n\nfunction onEditModalClose() {\n  editModalVisible.value = false;\n  refetch();\n}\n\nconst missingKeys = computed(() => {\n  return requiredKeys.value.filter((key) => !keys.value.includes(key.key));\n});\n</script>\n<template>\n  <div\n    class=\"flex items-center gap-2 rounded-lg border border-gray-100 px-2.5 py-2\"\n  >\n    <div\n      class=\"inline-flex size-8 flex-none items-center justify-center rounded-full\"\n      :class=\"{\n        'bg-indigo-50': !!data,\n        'bg-gray-100': !data,\n        'bg-red-50': !hasPermission,\n      }\"\n    >\n      <MingcuteFileSecurityLine\n        class=\"size-5\"\n        :class=\"{\n          'text-indigo-500': !!data,\n          'text-gray-500': !data,\n          'text-red-500': !hasPermission,\n        }\"\n      />\n    </div>\n    <div class=\"min-w-0 flex-1 shrink space-y-1\">\n      <div v-if=\"title\" class=\"line-clamp-1 text-sm font-semibold\">\n        {{ title }}\n      </div>\n      <div class=\"line-clamp-1 text-xs text-gray-500\">\n        {{ description || \"--\" }}\n      </div>\n    </div>\n    <div v-if=\"hasPermission\" class=\"flex items-center gap-1\">\n      <button\n        v-if=\"data\"\n        type=\"button\"\n        class=\"p-1 text-gray-500 hover:text-gray-900\"\n        @click=\"context.node.input('')\"\n      >\n        <IconClose class=\"size-4\" />\n      </button>\n      <button\n        v-if=\"data\"\n        type=\"button\"\n        class=\"p-1 text-gray-500 hover:text-gray-900\"\n        @click=\"editModalVisible = true\"\n      >\n        <RiEditBoxLine class=\"size-4\" />\n      </button>\n      <button\n        v-else\n        type=\"button\"\n        class=\"p-1 text-gray-500 hover:text-gray-900\"\n        @click=\"creationModalVisible = true\"\n      >\n        <IconAddCircle class=\"size-4\" />\n      </button>\n      <button\n        type=\"button\"\n        class=\"p-1 text-gray-500 hover:text-gray-900\"\n        @click=\"listModalVisible = true\"\n      >\n        <IconExchange class=\"size-4\" />\n      </button>\n    </div>\n  </div>\n\n  <div\n    v-if=\"currentValue && missingKeys.length > 0\"\n    class=\"my-2 flex items-center gap-1 text-xs text-red-500\"\n  >\n    <IconInformation />\n    <span>\n      {{\n        t(\"core.formkit.secret.missing_keys\", {\n          keys: missingKeys.map((key) => key.key).join(\", \"),\n        })\n      }}\n    </span>\n  </div>\n\n  <SecretCreationModal\n    v-if=\"creationModalVisible\"\n    :required-keys=\"requiredKeys\"\n    @close=\"creationModalVisible = false\"\n    @created=\"onCreated\"\n  />\n\n  <SecretEditModal\n    v-if=\"editModalVisible && data\"\n    :secret=\"data\"\n    :required-keys=\"requiredKeys\"\n    @close=\"onEditModalClose\"\n  />\n\n  <SecretListModal\n    v-if=\"listModalVisible\"\n    :selected-secret-name=\"currentValue\"\n    :required-keys=\"requiredKeys\"\n    @select=\"onSelect\"\n    @close=\"onListModalClose\"\n  />\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/components/SecretCreationModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport { VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { secretAnnotations } from \"@/constants/annotations\";\nimport { Q_KEY } from \"../composables/use-secrets-fetch\";\nimport type { RequiredKey, SecretFormState } from \"../types\";\nimport SecretForm from \"./SecretForm.vue\";\n\nconst queryClient = useQueryClient();\n\nwithDefaults(\n  defineProps<{\n    formState?: SecretFormState;\n    requiredKeys?: RequiredKey[];\n  }>(),\n  { formState: undefined, requiredKeys: () => [] }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"created\", secretName: string): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"create-secret\"],\n  mutationFn: async ({ data }: { data: SecretFormState }) => {\n    const stringData = data.stringDataArray\n      .filter(({ key }) => !!key)\n      .reduce(\n        (acc, { key, value }) => {\n          acc[key] = value || \"\";\n          return acc;\n        },\n        {} as Record<string, string>\n      );\n\n    return await coreApiClient.secret.createSecret({\n      secret: {\n        metadata: {\n          generateName: \"secret-\",\n          name: \"\",\n          annotations: {\n            [secretAnnotations.DESCRIPTION]: data.description || \"\",\n          },\n        },\n        kind: \"Secret\",\n        apiVersion: \"v1alpha1\",\n        type: \"Opaque\",\n        stringData: stringData,\n      },\n    });\n  },\n  onSuccess(data) {\n    queryClient.invalidateQueries({ queryKey: Q_KEY() });\n    emit(\"created\", data.data.metadata.name);\n    modal.value?.close();\n  },\n});\n\nfunction onSubmit(data: SecretFormState) {\n  mutate({ data });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    mount-to-body\n    :title=\"$t('core.formkit.secret.creation_modal.title')\"\n    :width=\"600\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <SecretForm\n      :form-state=\"formState\"\n      :required-keys=\"requiredKeys\"\n      @submit=\"onSubmit\"\n    />\n\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          @click=\"$formkit.submit('secret-form')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/components/SecretEditModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { coreApiClient, type Secret } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { secretAnnotations } from \"@/constants/annotations\";\nimport { Q_KEY } from \"../composables/use-secrets-fetch\";\nimport type { RequiredKey, SecretFormState } from \"../types\";\nimport SecretForm from \"./SecretForm.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    secret: Secret;\n    requiredKeys?: RequiredKey[];\n  }>(),\n  { requiredKeys: () => [] }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"create-secret\"],\n  mutationFn: async ({ data }: { data: SecretFormState }) => {\n    const stringData = data.stringDataArray\n      .filter(({ key }) => !!key)\n      .reduce(\n        (acc, { key, value }) => {\n          acc[key] = value || \"\";\n          return acc;\n        },\n        {} as Record<string, string>\n      );\n\n    return await coreApiClient.secret.patchSecret({\n      name: props.secret.metadata.name,\n      jsonPatchInner: [\n        {\n          op: \"add\",\n          path: \"/stringData\",\n          value: stringData,\n        },\n        {\n          op: \"add\",\n          path: `/metadata/annotations`,\n          value: {\n            ...props.secret.metadata.annotations,\n            [secretAnnotations.DESCRIPTION]: data.description,\n          },\n        },\n      ],\n    });\n  },\n  onSuccess() {\n    queryClient.invalidateQueries({ queryKey: Q_KEY() });\n    Toast.success(t(\"core.common.toast.save_success\"));\n    modal.value?.close();\n  },\n});\n\nfunction onSubmit(data: SecretFormState) {\n  mutate({ data });\n}\n\nconst formState: SecretFormState = {\n  description:\n    props.secret.metadata.annotations?.[secretAnnotations.DESCRIPTION],\n  stringDataArray: Object.entries(props.secret.stringData || {}).map(\n    ([key, value]) => ({\n      key: key || \"\",\n      value: value || \"\",\n    })\n  ),\n};\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    mount-to-body\n    :title=\"$t('core.formkit.secret.edit_modal.title')\"\n    :width=\"600\"\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <SecretForm\n      :form-state=\"formState\"\n      :required-keys=\"requiredKeys\"\n      @submit=\"onSubmit\"\n    />\n\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          @click=\"$formkit.submit('secret-form')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/components/SecretForm.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VAlert, VButton } from \"@halo-dev/components\";\nimport { computed, onMounted, ref } from \"vue\";\nimport type { RequiredKey, SecretFormState } from \"../types\";\n\nconst props = withDefaults(\n  defineProps<{\n    formState?: SecretFormState;\n    requiredKeys?: RequiredKey[];\n  }>(),\n  {\n    formState: undefined,\n    requiredKeys: () => [],\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"submit\", data: SecretFormState): void;\n}>();\n\nfunction onSubmit(data: SecretFormState) {\n  emit(\"submit\", data);\n}\n\nconst stringDataArray = ref<{ key: string; value: string }[]>([]);\n\nonMounted(() => {\n  if (props.formState) {\n    return;\n  }\n\n  for (const key of props.requiredKeys) {\n    stringDataArray.value.push({\n      key: key.key,\n      value: \"\",\n    });\n  }\n});\n\nfunction getHelpOfRequiredKey(key: string) {\n  const requiredKey = props.requiredKeys.find((k) => k.key === key);\n  return requiredKey?.help;\n}\n\nconst missingKeys = computed(() => {\n  return props.requiredKeys.filter(\n    (k) => !stringDataArray.value.some((s) => s.key === k.key)\n  );\n});\n\nfunction handleAddMissingKeys() {\n  for (const key of missingKeys.value) {\n    stringDataArray.value.push({\n      key: key.key,\n      value: \"\",\n    });\n  }\n}\n</script>\n\n<template>\n  <div v-if=\"missingKeys.length > 0\" class=\"mb-4\">\n    <VAlert :title=\"$t('core.common.text.tip')\">\n      <template #description>\n        {{ $t(\"core.formkit.secret.required_keys_tip\") }}\n        <ul class=\"mt-2\">\n          <li v-for=\"key in requiredKeys\" :key=\"key.key\">\n            {{ key.key }} : {{ key.help }}\n          </li>\n        </ul>\n      </template>\n      <template #actions>\n        <VButton size=\"sm\" @click=\"handleAddMissingKeys\">\n          {{ $t(\"core.common.buttons.add\") }}\n        </VButton>\n      </template>\n    </VAlert>\n  </div>\n  <FormKit\n    id=\"secret-form\"\n    type=\"form\"\n    :model-value=\"formState as unknown as Record<string, unknown>\"\n    name=\"secret-form\"\n    ignore\n    :config=\"{ validationVisibility: 'submit' }\"\n    @submit=\"onSubmit\"\n  >\n    <FormKit\n      :label=\"$t('core.formkit.secret.form.fields.description')\"\n      name=\"description\"\n    ></FormKit>\n    <!-- @vue-ignore -->\n    <FormKit\n      v-slot=\"{ value }\"\n      v-model=\"stringDataArray\"\n      type=\"repeater\"\n      name=\"stringDataArray\"\n      :label=\"$t('core.formkit.secret.form.fields.string_data')\"\n    >\n      <FormKit\n        validation=\"required\"\n        name=\"key\"\n        label=\"Key\"\n        :help=\"getHelpOfRequiredKey(value.key)\"\n      ></FormKit>\n      <FormKit\n        type=\"code\"\n        validation=\"required\"\n        name=\"value\"\n        label=\"Value\"\n      ></FormKit>\n    </FormKit>\n  </FormKit>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/components/SecretListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { coreApiClient, type Secret } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { secretAnnotations } from \"@/constants/annotations\";\nimport { Q_KEY } from \"../composables/use-secrets-fetch\";\nimport type { RequiredKey } from \"../types\";\nimport SecretEditModal from \"./SecretEditModal.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    secret: Secret;\n    selected?: boolean;\n    requiredKeys?: RequiredKey[];\n  }>(),\n  {\n    selected: false,\n    requiredKeys: () => [],\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"click\"): void;\n}>();\n\nfunction handleDelete() {\n  Dialog.warning({\n    title: t(\"core.formkit.secret.operations.delete.title\"),\n    description: t(\"core.formkit.secret.operations.delete.description\"),\n    confirmType: \"danger\",\n    async onConfirm() {\n      await coreApiClient.secret.deleteSecret({\n        name: props.secret.metadata.name,\n      });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n      queryClient.invalidateQueries({ queryKey: Q_KEY() });\n    },\n  });\n}\n\nconst editModalVisible = ref(false);\n\nconst description = computed(() => {\n  return (\n    props.secret.metadata.annotations?.[secretAnnotations.DESCRIPTION] || \"\"\n  );\n});\n\nconst keys = computed(() => {\n  return Object.keys(props.secret.stringData || {});\n});\n\nconst descriptionText = computed(() => {\n  if (keys.value.length > 0) {\n    return t(\"core.formkit.secret.includes_keys\", {\n      keys: keys.value.join(\", \"),\n    });\n  }\n  return t(\"core.formkit.secret.no_fields\");\n});\n</script>\n\n<template>\n  <SecretEditModal\n    v-if=\"editModalVisible\"\n    :secret=\"secret\"\n    :required-keys=\"requiredKeys\"\n    @close=\"editModalVisible = false\"\n  />\n  <VEntity :is-selected=\"selected\" @click=\"emit('click')\">\n    <template #checkbox>\n      <slot name=\"checkbox\" />\n    </template>\n    <template #start>\n      <VEntityField\n        :title=\"description || secret.metadata.name\"\n        :description=\"descriptionText\"\n      ></VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"secret.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n    </template>\n    <template #dropdownItems>\n      <VDropdownItem @click=\"editModalVisible = true\">\n        {{ $t(\"core.common.buttons.edit\") }}\n      </VDropdownItem>\n      <VDropdownDivider />\n      <VDropdownItem type=\"danger\" @click=\"handleDelete\">\n        {{ $t(\"core.common.buttons.delete\") }}\n      </VDropdownItem>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/components/SecretListModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconAddCircle,\n  VButton,\n  VEntityContainer,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { ref } from \"vue\";\nimport { useSecretsFetch } from \"../composables/use-secrets-fetch\";\nimport type { RequiredKey } from \"../types\";\nimport SecretCreationModal from \"./SecretCreationModal.vue\";\nimport SecretListItem from \"./SecretListItem.vue\";\n\nconst props = defineProps<{\n  selectedSecretName: string;\n  requiredKeys?: RequiredKey[];\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"select\", secretName: string): void;\n}>();\n\nconst { data } = useSecretsFetch();\n\nconst creationModalVisible = ref(false);\n\nconst selectedSecretName = ref(props.selectedSecretName);\n\nfunction handleSelect() {\n  emit(\"select\", selectedSecretName.value);\n  modal?.value?.close();\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :title=\"$t('core.formkit.secret.list_modal.title')\"\n    :width=\"650\"\n    mount-to-body\n    layer-closable\n    :centered=\"false\"\n    @close=\"emit('close')\"\n  >\n    <template #actions>\n      <span\n        v-tooltip=\"$t('core.common.buttons.new')\"\n        @click=\"creationModalVisible = true\"\n      >\n        <IconAddCircle />\n      </span>\n    </template>\n    <VEntityContainer>\n      <SecretListItem\n        v-for=\"secret in data\"\n        :key=\"secret.metadata.name\"\n        :secret=\"secret\"\n        :selected=\"selectedSecretName === secret.metadata.name\"\n        :required-keys=\"requiredKeys\"\n        @click=\"selectedSecretName = secret.metadata.name\"\n      >\n        <template #checkbox>\n          <input\n            v-model=\"selectedSecretName\"\n            type=\"radio\"\n            name=\"secret\"\n            :value=\"secret.metadata.name\"\n          />\n        </template>\n      </SecretListItem>\n    </VEntityContainer>\n    <template #footer>\n      <VSpace>\n        <VButton type=\"secondary\" @click=\"handleSelect\">\n          {{ $t(\"core.common.buttons.confirm\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n\n  <SecretCreationModal\n    v-if=\"creationModalVisible\"\n    :required-keys=\"requiredKeys\"\n    @close=\"creationModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/composables/use-secrets-fetch.ts",
    "content": "import {\n  coreApiClient,\n  paginate,\n  type Secret,\n  type SecretV1alpha1ApiListSecretRequest,\n} from \"@halo-dev/api-client\";\nimport { useQuery } from \"@tanstack/vue-query\";\n\nexport const Q_KEY = () => [\"secrets\"];\n\nexport function useSecretsFetch() {\n  return useQuery({\n    queryKey: Q_KEY(),\n    queryFn: async () => {\n      return await paginate<SecretV1alpha1ApiListSecretRequest, Secret>(\n        (params) => coreApiClient.secret.listSecret(params),\n        {\n          size: 1000,\n        }\n      );\n    },\n    refetchInterval(data) {\n      const hasDeletingData = data?.some(\n        (item) => !!item.metadata.deletionTimestamp\n      );\n      return hasDeletingData ? 1000 : false;\n    },\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/feature.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\n\nexport default function secretFeature(node: FormKitNode): void {\n  node.on(\"created\", () => {\n    node.props.requiredKeys = node.props.requiredKeys ?? [];\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/index.ts",
    "content": "import { initialValue, type FormKitInputs } from \"@formkit/inputs\";\nimport { createInput } from \"@formkit/vue\";\nimport { defineAsyncComponent } from \"vue\";\nimport secretFeature from \"./feature\";\nimport type { RequiredKey } from \"./types\";\n\nexport const secret = createInput(\n  defineAsyncComponent(() => import(\"./SecretSelect.vue\")),\n  {\n    type: \"input\",\n    props: [\"requiredKeys\"],\n    features: [initialValue, secretFeature],\n  }\n);\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    secret: {\n      type: \"secret\";\n      requiredKeys?: RequiredKey[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/sections/index.ts",
    "content": "import { createSection } from \"@formkit/inputs\";\n\nexport const SecretSection = createSection(\"SecretSection\", () => ({\n  $cmp: \"SecretSelect\",\n  props: {\n    context: \"$node.context\",\n  },\n}));\n"
  },
  {
    "path": "ui/src/formkit/inputs/secret/types/index.ts",
    "content": "export interface SecretFormState {\n  description?: string;\n  stringDataArray: { key: string; value: string }[];\n}\n\nexport interface RequiredKey {\n  key: string;\n  help?: string;\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/MultipleOverflow.vue",
    "content": "<script setup lang=\"ts\"></script>\n<template>\n  <div class=\"relative flex max-w-full flex-auto flex-wrap\">\n    <slot></slot>\n    <slot name=\"input\"></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/MultipleOverflowItem.vue",
    "content": "<script lang=\"ts\" setup></script>\n\n<template>\n  <div\n    class=\"overflow-item inline-flex max-w-full flex-none items-center self-center\"\n  >\n    <slot></slot>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/MultipleSelect.vue",
    "content": "<script lang=\"ts\" setup>\nimport type {\n  NodeDragEventData,\n  NodeTouchEventData,\n} from \"@formkit/drag-and-drop\";\nimport { useDragAndDrop } from \"@formkit/drag-and-drop/vue\";\nimport { watch } from \"vue\";\nimport MultipleOverflowItem from \"./MultipleOverflowItem.vue\";\nimport MultipleSelectItem from \"./MultipleSelectItem.vue\";\n\nconst props = defineProps<{\n  selectedOptions: Array<{\n    label: string;\n    value: string;\n  }>;\n  sortable: boolean;\n}>();\n\nconst emit = defineEmits<{\n  (\n    event: \"deleteItem\",\n    index: number,\n    option?: {\n      label: string;\n      value: string;\n    }\n  ): void;\n  (event: \"sort\", value: Array<{ label: string; value: string }>): void;\n}>();\n\nconst [parent, options] = useDragAndDrop<{\n  label: string;\n  value: string;\n}>(props.selectedOptions, {\n  disabled: !props.sortable,\n  sortable: true,\n  handleEnd: (\n    data:\n      | NodeDragEventData<{\n          label: string;\n          value: string;\n        }>\n      | NodeTouchEventData<{\n          label: string;\n          value: string;\n        }>\n  ) => {\n    const nodeData = data.targetData.node.data;\n    const dragBeforeIndex = props.selectedOptions.findIndex(\n      (option) => option.value === nodeData.value.value\n    );\n    if (dragBeforeIndex != nodeData.index) {\n      emit(\"sort\", options.value);\n    }\n  },\n});\n\nwatch(\n  () => props.selectedOptions,\n  (value) => {\n    options.value = value;\n  }\n);\n</script>\n\n<template>\n  <div ref=\"parent\">\n    <MultipleOverflowItem\n      v-for=\"(option, index) in options\"\n      :key=\"option.value\"\n    >\n      <MultipleSelectItem\n        class=\"mx-1 my-0.5 ml-0\"\n        @delete-select-item=\"emit('deleteItem', index, option)\"\n      >\n        <span\n          tabindex=\"-1\"\n          class=\"select-item mr-1 inline-block cursor-default overflow-hidden truncate whitespace-pre\"\n        >\n          {{ option.label }}\n        </span>\n      </MultipleSelectItem>\n    </MultipleOverflowItem>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/MultipleSelectItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconClose } from \"@halo-dev/components\";\n\nconst emit = defineEmits<{\n  (event: \"deleteSelectItem\"): void;\n}>();\n</script>\n<template>\n  <div\n    class=\"inline-flex select-none items-center self-center rounded bg-gray-200 px-2 py-1 text-sm font-medium text-gray-800\"\n  >\n    <slot></slot>\n    <span @click.stop=\"emit('deleteSelectItem')\">\n      <IconClose\n        class=\"h-4 w-4 cursor-pointer text-gray-400 hover:text-gray-700\"\n      />\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/MultipleSelectSearchInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport { nextTick, ref, watch } from \"vue\";\n\ndefineProps<{\n  searchable: boolean;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"search\", value: string, e?: Event): void;\n  (event: \"enter\", value: string): void;\n}>();\n\nconst inputHTMLRef = ref<HTMLInputElement | null>(null);\nconst inputMirrorRef = ref<HTMLSpanElement | null>(null);\nconst searchInputContainerRef = ref<HTMLSpanElement | null>(null);\nconst inputValue = ref(\"\");\nconst inputMirrorValue = ref(\"\");\n\nwatch(\n  inputMirrorValue,\n  () => {\n    nextTick(() => {\n      if (searchInputContainerRef.value && inputMirrorRef.value) {\n        searchInputContainerRef.value.style.width =\n          inputMirrorRef.value.offsetWidth + 10 + \"px\";\n      }\n    });\n  },\n  {\n    immediate: true,\n  }\n);\n\nconst handleInputSearch = (event: Event) => {\n  if (event instanceof InputEvent) {\n    const target = event.target as HTMLInputElement;\n    inputMirrorValue.value = target.value;\n  }\n  emit(\"search\", inputValue.value, event);\n};\n\ndefineExpose({\n  inputHTML: inputHTMLRef,\n});\n</script>\n\n<template>\n  <div class=\"relative max-w-full cursor-text\">\n    <span ref=\"searchInputContainerRef\" class=\"relative flex max-w-full\">\n      <input\n        ref=\"inputHTMLRef\"\n        v-model=\"inputValue\"\n        :readonly=\"!searchable\"\n        type=\"text\"\n        autocomplete=\"off\"\n        class=\"m-0 h-full w-full cursor-auto !appearance-none border-none bg-transparent p-0 pe-0 ps-0 text-base outline-none\"\n        @input=\"handleInputSearch\"\n      />\n      <span\n        ref=\"inputMirrorRef\"\n        class=\"invisible absolute end-auto start-0 m-0 whitespace-pre border-none p-0 text-base outline-none\"\n        aria-hidden=\"true\"\n      >\n        {{ inputMirrorValue }}\n        &nbsp;\n      </span>\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/MultipleSelectSelector.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport MultipleOverflow from \"./MultipleOverflow.vue\";\nimport MultipleOverflowItem from \"./MultipleOverflowItem.vue\";\nimport MultipleSelect from \"./MultipleSelect.vue\";\nimport MultipleSelectSearchInput from \"./MultipleSelectSearchInput.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    sortable: boolean;\n    placeholder?: string;\n    selectedOptions: Array<{\n      label: string;\n      value: string;\n    }>;\n    searchable: boolean;\n  }>(),\n  {\n    placeholder: \"Select...\",\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"search\", value: string): void;\n  (event: \"blur\", value: FocusEvent): void;\n  (\n    event: \"deleteItem\",\n    index: number,\n    option?: {\n      label: string;\n      value: string;\n    }\n  ): void;\n  (event: \"sort\", value: Array<{ label: string; value: string }>): void;\n}>();\n\nconst inputRef = ref();\nconst inputValue = ref(\"\");\nconst isCombinationInput = ref(false);\n\nconst handleSearch = (value: string, event?: Event) => {\n  inputValue.value = value;\n  if (event && event instanceof InputEvent) {\n    isCombinationInput.value = event.isComposing;\n  } else {\n    isCombinationInput.value = false;\n  }\n  emit(\"search\", value);\n};\n\nconst handleSearchInputBackspace = () => {\n  // If the input is in composition mode, do not delete the selected item\n  if (isCombinationInput.value) {\n    return;\n  }\n  if (!inputValue.value && props.selectedOptions.length > 0) {\n    emit(\"deleteItem\", props.selectedOptions.length - 1);\n  }\n};\n\nconst showPlaceholder = computed(() => {\n  return (\n    !props.selectedOptions.length &&\n    !inputValue.value &&\n    !isCombinationInput.value\n  );\n});\n\nconst handleFocusout = (event: FocusEvent) => {\n  if (event.relatedTarget) {\n    const target = event.relatedTarget as HTMLElement;\n    if (target && target.closest(\".select-item\")) {\n      event.stopPropagation();\n    }\n  }\n};\n</script>\n<template>\n  <MultipleOverflow\n    class=\"cursor-text\"\n    :class=\"{\n      '!cursor-pointer': !searchable,\n    }\"\n  >\n    <MultipleSelect\n      :selected-options=\"selectedOptions\"\n      :sortable=\"sortable\"\n      @delete-item=\"(index, option) => emit('deleteItem', index, option)\"\n      @sort=\"(options) => emit('sort', options)\"\n    />\n    <template #input>\n      <MultipleOverflowItem>\n        <MultipleSelectSearchInput\n          ref=\"inputRef\"\n          :searchable=\"searchable\"\n          @search=\"handleSearch\"\n          @keydown.backspace=\"handleSearchInputBackspace\"\n          @focusout=\"handleFocusout\"\n        ></MultipleSelectSearchInput>\n\n        <span\n          v-if=\"showPlaceholder\"\n          class=\"pointer-events-none absolute inset-y-0 left-0 w-full truncate text-sm\"\n        >\n          <span class=\"w-full text-gray-400\">\n            {{ placeholder }}\n          </span>\n        </span>\n      </MultipleOverflowItem>\n    </template>\n  </MultipleOverflow>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/SelectContainer.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconArrowDownLine,\n  IconCloseCircle,\n  VDropdown,\n} from \"@halo-dev/components\";\nimport {\n  computed,\n  nextTick,\n  onMounted,\n  onUnmounted,\n  ref,\n  type PropType,\n} from \"vue\";\nimport MultipleSelectSelector from \"./MultipleSelectSelector.vue\";\nimport SelectDropdownContainer from \"./SelectDropdownContainer.vue\";\nimport SelectSelector from \"./SelectSelector.vue\";\n\nconst props = defineProps({\n  multiple: {\n    type: Boolean,\n    default: false,\n  },\n  allowCreate: {\n    type: Boolean,\n    default: false,\n  },\n  maxCount: {\n    type: Number,\n    default: NaN,\n  },\n  sortable: {\n    type: Boolean,\n    default: true,\n  },\n  placeholder: {\n    type: String,\n    default: \"\",\n  },\n  options: {\n    type: Array as PropType<\n      Array<\n        Record<string, unknown> & {\n          label: string;\n          value: string;\n        }\n      >\n    >,\n    required: false,\n    default: () => [],\n  },\n  loading: {\n    type: Boolean,\n    default: false,\n  },\n  nextLoading: {\n    type: Boolean,\n    default: false,\n  },\n  selected: {\n    type: Array as PropType<\n      Array<{\n        label: string;\n        value: string;\n      }>\n    >,\n    required: false,\n    default: () => [],\n  },\n  remote: {\n    type: Boolean,\n    default: false,\n  },\n  clearable: {\n    type: Boolean,\n    default: false,\n  },\n  searchable: {\n    type: Boolean,\n    default: false,\n  },\n  autoSelect: {\n    type: Boolean,\n    default: true,\n  },\n});\n\nconst emit = defineEmits<{\n  (event: \"update\", value: Array<{ label: string; value: string }>): void;\n  (event: \"search\", value: string, e?: Event): void;\n  (event: \"loadMore\"): void;\n}>();\n\nconst selectContainerRef = ref<HTMLElement>();\nconst inputRef = ref<HTMLInputElement | null>();\nconst searchKeyword = ref<string>(\"\");\nconst isDropdownVisible = ref<boolean>(false);\nconst selectedOptions = computed({\n  get: () => [...props.selected],\n  set: (value) => {\n    emit(\"update\", value);\n  },\n});\nconst hasClearable = computed(\n  () =>\n    props.clearable && (selectedOptions.value.length > 0 || searchKeyword.value)\n);\n\nconst getInputHTMLRef = () => {\n  nextTick(() => {\n    inputRef.value = selectContainerRef.value?.querySelector(\"input\");\n  });\n};\n\nonMounted(() => {\n  getInputHTMLRef();\n  if (selectContainerRef.value) {\n    observer.observe(selectContainerRef.value);\n  }\n});\n\nonUnmounted(() => {\n  observer.disconnect();\n});\n\n// resolve the issue of the dropdown position when the container size changes\n// https://github.com/Akryum/floating-vue/issues/977#issuecomment-1651898070\nconst observer = new ResizeObserver(() => {\n  window.dispatchEvent(new Event(\"resize\"));\n});\n\nconst handleOptionSelect = (\n  option: Record<string, unknown> & {\n    label: string;\n    value: string;\n  }\n) => {\n  if (!props.multiple) {\n    selectedOptions.value = [option];\n    isDropdownVisible.value = false;\n  } else {\n    const index = selectedOptions.value.findIndex(\n      (selected) => selected.value === option.value\n    );\n    if (index === -1) {\n      selectedOptions.value.push(option);\n    } else {\n      selectedOptions.value.splice(index, 1);\n    }\n    selectedOptions.value = [...selectedOptions.value];\n  }\n  clearInputValue();\n};\n\n/**\n * When the search box loses focus due to option selection, check if the focus is on an option, and keep the focus if so.\n *\n * @param event FocusEvent\n */\nconst handleSearchFocusout = (event: FocusEvent) => {\n  const target = event.relatedTarget as HTMLElement;\n  if (props.multiple && inputRef.value) {\n    if (\n      target &&\n      (target.closest(\"#select-option\") || target.closest(\".select-container\"))\n    ) {\n      inputRef.value.focus();\n      return;\n    }\n  }\n  clearInputValue();\n  isDropdownVisible.value = false;\n};\n\nconst handleDeleteSelectItem = (index: number) => {\n  selectedOptions.value.splice(index, 1);\n  selectedOptions.value = [...selectedOptions.value];\n};\n\nconst handleSearchClick = () => {\n  if (!isDropdownVisible.value && inputRef.value) {\n    inputRef.value.focus();\n    isDropdownVisible.value = true;\n    return;\n  }\n  if (isDropdownVisible.value) {\n    isDropdownVisible.value = false;\n    return;\n  }\n};\n\nconst handleSearch = (value: string, event?: Event) => {\n  searchKeyword.value = value;\n  if (!isDropdownVisible.value && !!value) {\n    isDropdownVisible.value = true;\n  }\n  emit(\"search\", value, event);\n};\n\nconst clearInputValue = () => {\n  if (!inputRef.value) {\n    return;\n  }\n  inputRef.value.value = \"\";\n  // Manually trigger input event\n  const event = new Event(\"input\", { bubbles: true });\n  inputRef.value.dispatchEvent(event);\n};\n\nconst handleKeyDown = (event: KeyboardEvent) => {\n  const key = event.key;\n  if (key === \"Enter\" || key.startsWith(\"Arrow\")) {\n    if (!isDropdownVisible.value) {\n      isDropdownVisible.value = true;\n      event.stopPropagation();\n    }\n  }\n\n  if (key === \"Escape\" && isDropdownVisible.value) {\n    clearInputValue();\n    isDropdownVisible.value = false;\n  }\n};\n\nconst clearAllSelectedOptions = () => {\n  if (!hasClearable.value) {\n    return;\n  }\n\n  selectedOptions.value = [];\n  clearInputValue();\n};\n\nconst handleSortSelectedOptions = (\n  options: Array<{ label: string; value: string }>\n) => {\n  emit(\"update\", options);\n};\n</script>\n\n<template>\n  <VDropdown\n    :triggers=\"[]\"\n    :shown=\"isDropdownVisible\"\n    auto-size\n    :auto-hide=\"false\"\n    :distance=\"10\"\n    container=\"body\"\n    class=\"w-full\"\n    popper-class=\"select-container-dropdown\"\n  >\n    <div\n      ref=\"selectContainerRef\"\n      tabindex=\"-1\"\n      class=\"select-container relative items-center\"\n      @focusout.stop=\"handleSearchFocusout\"\n      @click.stop=\"handleSearchClick\"\n    >\n      <div class=\"relative h-full items-center rounded-md pe-7 ps-3 text-sm\">\n        <component\n          :is=\"multiple ? MultipleSelectSelector : SelectSelector\"\n          v-bind=\"{\n            placeholder: placeholder,\n            isDropdownVisible,\n            selectedOptions,\n            sortable,\n            searchable,\n          }\"\n          @search=\"handleSearch\"\n          @delete-item=\"handleDeleteSelectItem\"\n          @keydown=\"handleKeyDown\"\n          @sort=\"handleSortSelectedOptions\"\n        ></component>\n      </div>\n      <span\n        class=\"absolute inset-y-0 right-2.5 flex items-center text-gray-500 hover:text-gray-700\"\n      >\n        <IconArrowDownLine\n          class=\"pointer-events-none\"\n          :class=\"{\n            'group-hover/select:hidden': hasClearable,\n          }\"\n        />\n        <IconCloseCircle\n          class=\"hidden cursor-pointer\"\n          :class=\"{\n            'group-hover/select:block': hasClearable,\n          }\"\n          @click.stop=\"clearAllSelectedOptions\"\n        />\n      </span>\n    </div>\n    <template #popper>\n      <SelectDropdownContainer\n        v-if=\"isDropdownVisible\"\n        :loading=\"loading\"\n        :next-loading=\"nextLoading\"\n        :options=\"options\"\n        :remote=\"remote\"\n        :keyword=\"searchKeyword\"\n        :multiple=\"multiple || false\"\n        :selected-options=\"selectedOptions\"\n        :allow-create=\"allowCreate\"\n        :max-count=\"maxCount\"\n        @selected=\"handleOptionSelect\"\n        @load-more=\"emit('loadMore')\"\n      />\n    </template>\n  </VDropdown>\n</template>\n<style lang=\"scss\">\n.select-container-dropdown {\n  .v-popper__arrow-container {\n    display: none;\n  }\n  .v-popper__inner {\n    padding: 0 !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/SelectDropdownContainer.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VEmpty, VLoading } from \"@halo-dev/components\";\nimport { useTimeout } from \"@vueuse/shared\";\nimport { computed, watch } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport SelectOption from \"./SelectOption.vue\";\n\nconst props = defineProps<{\n  options?: Array<Record<string, unknown> & { label: string; value: string }>;\n  keyword?: string;\n  selectedOptions?: Array<{\n    label: string;\n    value: string;\n  }>;\n  multiple: boolean;\n  loading: boolean;\n  nextLoading: boolean;\n  remote: boolean;\n  allowCreate: boolean;\n  maxCount: number;\n}>();\n\nconst emit = defineEmits<{\n  (\n    event: \"selected\",\n    value: Record<string, unknown> & { label: string; value: string }\n  ): void;\n  (event: \"loadMore\"): void;\n}>();\n\nconst filterOptions = computed(() => {\n  if (!props.options) {\n    return [];\n  }\n\n  if (props.remote) {\n    return props.options;\n  }\n\n  const keyword = props.keyword;\n  if (!keyword) {\n    return props.options;\n  }\n\n  const options = props.options.filter((option) => {\n    if (option.label) {\n      return option.label\n        .toLocaleLowerCase()\n        .includes(keyword.toLocaleLowerCase());\n    }\n    return false;\n  });\n\n  if (props.allowCreate) {\n    const hasKeyword = options.some((option) => {\n      return option.value === keyword;\n    });\n\n    if (!hasKeyword) {\n      options.unshift({\n        label: keyword,\n        value: keyword,\n      });\n    }\n  }\n  return options;\n});\n\nconst { ready, start, stop } = useTimeout(200, { controls: true });\n\nwatch(\n  () => props.loading,\n  (loading) => {\n    stop();\n    if (loading) {\n      start();\n    }\n  }\n);\n\nconst handleLoadMore = () => {\n  if (props.remote) {\n    emit(\"loadMore\");\n  }\n};\n</script>\n\n<template>\n  <div class=\"w-full\">\n    <div v-if=\"ready && loading && !nextLoading\">\n      <VLoading></VLoading>\n    </div>\n    <div v-else-if=\"filterOptions && filterOptions.length > 0\">\n      <SelectOption\n        v-bind=\"$props\"\n        :options=\"filterOptions\"\n        @selected=\"(option) => emit('selected', option)\"\n        @load-more=\"handleLoadMore\"\n      ></SelectOption>\n    </div>\n    <div v-else>\n      <VEmpty :title=\"i18n.global.t('core.formkit.select.no_data')\"></VEmpty>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/SelectMain.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { axiosInstance } from \"@halo-dev/api-client\";\nimport { useDebounceFn } from \"@vueuse/core\";\nimport { useFuse } from \"@vueuse/integrations/useFuse\";\nimport type { AxiosRequestConfig } from \"axios\";\nimport { get, has } from \"es-toolkit/compat\";\n// TODO: remove lodash-es dependency in the future\nimport { type PropertyPath } from \"lodash-es\";\nimport {\n  computed,\n  onMounted,\n  ref,\n  shallowReactive,\n  shallowRef,\n  watch,\n  type PropType,\n} from \"vue\";\nimport { isFalse } from \"./isFalse\";\nimport SelectContainer from \"./SelectContainer.vue\";\n\nexport interface SelectProps {\n  /**\n   * URL for asynchronous requests.\n   */\n  action?: string;\n\n  /**\n   * Configuration for asynchronous requests.\n   */\n  requestOption?: SelectActionRequest;\n\n  /**\n   * Enables remote search, controlled by `remoteOption` when enabled.\n   * Differs from `action`, which controls the asynchronous request options.\n   */\n  remote?: boolean;\n\n  /**\n   * Configuration for remote search, required when `remote` is true.\n   */\n  remoteOption?: SelectRemoteOption;\n\n  /**\n   * Enables remote search optimization, default is true.\n   */\n  remoteOptimize?: boolean;\n\n  /**\n   * Allows the creation of new options, only available in local mode. Default is false.\n   */\n  allowCreate?: boolean;\n\n  /**\n   * Allows options to be cleared.\n   */\n  clearable?: boolean;\n\n  /**\n   * Enables multiple selection, default is false.\n   */\n  multiple?: boolean;\n\n  /**\n   * Maximum number of selections allowed in multiple mode, valid only when `multiple` is true.\n   */\n  maxCount?: number;\n\n  /**\n   * Allows sorting in multiple selection mode, default is true. Only valid when `multiple` is true.\n   */\n  sortable?: boolean;\n\n  /**\n   * Default placeholder text.\n   */\n  placeholder?: string;\n\n  /**\n   * Whether to enable search, default is false.\n   */\n  searchable?: boolean;\n\n  /**\n   * Whether to automatically select the first option. default is true.\n   *\n   * Only valid when `multiple` is false.\n   */\n  autoSelect?: boolean;\n}\n\nexport interface SelectResponse {\n  options: Array<\n    Record<string, unknown> & {\n      label: string;\n      value: string;\n    }\n  >;\n\n  page: number;\n\n  size: number;\n\n  total: number;\n}\n\nexport interface SelectRemoteRequest {\n  keyword: string;\n  page: number;\n  size: number;\n}\n\nexport interface SelectRemoteOption {\n  search: ({\n    keyword,\n    page,\n    size,\n  }: SelectRemoteRequest) => Promise<SelectResponse>;\n\n  findOptionsByValues: (values: string[]) => Promise<\n    Array<{\n      label: string;\n      value: string;\n    }>\n  >;\n}\n\nexport interface SelectActionRequest {\n  method?: \"GET\" | \"POST\";\n\n  /**\n   * Parses the returned data from the request.\n   */\n  parseData?: (data: unknown) => SelectResponse;\n\n  /**\n   * Field name for the page number in the request parameters, default is `page`.\n   */\n  pageField?: PropertyPath;\n\n  /**\n   * Field name for size, default is `size`.\n   */\n  sizeField?: PropertyPath;\n\n  /**\n   * Field name for total, default is `total`.\n   */\n  totalField?: PropertyPath;\n\n  /**\n   * Field name for items, default is `items`.\n   */\n  itemsField?: PropertyPath;\n\n  /**\n   * Field name for label, default is `label`.\n   */\n  labelField?: PropertyPath;\n\n  /**\n   * Field name for value, default is `value`.\n   */\n  valueField?: PropertyPath;\n\n  /**\n   * When using value to query detailed information, the default query\n   * parameter key for fieldSelector is `metadata.name`.\n   */\n  fieldSelectorKey?: PropertyPath;\n}\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst options = shallowRef<\n  | Array<\n      Record<string, unknown> & {\n        label: string;\n        value: string;\n      }\n    >\n  | undefined\n>(undefined);\nconst selectOptions = shallowRef<\n  | Array<{\n      label: string;\n      value: string;\n    }>\n  | undefined\n>(undefined);\n\nconst selectProps: SelectProps = shallowReactive({\n  multiple: false,\n  maxCount: NaN,\n  sortable: true,\n  placeholder: \"\",\n});\n\nconst isRemote = computed(() => !!selectProps.action || !!selectProps.remote);\nconst hasMoreOptions = computed(\n  () => options.value && options.value.length < total.value\n);\n\nconst initSelectProps = () => {\n  const nodeProps = props.context.node.props;\n  selectProps.maxCount = nodeProps.maxCount ?? NaN;\n  selectProps.placeholder = nodeProps.placeholder ?? \"\";\n  selectProps.action = nodeProps.action ?? \"\";\n  selectProps.remoteOptimize = !isFalse(nodeProps.remoteOptimize, true);\n  selectProps.requestOption = {\n    ...{\n      method: \"GET\",\n      itemsField: \"items\",\n      labelField: \"label\",\n      valueField: \"value\",\n      totalField: \"total\",\n      fieldSelectorKey: \"metadata.name\",\n      pageField: \"page\",\n      sizeField: \"size\",\n      parseData: undefined,\n    },\n    ...(nodeProps.requestOption ?? {}),\n  };\n  selectProps.multiple = !isFalse(nodeProps.multiple);\n  selectProps.sortable = !isFalse(nodeProps.sortable, true);\n  selectProps.remote = !isFalse(nodeProps.remote);\n  selectProps.allowCreate = !isFalse(nodeProps.allowCreate);\n  selectProps.clearable = !isFalse(nodeProps.clearable);\n  selectProps.searchable = !isFalse(nodeProps.searchable);\n  selectProps.autoSelect = !isFalse(nodeProps.autoSelect, true);\n  if (selectProps.remote) {\n    if (!nodeProps.remoteOption) {\n      throw new Error(\"remoteOption is required when remote is true.\");\n    }\n    selectProps.remoteOption = nodeProps.remoteOption;\n  }\n};\n\nconst isLoading = ref(false);\nconst isFetchingMore = ref(false);\nconst page = ref(1);\nconst size = ref(20);\nconst total = ref(0);\nconst searchKeyword = ref(\"\");\nconst noNeedFetchOptions = ref(false);\n// be no need to fetch options when total is less than or equal to size, cache all options\nconst cacheAllOptions = ref<\n  Array<{ label: string; value: string }> | undefined\n>(undefined);\n\nconst requestOptions = async (\n  searchParams: SelectRemoteRequest\n): Promise<SelectResponse> => {\n  const responseData = {\n    options: [],\n    page: 1,\n    size: 20,\n    total: 0,\n  };\n  if (!selectProps.action) {\n    return responseData;\n  }\n  const requestConfig: AxiosRequestConfig = {\n    method: selectProps.requestOption?.method || \"GET\",\n    url: selectProps.action,\n    [selectProps.requestOption?.method === \"GET\" ? \"params\" : \"data\"]:\n      searchParams,\n  };\n  const response = await axiosInstance.request(requestConfig);\n  const { data } = response;\n  const parseSelectData = parseSelectResponse(data);\n  if (!parseSelectData) {\n    throw new Error(\n      \"Error parsing response, please check the requestOption object.\"\n    );\n  }\n  return parseSelectData;\n};\n\nconst parseSelectResponse = (data: object): SelectResponse | undefined => {\n  if (!selectProps.requestOption) {\n    return;\n  }\n  const { parseData } = selectProps.requestOption;\n  if (parseData) {\n    return parseData(data);\n  }\n  const { labelField, valueField, itemsField } = selectProps.requestOption;\n  if (!has(data, itemsField as PropertyPath)) {\n    console.error(\n      `itemsField: ${itemsField?.toString()} not found in response data.`\n    );\n    return;\n  }\n  const items = get(data, itemsField as PropertyPath);\n  return {\n    options: formatOptionsData(\n      items,\n      labelField as PropertyPath,\n      valueField as PropertyPath\n    ),\n    page: get(data, selectProps.requestOption.pageField as PropertyPath, \"1\"),\n    size: get(data, selectProps.requestOption.sizeField as PropertyPath, \"20\"),\n    total: get(data, selectProps.requestOption.totalField as PropertyPath, \"0\"),\n  };\n};\n\nconst formatOptionsData = (\n  items: Array<object>,\n  labelField: PropertyPath,\n  valueField: PropertyPath\n) => {\n  if (!items) {\n    console.warn(\n      \"Select options: data items are empty, please check the itemsField.\"\n    );\n    return [];\n  }\n  return items.map((item) => {\n    if (!has(item, labelField as PropertyPath)) {\n      console.error(\n        `labelField: ${labelField?.toString()} not found in response data items.`\n      );\n      return { label: \"\", value: \"\" };\n    }\n    if (!has(item, valueField as PropertyPath)) {\n      console.error(\n        `valueField: ${valueField?.toString()} not found in response data items.`\n      );\n      return { label: \"\", value: \"\" };\n    }\n    return {\n      label: get(item, labelField),\n      value: get(item, valueField),\n    };\n  });\n};\n\n/**\n * Retrieves the mapping of selected values and available options.\n *\n * If the selected value is found in the current options, it will be converted to a label and value format.\n * If the selected value is not found in the current options, the `mapUnresolvedOptions` method will be used.\n */\nconst fetchSelectedOptions = async (): Promise<\n  | Array<{\n      label: string;\n      value: string;\n    }>\n  | undefined\n> => {\n  const node = props.context.node;\n  const value = node.value;\n\n  const selectedValues: Array<unknown> = [];\n  if (Array.isArray(value)) {\n    selectedValues.push(...value);\n  } else if (\n    typeof value === \"string\" ||\n    typeof value === \"number\" ||\n    typeof value === \"boolean\" ||\n    value === void 0\n  ) {\n    selectedValues.push(value);\n  }\n\n  const currentOptions = options.value?.filter((option) =>\n    selectedValues.includes(option.value)\n  );\n\n  // Get options that are not yet mapped.\n  const unmappedSelectValues = selectedValues\n    .filter(\n      (value) => !currentOptions?.find((option) => option.value === value)\n    )\n    .filter(Boolean);\n  if (unmappedSelectValues.length === 0) {\n    if (!currentOptions || currentOptions.length === 0) {\n      return;\n    }\n    return currentOptions?.sort((a, b) =>\n      selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1\n    );\n  }\n\n  // Map the unresolved options to label and value format.\n  const mappedSelectOptions = await mapUnresolvedOptions(\n    unmappedSelectValues.map(String)\n  );\n  // Merge currentOptions and mappedSelectOptions, then sort them according to selectValues order.\n  return [...(currentOptions || []), ...mappedSelectOptions].sort((a, b) =>\n    selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1\n  );\n};\n\n/**\n * Maps unresolved options to label and value format.\n *\n * There are several possible scenarios:\n *\n * 1. If it's an asynchronous request for options, fetch the label and value via an API call.\n *  a. If all selected values are found in the response, return the data directly.\n *  b. If only some of the values are found, check if new options can be created.\n *     If allowed, create new options for the remaining values.\n * 2. If it's a static option and creating new options is allowed, create new options for the remaining values.\n *    If not allowed, return an empty array.\n *\n * @param unmappedSelectValues Unresolved options\n */\nconst mapUnresolvedOptions = async (\n  unmappedSelectValues: string[]\n): Promise<\n  Array<{\n    label: string;\n    value: string;\n  }>\n> => {\n  if (!isRemote.value) {\n    if (selectProps.allowCreate) {\n      // TODO: Add mapped values to options\n      return unmappedSelectValues.map((value) => ({ label: value, value }));\n    }\n    // Creation not allowed but there are unmapped values, return an empty array and issue a warning.\n    console.warn(\n      `It is not allowed to create options but has unmapped values. ${unmappedSelectValues}`\n    );\n    return unmappedSelectValues.map((value) => ({ label: value, value }));\n  }\n\n  // Asynchronous request for options, fetch label and value via API.\n  let mappedOptions:\n    | Array<{\n        label: string;\n        value: string;\n      }>\n    | undefined = undefined;\n  if (noNeedFetchOptions.value) {\n    mappedOptions = cacheAllOptions.value?.filter((option) =>\n      unmappedSelectValues.includes(option.value)\n    );\n  } else {\n    if (selectProps.action) {\n      mappedOptions = await fetchRemoteMappedOptions(unmappedSelectValues);\n    } else if (selectProps.remote) {\n      const remoteOption = selectProps.remoteOption as SelectRemoteOption;\n      mappedOptions =\n        await remoteOption.findOptionsByValues(unmappedSelectValues);\n    }\n  }\n\n  if (!mappedOptions) {\n    return unmappedSelectValues.map((value) => ({ label: value, value }));\n  }\n  // Get values that are still unresolved.\n  const unmappedValues = unmappedSelectValues.filter(\n    (value) => !mappedOptions.find((option) => option.value === value)\n  );\n  if (unmappedValues.length === 0) {\n    return mappedOptions;\n  }\n\n  if (!selectProps.allowCreate) {\n    console.warn(\n      `It is not allowed to create options but has unmapped values. ${unmappedSelectValues}`\n    );\n    return mappedOptions;\n  }\n\n  // Create new options for remaining values.\n  return [\n    ...mappedOptions,\n    ...unmappedValues.map((value) => ({ label: value, value })),\n  ];\n};\n\nconst fetchRemoteMappedOptions = async (\n  unmappedSelectValues: string[]\n): Promise<Array<{ label: string; value: string }>> => {\n  const requestConfig: AxiosRequestConfig = {\n    method: selectProps.requestOption?.method || \"GET\",\n    url: selectProps.action,\n  };\n  if (requestConfig.method === \"GET\") {\n    requestConfig.params = {\n      fieldSelector: `${selectProps.requestOption?.fieldSelectorKey?.toString()}=(${unmappedSelectValues.join(\n        \",\"\n      )})`,\n    };\n  } else {\n    requestConfig.data = {\n      fieldSelector: `${selectProps.requestOption?.fieldSelectorKey?.toString()}=(${unmappedSelectValues.join(\n        \",\"\n      )})`,\n    };\n  }\n  const response = await axiosInstance.request(requestConfig);\n  const { data } = response;\n  const parsedData = parseSelectResponse(data);\n  if (!parsedData) {\n    throw new Error(\n      \"fetchRemoteMappedOptions error, please check the requestOption object.\"\n    );\n  }\n  return parsedData.options;\n};\n\nonMounted(async () => {\n  initSelectProps();\n  if (!isRemote.value) {\n    options.value = props.context.attrs.options;\n  } else {\n    const response = await fetchOptions();\n    if (response) {\n      options.value = response.options;\n      if (selectProps.remoteOptimize) {\n        if (total.value !== 0 && total.value <= size.value) {\n          noNeedFetchOptions.value = true;\n          cacheAllOptions.value = response.options;\n        }\n      }\n    }\n  }\n});\n\nconst getAutoSelectedOption = ():\n  | {\n      label: string;\n      value: string;\n    }\n  | undefined => {\n  if (!options.value || options.value.length === 0) {\n    return;\n  }\n\n  // Find the first option that is not disabled.\n  return options.value.find((option) => {\n    const attrs = option.attrs as Record<string, unknown>;\n    return isFalse(attrs?.disabled as string | boolean | undefined);\n  });\n};\n\nwatch(\n  () => props.context.value,\n  async (newValue) => {\n    const selectedValues = selectOptions.value?.map((item) => item.value) || [];\n    if (selectedValues.length > 0 && selectedValues.includes(newValue)) {\n      return;\n    }\n    const selectedOption = await fetchSelectedOptions();\n    selectOptions.value = selectedOption;\n  }\n);\n\nwatch(\n  () => options.value,\n  async (newOptions) => {\n    if (newOptions && newOptions.length > 0) {\n      const selectedOption = await fetchSelectedOptions();\n      if (selectedOption) {\n        selectOptions.value = selectedOption;\n      }\n    }\n  }\n);\n\nconst enableAutoSelect = () => {\n  if (!selectProps.autoSelect) {\n    return false;\n  }\n  if (selectProps.multiple || selectProps.placeholder) {\n    return false;\n  }\n\n  const value = props.context.node.value;\n  if (value === void 0 || value === null) {\n    return true;\n  }\n\n  return false;\n};\n\nwatch(\n  () => options.value,\n  async (newOptions) => {\n    if (newOptions && newOptions.length > 0) {\n      if (enableAutoSelect()) {\n        // Automatically select the first option when the selected value is empty.\n        const autoSelectedOption = getAutoSelectedOption();\n        if (autoSelectedOption) {\n          handleUpdate([autoSelectedOption]);\n        }\n      }\n    }\n  },\n  { once: true }\n);\n\n// When attr options are processed asynchronously, it is necessary to monitor\n// changes in attr options and update options accordingly.\nwatch(\n  () => props.context.attrs.options,\n  async (attrOptions) => {\n    if (!isRemote.value) {\n      options.value = attrOptions;\n    }\n  }\n);\n\nconst handleUpdate = async (value: Array<{ label: string; value: string }>) => {\n  const oldSelectValue = selectOptions.value;\n  if (\n    oldSelectValue &&\n    value.length === oldSelectValue.length &&\n    value.every((item, index) => item.value === oldSelectValue[index].value)\n  ) {\n    return;\n  }\n  const newValue = value.map((item) => {\n    return {\n      label: item.label,\n      value: item.value,\n    };\n  });\n  handleSetNodeValue(newValue);\n  await props.context.node.settled;\n  props.context.attrs.onChange?.(newValue);\n};\n\nconst handleSetNodeValue = (value: Array<{ label: string; value: string }>) => {\n  const values = value.map((item) => item.value);\n  selectOptions.value = value;\n  if (selectProps.multiple) {\n    props.context.node.input(values);\n    return;\n  }\n  if (values.length === 0) {\n    props.context.node.input(\"\");\n    return;\n  }\n  props.context.node.input(values[0]);\n};\n\nconst fetchOptions = async (\n  tempKeyword = searchKeyword.value,\n  tempPage = page.value,\n  tempSize = size.value\n): Promise<SelectResponse | undefined> => {\n  if (isLoading.value || !isRemote.value) {\n    return;\n  }\n  // If the total number of options is less than the page size, no more requests are made.\n  if (noNeedFetchOptions.value) {\n    const { results } = useFuse<{\n      label: string;\n      value: string;\n    }>(tempKeyword, cacheAllOptions.value || [], {\n      fuseOptions: {\n        keys: [\"label\", \"value\"],\n        threshold: 0,\n        ignoreLocation: true,\n      },\n      matchAllWhenSearchEmpty: true,\n    });\n    const filterOptions = results.value?.map((fuseItem) => fuseItem.item) || [];\n    return {\n      options: filterOptions || [],\n      page: page.value,\n      size: size.value,\n      total: filterOptions.length || 0,\n    };\n  }\n  isLoading.value = true;\n  try {\n    let response: SelectResponse | undefined;\n    if (selectProps.action) {\n      response = await requestOptions({\n        page: tempPage,\n        size: tempSize,\n        keyword: tempKeyword,\n      });\n    }\n    if (selectProps.remote) {\n      const remoteOption = selectProps.remoteOption as SelectRemoteOption;\n      response = await remoteOption.search({\n        keyword: tempKeyword,\n        page: tempPage,\n        size: tempSize,\n      });\n    }\n    page.value = response?.page || 1;\n    size.value = response?.size || 20;\n    total.value = response?.total || 0;\n    return response as SelectResponse;\n  } catch (error) {\n    console.error(\"fetchOptions error\", error);\n  } finally {\n    isLoading.value = false;\n  }\n};\n\nconst debouncedFetchOptions = useDebounceFn(async () => {\n  const response = await fetchOptions(searchKeyword.value, 1);\n  if (!response) {\n    return;\n  }\n  options.value = response.options;\n}, 500);\n\nconst handleSearch = async (value: string, event?: Event) => {\n  if (event && event instanceof InputEvent) {\n    if (event.isComposing) {\n      return;\n    }\n  }\n  // When the search keyword does not change, the data is no longer requested.\n  if (\n    value === searchKeyword.value &&\n    options.value &&\n    options.value?.length > 0\n  ) {\n    return;\n  }\n  searchKeyword.value = value;\n  if (selectProps.action || selectProps.remote) {\n    if (noNeedFetchOptions.value) {\n      const response = await fetchOptions(searchKeyword.value, 1);\n      if (!response) {\n        return;\n      }\n      options.value = response.options;\n    } else {\n      debouncedFetchOptions();\n    }\n  }\n};\n\nconst hasNextPage = computed(() => {\n  const totalPages = Math.ceil(total.value / size.value);\n  return (\n    hasMoreOptions.value &&\n    !isFetchingMore.value &&\n    !isLoading.value &&\n    page.value < totalPages\n  );\n});\n\nconst handleNextPage = async () => {\n  if (!hasNextPage.value) {\n    return;\n  }\n  isFetchingMore.value = true;\n  const response = await fetchOptions(searchKeyword.value, page.value + 1);\n  isLoading.value = false;\n  isFetchingMore.value = false;\n  if (!response) {\n    return;\n  }\n  options.value = [...(options.value || []), ...response.options];\n};\n</script>\n<template>\n  <SelectContainer\n    :allow-create=\"selectProps.allowCreate\"\n    :max-count=\"selectProps.maxCount\"\n    :multiple=\"selectProps.multiple\"\n    :sortable=\"selectProps.sortable\"\n    :placeholder=\"selectProps.placeholder\"\n    :loading=\"isLoading\"\n    :next-loading=\"isFetchingMore\"\n    :options=\"options\"\n    :selected=\"selectOptions\"\n    :remote=\"isRemote\"\n    :clearable=\"selectProps.clearable\"\n    :searchable=\"selectProps.searchable\"\n    :auto-select=\"selectProps.autoSelect\"\n    @update=\"handleUpdate\"\n    @search=\"handleSearch\"\n    @load-more=\"handleNextPage\"\n  />\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/SelectOption.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VLoading } from \"@halo-dev/components\";\nimport { vScroll } from \"@vueuse/components\";\nimport { useEventListener, type UseScrollReturn } from \"@vueuse/core\";\nimport { computed, ref, watch } from \"vue\";\nimport { isFalse } from \"./isFalse\";\nimport SelectOptionItem from \"./SelectOptionItem.vue\";\n\nconst props = defineProps<{\n  options: Array<Record<string, unknown> & { label: string; value: string }>;\n  selectedOptions?: Array<{\n    label: string;\n    value: string;\n  }>;\n  multiple: boolean;\n  maxCount: number;\n  nextLoading: boolean;\n}>();\n\nconst emit = defineEmits<{\n  (\n    event: \"selected\",\n    value: Record<string, unknown> & { label: string; value: string }\n  ): void;\n  (event: \"loadMore\"): void;\n}>();\n\nconst selectedIndex = ref<number>(0);\nconst selectOptionRef = ref<HTMLElement>();\nconst oldEvent = ref<MouseEvent | undefined>();\nconst isCursorHidden = ref(false);\n\nconst selectedValues = computed(() =>\n  props.selectedOptions?.map((option) => option.value)\n);\n\nconst getSelectedIndex = () => {\n  if (props.multiple) {\n    return 0;\n  }\n  if (selectedValues.value && selectedValues.value.length > 0) {\n    const value = selectedValues.value[0];\n    const index = props.options.findIndex((option) => option.value === value);\n    return index === -1 ? 0 : index;\n  }\n\n  return 0;\n};\n\nconst handleKeydown = (event: KeyboardEvent) => {\n  if (selectOptionRef.value) {\n    isCursorHidden.value = true;\n  }\n  const key = event.key;\n  if (key === \"ArrowUp\") {\n    selectedIndex.value =\n      selectedIndex.value - 1 < 0\n        ? props.options.length - 1\n        : selectedIndex.value - 1;\n    event.preventDefault();\n  }\n\n  if (key === \"ArrowDown\") {\n    selectedIndex.value =\n      selectedIndex.value + 1 >= props.options.length\n        ? 0\n        : selectedIndex.value + 1;\n    event.preventDefault();\n  }\n\n  if (key === \"Enter\") {\n    handleSelected(selectedIndex.value);\n    event.preventDefault();\n  }\n};\n\nuseEventListener(document, \"keydown\", handleKeydown);\n\nconst handleSelected = (index: number) => {\n  const option = props.options[index];\n  if (reachMaximumLimit.value) {\n    const index = props.selectedOptions?.findIndex(\n      (selected) => selected.value === option.value\n    );\n    if (index === -1) {\n      return;\n    }\n  }\n  selectedIndex.value = index;\n  if (option && !isDisabled(option)) {\n    emit(\"selected\", option);\n  }\n};\n\nconst handleScrollIntoView = () => {\n  if (selectedIndex.value === -1) {\n    return;\n  }\n  const optionElement = document.querySelector(\n    `#select-option > div:nth-child(${selectedIndex.value + 1})`\n  );\n  if (optionElement) {\n    optionElement.scrollIntoView({\n      behavior: \"instant\",\n      block: \"nearest\",\n      inline: \"nearest\",\n    });\n  }\n};\n\nconst reachMaximumLimit = computed(() => {\n  if (!props.multiple || isNaN(props.maxCount)) {\n    return false;\n  }\n  if (props.selectedOptions && props.selectedOptions.length >= props.maxCount) {\n    return true;\n  }\n  return false;\n});\n\nconst isDisabled = (\n  option: Record<string, unknown> & { label: string; value: string }\n) => {\n  const attrs = option.attrs as Record<string, unknown>;\n  return (\n    (reachMaximumLimit.value &&\n      selectedValues.value &&\n      !selectedValues.value.includes(option.value)) ||\n    !isFalse(attrs?.disabled as string | boolean | undefined)\n  );\n};\n\nconst handleOptionScroll = (state: UseScrollReturn) => {\n  if (selectOptionRef.value) {\n    const scrollHeight = (selectOptionRef.value as HTMLElement).scrollHeight;\n    const clientHeight = (selectOptionRef.value as HTMLElement).clientHeight;\n    const scrollPercentage =\n      (state.y.value / (scrollHeight - clientHeight)) * 100;\n    if (scrollPercentage > 50) {\n      emit(\"loadMore\");\n    }\n  }\n};\n\nwatch(\n  props.options,\n  () => {\n    selectedIndex.value = getSelectedIndex();\n  },\n  {\n    immediate: true,\n  }\n);\n\nwatch(\n  () => selectedIndex.value,\n  () => {\n    handleScrollIntoView();\n  }\n);\n\n/**\n * check if cursor is changed\n *\n * @param newEvent\n * @param oldEvent\n */\nconst isCursorChanged = (\n  newEvent: MouseEvent,\n  oldEvent: MouseEvent | undefined\n) => {\n  if (!oldEvent) {\n    return true;\n  }\n  return (\n    newEvent.screenX !== oldEvent.screenX ||\n    newEvent.screenY !== oldEvent.screenY\n  );\n};\n\nconst handleMouseover = (event: MouseEvent) => {\n  if (isCursorHidden.value) {\n    if (!oldEvent.value) {\n      oldEvent.value = event;\n    }\n\n    if (isCursorChanged(event, oldEvent.value)) {\n      isCursorHidden.value = false;\n      oldEvent.value = undefined;\n    }\n    return;\n  }\n  const target = event.target as HTMLElement;\n  if (\n    target.classList.contains(\"select-option-item\") &&\n    target instanceof HTMLElement\n  ) {\n    const parentElement = target.parentElement as HTMLElement;\n    const index = Array.from(parentElement.children).indexOf(target);\n    selectedIndex.value = index;\n  }\n};\n</script>\n\n<template>\n  <div\n    id=\"select-option\"\n    ref=\"selectOptionRef\"\n    v-scroll=\"[handleOptionScroll, { throttle: 10 }]\"\n    class=\"select max-h-64 overflow-y-auto p-1.5\"\n    :class=\"[isCursorHidden ? 'cursor-none' : 'cursor-pointer']\"\n    role=\"list\"\n    tabindex=\"-1\"\n    @keydown=\"handleKeydown\"\n    @mouseover=\"handleMouseover\"\n  >\n    <template v-for=\"(option, index) in options\" :key=\"option.value\">\n      <SelectOptionItem\n        class=\"select-option-item\"\n        :option=\"option\"\n        :class=\"{\n          'bg-zinc-100': !isDisabled(option) && selectedIndex === index,\n          'selected !bg-zinc-200/60':\n            selectedValues && selectedValues.includes(option.value),\n          'cursor-not-allowed opacity-25': isDisabled(option),\n        }\"\n        @mousedown.stop=\"handleSelected(index)\"\n      >\n      </SelectOptionItem>\n    </template>\n    <div v-if=\"nextLoading\">\n      <VLoading></VLoading>\n    </div>\n  </div>\n</template>\n<style lang=\"scss\">\n.select-option-item:has(+ .select-option-item:not(.selected))\n  + .select-option-item.selected {\n  border-end-start-radius: 0;\n  border-end-end-radius: 0;\n}\n\n.select-option-item.selected + .select-option-item.selected {\n  border-start-start-radius: 0;\n  border-start-end-radius: 0;\n}\n\n.select-option-item.selected:has(+ .select-option-item.selected) {\n  border-end-start-radius: 0;\n  border-end-end-radius: 0;\n}\n\n.select-option-item.selected + .select-option-item:not(.selected) {\n  border-start-start-radius: 0;\n  border-start-end-radius: 0;\n}\n</style>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/SelectOptionItem.vue",
    "content": "<script lang=\"ts\" setup>\ndefineProps<{\n  option: Record<string, unknown> & { label: string; value: unknown };\n}>();\n</script>\n\n<template>\n  <div class=\"flex h-8 w-full items-center rounded px-3 py-1 text-base\">\n    <span class=\"flex-1 truncate text-sm\"> {{ option.label }} </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/SelectSearchInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref } from \"vue\";\n\ndefineProps<{\n  searchable: boolean;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"search\", value: string, e?: Event): void;\n  (event: \"enter\", value: string): void;\n}>();\n\nconst inputHTMLRef = ref<HTMLInputElement | null>(null);\nconst searchInputContainerRef = ref<HTMLSpanElement | null>(null);\nconst inputValue = ref(\"\");\n</script>\n\n<template>\n  <div class=\"relative w-full cursor-text\">\n    <span ref=\"searchInputContainerRef\" class=\"relative flex w-full\">\n      <input\n        ref=\"inputHTMLRef\"\n        v-model=\"inputValue\"\n        type=\"text\"\n        :readonly=\"!searchable\"\n        autocomplete=\"off\"\n        class=\"m-0 h-full w-full cursor-auto !appearance-none border-none bg-transparent p-0 pe-0 ps-0 text-base outline-none\"\n        :class=\"{\n          '!cursor-pointer': !searchable,\n        }\"\n        @input=\"(event) => emit('search', inputValue, event)\"\n      />\n    </span>\n    <span\n      class=\"pointer-events-none absolute inset-y-0 left-0 w-full truncate text-sm\"\n    >\n      <slot name=\"placeholder\"></slot>\n    </span>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/SelectSelector.vue",
    "content": "<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport SelectSearchInput from \"./SelectSearchInput.vue\";\n\nconst props = defineProps<{\n  placeholder?: string;\n  selectedOptions: Array<{\n    label: string;\n    value: string;\n  }>;\n  isDropdownVisible: boolean;\n  searchable: boolean;\n}>();\n\nconst emit = defineEmits<{\n  (event: \"search\", value: string, e?: Event): void;\n}>();\n\nconst selectLabel = computed(() => {\n  if (props.selectedOptions && props.selectedOptions.length > 0) {\n    return props.selectedOptions[0].label.toString();\n  }\n  return undefined;\n});\n\nconst inputValue = ref(\"\");\nconst isCombinationInput = ref(false);\nconst handleSearch = (value: string, event?: Event) => {\n  inputValue.value = value;\n  if (event && event instanceof InputEvent) {\n    isCombinationInput.value = event.isComposing;\n  } else {\n    isCombinationInput.value = false;\n  }\n  emit(\"search\", value, event);\n};\n\nconst showPlaceholder = computed(() => {\n  return !inputValue.value && !isCombinationInput.value;\n});\n</script>\n\n<template>\n  <SelectSearchInput :searchable=\"searchable\" @search=\"handleSearch\">\n    <template #placeholder>\n      <span\n        v-if=\"showPlaceholder\"\n        :class=\"{\n          'text-gray-400': isDropdownVisible || !selectLabel,\n        }\"\n      >\n        {{ selectLabel || placeholder }}\n      </span>\n    </template>\n  </SelectSearchInput>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  help,\n  icon,\n  inner,\n  label,\n  message,\n  messages,\n  outer,\n  prefix,\n  suffix,\n  wrapper,\n} from \"@formkit/inputs\";\nimport { SelectSection } from \"./sections/index\";\nimport SelectMain from \"./SelectMain.vue\";\n\n/**\n * Custom Select component.\n *\n * features:\n *\n * 1. Supports both single and multiple selection, controlled by the `multiple` prop. The display format of the input differs between single and multiple selection modes.\n * 2. Supports passing in an `options` prop to render dropdown options, or using the `action` prop to pass a function for dynamically retrieving options. The handling differs based on the method:\n *    a. If `options` is passed, it will be used directly to render the dropdown options.\n *    b. If the `action` prop is provided, it should be used to fetch options from an API, and additional features like pagination and search may also be enabled.\n * 3. Supports sorting functionality. If sorting is enabled, the list allows drag-and-drop sorting, and the current position of the node can be displayed in the dropdown.\n * 4. Supports an add feature. If the target content is not in the list, it allows adding the currently entered content as a `value`.\n * 5. Allows restricting the maximum number of selections, controlled by the `maxCount` prop.\n */\nexport const select: FormKitTypeDefinition = {\n  schema: outer(\n    wrapper(\n      label(\"$label\"),\n      inner(icon(\"prefix\"), prefix(), SelectSection(), suffix(), icon(\"suffix\"))\n    ),\n    help(\"$help\"),\n    messages(message(\"$message.value\"))\n  ),\n\n  type: \"input\",\n\n  props: [\n    \"clearable\",\n    \"multiple\",\n    \"maxCount\",\n    \"sortable\",\n    \"action\",\n    \"requestOption\",\n    \"placeholder\",\n    \"remote\",\n    \"remoteOption\",\n    \"allowCreate\",\n    \"remoteOptimize\",\n    \"searchable\",\n    \"autoSelect\",\n  ],\n\n  library: {\n    SelectMain: SelectMain,\n  },\n\n  schemaMemoKey: \"custom-select\",\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/isFalse.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const isFalse = (value: any, onlyBoolean = false) => {\n  if (onlyBoolean) {\n    return [false, \"false\"].includes(value);\n  }\n  return [undefined, null, \"false\", false].includes(value);\n};\n"
  },
  {
    "path": "ui/src/formkit/inputs/select/sections/index.ts",
    "content": "import { createSection } from \"@formkit/inputs\";\n\nexport const SelectSection = createSection(\"SelectMain\", () => ({\n  $cmp: \"SelectMain\",\n  props: {\n    context: \"$node.context\",\n  },\n}));\n"
  },
  {
    "path": "ui/src/formkit/inputs/singlePage-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport type { FormKitInputs } from \"@formkit/inputs\";\nimport {\n  consoleApiClient,\n  paginate,\n  type ListedSinglePage,\n  type SinglePageV1alpha1ConsoleApiListSinglePagesRequest,\n} from \"@halo-dev/api-client\";\nimport { singlePageLabels } from \"@/constants/labels\";\nimport { select } from \"./select\";\n\nasync function search({ page, size, keyword }) {\n  const { data } = await consoleApiClient.content.singlePage.listSinglePages({\n    page,\n    size,\n    keyword,\n    labelSelector: [\n      `${singlePageLabels.DELETED}=false`,\n      `${singlePageLabels.PUBLISHED}=true`,\n    ],\n  });\n\n  return {\n    options: data.items.map((singlePage) => {\n      return {\n        value: singlePage.page.metadata.name,\n        label: singlePage.page.spec.title,\n      };\n    }),\n    total: data.total,\n    size: data.size,\n    page: data.page,\n  };\n}\n\nasync function findOptionsByValues(values: string[]) {\n  if (values.length === 0) {\n    return [];\n  }\n\n  const singlePages = await paginate<\n    SinglePageV1alpha1ConsoleApiListSinglePagesRequest,\n    ListedSinglePage\n  >((params) => consoleApiClient.content.singlePage.listSinglePages(params), {\n    fieldSelector: [`metadata.name=(${values.join(\",\")})`],\n    size: 1000,\n  });\n\n  return singlePages.map((singlePage) => {\n    return {\n      value: singlePage.page.metadata.name,\n      label: singlePage.page.spec.title,\n    };\n  });\n}\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    node.props = {\n      ...node.props,\n      remote: true,\n      remoteOption: {\n        search,\n        findOptionsByValues,\n      },\n      searchable: true,\n    };\n  });\n}\n\nexport const singlePageSelect: FormKitTypeDefinition = {\n  ...select,\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    singlePageSelect: {\n      type: \"singlePageSelect\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/switch/SwitchInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { VSwitch } from \"@halo-dev/components\";\nimport { computed, type PropType } from \"vue\";\n\nconst { context } = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nfunction onChange(value) {\n  context.node.input(\n    value ? context.node.props.onValue : context.node.props.offValue\n  );\n}\n\nconst isChecked = computed(() => {\n  return context.value === context.node.props.onValue;\n});\n\nconst disabled = computed(() => {\n  return context.node.props.disabled;\n});\n</script>\n\n<template>\n  <VSwitch\n    :model-value=\"isChecked\"\n    :disabled=\"disabled\"\n    @update:model-value=\"onChange\"\n  ></VSwitch>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/switch/feature.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { normalizeBoxes } from \"@formkit/inputs\";\nimport { has, undefine } from \"@formkit/utils\";\n\n/**\n * A feature that adds switch selection support.\n *\n * @param node - A {@link @formkit/core#FormKitNode | FormKitNode}.\n *\n * @public\n */\nexport default function switchFeature(node: FormKitNode): void {\n  node.on(\"created\", () => {\n    if (!has(node.props, \"onValue\")) {\n      node.props.onValue = true;\n    }\n    if (!has(node.props, \"offValue\")) {\n      node.props.offValue = false;\n    }\n\n    node.props.disabled = undefine(node.props.disabled);\n  });\n\n  node.hook.prop(normalizeBoxes(node));\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/switch/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  $extend,\n  boxInner,\n  boxLabel,\n  boxWrapper,\n  createSection,\n  help,\n  message,\n  messages,\n  outer,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport { defineAsyncComponent } from \"vue\";\nimport switchFeature from \"./feature\";\n\nconst cmpName = \"SwitchInput\";\n\nconst switchSection = createSection(\"input\", () => ({\n  $cmp: cmpName,\n  props: {\n    context: \"$node.context\",\n  },\n}));\n\n/**\n * Input definition for a switch input.\n * @public\n */\nexport const switchInput: FormKitTypeDefinition = {\n  schema: outer(\n    boxWrapper(\n      boxInner(switchSection()),\n      $extend(boxLabel(\"$label\"), {\n        if: \"$label\",\n      })\n    ),\n    help(\"$help\"),\n    messages(message(\"$message.value\"))\n  ),\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"input\",\n  /**\n   * The family of inputs this one belongs too. For example \"text\" and \"email\"\n   * are both part of the \"text\" family. This is primary used for styling.\n   */\n  family: \"box\",\n\n  props: [\"onValue\", \"offValue\", \"disabled\"],\n\n  features: [switchFeature],\n\n  library: {\n    SwitchInput: defineAsyncComponent(() => import(\"./SwitchInput.vue\")),\n  },\n  /**\n   * The key used to memoize the schema.\n   */\n  schemaMemoKey: \"custom-switch-input\",\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    switch: {\n      type: \"switch\";\n      onValue?: unknown;\n      offValue?: unknown;\n      disabled?: boolean;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/tag-checkbox.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  checkbox,\n  checkboxes,\n  defaultIcon,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport {\n  coreApiClient,\n  paginate,\n  type Tag,\n  type TagV1alpha1ApiListTagRequest,\n} from \"@halo-dev/api-client\";\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    const tags = await paginate<TagV1alpha1ApiListTagRequest, Tag>(\n      (params) => coreApiClient.content.tag.listTag(params),\n      {\n        sort: [\"metadata.creationTimestamp,desc\"],\n      }\n    );\n\n    node.props.options = tags.map((tag) => {\n      return {\n        value: tag.metadata.name,\n        label: tag.spec.displayName,\n      };\n    });\n  });\n}\n\nexport const tagCheckbox: FormKitTypeDefinition = {\n  ...checkbox,\n  props: [\"onValue\", \"offValue\"],\n  forceTypeProp: \"checkbox\",\n  features: [\n    optionsHandler,\n    checkboxes,\n    defaultIcon(\"decorator\", \"checkboxDecorator\"),\n  ],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    tagCheckbox: {\n      type: \"tagCheckbox\";\n      value?: string[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/tag-select/TagSelect.vue",
    "content": "<script lang=\"ts\" setup>\nimport PostTag from \"@console/modules/contents/posts/tags/components/PostTag.vue\";\nimport { useAllPostTagsQuery } from \"@console/modules/contents/posts/tags/composables/use-post-tag\";\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport type { Tag } from \"@halo-dev/api-client\";\nimport { coreApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconArrowRight,\n  IconCheckboxCircle,\n  IconClose,\n  VDropdown,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { onClickOutside, useResizeObserver } from \"@vueuse/core\";\nimport Fuse from \"fuse.js\";\nimport ShortUniqueId from \"short-unique-id\";\nimport { slugify } from \"transliteration\";\nimport { computed, ref, useTemplateRef, watch, type PropType } from \"vue\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst multiple = computed(() => {\n  const { multiple } = props.context;\n  if (multiple === undefined) {\n    return false;\n  }\n\n  if (typeof multiple === \"boolean\") {\n    return multiple;\n  }\n\n  return multiple === \"true\";\n});\n\nconst selectedTag = ref<Tag>();\nconst dropdownVisible = ref(false);\nconst text = ref(\"\");\nconst wrapperRef = useTemplateRef<HTMLElement>(\"wrapperRef\");\nconst popperRef = useTemplateRef<HTMLElement>(\"popperRef\");\n\n// resolve the issue of the dropdown position when the container size changes\n// https://github.com/Akryum/floating-vue/issues/977#issuecomment-1651898070\nuseResizeObserver(wrapperRef, () => {\n  window.dispatchEvent(new Event(\"resize\"));\n});\n\nonClickOutside(\n  wrapperRef,\n  () => {\n    dropdownVisible.value = false;\n  },\n  {\n    ignore: [popperRef],\n  }\n);\n\nconst { data: postTags, refetch: handleFetchTags } = useAllPostTagsQuery();\n\n// search\nlet fuse: Fuse<Tag> | undefined = undefined;\n\nconst searchResults = computed(() => {\n  if (!text.value) {\n    return postTags.value;\n  }\n\n  return fuse?.search(text.value).map((item) => item.item) || [];\n});\n\nwatch(\n  () => searchResults.value,\n  (value) => {\n    if (value?.length && text.value) {\n      selectedTag.value = value[0];\n      scrollToSelected();\n    } else {\n      selectedTag.value = undefined;\n    }\n  }\n);\n\nwatch(\n  () => postTags.value,\n  () => {\n    fuse = new Fuse(postTags.value || [], {\n      keys: [\"spec.displayName\", \"metadata.name\", \"spec.email\"],\n      useExtendedSearch: true,\n      threshold: 0.2,\n    });\n    if (props.context) {\n      // eslint-disable-next-line vue/no-mutating-props\n      props.context.options =\n        postTags.value?.map((tag) => {\n          return {\n            label: tag.spec.displayName,\n            value: tag.metadata.name,\n          };\n        }) || [];\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nconst selectedTags = computed(() => {\n  if (multiple.value) {\n    const selectedTagNames = (props.context._value as string[]) || [];\n    return selectedTagNames\n      .map((tagName): Tag | undefined => {\n        return postTags.value?.find((tag) => tag.metadata.name === tagName);\n      })\n      .filter(Boolean) as Tag[];\n  }\n\n  const tag = postTags.value?.find(\n    (tag) => tag.metadata.name === props.context._value\n  );\n\n  return [tag].filter(Boolean) as Tag[];\n});\n\nconst isSelected = (tag: Tag) => {\n  if (multiple.value) {\n    return (props.context._value || []).includes(tag.metadata.name);\n  }\n\n  return props.context._value === tag.metadata.name;\n};\n\nconst handleSelect = (tag: Tag) => {\n  if (multiple.value) {\n    const currentValue = props.context._value || [];\n    if (currentValue.includes(tag.metadata.name)) {\n      props.context.node.input(\n        currentValue.filter((t) => t !== tag.metadata.name)\n      );\n    } else {\n      props.context.node.input([...currentValue, tag.metadata.name]);\n      text.value = \"\";\n    }\n    return;\n  }\n\n  props.context.node.input(\n    tag.metadata.name === props.context._value ? \"\" : tag.metadata.name\n  );\n};\n\nconst handleKeydown = (e: KeyboardEvent) => {\n  if (!searchResults.value) return;\n\n  if (e.key === \"ArrowDown\") {\n    e.preventDefault();\n\n    const index = searchResults.value.findIndex(\n      (tag) => tag.metadata.name === selectedTag.value?.metadata.name\n    );\n    if (index < searchResults.value.length - 1) {\n      selectedTag.value = searchResults.value[index + 1];\n    }\n\n    scrollToSelected();\n  }\n  if (e.key === \"ArrowUp\") {\n    e.preventDefault();\n\n    const index = searchResults.value.findIndex(\n      (tag) => tag.metadata.name === selectedTag.value?.metadata.name\n    );\n    if (index > 0) {\n      selectedTag.value = searchResults.value[index - 1];\n    } else {\n      selectedTag.value = undefined;\n    }\n\n    scrollToSelected();\n  }\n\n  if (e.key === \"Enter\") {\n    if (!selectedTag.value && text.value) {\n      handleCreateTag();\n      return;\n    }\n\n    if (selectedTag.value) {\n      handleSelect(selectedTag.value);\n      text.value = \"\";\n\n      e.preventDefault();\n    }\n  }\n};\n\nconst scrollToSelected = () => {\n  const selectedNodeName = selectedTag.value\n    ? selectedTag.value?.metadata.name\n    : \"tag-create\";\n\n  const selectedNode = document.getElementById(selectedNodeName);\n\n  if (selectedNode) {\n    selectedNode.scrollIntoView({\n      behavior: \"smooth\",\n      block: \"nearest\",\n      inline: \"start\",\n    });\n  }\n};\n\nconst uid = new ShortUniqueId();\n\nconst handleCreateTag = async () => {\n  if (!utils.permission.has([\"system:posts:manage\"])) {\n    return;\n  }\n\n  let slug = slugify(text.value, { trim: true });\n\n  // Check if slug is unique, if not, add -1 to the slug\n  const { data: tagsWithSameSlug } = await coreApiClient.content.tag.listTag({\n    fieldSelector: [`spec.slug=${slug}`],\n    page: 1,\n    size: 1,\n  });\n\n  if (tagsWithSameSlug.total) {\n    slug = `${slug}-${uid.randomUUID(8)}`;\n  }\n\n  const { data } = await coreApiClient.content.tag.createTag({\n    tag: {\n      spec: {\n        displayName: text.value,\n        slug,\n        cover: \"\",\n      },\n      apiVersion: \"content.halo.run/v1alpha1\",\n      kind: \"Tag\",\n      metadata: {\n        name: \"\",\n        generateName: \"tag-\",\n      },\n    },\n  });\n\n  handleFetchTags();\n\n  handleSelect(data);\n\n  text.value = \"\";\n};\n\n// update value immediately during IME composition\n// please see https://vuejs.org//guide/essentials/forms.html#text\nconst onTextInput = (e: Event) => {\n  text.value = (e.target as HTMLInputElement).value;\n};\n\n// delete last tag when text input is empty\nconst handleDelete = () => {\n  if (!text.value) {\n    if (multiple.value) {\n      const selectedTagNames = (props.context._value as string[]) || [];\n      props.context.node.input(selectedTagNames.slice(0, -1));\n      return;\n    }\n    props.context.node.input(\"\");\n  }\n};\n</script>\n\n<template>\n  <VDropdown\n    :triggers=\"[]\"\n    :shown=\"dropdownVisible\"\n    auto-size\n    :auto-hide=\"false\"\n    container=\"body\"\n    :distance=\"10\"\n    class=\"w-full\"\n    popper-class=\"post-tag-dropdown\"\n  >\n    <div\n      ref=\"wrapperRef\"\n      :class=\"context.classes['post-tags-wrapper']\"\n      @keydown=\"handleKeydown\"\n    >\n      <div :class=\"context.classes['post-tags']\">\n        <div\n          v-for=\"(tag, index) in selectedTags\"\n          :key=\"index\"\n          :class=\"context.classes['post-tag-wrapper']\"\n        >\n          <PostTag :tag=\"tag\" rounded>\n            <template #rightIcon>\n              <IconClose\n                :class=\"context.classes['post-tag-close']\"\n                @click=\"handleSelect(tag)\"\n              />\n            </template>\n          </PostTag>\n        </div>\n        <input\n          :value=\"text\"\n          :class=\"context.classes.input\"\n          type=\"text\"\n          @input=\"onTextInput\"\n          @focus=\"dropdownVisible = true\"\n          @keydown.delete=\"handleDelete\"\n        />\n      </div>\n\n      <div\n        :class=\"context.classes['post-tags-button']\"\n        @click=\"dropdownVisible = !dropdownVisible\"\n      >\n        <IconArrowRight class=\"rotate-90 text-gray-500 hover:text-gray-700\" />\n      </div>\n    </div>\n    <template #popper>\n      <div ref=\"popperRef\" :class=\"context.classes['dropdown-wrapper']\">\n        <ul class=\"p-1\">\n          <HasPermission\n            v-if=\"text.trim()\"\n            :permissions=\"['system:posts:manage']\"\n          >\n            <li\n              id=\"tag-create\"\n              class=\"group flex cursor-pointer items-center justify-between rounded p-2\"\n              :class=\"{\n                'bg-gray-100': selectedTag === undefined,\n              }\"\n              @click=\"handleCreateTag\"\n            >\n              <span class=\"text-xs text-gray-700 group-hover:text-gray-900\">\n                {{\n                  $t(\"core.formkit.tag_select.creation_label\", { text: text })\n                }}\n              </span>\n            </li>\n          </HasPermission>\n          <li\n            v-for=\"tag in searchResults\"\n            :id=\"tag.metadata.name\"\n            :key=\"tag.metadata.name\"\n            class=\"group flex cursor-pointer items-center justify-between rounded p-2 hover:bg-gray-100\"\n            :class=\"{\n              'bg-gray-100': selectedTag?.metadata.name === tag.metadata.name,\n            }\"\n            @click=\"handleSelect(tag)\"\n          >\n            <div class=\"inline-flex items-center overflow-hidden\">\n              <PostTag :tag=\"tag\" />\n            </div>\n            <IconCheckboxCircle v-if=\"isSelected(tag)\" class=\"text-primary\" />\n          </li>\n        </ul>\n      </div>\n    </template>\n  </VDropdown>\n</template>\n<style lang=\"scss\">\n.post-tag-dropdown {\n  .v-popper__arrow-container {\n    display: none;\n  }\n  .v-popper__inner {\n    padding: 0 !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/src/formkit/inputs/tag-select/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  help,\n  icon,\n  inner,\n  label,\n  message,\n  messages,\n  outer,\n  prefix,\n  suffix,\n  wrapper,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport { defineAsyncComponent } from \"vue\";\nimport { TagSelectSection } from \"./sections\";\n\nexport const tagSelect: FormKitTypeDefinition = {\n  schema: outer(\n    wrapper(\n      label(\"$label\"),\n      inner(\n        icon(\"prefix\"),\n        prefix(),\n        TagSelectSection(),\n        suffix(),\n        icon(\"suffix\")\n      )\n    ),\n    help(\"$help\"),\n    messages(message(\"$message.value\"))\n  ),\n  type: \"input\",\n  props: [\"multiple\"],\n  library: {\n    TagSelect: defineAsyncComponent(() => import(\"./TagSelect.vue\")),\n  },\n  schemaMemoKey: \"custom-tag-select\",\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    tagSelect: {\n      type: \"tagSelect\";\n      value?: string | string[];\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/tag-select/sections/index.ts",
    "content": "import { createSection } from \"@formkit/inputs\";\n\nexport const TagSelectSection = createSection(\"TagSelectSection\", () => ({\n  $cmp: \"TagSelect\",\n  props: {\n    context: \"$node.context\",\n  },\n}));\n"
  },
  {
    "path": "ui/src/formkit/inputs/toggle/ToggleInput.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { FormKitFrameworkContext } from \"@formkit/core\";\nimport { computed } from \"vue\";\nimport type { ToggleOption, ToggleValue } from \".\";\n\nconst { context } = defineProps<{\n  context: FormKitFrameworkContext;\n}>();\n\nconst options = computed<ToggleOption[]>(\n  () => context.node.props.options ?? []\n);\n\nconst multiple = computed<boolean>(() => context.node.props.multiple ?? false);\nconst renderType = computed<\"text\" | \"image\" | \"color\">(\n  () => context.node.props.renderType ?? \"text\"\n);\n\nconst currentValue = computed<ToggleValue | ToggleValue[]>(\n  () => context._value\n);\n\nfunction isSelected(value: ToggleValue) {\n  if (multiple.value) {\n    const valueArray = (currentValue.value as ToggleValue[]) || [];\n    return valueArray.includes(value);\n  }\n\n  return currentValue.value === value;\n}\n\nfunction handleSelect(value: ToggleValue) {\n  if (multiple.value) {\n    const valueArray = (currentValue.value as ToggleValue[]) || [];\n    if (valueArray.includes(value)) {\n      context.node.input(valueArray.filter((v) => v !== value));\n    } else {\n      context.node.input([...valueArray, value]);\n    }\n    return;\n  }\n\n  context.node.input(value);\n}\n\nconst defaultSize = () => {\n  switch (renderType.value) {\n    case \"image\":\n      return \"100px\";\n    case \"text\":\n      return \"100px\";\n    case \"color\":\n      return \"40px\";\n    default:\n      return \"100px\";\n  }\n};\n\nconst size = computed(() => {\n  const size = context.node.props.size;\n  if (!size) {\n    return defaultSize();\n  }\n\n  const sizeValue = Number(size);\n  if (Number.isNaN(sizeValue)) {\n    return defaultSize();\n  }\n  return `${sizeValue}px`;\n});\n\nconst gap = computed(() => {\n  const gap = context.node.props.gap;\n  if (!gap) {\n    return \"12px\";\n  }\n\n  const gapValue = Number(gap);\n  if (Number.isNaN(gapValue)) {\n    return \"12px\";\n  }\n  return `${gap}px`;\n});\n</script>\n<template>\n  <div class=\"flex flex-wrap\" :style=\"{ gap: gap }\">\n    <div\n      v-for=\"option in options\"\n      :key=\"String(option.value)\"\n      class=\"group flex cursor-pointer flex-col items-center justify-center gap-2\"\n      @click=\"handleSelect(option.value)\"\n    >\n      <div\n        class=\"border-2 p-0.5 transition-all duration-200\"\n        :class=\"[\n          isSelected(option.value)\n            ? 'border-primary shadow-md'\n            : 'border-transparent group-hover:border-gray-300 group-hover:shadow-sm',\n          renderType === 'color' ? 'rounded-full' : 'rounded-lg',\n        ]\"\n      >\n        <img\n          v-if=\"renderType === 'image'\"\n          :src=\"option.render\"\n          :alt=\"option.label\"\n          :style=\"{ height: size }\"\n          class=\"rounded-md object-cover\"\n        />\n        <div\n          v-if=\"renderType === 'color'\"\n          :style=\"{ backgroundColor: option.render, height: size, width: size }\"\n          class=\"rounded-full\"\n        ></div>\n        <span\n          v-if=\"renderType === 'text'\"\n          class=\"flex items-center justify-center rounded-md p-1 text-base font-medium\"\n          :class=\"\n            isSelected(option.value)\n              ? 'text-primary'\n              : 'text-gray-600 group-hover:text-gray-900'\n          \"\n        >\n          {{ option.render || option.label || option.value }}\n        </span>\n      </div>\n      <label\n        v-if=\"option.label && renderType !== 'text'\"\n        class=\"cursor-pointer select-none text-sm transition-colors duration-200\"\n        :class=\"\n          isSelected(option.value)\n            ? 'font-semibold text-gray-900'\n            : 'text-gray-500 group-hover:text-gray-700'\n        \"\n      >\n        {{ option.label }}\n      </label>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/toggle/feature.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { undefine } from \"@formkit/utils\";\n\nexport default function toggleFeature(node: FormKitNode): void {\n  node.on(\"created\", () => {\n    node.props.multiple = undefine(node.props.multiple) ?? false;\n    node.props.renderType = node.props.renderType ?? \"text\";\n\n    if (node.props.multiple) {\n      if (node.value === undefined) {\n        node.input([], false);\n      }\n    }\n\n    if (node.context) {\n      node.context.initialValue = node.value || \"\";\n    }\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/toggle/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  createSection,\n  help,\n  label,\n  message,\n  messages,\n  outer,\n  wrapper,\n  type FormKitInputs,\n} from \"@formkit/inputs\";\nimport { defineAsyncComponent } from \"vue\";\nimport toggleFeature from \"./feature\";\n\nconst cmpName = \"ToggleInput\";\n\nexport const toggleSection = createSection(\"toggleSection\", () => ({\n  $cmp: cmpName,\n  props: {\n    context: \"$node.context\",\n  },\n}));\n\n/**\n * Input definition for a toggle input.\n * @public\n */\nexport const toggle: FormKitTypeDefinition = {\n  /**\n   * The actual schema of the input, or a function that returns the schema.\n   */\n  schema: outer(\n    wrapper(label(\"$label\"), help(\"$help\"), toggleSection()),\n    messages(message(\"$message.value\"))\n  ),\n\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"input\",\n  /**\n   * An array of extra props to accept for this input.\n   */\n  props: [\"options\", \"multiple\", \"renderType\", \"size\", \"gap\"],\n\n  features: [toggleFeature],\n\n  /**\n   * Additional features that make this input work.\n   */\n  library: {\n    ToggleInput: defineAsyncComponent(() => import(\"./ToggleInput.vue\")),\n  },\n  schemaMemoKey: \"custom-toggle\",\n};\n\nexport type ToggleValue = string | number | boolean;\n\nexport type ToggleOption = {\n  render?: string;\n  label?: string;\n  value: string | number | boolean;\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    toggle: {\n      type: \"toggle\";\n      options: ToggleOption[];\n      value?: ToggleValue | ToggleValue[];\n      multiple?: boolean;\n      renderType?: \"text\" | \"image\" | \"color\";\n      size?: number;\n      gap?: number;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/user-select.ts",
    "content": "import type { FormKitNode, FormKitTypeDefinition } from \"@formkit/core\";\nimport type { FormKitInputs } from \"@formkit/inputs\";\nimport {\n  consoleApiClient,\n  paginate,\n  type ListedUser,\n  type UserV1alpha1ConsoleApiListUsersRequest,\n} from \"@halo-dev/api-client\";\nimport { select } from \"./select\";\n\nconst ANONYMOUSUSER_NAME = \"anonymousUser\";\nconst DELETEDUSER_NAME = \"ghost\";\n\nconst search = async ({ page, size, keyword }) => {\n  const { data } = await consoleApiClient.user.listUsers({\n    page,\n    size,\n    keyword,\n    fieldSelector: [`name!=${ANONYMOUSUSER_NAME}`, `name!=${DELETEDUSER_NAME}`],\n  });\n  return {\n    options: data.items?.map((user) => {\n      return {\n        value: user.user.metadata.name,\n        label: `${user.user.spec.displayName}(${user.user.metadata.name})`,\n      };\n    }),\n    total: data.total,\n    size: data.size,\n    page: data.page,\n  };\n};\n\nconst findOptionsByValues = async (values: string[]) => {\n  if (values.length === 0) {\n    return [];\n  }\n\n  const users = await paginate<\n    UserV1alpha1ConsoleApiListUsersRequest,\n    ListedUser\n  >((params) => consoleApiClient.user.listUsers(params), {\n    fieldSelector: [`metadata.name=(${values.join(\",\")})`],\n    size: 1000,\n  });\n\n  return users.map((user) => {\n    return {\n      value: user.user.metadata.name,\n      label: user.user.spec.displayName,\n    };\n  });\n};\n\nfunction optionsHandler(node: FormKitNode) {\n  node.on(\"created\", async () => {\n    node.props = {\n      ...node.props,\n      remote: true,\n      remoteOption: {\n        search,\n        findOptionsByValues,\n      },\n      searchable: true,\n    };\n  });\n}\n\nexport const userSelect: FormKitTypeDefinition = {\n  ...select,\n  forceTypeProp: \"select\",\n  features: [optionsHandler],\n};\n\ndeclare module \"@formkit/inputs\" {\n  export interface FormKitInputProps<Props extends FormKitInputs<Props>> {\n    userSelect: {\n      type: \"userSelect\";\n      value?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/verify-form/VerificationButton.vue",
    "content": "<script lang=\"ts\" setup>\nimport { createMessage, type FormKitFrameworkContext } from \"@formkit/core\";\nimport { axiosInstance } from \"@halo-dev/api-client\";\nimport {\n  IconCheckboxCircle,\n  IconErrorWarning,\n  Toast,\n  VButton,\n} from \"@halo-dev/components\";\nimport type { PropType } from \"vue\";\nimport { nextTick, onMounted, ref } from \"vue\";\nimport { i18n } from \"@/locales\";\n\nconst props = defineProps({\n  context: {\n    type: Object as PropType<FormKitFrameworkContext>,\n    required: true,\n  },\n});\n\nconst loadingState = ref<boolean>(false);\nconst stateMessage = ref<{\n  state: \"default\" | \"success\" | \"error\";\n  message?: string;\n}>({\n  state: \"default\",\n});\n\nconst loading = createMessage({\n  key: \"loading\",\n  value: true,\n  visible: false,\n});\n\n/**\n * Handle the verify event.\n *\n * @param e - The event\n *\n * @internal\n */\nasync function handleVerification(event: Event) {\n  const node = props.context.node;\n  event.preventDefault();\n  await node.settled;\n\n  if (!node.ledger.value(\"blocking\")) {\n    verifyAction();\n  }\n}\n\n/**\n * verify action\n * @param node\n */\nfunction verifyAction() {\n  const node = props.context.node;\n  const action = node.props.action;\n  if (!action) {\n    const message = i18n.global.t(\n      \"core.formkit.verification_form.no_action_defined\",\n      {\n        label: node.props.label,\n      }\n    );\n    stateMessage.value = {\n      state: \"error\",\n      message,\n    };\n    Toast.error(message);\n    return;\n  }\n  loadingState.value = true;\n  const val = node.value as Record<string, unknown>;\n  node.store.set(loading);\n  // call onSubmit\n  axiosInstance\n    .post(action, val)\n    .then(() => {\n      stateMessage.value = {\n        state: \"success\",\n      };\n      Toast.success(\n        i18n.global.t(\"core.formkit.verification_form.verify_success\", {\n          label: node.props.label,\n        })\n      );\n    })\n    .catch((error) => {\n      stateMessage.value = {\n        state: \"error\",\n      };\n      const errorResponse = error.response;\n      const { title, detail } = errorResponse.data;\n      if (title || detail) {\n        stateMessage.value.message = detail || title;\n      }\n    })\n    .finally(() => {\n      node.store.remove(\"loading\");\n      loadingState.value = false;\n    });\n}\n\nonMounted(() => {\n  nextTick(() => {\n    const node = props.context.node;\n    node.on(\"commit\", () => {\n      stateMessage.value = {\n        state: \"default\",\n      };\n    });\n  });\n});\n</script>\n\n<template>\n  <div :class=\"context.classes.submit\" class=\"py-4\" @click=\"handleVerification\">\n    <VButton\n      v-tooltip=\"stateMessage.message\"\n      :disabled=\"context.node.props.buttonAttrs.disabled\"\n      :loading=\"loadingState\"\n    >\n      {{ context.node.props.label }}\n      <template v-if=\"stateMessage.state !== 'default'\" #icon>\n        <IconCheckboxCircle\n          v-if=\"stateMessage.state === 'success'\"\n          class=\"h-full w-full text-green-500\"\n        />\n        <IconErrorWarning\n          v-else-if=\"stateMessage.state === 'error'\"\n          class=\"h-full w-full text-red-500\"\n        />\n      </template>\n    </VButton>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/formkit/inputs/verify-form/features/index.ts",
    "content": "import { type FormKitNode } from \"@formkit/core\";\nimport { i18n } from \"@/locales\";\n\nfunction buildVerifyFormValue(node: FormKitNode) {\n  if (!node.parent) return {};\n  const parentValue = {\n    ...(node.parent.value as Record<string, unknown>),\n  };\n  delete parentValue[node.name];\n  return parentValue;\n}\n\n/**\n * A feature to add a submit handler and actions section.\n *\n * @param node - A {@link @formkit/core#FormKitNode | FormKitNode}.\n *\n * @public\n */\nexport default function verify(node: FormKitNode): void {\n  node.props.buttonAttrs ??= {\n    disabled: node.props.disabled,\n  };\n\n  node.props.label ??= i18n.global.t(\"core.common.buttons.verify\");\n\n  node.on(\"prop:disabled\", ({ payload: disabled }) => {\n    node.props.buttonAttrs = { ...node.props.buttonAttrs, disabled };\n  });\n\n  node.on(\"created\", () => {\n    if (node.parent) {\n      node.parent.hook.commit((val) => {\n        const parentValue = {\n          ...val,\n        };\n        const verifyFormVal = (parentValue[node.name] || {}) as Record<\n          string,\n          unknown\n        >;\n        delete parentValue[node.name];\n        const mergeFormValue = {};\n        Object.keys(verifyFormVal).forEach((key) => {\n          if (node.children.find((child) => child.name === key)) {\n            mergeFormValue[key] = verifyFormVal[key];\n          }\n        });\n        return { ...parentValue, ...mergeFormValue };\n      });\n\n      node.hook.input(() => {\n        return buildVerifyFormValue(node);\n      });\n\n      node.input(buildVerifyFormValue(node));\n    }\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/inputs/verify-form/index.ts",
    "content": "import type { FormKitTypeDefinition } from \"@formkit/core\";\nimport {\n  createSection,\n  disablesChildren,\n  message,\n  messages,\n} from \"@formkit/inputs\";\nimport { default as verifyFeature } from \"./features\";\nimport VerificationButton from \"./VerificationButton.vue\";\n\nexport const verifyInput = createSection(\"verificationForm\", () => ({\n  $el: \"div\",\n  bind: \"$attrs\",\n  attrs: {\n    id: \"$id\",\n    name: \"$node.name\",\n    \"data-loading\": \"$state.loading || undefined\",\n  },\n}));\n\nexport const actions = createSection(\"actions\", () => ({\n  $el: \"div\",\n}));\n\nexport const verificationButton = createSection(\"verificationButton\", () => ({\n  $cmp: \"VerificationButton\",\n  type: \"button\",\n  bind: \"$buttonAttrs\",\n  props: {\n    context: \"$node.context\",\n  },\n}));\n\n/**\n * Input definition for a form.\n * @public\n */\nexport const verificationForm: FormKitTypeDefinition = {\n  /**\n   * The actual schema of the input, or a function that returns the schema.\n   */\n  schema: verifyInput(\n    \"$slots.default\",\n    messages(message(\"$message.value\")),\n    actions(verificationButton())\n  ),\n  /**\n   * The type of node, can be a list, group, or input.\n   */\n  type: \"group\",\n  /**\n   * An array of extra props to accept for this input.\n   */\n  props: [\"action\", \"label\", \"buttonAttrs\"],\n\n  /**\n   * Additional features that should be added to your input\n   */\n  features: [verifyFeature, disablesChildren],\n\n  library: {\n    VerificationButton,\n  },\n};\n"
  },
  {
    "path": "ui/src/formkit/plugins/auto-scroll-to-errors.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\n\nexport default function autoScrollToErrors(node: FormKitNode) {\n  const scrollTo = (node: FormKitNode) => {\n    if (!node.props.id) {\n      return;\n    }\n    const el = document.getElementById(node.props.id);\n    if (el) {\n      el.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n    }\n  };\n\n  const scrollToErrors = () => {\n    node.walk((child) => {\n      if (child.ledger.value(\"blocking\") || child.ledger.value(\"errors\")) {\n        scrollTo(child);\n        return false;\n      }\n    }, true);\n  };\n\n  if (node.props.type === \"form\") {\n    const onOldSubmitInvalid = node.props.onSubmitInvalid;\n    node.props.onSubmitInvalid = () => {\n      if (onOldSubmitInvalid) {\n        onOldSubmitInvalid(node);\n      }\n      scrollToErrors();\n    };\n    node.on(\"unsettled:errors\", scrollToErrors);\n  }\n  return false;\n}\n"
  },
  {
    "path": "ui/src/formkit/plugins/password-prevent-autocomplete.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\n\nexport default function passwordPreventAutocomplete(node: FormKitNode) {\n  if (node.props.type === \"password\" && !node.props.attrs?.autocomplete) {\n    if (!node.props.attrs) node.props.attrs = {};\n    // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion#preventing_autofilling_with_autocompletenew-password\n    node.props.attrs.autocomplete = \"new-password\";\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/plugins/radio-alt.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\n\nlet i = 0;\n\nexport default function radioAlt(node: FormKitNode) {\n  if (node.props.type === \"radio\") {\n    node.props.altName = `radio_${node.name}_${++i}`;\n  }\n}\n"
  },
  {
    "path": "ui/src/formkit/plugins/required-asterisk.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\n\nconst hasLegendNode = (node) =>\n  [\"checkbox\", \"radio\", \"repeater\", \"group\"].includes(node.props.type);\n\nexport default function requiredAsterisk(node: FormKitNode) {\n  node.on(\"created\", () => {\n    const isRequired = node.props.parsedRules.some(\n      (rule) => rule.name === \"required\"\n    );\n\n    if (!isRequired) return;\n\n    if (!node.props.definition) return;\n\n    node.props.definition.schemaMemoKey = `required_${\n      hasLegendNode(node) ? \"multi_\" : \"\"\n    }${node.props.definition.schemaMemoKey}`;\n\n    const schemaFn = node.props.definition?.schema;\n\n    if (typeof schemaFn !== \"function\") return;\n\n    node.props.definition.schema = (sectionsSchema = {}) => {\n      if (hasLegendNode(node)) {\n        sectionsSchema.legend = {\n          children: [\"$label\", \" *\"],\n        };\n      } else {\n        sectionsSchema.label = {\n          children: [\"$label\", \" *\"],\n        };\n      }\n\n      return schemaFn(sectionsSchema);\n    };\n  });\n}\n"
  },
  {
    "path": "ui/src/formkit/plugins/stop-implicit-submission.ts",
    "content": "import type { FormKitNode } from \"@formkit/core\";\nimport { onMounted } from \"vue\";\nimport { useRoute, type Router } from \"vue-router\";\nfunction handleKeydown(e: KeyboardEvent) {\n  if (\n    e.key == \"Enter\" &&\n    e.altKey == false &&\n    e.ctrlKey == false &&\n    e.metaKey == false\n  ) {\n    e.stopImmediatePropagation();\n    e.preventDefault();\n    e.stopPropagation();\n  }\n}\n\n// 以下表单键入enter引起表单提交，\n//https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission\nconst implicitSubmissionType = [\n  \"text\",\n  \"search\",\n  \"url\",\n  \"email\",\n  \"password\",\n  \"date\",\n  \"month\",\n  \"week\",\n  \"time\",\n  \"datetime-local\",\n  \"number\",\n];\n\nconst FormKeydownEventControllerMap = new Map<string, AbortController>();\nlet routeCleanupRegistered = false;\n\nconst clearFormKeydownEventByPath = (fullPath: string) => {\n  if (FormKeydownEventControllerMap.size) {\n    FormKeydownEventControllerMap.forEach((controller, path) => {\n      if (!fullPath.includes(path)) {\n        controller.abort();\n        FormKeydownEventControllerMap.delete(path);\n      }\n    });\n  }\n};\n\n//https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener#%E5%8F%82%E6%95%B0\n//使用AbortSignal来取消事件监听\nconst inputPreventFn = (node: FormKitNode) => {\n  const id = node.props.id ?? \"\";\n  if (node.type == \"group\") {\n    onMounted(() => {\n      const { path } = useRoute();\n      let controller = FormKeydownEventControllerMap.get(path);\n      if (\n        node.children.length == 1 &&\n        node.props.type == \"form\" &&\n        \"props\" in node.children[0] &&\n        implicitSubmissionType.includes(node.children[0].props.type)\n      ) {\n        if (!controller) {\n          controller = new AbortController();\n          FormKeydownEventControllerMap.set(path, controller);\n        }\n        const rootForm = document.getElementById(id);\n        rootForm?.addEventListener(\"keydown\", handleKeydown, {\n          signal: controller.signal,\n        });\n      }\n    });\n  }\n};\n\nexport function setupStopImplicitSubmission(router: Router) {\n  if (routeCleanupRegistered) {\n    return;\n  }\n\n  router.beforeEach(({ fullPath }) => {\n    clearFormKeydownEventByPath(fullPath);\n  });\n  routeCleanupRegistered = true;\n}\n\nexport default inputPreventFn;\n"
  },
  {
    "path": "ui/src/formkit/theme.ts",
    "content": "const textClassification = {\n  label: \"block text-sm font-medium text-gray-700 formkit-invalid:text-red-500\",\n  wrapper: \"flex flex-col gap-4\",\n  inner:\n    \"bg-white inline-flex items-center w-full relative box-border border border-gray-300 formkit-invalid:border-red-500 h-9 rounded-base overflow-hidden focus-within:border-primary focus-within:shadow-sm w-full sm:max-w-lg transition-all\",\n  input: \"resize-none w-full text-black block transition-all px-3 text-sm\",\n};\n\nconst boxClassification = {\n  label: textClassification.label,\n  legend: `${textClassification.label} px-2`,\n  fieldset:\n    \"group border border-gray-300 rounded-base px-2 py-2 focus-within:border-primary max-w-lg\",\n  wrapper:\n    \"flex items-center mb-1 cursor-pointer group-[.formkit-fieldset]:px-2\",\n  help: \"mb-2 mt-0\",\n  input: \"form-check-input mr-2 bg-white\",\n  inner: \"flex items-center\",\n};\n\nconst buttonClassification = {\n  wrapper: \"mb-1\",\n  input:\n    \"bg-blue-500 hover:bg-blue-700 text-white text-sm font-normal py-3 px-5 rounded\",\n};\n\nconst selectClassification = {\n  label: textClassification.label,\n  wrapper: textClassification.wrapper,\n  inner:\n    \"group/select py-0.5 min-h-[36px] inline-flex items-center w-full relative box-border border border-gray-300 formkit-invalid:border-red-500 rounded-base overflow-hidden focus-within:border-primary focus-within:shadow-sm w-full sm:max-w-lg transition-all\",\n};\n\nconst theme: Record<string, Record<string, string>> = {\n  global: {\n    form: \"divide-y divide-gray-100\",\n    outer:\n      \"formkit-disabled:opacity-70 formkit-disabled:cursor-not-allowed formkit-disabled:pointer-events-none py-4 first:pt-0 last:pb-0 transition-all\",\n    help: \"text-xs mt-2 text-gray-500\",\n    messages: \"list-none p-0 mt-1.5 mb-0 transition-all\",\n    message: \"text-red-500 mt-2 text-xs\",\n    verificationForm: \"pt-4 divide-y divide-gray-100\",\n  },\n  button: buttonClassification,\n  color: {\n    ...textClassification,\n    inner: \"inline-flex items-center gap-1\",\n    input:\n      \"w-16 h-8 appearance-none cursor-pointer border border-gray-300 rounded-md p-1\",\n  },\n  iconify: {\n    ...textClassification,\n    inner: \"inline-flex items-center gap-1\",\n  },\n  file: {\n    ...textClassification,\n    noFiles: \"block text-gray-800 text-sm mb-1\",\n    fileItem: \"block flex text-gray-800 text-sm mb-1\",\n    fileRemove: \"ml-auto text-blue-500 text-sm\",\n  },\n  checkbox: {\n    ...boxClassification,\n    wrapper: \"flex items-center mb-1 cursor-pointer\",\n  },\n  switch: {\n    ...boxClassification,\n    wrapper: \"flex items-center mb-1 gap-2 cursor-pointer\",\n  },\n  radio: {\n    ...boxClassification,\n    input: boxClassification.input.replace(\"rounded-sm\", \"rounded-full\"),\n    help: `${boxClassification.help} px-2`,\n  },\n  range: {\n    ...textClassification,\n    inner: \"w-full sm:max-w-lg\",\n    input:\n      \"form-range appearance-none w-full h-2 p-0 bg-gray-200 rounded-full focus:outline-none focus:ring-0 focus:shadow-none\",\n  },\n  search: textClassification,\n  select: selectClassification,\n  nativeSelect: textClassification,\n  submit: buttonClassification,\n  tel: textClassification,\n  text: textClassification,\n  time: textClassification,\n  url: textClassification,\n  week: textClassification,\n  month: textClassification,\n  number: textClassification,\n  password: textClassification,\n  email: textClassification,\n  date: textClassification,\n  \"datetime-local\": textClassification,\n  textarea: {\n    ...textClassification,\n    inner: textClassification.inner.replace(\"h-9\", \"h-full\"),\n    input:\n      textClassification.input.replace(\"resize-none\", \"resize-y\") + \" py-2\",\n  },\n  repeater: {\n    label: textClassification.label,\n    legend: `${textClassification.label} px-2`,\n    fieldset: boxClassification.fieldset,\n    wrapper: boxClassification.wrapper,\n    help: `${boxClassification.wrapper} mb-2`,\n    inner: \"flex flex-col gap-4\",\n    items: \"flex flex-col w-full gap-2 rounded-base\",\n    item: \"border rounded-base grid grid-cols-12 focus-within:border-primary transition-all overflow-visible focus-within:shadow-sm\",\n    content: \"flex-1 p-2 col-span-11 divide-y divide-gray-100\",\n    controls:\n      \"flex flex-col items-center justify-center gap-1.5 py-2 bg-gray-200 col-span-1 flex items-center justify-center\",\n    control:\n      \"cursor-pointer text-gray-500 transition-all hover:text-primary disabled:!cursor-not-allowed disabled:opacity-50 disabled:hover:!text-gray-500\",\n  },\n  array: {\n    label: textClassification.label,\n    wrapper: \"flex flex-col gap-0\",\n    help: \"!mt-1\",\n  },\n  toggle: {\n    label: textClassification.label,\n    wrapper: textClassification.wrapper,\n    help: \"mb-1 !mt-0\",\n    inner: \"inline-flex items-center gap-1\",\n  },\n  attachment: {\n    label: textClassification.label,\n    inner: \"inline-flex w-full\",\n    wrapper: \"flex flex-col gap-4 w-full\",\n  },\n  list: {\n    label: textClassification.label,\n    legend: `${textClassification.label} px-2`,\n    fieldset: boxClassification.fieldset,\n    wrapper: boxClassification.wrapper,\n    help: `${boxClassification.wrapper} mb-2`,\n    inner: \"flex flex-col gap-4\",\n    items: \"flex flex-col w-full gap-2 rounded-base\",\n    item: \"grid grid-cols-12 focus-within:border-primary transition-all gap-1.5 overflow-visible\",\n    content: \"flex-1 col-span-9 divide-y divide-gray-100\",\n    controls: \"flex items-center justify-center gap-1 col-span-3\",\n    control:\n      \"cursor-pointer text-gray-500 transition-all hover:text-primary disabled:!cursor-not-allowed disabled:opacity-50 disabled:hover:!text-gray-500\",\n    up: \"flex\",\n    remove: \"flex\",\n    insert: \"flex\",\n    down: \"flex\",\n  },\n  group: {\n    label: textClassification.label,\n    legend: `${textClassification.label} px-2`,\n    fieldset: boxClassification.fieldset,\n    wrapper: boxClassification.wrapper,\n    help: `${boxClassification.wrapper} mb-2`,\n    inner: \"flex flex-col px-2 divide-y divide-gray-100\",\n  },\n  tagSelect: {\n    ...textClassification,\n    inner: `${textClassification.inner} !overflow-visible !h-auto min-h-[2.25rem]`,\n    input: `w-0 flex-grow bg-transparent py-1 px-3 block transition-all text-sm`,\n    \"post-tags-wrapper\": \"flex w-full items-center\",\n    \"post-tags\": \"flex w-full flex-wrap items-center\",\n    \"post-tag-wrapper\": \"inline-flex items-center p-1\",\n    \"post-tag-close\":\n      \"h-4 w-4 cursor-pointer text-gray-600 hover:text-gray-900\",\n    \"post-tags-button\": \"inline-flex h-full cursor-pointer items-center px-1\",\n    \"dropdown-wrapper\": \"max-h-96 w-full overflow-auto bg-white\",\n  },\n  categorySelect: {\n    ...textClassification,\n    inner: `${textClassification.inner} !overflow-visible !h-auto min-h-[2.25rem]`,\n    input: `w-0 flex-grow bg-transparent py-1 px-3 block transition-all text-sm`,\n    \"post-categories-wrapper\": \"flex w-full items-center\",\n    \"post-categories\": \"flex w-full flex-wrap items-center\",\n    \"post-categories-button\":\n      \"inline-flex h-full cursor-pointer items-center px-1\",\n    \"dropdown-wrapper\": \"max-h-96 w-full overflow-auto bg-white\",\n  },\n  secret: {\n    inner: \"w-full sm:max-w-lg\",\n    wrapper: textClassification.wrapper,\n    label: textClassification.label,\n  },\n};\n\nexport default theme;\n"
  },
  {
    "path": "ui/src/formkit/utils/focus.ts",
    "content": "export function setFocus(id: string) {\n  const inputElement = document.getElementById(id);\n  if (\n    inputElement instanceof HTMLInputElement ||\n    inputElement instanceof HTMLTextAreaElement\n  ) {\n    const timer = setTimeout(() => {\n      const end = inputElement.value.length;\n      inputElement.setSelectionRange(end, end);\n      inputElement?.focus();\n      clearTimeout(timer);\n    }, 0);\n  }\n}\n"
  },
  {
    "path": "ui/src/layouts/MobileMenu.vue",
    "content": "<script lang=\"ts\" setup>\nimport { IconMore, VMenu, VMenuItem } from \"@halo-dev/components\";\nimport type { MenuGroupType, MenuItemType } from \"@halo-dev/ui-shared\";\nimport type { OverlayScrollbars } from \"overlayscrollbars\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { inject, ref, watch } from \"vue\";\nimport { useRoute } from \"vue-router\";\nimport RiArrowLeftLine from \"~icons/ri/arrow-left-line\";\nimport { RoutesMenu } from \"@/components/menu/RoutesMenu\";\nimport UserProfileBanner from \"./UserProfileBanner.vue\";\n\ndefineProps<{\n  platform?: \"console\" | \"uc\";\n  menus: MenuGroupType[];\n  minimenus: MenuItemType[];\n}>();\n\nconst route = useRoute();\n\nconst moreMenuVisible = ref(false);\n\nconst bodyScrollInstance =\n  inject<() => OverlayScrollbars | null>(\"bodyScrollInstance\");\n\nwatch(\n  () => moreMenuVisible.value,\n  (value) => {\n    // Lock body scroll when the drawer is open\n    bodyScrollInstance?.()?.options({\n      overflow: {\n        x: value ? \"hidden\" : \"scroll\",\n        y: value ? \"hidden\" : \"scroll\",\n      },\n    });\n  }\n);\n\nfunction handleSelectHome() {\n  window.open(\"/\", \"_blank\");\n}\n</script>\n<template>\n  <div v-if=\"minimenus\" class=\"mobile-nav mobile-nav--fixed\">\n    <div\n      v-for=\"(menu, index) in minimenus\"\n      :key=\"index\"\n      :class=\"{ 'mobile-nav__item--active': route.path === menu?.path }\"\n      class=\"mobile-nav__item\"\n      @click=\"$router.push(menu?.path)\"\n    >\n      <div class=\"mobile-nav__button\">\n        <div class=\"mobile-nav__icon-container\">\n          <div class=\"mobile-nav__icon\">\n            <Component :is=\"menu?.icon\" />\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"mobile-nav__item\" @click=\"moreMenuVisible = true\">\n      <div class=\"mobile-nav__button\">\n        <div class=\"mobile-nav__icon-container\">\n          <div class=\"mobile-nav__icon\">\n            <IconMore />\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <Teleport to=\"body\">\n      <div v-if=\"moreMenuVisible\" class=\"drawer drawer--visible\">\n        <transition\n          enter-active-class=\"ease-out duration-400\"\n          enter-from-class=\"opacity-0\"\n          enter-to-class=\"opacity-100\"\n          leave-active-class=\"ease-in duration-200\"\n          leave-from-class=\"opacity-100\"\n          leave-to-class=\"opacity-0\"\n          appear\n        >\n          <div class=\"drawer__overlay\" @click=\"moreMenuVisible = false\"></div>\n        </transition>\n        <transition\n          enter-active-class=\"transform transition ease-out duration-500\"\n          enter-from-class=\"translate-y-full scale-95\"\n          enter-to-class=\"translate-y-0 scale-100\"\n          leave-active-class=\"transform transition ease-in duration-300\"\n          leave-from-class=\"translate-y-0 scale-100\"\n          leave-to-class=\"translate-y-full scale-95\"\n          appear\n        >\n          <div class=\"drawer__content\">\n            <OverlayScrollbarsComponent\n              element=\"div\"\n              :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n              class=\"drawer__body\"\n              defer\n            >\n              <VMenu class=\"!pb-1\">\n                <VMenuItem\n                  id=\"home\"\n                  :title=\"$t('core.sidebar.menu.items.home')\"\n                  @select=\"handleSelectHome\"\n                >\n                  <template #icon>\n                    <RiArrowLeftLine />\n                  </template>\n                </VMenuItem>\n              </VMenu>\n              <RoutesMenu :menus=\"menus\" @select=\"moreMenuVisible = false\" />\n            </OverlayScrollbarsComponent>\n            <div class=\"drawer__footer\">\n              <UserProfileBanner :platform=\"platform\" />\n            </div>\n          </div>\n        </transition>\n      </div>\n    </Teleport>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.mobile-nav {\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  display: grid;\n  grid-template-columns: repeat(6, 1fr);\n  border-top: 2px solid theme(\"colors.black\");\n  background-color: theme(\"colors.secondary\");\n  box-shadow: theme(\"dropShadow.2xl\");\n  margin-top: env(safe-area-inset-top);\n  padding-bottom: env(safe-area-inset-bottom);\n\n  @media (min-width: theme(\"screens.md\")) {\n    display: none;\n  }\n\n  &__item {\n    &--active {\n      background-color: theme(\"colors.black\");\n    }\n  }\n\n  &__button {\n    display: flex;\n    width: 100%;\n    cursor: pointer;\n    align-items: center;\n    justify-content: center;\n    padding: 0.25rem;\n    color: theme(\"colors.white\");\n  }\n\n  &__icon-container {\n    display: flex;\n    height: 2.5rem;\n    width: 2.5rem;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n  }\n\n  &__icon {\n    font-size: 1rem;\n  }\n}\n\n.drawer {\n  position: fixed;\n  left: 0;\n  top: 0;\n  z-index: 999;\n  display: flex;\n  height: 100%;\n  width: 100%;\n  flex-direction: row;\n  align-items: flex-end;\n  justify-content: center;\n\n  &__overlay {\n    position: absolute;\n    left: 0;\n    top: 0;\n    height: 100%;\n    width: 100%;\n    flex: none;\n    background-color: theme(\"colors.gray.500\");\n    opacity: 0.75;\n    transition: opacity;\n  }\n\n  &__content {\n    position: relative;\n    display: flex;\n    height: 75%;\n    width: 100vw;\n    flex-direction: column;\n    align-items: stretch;\n    border-top-left-radius: 0.75rem;\n    border-top-right-radius: 0.75rem;\n    background-color: theme(\"colors.white\");\n    box-shadow: theme(\"boxShadow.xl\");\n  }\n\n  &__body {\n    flex: 1;\n    min-height: 0;\n    overflow-y: auto;\n  }\n\n  &__footer {\n    width: 100%;\n    flex: none;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/src/layouts/UserProfileBanner.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  Dialog,\n  IconAccountCircleLine,\n  IconArrowDownLine,\n  IconLogoutCircleRLine,\n  IconSettings3Line,\n  IconShieldUser,\n  VAvatar,\n  VDropdown,\n  VTag,\n} from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { storeToRefs } from \"pinia\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport { SUPER_ROLE_NAME } from \"@/constants/constants\";\n\nconst props = defineProps<{\n  platform?: \"console\" | \"uc\";\n}>();\n\nconst { t } = useI18n();\n\nconst { currentUser } = storeToRefs(stores.currentUser());\n\nconst handleLogout = () => {\n  Dialog.warning({\n    title: t(\"core.sidebar.operations.logout.title\"),\n    description: t(\"core.sidebar.operations.logout.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      window.location.href = \"/logout\";\n    },\n  });\n};\n\nconst disallowAccessConsole = computed(() => {\n  if (\n    currentUser.value?.roles.some(\n      (role) => role.metadata.name === SUPER_ROLE_NAME\n    )\n  ) {\n    return false;\n  }\n\n  const hasDisallowAccessConsoleRole = currentUser.value?.roles.some((role) => {\n    return (\n      role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===\n      \"true\"\n    );\n  });\n  return !!hasDisallowAccessConsoleRole;\n});\n\nconst actions = computed(() => {\n  const items = [\n    {\n      label: t(\"core.sidebar.operations.logout.tooltip\"),\n      icon: IconLogoutCircleRLine,\n      onClick: handleLogout,\n    },\n  ];\n\n  if (props.platform === \"console\") {\n    items.unshift({\n      label: t(\"core.sidebar.operations.profile.tooltip\"),\n      icon: IconAccountCircleLine,\n      onClick: () => {\n        window.location.href = \"/uc\";\n      },\n    });\n  }\n\n  if (props.platform === \"uc\" && !disallowAccessConsole.value) {\n    items.unshift({\n      label: t(\"core.uc_sidebar.operations.console.tooltip\"),\n      icon: IconSettings3Line,\n      onClick: () => {\n        window.location.href = \"/console\";\n      },\n    });\n  }\n\n  return items;\n});\n</script>\n<template>\n  <div class=\"user-profile\">\n    <div v-if=\"currentUser?.user.spec.avatar\" class=\"user-profile__avatar\">\n      <VAvatar\n        :key=\"currentUser?.user.spec.avatar\"\n        :src=\"currentUser?.user.spec.avatar\"\n        :alt=\"currentUser?.user.spec.displayName\"\n        size=\"sm\"\n        circle\n      ></VAvatar>\n    </div>\n    <div class=\"user-profile__info\">\n      <div\n        class=\"user-profile__name\"\n        :title=\"currentUser?.user.spec.displayName\"\n      >\n        {{ currentUser?.user.spec.displayName }}\n      </div>\n      <div v-if=\"currentUser?.roles.length\" class=\"user-profile__roles\">\n        <VTag v-if=\"currentUser.roles.length === 1\">\n          <template #leftIcon>\n            <IconShieldUser />\n          </template>\n          {{\n            currentUser.roles[0].metadata.annotations?.[\n              rbacAnnotations.DISPLAY_NAME\n            ] || currentUser.roles[0].metadata.name\n          }}\n        </VTag>\n        <VDropdown v-else :triggers=\"['click']\">\n          <div class=\"user-profile__roles-dropdown\">\n            <VTag>\n              <template #leftIcon>\n                <IconShieldUser />\n              </template>\n              {{ $t(\"core.sidebar.profile.aggregate_role\") }}\n            </VTag>\n            <IconArrowDownLine />\n          </div>\n          <template #popper>\n            <div class=\"user-profile__roles-popper\">\n              <h2 class=\"user-profile__roles-title\">\n                {{ $t(\"core.sidebar.profile.aggregate_role\") }}\n              </h2>\n              <div class=\"user-profile__roles-list\">\n                <VTag\n                  v-for=\"role in currentUser.roles\"\n                  :key=\"role.metadata.name\"\n                  class=\"user-profile__role-tag\"\n                >\n                  <template #leftIcon>\n                    <IconShieldUser />\n                  </template>\n                  {{\n                    role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||\n                    role.metadata.name\n                  }}\n                </VTag>\n              </div>\n            </div>\n          </template>\n        </VDropdown>\n      </div>\n    </div>\n\n    <div class=\"user-profile__actions\">\n      <button\n        v-for=\"action in actions\"\n        :key=\"action.label\"\n        v-tooltip=\"action.label\"\n        class=\"user-profile__action-button\"\n        @click=\"action.onClick\"\n      >\n        <component :is=\"action.icon\" class=\"user-profile__action-icon\" />\n      </button>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\">\n.user-profile {\n  height: 70px;\n  display: flex;\n  width: 100%;\n  gap: 0.75rem;\n  background-color: theme(\"colors.white\");\n  padding: 0.75rem;\n\n  &__avatar {\n    display: flex;\n    align-items: center;\n    align-self: center;\n  }\n\n  &__info {\n    flex: 1;\n    align-self: center;\n    overflow: hidden;\n  }\n\n  &__name {\n    display: flex;\n    font-size: 0.875rem;\n    font-weight: 500;\n  }\n\n  &__roles {\n    display: flex;\n    margin-top: 0.25rem;\n  }\n\n  &__roles-dropdown {\n    display: flex;\n    gap: 0.25rem;\n  }\n\n  &__roles-popper {\n    padding: 0.25rem;\n  }\n\n  &__roles-title {\n    color: theme(\"colors.gray.600\");\n    font-size: 0.875rem;\n    font-weight: 600;\n    border-bottom: 1px solid theme(\"colors.gray.100\");\n    padding-bottom: 0.375rem;\n  }\n\n  &__roles-list {\n    display: flex;\n    gap: 0.5rem;\n    flex-wrap: wrap;\n    margin-top: 0.5rem;\n  }\n\n  &__actions {\n    display: flex;\n    align-items: center;\n    gap: 0.25rem;\n  }\n\n  &__action-button {\n    display: inline-block;\n    cursor: pointer;\n    border-radius: 9999px;\n    padding: 0.375rem;\n    transition: all;\n\n    &:hover {\n      background-color: theme(\"colors.gray.100\");\n    }\n  }\n\n  &__action-icon {\n    height: 1.25rem;\n    width: 1.25rem;\n    color: theme(\"colors.gray.600\");\n\n    .user-profile__action-button:hover & {\n      color: theme(\"colors.gray.900\");\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/src/locales/en.json",
    "content": "{\n  \"core.sidebar.search.placeholder\": \"Search\",\n  \"core.sidebar.menu.groups.content\": \"Content\",\n  \"core.sidebar.menu.groups.interface\": \"Interface\",\n  \"core.sidebar.menu.groups.system\": \"System\",\n  \"core.sidebar.menu.groups.tool\": \"Tool\",\n  \"core.sidebar.menu.items.home\": \"Home\",\n  \"core.sidebar.menu.items.dashboard\": \"Dashboard\",\n  \"core.sidebar.menu.items.posts\": \"Posts\",\n  \"core.sidebar.menu.items.single_pages\": \"Pages\",\n  \"core.sidebar.menu.items.comments\": \"Comments\",\n  \"core.sidebar.menu.items.attachments\": \"Attachments\",\n  \"core.sidebar.menu.items.themes\": \"Themes\",\n  \"core.sidebar.menu.items.menus\": \"Menus\",\n  \"core.sidebar.menu.items.plugins\": \"Plugins\",\n  \"core.sidebar.menu.items.users\": \"Users\",\n  \"core.sidebar.menu.items.settings\": \"Settings\",\n  \"core.sidebar.menu.items.overview\": \"Overview\",\n  \"core.sidebar.menu.items.backup\": \"Backup\",\n  \"core.sidebar.menu.items.tools\": \"Tools\",\n  \"core.sidebar.operations.logout.tooltip\": \"Logout\",\n  \"core.sidebar.operations.logout.title\": \"Logout\",\n  \"core.sidebar.operations.logout.description\": \"Clicking Confirm will redirect to the logout page. Please ensure that the content you are editing is saved.\",\n  \"core.sidebar.operations.profile.tooltip\": \"Profile\",\n  \"core.sidebar.operations.visit_homepage.title\": \"Visit homepage\",\n  \"core.sidebar.profile.aggregate_role\": \"Aggregate Role\",\n  \"core.uc_sidebar.menu.items.profile\": \"Profile\",\n  \"core.uc_sidebar.menu.items.notification\": \"Notifications\",\n  \"core.uc_sidebar.menu.items.posts\": \"Posts\",\n  \"core.uc_sidebar.operations.console.tooltip\": \"Console\",\n  \"core.uc_sidebar.profile.aggregate_role\": \"Aggregate Role\",\n  \"core.dashboard.title\": \"Dashboard\",\n  \"core.dashboard.actions.setting\": \"Settings\",\n  \"core.dashboard.widgets.common_form.fields.enable_animation.label\": \"Enable animation\",\n  \"core.dashboard.widgets.groups.post\": \"Post\",\n  \"core.dashboard.widgets.groups.page\": \"Page\",\n  \"core.dashboard.widgets.groups.comment\": \"Comment\",\n  \"core.dashboard.widgets.groups.user\": \"User\",\n  \"core.dashboard.widgets.groups.other\": \"Other\",\n  \"core.dashboard.widgets.presets.post_stats.title\": \"Posts\",\n  \"core.dashboard.widgets.presets.page_stats.title\": \"Pages\",\n  \"core.dashboard.widgets.presets.recent_published.title\": \"Recent Posts\",\n  \"core.dashboard.widgets.presets.recent_published.visits\": \"{visits} Visits\",\n  \"core.dashboard.widgets.presets.recent_published.comments\": \"{comments} Comments\",\n  \"core.dashboard.widgets.presets.recent_published.empty.title\": \"No published posts\",\n  \"core.dashboard.widgets.presets.recent_published.comments-with-pending\": \" ({count} pending comments)\",\n  \"core.dashboard.widgets.presets.notification.title\": \"Notifications\",\n  \"core.dashboard.widgets.presets.notification.empty.title\": \"No unread notifications\",\n  \"core.dashboard.widgets.presets.quickaction.title\": \"Quick Action\",\n  \"core.dashboard.widgets.presets.quickaction.actions.user_center.title\": \"User Center\",\n  \"core.dashboard.widgets.presets.quickaction.actions.view_site.title\": \"View Site\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_post.title\": \"New Post\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_page.title\": \"New Page\",\n  \"core.dashboard.widgets.presets.quickaction.actions.upload_attachment.title\": \"Upload Attachment\",\n  \"core.dashboard.widgets.presets.quickaction.actions.theme_manage.title\": \"Manage Themes\",\n  \"core.dashboard.widgets.presets.quickaction.actions.plugin_manage.title\": \"Manage Plugins\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_user.title\": \"New User\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.title\": \"Refresh Search Engine\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_title\": \"Refresh the search engine index\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_content\": \"This operation will recreate search engine indexes for all published posts.\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.success_message\": \"Refresh search engine index successfully.\",\n  \"core.dashboard.widgets.presets.quickaction.config.fields.enabled_items.label\": \"Enabled Items\",\n  \"core.dashboard.widgets.presets.user_stats.title\": \"Users\",\n  \"core.dashboard.widgets.presets.comment_stats.title\": \"Comments\",\n  \"core.dashboard.widgets.presets.pending_comments.title\": \"Pending Comments\",\n  \"core.dashboard.widgets.presets.pending_comments.empty.title\": \"No pending comments\",\n  \"core.dashboard.widgets.presets.views_stats.title\": \"Visits\",\n  \"core.dashboard.widgets.presets.iframe.title\": \"Iframe\",\n  \"core.dashboard.widgets.presets.iframe.empty.title\": \"Please enter the URL in the configuration\",\n  \"core.dashboard.widgets.presets.iframe.config.fields.title.label\": \"Title\",\n  \"core.dashboard.widgets.presets.stack.title\": \"Widget Stack\",\n  \"core.dashboard.widgets.presets.stack.operations.add_widget.button\": \"Add Widget\",\n  \"core.dashboard.widgets.presets.stack.config_modal.title\": \"Widget Stack Config\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play.label\": \"Auto play\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play_interval.label\": \"Auto play interval\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.widgets.label\": \"Widgets\",\n  \"core.dashboard.widgets.presets.stack.config_modal.toast.nest_warning\": \"You cannot add a stack widget to a stack widget\",\n  \"core.dashboard_designer.title\": \"Edit Dashboard\",\n  \"core.dashboard_designer.actions.add_widget\": \"Add Widget\",\n  \"core.dashboard_designer.widgets_modal.title\": \"Widgets\",\n  \"core.dashboard_designer.config_modal.title\": \"Widget Config\",\n  \"core.dashboard_designer.breakpoints.lg\": \"Desktop\",\n  \"core.dashboard_designer.breakpoints.md\": \"Tablet (Horizontal)\",\n  \"core.dashboard_designer.breakpoints.sm\": \"Tablet\",\n  \"core.dashboard_designer.breakpoints.xs\": \"Mobile\",\n  \"core.dashboard_designer.operations.back.title\": \"Layout not saved\",\n  \"core.dashboard_designer.operations.back.description\": \"The current layout has not been saved, if you leave, the current layout will be lost, do you want to continue?\",\n  \"core.dashboard_designer.operations.back.confirm_text\": \"Leave\",\n  \"core.dashboard_designer.operations.change_breakpoint.tips_not_saved\": \"Please save the current layout first\",\n  \"core.dashboard_designer.operations.copy_from_layout.button\": \"Copy from layout\",\n  \"core.dashboard_designer.operations.open_widgets_hub.button\": \"Widgets Hub\",\n  \"core.post.title\": \"Posts\",\n  \"core.post.actions.categories\": \"Categories\",\n  \"core.post.actions.tags\": \"Tags\",\n  \"core.post.actions.recycle_bin\": \"Recycle Bin\",\n  \"core.post.empty.title\": \"There are no posts currently.\",\n  \"core.post.empty.message\": \"You can try to refresh or create a new post.\",\n  \"core.post.operations.delete.title\": \"Delete post\",\n  \"core.post.operations.delete.description\": \"This operation will move the post to the recycle bin, and it can be restored from the recycle bin later.\",\n  \"core.post.operations.delete_in_batch.title\": \"Delete the selected posts\",\n  \"core.post.operations.delete_in_batch.description\": \"This operation will move the posts to the recycle bin, and it can be restored from the recycle bin later.\",\n  \"core.post.operations.publish_in_batch.title\": \"Publish posts\",\n  \"core.post.operations.publish_in_batch.description\": \"Batch publish posts, the selected posts will be set to published status\",\n  \"core.post.operations.cancel_publish_in_batch.title\": \"Cancel publish posts\",\n  \"core.post.operations.cancel_publish_in_batch.description\": \"Batch cancel publish posts, the selected posts will be set to unpublished status\",\n  \"core.post.operations.batch_setting.button\": \"Batch settings\",\n  \"core.post.filters.status.items.published\": \"Published\",\n  \"core.post.filters.status.items.draft\": \"Draft\",\n  \"core.post.filters.status.items.scheduling\": \"Scheduling publish\",\n  \"core.post.filters.visible.label\": \"Visible\",\n  \"core.post.filters.visible.result\": \"Visible: {visible}\",\n  \"core.post.filters.visible.items.public\": \"Public\",\n  \"core.post.filters.visible.items.private\": \"Private\",\n  \"core.post.filters.category.label\": \"Category\",\n  \"core.post.filters.category.result\": \"Category: {category}\",\n  \"core.post.filters.tag.label\": \"Tag\",\n  \"core.post.filters.tag.result\": \"Tag: {tag}\",\n  \"core.post.filters.author.label\": \"Author\",\n  \"core.post.filters.author.result\": \"Author: {author}\",\n  \"core.post.filters.sort.items.publish_time_desc\": \"Latest Published\",\n  \"core.post.filters.sort.items.publish_time_asc\": \"Earliest Published\",\n  \"core.post.filters.sort.items.create_time_desc\": \"Latest Created\",\n  \"core.post.filters.sort.items.create_time_asc\": \"Earliest Created\",\n  \"core.post.filters.sort.items.visit_desc\": \"Most Visits\",\n  \"core.post.filters.sort.items.comment_desc\": \"Most Comments\",\n  \"core.post.filters.sort.items.last_modify_time_desc\": \"Recently Updated\",\n  \"core.post.filters.sort.items.last_modify_time_asc\": \"Earliest Updated\",\n  \"core.post.list.fields.categories\": \"Categories:\",\n  \"core.post.list.fields.visits\": \"{visits} Visits\",\n  \"core.post.list.fields.comments\": \"{comments} Comments\",\n  \"core.post.list.fields.pinned\": \"Pinned\",\n  \"core.post.list.fields.schedule_publish.tooltip\": \"Schedule publish\",\n  \"core.post.list.fields.comments-with-pending\": \" ({count} pending comments)\",\n  \"core.post.settings.title\": \"Settings\",\n  \"core.post.settings.groups.general\": \"General\",\n  \"core.post.settings.groups.advanced\": \"Advanced\",\n  \"core.post.settings.groups.annotations\": \"Annotations\",\n  \"core.post.settings.fields.title.label\": \"Title\",\n  \"core.post.settings.fields.slug.label\": \"Slug\",\n  \"core.post.settings.fields.slug.help\": \"Usually used for generating permalink to posts\",\n  \"core.post.settings.fields.slug.refresh_message\": \"Regenerate slug based on title.\",\n  \"core.post.settings.fields.categories.label\": \"Categories\",\n  \"core.post.settings.fields.tags.label\": \"Tags\",\n  \"core.post.settings.fields.auto_generate_excerpt.label\": \"Auto Generate Excerpt\",\n  \"core.post.settings.fields.raw_excerpt.label\": \"Excerpt\",\n  \"core.post.settings.fields.allow_comment.label\": \"Allow Comment\",\n  \"core.post.settings.fields.pinned.label\": \"Pinned\",\n  \"core.post.settings.fields.visible.label\": \"Visible\",\n  \"core.post.settings.fields.publish_time.label\": \"Publish Time\",\n  \"core.post.settings.fields.publish_time.help.schedule_publish\": \"Schedule a timed task and publish it at {datetime}\",\n  \"core.post.settings.fields.template.label\": \"Template\",\n  \"core.post.settings.fields.cover.label\": \"Cover\",\n  \"core.post.settings.fields.owner.label\": \"Owner\",\n  \"core.post.tag.filters.sort.items.create_time_desc\": \"Latest Created\",\n  \"core.post.tag.filters.sort.items.create_time_asc\": \"Earliest Created\",\n  \"core.post.tag.filters.sort.items.display_name_desc\": \"Descending order by tag name\",\n  \"core.post.tag.filters.sort.items.display_name_asc\": \"Ascending order by tag name\",\n  \"core.post.tag.filters.sort.items.post_desc\": \"Most Posts\",\n  \"core.post.batch_setting_modal.title\": \"Post batch settings\",\n  \"core.post.batch_setting_modal.fields.common.enabled\": \"Enabled\",\n  \"core.post.batch_setting_modal.fields.common.op.label\": \"Operate\",\n  \"core.post.batch_setting_modal.fields.common.op.options.add\": \"Add\",\n  \"core.post.batch_setting_modal.fields.common.op.options.replace\": \"Replace\",\n  \"core.post.batch_setting_modal.fields.common.op.options.remove_all\": \"Remove all\",\n  \"core.post.batch_setting_modal.fields.category_group\": \"Category\",\n  \"core.post.batch_setting_modal.fields.category_names\": \"Select categories\",\n  \"core.post.batch_setting_modal.fields.tag_group\": \"Tag\",\n  \"core.post.batch_setting_modal.fields.tag_names\": \"Select tags\",\n  \"core.post.batch_setting_modal.fields.visible_group\": \"Visible\",\n  \"core.post.batch_setting_modal.fields.visible_value\": \"Select visibility\",\n  \"core.post.batch_setting_modal.fields.allow_comment_group\": \"Allow comments\",\n  \"core.post.batch_setting_modal.fields.allow_comment_value\": \"Choose whether to allow comments\",\n  \"core.post.batch_setting_modal.fields.owner_group\": \"Owner\",\n  \"core.post.batch_setting_modal.fields.owner_value\": \"Select owner\",\n  \"core.deleted_post.title\": \"Deleted Posts\",\n  \"core.deleted_post.empty.title\": \"No posts have been put in the recycle bin.\",\n  \"core.deleted_post.empty.message\": \"You can try to refresh or go back to the previous page.\",\n  \"core.deleted_post.operations.delete.title\": \"Delete post\",\n  \"core.deleted_post.operations.delete.description\": \"After deletion, it will not be possible to recover.\",\n  \"core.deleted_post.operations.delete_in_batch.title\": \"Delete the selected posts\",\n  \"core.deleted_post.operations.delete_in_batch.description\": \"After deletion, it will not be possible to recover.\",\n  \"core.deleted_post.operations.recovery.title\": \"Restore post\",\n  \"core.deleted_post.operations.recovery.description\": \"This operation will restore the post to its state before deletion.\",\n  \"core.deleted_post.operations.recovery_in_batch.title\": \"Restore the selected posts\",\n  \"core.deleted_post.operations.recovery_in_batch.description\": \"This operation will restore the posts to their state before deletion.\",\n  \"core.post_editor.title\": \"Post Editor\",\n  \"core.post_editor.untitled\": \"Untitled post\",\n  \"core.post_editor.actions.snapshots\": \"Snapshots\",\n  \"core.post_tag.title\": \"Post tags\",\n  \"core.post_tag.header.title\": \"{count} Tags\",\n  \"core.post_tag.empty.title\": \"There are no tags currently.\",\n  \"core.post_tag.empty.message\": \"You can try to refresh or create a new tag.\",\n  \"core.post_tag.operations.delete.title\": \"Delete tag\",\n  \"core.post_tag.operations.delete.description\": \"After deleting this tag, the association with the corresponding article will be removed. This operation cannot be undone.\",\n  \"core.post_tag.operations.delete_in_batch.title\": \"Delete the selected tags\",\n  \"core.post_tag.editing_modal.titles.update\": \"Update post tag\",\n  \"core.post_tag.editing_modal.titles.create\": \"Create post tag\",\n  \"core.post_tag.editing_modal.groups.general\": \"General\",\n  \"core.post_tag.editing_modal.groups.annotations\": \"Annotations\",\n  \"core.post_tag.editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.post_tag.editing_modal.fields.slug.label\": \"Slug\",\n  \"core.post_tag.editing_modal.fields.slug.help\": \"Often used to generate permalink for tags\",\n  \"core.post_tag.editing_modal.fields.slug.refresh_message\": \"Regenerate slug based on display name.\",\n  \"core.post_tag.editing_modal.fields.color.label\": \"Color\",\n  \"core.post_tag.editing_modal.fields.color.help\": \"Theme adaptation is required to support\",\n  \"core.post_tag.editing_modal.fields.cover.label\": \"Cover\",\n  \"core.post_tag.editing_modal.fields.cover.help\": \"Theme adaptation is required to support\",\n  \"core.post_tag.editing_modal.fields.description.label\": \"Description\",\n  \"core.post_tag.editing_modal.fields.description.help\": \"The description will be automatically added to the page's meta description tag for SEO; other display purposes require theme adaptation\",\n  \"core.post_category.title\": \"Post categories\",\n  \"core.post_category.header.title\": \"{count} Categories\",\n  \"core.post_category.empty.title\": \"There are no categories currently.\",\n  \"core.post_category.empty.message\": \"You can try to refresh or create a new category.\",\n  \"core.post_category.operations.delete.title\": \"Delete category\",\n  \"core.post_category.operations.delete.description\": \"After deleting this category, the association with corresponding articles will be removed. This operation cannot be undone.\",\n  \"core.post_category.operations.add_sub_category.button\": \"Add sub category\",\n  \"core.post_category.editing_modal.titles.update\": \"Update post category\",\n  \"core.post_category.editing_modal.titles.create\": \"Create post category\",\n  \"core.post_category.editing_modal.groups.general\": \"General\",\n  \"core.post_category.editing_modal.groups.annotations\": \"Annotations\",\n  \"core.post_category.editing_modal.fields.parent.label\": \"Parent\",\n  \"core.post_category.editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.post_category.editing_modal.fields.slug.label\": \"Slug\",\n  \"core.post_category.editing_modal.fields.slug.help\": \"Often used to generate permalink for categories\",\n  \"core.post_category.editing_modal.fields.slug.refresh_message\": \"Regenerate slug based on display name.\",\n  \"core.post_category.editing_modal.fields.template.label\": \"Custom template\",\n  \"core.post_category.editing_modal.fields.template.help\": \"Customize the rendering template of the category archive page, which requires support from the theme\",\n  \"core.post_category.editing_modal.fields.cover.label\": \"Cover\",\n  \"core.post_category.editing_modal.fields.cover.help\": \"Theme adaptation is required to support\",\n  \"core.post_category.editing_modal.fields.description.label\": \"Description\",\n  \"core.post_category.editing_modal.fields.description.help\": \"The description will be automatically added to the page's meta description tag for SEO; other display purposes require theme adaptation\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.label\": \"Prevent Parent Post Cascade Query\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.help\": \"Prevent parent category from including this category and its subcategories in cascade post queries\",\n  \"core.post_category.editing_modal.fields.post_template.label\": \"Custom post template\",\n  \"core.post_category.editing_modal.fields.post_template.help\": \"Customize the rendering template of posts in the current category, which requires support from the theme\",\n  \"core.post_category.editing_modal.fields.hide_from_list.label\": \"Hide from list\",\n  \"core.post_category.editing_modal.fields.hide_from_list.help\": \"After turning on this option, this category and its subcategories, as well as its posts, will not be displayed in the front-end list. You need to actively visit the category archive page. This feature is only effective for the first-level directory.\",\n  \"core.post_category.list.fields.prevent_parent_post_cascade_query\": \"Prevent parent category from including this category and its subcategories in cascade post queries\",\n  \"core.post_category.list.fields.hide_from_list\": \"This category is hidden. This category and its subcategories, as well as its posts, will not be displayed in the front-end list. You need to visit the category archive page directly.\",\n  \"core.page.title\": \"Pages\",\n  \"core.page.actions.recycle_bin\": \"Recycle Bin\",\n  \"core.page.empty.title\": \"There are no pages currently.\",\n  \"core.page.empty.message\": \"You can try to refresh or create a new page.\",\n  \"core.page.operations.delete.title\": \"Delete page\",\n  \"core.page.operations.delete.description\": \"This operation will move the page to the recycle bin, and it can be restored from the recycle bin later.\",\n  \"core.page.operations.delete_in_batch.title\": \"Delete the selected pages\",\n  \"core.page.operations.delete_in_batch.description\": \"This operation will move the pages to the recycle bin, and it can be restored from the recycle bin later.\",\n  \"core.page.filters.status.items.published\": \"Published\",\n  \"core.page.filters.status.items.draft\": \"Draft\",\n  \"core.page.filters.visible.label\": \"Visible\",\n  \"core.page.filters.visible.result\": \"Visible: {visible}\",\n  \"core.page.filters.visible.items.public\": \"Public\",\n  \"core.page.filters.visible.items.private\": \"Private\",\n  \"core.page.filters.author.label\": \"Author\",\n  \"core.page.filters.author.result\": \"Author: {author}\",\n  \"core.page.filters.sort.items.publish_time_desc\": \"Latest Published\",\n  \"core.page.filters.sort.items.publish_time_asc\": \"Earliest Published\",\n  \"core.page.filters.sort.items.create_time_desc\": \"Latest Created\",\n  \"core.page.filters.sort.items.create_time_asc\": \"Earliest Created\",\n  \"core.page.list.fields.visits\": \"{visits} Visits\",\n  \"core.page.list.fields.comments\": \"{comments} Comments\",\n  \"core.page.list.fields.comments-with-pending\": \" ({count} pending comments)\",\n  \"core.page.settings.title\": \"Settings\",\n  \"core.page.settings.groups.general\": \"General\",\n  \"core.page.settings.groups.advanced\": \"Advanced\",\n  \"core.page.settings.groups.annotations\": \"Annotations\",\n  \"core.page.settings.fields.title.label\": \"Title\",\n  \"core.page.settings.fields.slug.label\": \"Slug\",\n  \"core.page.settings.fields.slug.help\": \"Usually used for generating permalink to pages\",\n  \"core.page.settings.fields.slug.refresh_message\": \"Regenerate slug based on title.\",\n  \"core.page.settings.fields.auto_generate_excerpt.label\": \"Auto Generate Excerpt\",\n  \"core.page.settings.fields.raw_excerpt.label\": \"Excerpt\",\n  \"core.page.settings.fields.allow_comment.label\": \"Allow Comment\",\n  \"core.page.settings.fields.pinned.label\": \"Pinned\",\n  \"core.page.settings.fields.visible.label\": \"Visible\",\n  \"core.page.settings.fields.publish_time.label\": \"Publish Time\",\n  \"core.page.settings.fields.template.label\": \"Template\",\n  \"core.page.settings.fields.cover.label\": \"Cover\",\n  \"core.deleted_page.title\": \"Deleted Pages\",\n  \"core.deleted_page.empty.title\": \"No pages have been put in the recycle bin.\",\n  \"core.deleted_page.empty.message\": \"You can try to refresh or go back to the previous page.\",\n  \"core.deleted_page.operations.delete.title\": \"Delete page\",\n  \"core.deleted_page.operations.delete.description\": \"After deletion, it will not be possible to recover.\",\n  \"core.deleted_page.operations.delete_in_batch.title\": \"Delete the selected pages\",\n  \"core.deleted_page.operations.delete_in_batch.description\": \"After deletion, it will not be possible to recover.\",\n  \"core.deleted_page.operations.recovery.title\": \"Restore page\",\n  \"core.deleted_page.operations.recovery.description\": \"This operation will restore the page to its state before deletion.\",\n  \"core.deleted_page.operations.recovery_in_batch.title\": \"Restore the selected pages\",\n  \"core.deleted_page.operations.recovery_in_batch.description\": \"This operation will restore the pages to its state before deletion.\",\n  \"core.page_editor.title\": \"Page Editor\",\n  \"core.page_editor.untitled\": \"Untitled page\",\n  \"core.page_editor.actions.snapshots\": \"Snapshots\",\n  \"core.comment.title\": \"Comments\",\n  \"core.comment.empty.title\": \"There are no comments currently.\",\n  \"core.comment.empty.message\": \"You can try to refresh or modify the filtering criteria.\",\n  \"core.comment.reply_empty.title\": \"There are no replies currently.\",\n  \"core.comment.reply_empty.message\": \"You can try refreshing or creating a new reply.\",\n  \"core.comment.reply_empty.new\": \"New Reply\",\n  \"core.comment.text.commented_on\": \"commented on\",\n  \"core.comment.text.replied_below\": \"replied:\",\n  \"core.comment.operations.delete_comment.title\": \"Delete comment\",\n  \"core.comment.operations.delete_comment.description\": \"All replies under the comments will be deleted at the same time, and this operation cannot be undone.\",\n  \"core.comment.operations.delete_comment_in_batch.title\": \"Delete the selected comments\",\n  \"core.comment.operations.delete_comment_in_batch.description\": \"All replies under the comments will be deleted at the same time, and this operation cannot be undone.\",\n  \"core.comment.operations.approve_comment_in_batch.button\": \"Approve\",\n  \"core.comment.operations.approve_comment_in_batch.title\": \"Are you sure you want to approve the selected comments for review?\",\n  \"core.comment.operations.approve_applies_in_batch.button\": \"Approve all replies\",\n  \"core.comment.operations.approve_applies_in_batch.title\": \"Are you sure you want to approve all replies to this comment for review?\",\n  \"core.comment.operations.delete_reply.title\": \"Delete reply\",\n  \"core.comment.operations.review.button\": \"Review\",\n  \"core.comment.operations.reply.button\": \"Reply\",\n  \"core.comment.operations.approve.button\": \"Approve\",\n  \"core.comment.operations.reply_and_approve.button\": \"Reply and approve\",\n  \"core.comment.operations.cancel_approve.button\": \"Cancel approve\",\n  \"core.comment.filters.status.items.approved\": \"Approved\",\n  \"core.comment.filters.status.items.pending_review\": \"Pending Review\",\n  \"core.comment.filters.owner.label\": \"Owner\",\n  \"core.comment.filters.owner.result\": \"Owner: {owner}\",\n  \"core.comment.filters.sort.items.last_reply_time_desc\": \"Recent reply\",\n  \"core.comment.filters.sort.items.last_reply_time_asc\": \"Earlier reply\",\n  \"core.comment.filters.sort.items.reply_count_desc\": \"More replies\",\n  \"core.comment.filters.sort.items.reply_count_asc\": \"Fewer replies\",\n  \"core.comment.filters.sort.items.create_time_desc\": \"Latest Created\",\n  \"core.comment.filters.sort.items.create_time_asc\": \"Earliest Created\",\n  \"core.comment.list.fields.reply_count\": \"{count} Replies\",\n  \"core.comment.list.fields.has_new_replies\": \"New replies\",\n  \"core.comment.list.fields.pending_review\": \"Pending review\",\n  \"core.comment.list.fields.private\": \"Private\",\n  \"core.comment.subject_refs.post\": \"Post\",\n  \"core.comment.subject_refs.page\": \"Page\",\n  \"core.comment.subject_refs.unknown\": \"Unknown\",\n  \"core.comment.reply_modal.title\": \"Reply\",\n  \"core.comment.reply_modal.fields.content.label\": \"Content\",\n  \"core.comment.reply_modal.operations.submit.toast_success\": \"Reply successfully\",\n  \"core.comment.detail_modal.fields.owner\": \"Commentator\",\n  \"core.comment.detail_modal.fields.creation_time\": \"Comment time\",\n  \"core.comment.detail_modal.fields.commented_on\": \"Commented on\",\n  \"core.comment.detail_modal.fields.new_reply\": \"New reply\",\n  \"core.comment.detail_modal.fields.user_agent\": \"User agent\",\n  \"core.comment.comment_detail_modal.title\": \"Comment details\",\n  \"core.comment.comment_detail_modal.fields.content\": \"Comment content\",\n  \"core.comment.reply_detail_modal.title\": \"Reply details\",\n  \"core.comment.reply_detail_modal.fields.original_comment\": \"Original comment\",\n  \"core.comment.reply_detail_modal.fields.content\": \"Reply content\",\n  \"core.attachment.title\": \"Attachments\",\n  \"core.attachment.common.text.ungrouped\": \"Ungrouped\",\n  \"core.attachment.common.text.original\": \"Original\",\n  \"core.attachment.actions.storage_policies\": \"Storage Policies\",\n  \"core.attachment.empty.title\": \"There are no attachments in the current group.\",\n  \"core.attachment.empty.message\": \"The current group has no attachments, you can try refreshing or uploading attachments.\",\n  \"core.attachment.empty.actions.upload\": \"Upload Attachment\",\n  \"core.attachment.operations.delete.title\": \"Delete attachment\",\n  \"core.attachment.operations.delete_in_batch.title\": \"Delete the selected attachments\",\n  \"core.attachment.operations.deselect_items.button\": \"Deselect items\",\n  \"core.attachment.operations.move.button\": \"Move\",\n  \"core.attachment.operations.move.toast_success\": \"Move successful\",\n  \"core.attachment.filters.storage_policy.label\": \"Storage Policy\",\n  \"core.attachment.filters.storage_policy.result\": \"Storage policy: {storage_policy}\",\n  \"core.attachment.filters.owner.label\": \"Owner\",\n  \"core.attachment.filters.owner.result\": \"Owner: {owner}\",\n  \"core.attachment.filters.sort.items.create_time_desc\": \"Latest uploaded\",\n  \"core.attachment.filters.sort.items.create_time_asc\": \"Earliest uploaded\",\n  \"core.attachment.filters.sort.items.size_desc\": \"Descending order by file size\",\n  \"core.attachment.filters.sort.items.size_asc\": \"Ascending order by file size\",\n  \"core.attachment.filters.sort.items.display_name_asc\": \"Ascending order by display name\",\n  \"core.attachment.filters.sort.items.display_name_desc\": \"Descending order by display name\",\n  \"core.attachment.filters.view_type.items.grid\": \"Grid Mode\",\n  \"core.attachment.filters.view_type.items.list\": \"List Mode\",\n  \"core.attachment.filters.accept.label\": \"Type\",\n  \"core.attachment.filters.accept.items.image\": \"Image\",\n  \"core.attachment.filters.accept.items.audio\": \"Audio\",\n  \"core.attachment.filters.accept.items.video\": \"Video\",\n  \"core.attachment.filters.accept.items.file\": \"File\",\n  \"core.attachment.detail_modal.title\": \"Attachment: {display_name}\",\n  \"core.attachment.detail_modal.fields.preview\": \"Preview\",\n  \"core.attachment.detail_modal.fields.storage_policy\": \"Storage policy\",\n  \"core.attachment.detail_modal.fields.group\": \"Group\",\n  \"core.attachment.detail_modal.fields.display_name\": \"Display name\",\n  \"core.attachment.detail_modal.fields.media_type\": \"Media type\",\n  \"core.attachment.detail_modal.fields.size\": \"Size\",\n  \"core.attachment.detail_modal.fields.owner\": \"Owner\",\n  \"core.attachment.detail_modal.fields.creation_time\": \"Creation time\",\n  \"core.attachment.detail_modal.fields.permalink\": \"Permalink\",\n  \"core.attachment.detail_modal.display_name_form.help\": \"Custom attachment name, only for display in the management interface, does not affect the file name and access link itself\",\n  \"core.attachment.detail_modal.preview.video_not_support\": \"The current browser does not support video playback.\",\n  \"core.attachment.detail_modal.preview.audio_not_support\": \"The current browser does not support audio playback.\",\n  \"core.attachment.detail_modal.preview.not_support\": \"This file does not support preview.\",\n  \"core.attachment.permalink_list.relative\": \"Relative path\",\n  \"core.attachment.permalink_list.absolute\": \"Absolute path\",\n  \"core.attachment.group_editing_modal.titles.create\": \"Create attachment group\",\n  \"core.attachment.group_editing_modal.titles.update\": \"Update attachment group\",\n  \"core.attachment.group_editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.attachment.group_editing_modal.toast.group_name_exists\": \"Group name already exists\",\n  \"core.attachment.group_list.internal_groups.all\": \"All\",\n  \"core.attachment.group_list.operations.rename.button\": \"Rename\",\n  \"core.attachment.group_list.operations.delete.button\": \"Delete and move attachments to ungrouped\",\n  \"core.attachment.group_list.operations.delete.title\": \"Delete attachment group\",\n  \"core.attachment.group_list.operations.delete.description\": \"The group will be deleted, and the attachments under the group will be moved to ungrouped. This operation cannot be undone.\",\n  \"core.attachment.group_list.operations.delete.toast_success\": \"Deletion successful, {total} attachments have been moved to ungrouped\",\n  \"core.attachment.group_list.operations.delete_with_attachments.button\": \"Also delete attachments\",\n  \"core.attachment.group_list.operations.delete_with_attachments.title\": \"Delete attachment group\",\n  \"core.attachment.group_list.operations.delete_with_attachments.description\": \"Deleting the group and all attachments within it, this action cannot be undone.\",\n  \"core.attachment.group_list.operations.delete_with_attachments.toast_success\": \"Deletion successful, {total} attachments have been deleted simultaneously\",\n  \"core.attachment.policies_modal.title\": \"Storage Policies\",\n  \"core.attachment.policies_modal.empty.title\": \"There is currently no available storage strategy.\",\n  \"core.attachment.policies_modal.empty.message\": \"There are no available storage policies at the moment. You can try refreshing or creating a new policy.\",\n  \"core.attachment.policies_modal.operations.delete.title\": \"Delete attachment policy\",\n  \"core.attachment.policies_modal.operations.delete.description\": \"There are no uploaded attachments under the current policy.\",\n  \"core.attachment.policies_modal.operations.can_not_delete.title\": \"Deletion failed\",\n  \"core.attachment.policies_modal.operations.can_not_delete.description\": \"There are attachments under this policy, which cannot be deleted.\",\n  \"core.attachment.policy_editing_modal.titles.create\": \"New policy: {policy_template}\",\n  \"core.attachment.policy_editing_modal.titles.update\": \"Edit policy: {policy}\",\n  \"core.attachment.policy_editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.attachment.policy_editing_modal.fields.config.label\": \"Policy configuration\",\n  \"core.attachment.policy_editing_modal.fields.hidden.label\": \"Hide on upload screen\",\n  \"core.attachment.policy_editing_modal.fields.hidden.help\": \"If enabled, this storage policy will not be visible on the upload screen\",\n  \"core.attachment.policy_editing_modal.fields.priority.label\": \"Display priority on upload screen\",\n  \"core.attachment.policy_editing_modal.fields.priority.help\": \"Set the display priority on the upload screen. The higher the number, the closer it appears to the top of the list\",\n  \"core.attachment.policy_editing_modal.toast.policy_name_exists\": \"Storage policy name already exists\",\n  \"core.attachment.upload_modal.title\": \"Upload attachment\",\n  \"core.attachment.upload_modal.filters.group.label\": \"Group:\",\n  \"core.attachment.upload_modal.filters.policy.label\": \"Storage policy:\",\n  \"core.attachment.upload_modal.filters.policy.empty.title\": \"No storage policy\",\n  \"core.attachment.upload_modal.filters.policy.empty.description\": \"Before uploading, a new storage policy needs to be created.\",\n  \"core.attachment.upload_modal.filters.policy.not_select\": \"Please select a storage policy first.\",\n  \"core.attachment.upload_modal.upload_options.local_upload\": \"Upload\",\n  \"core.attachment.upload_modal.upload_options.download\": \"Download from URL\",\n  \"core.attachment.upload_modal.download_form.fields.url.label\": \"URL\",\n  \"core.attachment.upload_modal.download_form.toast.success\": \"Downloaded successfully\",\n  \"core.attachment.select_modal.title\": \"Select attachment\",\n  \"core.attachment.select_modal.providers.default.label\": \"Attachments\",\n  \"core.attachment.select_modal.operations.select.result\": \"({count} items selected)\",\n  \"core.uc_attachment.empty.title\": \"There are no attachments.\",\n  \"core.uc_attachment.empty.message\": \"There are no attachments, you can try refreshing or uploading attachments.\",\n  \"core.uc_attachment.empty.actions.upload\": \"Upload Attachment\",\n  \"core.uc_attachment.filters.sort.items.create_time_desc\": \"Latest uploaded\",\n  \"core.uc_attachment.filters.sort.items.create_time_asc\": \"Earliest uploaded\",\n  \"core.uc_attachment.filters.sort.items.size_desc\": \"Descending order by file size\",\n  \"core.uc_attachment.filters.sort.items.size_asc\": \"Ascending order by file size\",\n  \"core.uc_attachment.filters.sort.items.display_name_asc\": \"Ascending order by display name\",\n  \"core.uc_attachment.filters.sort.items.display_name_desc\": \"Descending order by display name\",\n  \"core.uc_attachment.filters.view_type.items.grid\": \"Grid Mode\",\n  \"core.uc_attachment.filters.view_type.items.list\": \"List Mode\",\n  \"core.uc_attachment.detail_modal.title\": \"Attachment: {display_name}\",\n  \"core.uc_attachment.detail_modal.fields.preview\": \"Preview\",\n  \"core.uc_attachment.detail_modal.fields.display_name\": \"Display name\",\n  \"core.uc_attachment.detail_modal.fields.media_type\": \"Media type\",\n  \"core.uc_attachment.detail_modal.fields.size\": \"Size\",\n  \"core.uc_attachment.detail_modal.fields.owner\": \"Owner\",\n  \"core.uc_attachment.detail_modal.fields.creation_time\": \"Creation time\",\n  \"core.uc_attachment.detail_modal.fields.permalink\": \"Permalink\",\n  \"core.uc_attachment.detail_modal.preview.click_to_exit\": \"Click to exit preview\",\n  \"core.uc_attachment.detail_modal.preview.video_not_support\": \"The current browser does not support video playback.\",\n  \"core.uc_attachment.detail_modal.preview.audio_not_support\": \"The current browser does not support audio playback.\",\n  \"core.uc_attachment.detail_modal.preview.not_support\": \"This file does not support preview.\",\n  \"core.uc_attachment.upload_modal.title\": \"Upload attachment\",\n  \"core.uc_attachment.select_modal.title\": \"Select attachment\",\n  \"core.uc_attachment.select_modal.providers.default.label\": \"Attachments\",\n  \"core.uc_attachment.select_modal.operations.select.result\": \"({count} items selected)\",\n  \"core.theme.title\": \"Themes\",\n  \"core.theme.common.buttons.install\": \"Install Theme\",\n  \"core.theme.tabs.detail\": \"Detail\",\n  \"core.theme.actions.management\": \"Themes\",\n  \"core.theme.empty.title\": \"There are currently no activated or selected themes.\",\n  \"core.theme.empty.message\": \"There are currently no activated or selected themes, you can switch themes or install new ones.\",\n  \"core.theme.empty.actions.switch\": \"Switch Theme\",\n  \"core.theme.operations.active.title\": \"Active theme\",\n  \"core.theme.operations.active.toast_success\": \"Active theme successful\",\n  \"core.theme.operations.reset.title\": \"Reset configurations of the theme\",\n  \"core.theme.operations.reset.description\": \"This operation will delete the saved configuration and reset it to default settings.\",\n  \"core.theme.operations.reset.toast_success\": \"Reset configuration successful\",\n  \"core.theme.operations.reload.button\": \"Reload\",\n  \"core.theme.operations.reload.title\": \"Reload configurations of the theme\",\n  \"core.theme.operations.reload.description\": \"This operation will only reload the theme configuration and settings form definition, and will not delete any saved configurations.\",\n  \"core.theme.operations.reload.toast_success\": \"Reload configuration successful\",\n  \"core.theme.operations.uninstall.title\": \"Uninstall theme\",\n  \"core.theme.operations.uninstall_and_delete_config.button\": \"Uninstall and delete config\",\n  \"core.theme.operations.uninstall_and_delete_config.title\": \"Uninstall this theme and its corresponding settings\",\n  \"core.theme.operations.remote_download.title\": \"Remote download address detected, do you want to download?\",\n  \"core.theme.operations.remote_download.description\": \"Please carefully verify whether this address can be trusted: {url}\",\n  \"core.theme.operations.existed_during_installation.title\": \"The theme already exists.\",\n  \"core.theme.operations.existed_during_installation.description\": \"The currently installed theme already exists, do you want to upgrade?\",\n  \"core.theme.operations.clear_templates_cache.button\": \"Clear templates cache\",\n  \"core.theme.operations.clear_templates_cache.title\": \"Clear templates cache\",\n  \"core.theme.operations.clear_templates_cache.description\": \"This feature allows you to refresh the cache to view the latest web results after modifying template files at runtime.\",\n  \"core.theme.operations.export_configuration.button\": \"Export theme configuration\",\n  \"core.theme.operations.import_configuration.button\": \"Import theme configuration\",\n  \"core.theme.operations.import_configuration.version_mismatch.title\": \"Version mismatch\",\n  \"core.theme.operations.import_configuration.version_mismatch.description\": \"The imported configuration file version does not match the current theme version, which may lead to compatibility issues. Do you want to continue importing?\",\n  \"core.theme.operations.import_configuration.invalid_format\": \"Invalid theme configuration file\",\n  \"core.theme.operations.import_configuration.mismatched_theme\": \"Configuration file does not match the selected theme\",\n  \"core.theme.list_modal.tabs.installed\": \"Installed\",\n  \"core.theme.list_modal.tabs.not_installed\": \"Not installed\",\n  \"core.theme.list_modal.tabs.local_upload\": \"Local install / upgrade\",\n  \"core.theme.list_modal.tabs.remote_download.label\": \"Remote\",\n  \"core.theme.list_modal.tabs.remote_download.fields.url\": \"Remote URL\",\n  \"core.theme.list_modal.empty.title\": \"There are no installed themes currently.\",\n  \"core.theme.list_modal.empty.message\": \"There are currently no installed themes, you can try refreshing or installing a new theme.\",\n  \"core.theme.list_modal.not_installed_empty.title\": \"There are currently no uninstalled themes.\",\n  \"core.theme.preview_model.title\": \"Preview theme: {display_name}\",\n  \"core.theme.preview_model.actions.switch\": \"Switch theme\",\n  \"core.theme.preview_model.actions.setting\": \"Settings\",\n  \"core.theme.preview_model.actions.open\": \"Open\",\n  \"core.theme.detail.fields.author\": \"Author\",\n  \"core.theme.detail.fields.homepage\": \"Website\",\n  \"core.theme.detail.fields.repo\": \"Source Repository\",\n  \"core.theme.detail.fields.version\": \"Version\",\n  \"core.theme.detail.fields.requires\": \"Requires\",\n  \"core.theme.detail.fields.storage_location\": \"Storage Location\",\n  \"core.theme.detail.fields.plugin_requires\": \"Plugin Requires\",\n  \"core.theme.detail.fields.description\": \"Description\",\n  \"core.theme.detail.fields.license\": \"License\",\n  \"core.theme.detail.fields.issues\": \"Issues feedback\",\n  \"core.theme.settings.title\": \"Theme settings\",\n  \"core.theme.custom_templates.default\": \"Default\",\n  \"core.menu.title\": \"Menus\",\n  \"core.menu.empty.title\": \"There is no menu currently.\",\n  \"core.menu.empty.message\": \"You can try to refresh or create a new menu.\",\n  \"core.menu.menu_item_empty.title\": \"There are no menu items currently.\",\n  \"core.menu.menu_item_empty.message\": \"You can try to refresh or create a new menu item.\",\n  \"core.menu.operations.set_primary.button\": \"Set as primary menu\",\n  \"core.menu.operations.set_primary.toast_success\": \"Setting successful\",\n  \"core.menu.operations.delete_menu.title\": \"Delete menu\",\n  \"core.menu.operations.delete_menu.description\": \"All menu items under this menu will be deleted at the same time, and this operation cannot be undone.\",\n  \"core.menu.operations.delete_menu_item.title\": \"Delete menu item\",\n  \"core.menu.operations.delete_menu_item.description\": \"All sub-menu items will be deleted at the same time, and cannot be restored after deletion.\",\n  \"core.menu.operations.add_sub_menu_item.button\": \"Add sub menu item\",\n  \"core.menu.list.fields.primary\": \"Primary\",\n  \"core.menu.list.fields.items_count\": \"{count} items\",\n  \"core.menu.menu_editing_modal.titles.create\": \"Create menu\",\n  \"core.menu.menu_editing_modal.titles.update\": \"Update menu\",\n  \"core.menu.menu_editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.menu.menu_item_editing_modal.titles.create\": \"Create menu item\",\n  \"core.menu.menu_item_editing_modal.titles.update\": \"Update menu item\",\n  \"core.menu.menu_item_editing_modal.groups.general\": \"General\",\n  \"core.menu.menu_item_editing_modal.groups.annotations\": \"Annotations\",\n  \"core.menu.menu_item_editing_modal.fields.parent.label\": \"Parent\",\n  \"core.menu.menu_item_editing_modal.fields.parent.placeholder\": \"Select the parent menu item\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.label\": \"Type\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.placeholder\": \"Please select {label}\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.custom\": \"Custom\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.post\": \"Post\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page\": \"Page\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.category\": \"Category\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.tag\": \"Tag\",\n  \"core.menu.menu_item_editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.menu.menu_item_editing_modal.fields.href.label\": \"Link address\",\n  \"core.menu.menu_item_editing_modal.fields.target.label\": \"Target\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.self\": \"_self\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.blank\": \"_blank\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.parent\": \"_parent\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.top\": \"_top\",\n  \"core.plugin.title\": \"Plugins\",\n  \"core.plugin.tabs.detail\": \"Detail\",\n  \"core.plugin.list.fields.system_reserved\": \"System Reserved\",\n  \"core.plugin.empty.title\": \"There are no installed plugins currently.\",\n  \"core.plugin.empty.message\": \"There are no installed plugins currently, you can try refreshing or installing new plugins.\",\n  \"core.plugin.empty.actions.install\": \"Install Plugin\",\n  \"core.plugin.operations.reset.title\": \"Reset configurations of the plugin\",\n  \"core.plugin.operations.reset.description\": \"This operation will delete the saved configuration and reset it to default settings.\",\n  \"core.plugin.operations.reset.toast_success\": \"Reset configuration successfully\",\n  \"core.plugin.operations.uninstall.title\": \"Uninstall plugin\",\n  \"core.plugin.operations.uninstall_in_batch.title\": \"Uninstall the selected plugins\",\n  \"core.plugin.operations.uninstall_and_delete_config.button\": \"Uninstall and delete config\",\n  \"core.plugin.operations.uninstall_and_delete_config.title\": \"Uninstall plugin and its corresponding configuration\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.button\": \"Uninstall and delete config\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.title\": \"Uninstall the selected plugins and their corresponding configurations\",\n  \"core.plugin.operations.uninstall_when_enabled.confirm_text\": \"Stop running and uninstall\",\n  \"core.plugin.operations.uninstall_when_enabled.description\": \"The current plugin is still in the enabled state and will be uninstalled after it stops running. This operation cannot be undone.\",\n  \"core.plugin.operations.change_status_in_batch.activate_title\": \"Activate the selected plugins\",\n  \"core.plugin.operations.change_status_in_batch.inactivate_title\": \"Inactivate the selected plugins\",\n  \"core.plugin.operations.remote_download.title\": \"Remote download address detected, do you want to download?\",\n  \"core.plugin.operations.remote_download.description\": \"Please carefully verify whether this address can be trusted: {url}\",\n  \"core.plugin.operations.reload_window.button\": \"Reload required\",\n  \"core.plugin.filters.status.items.active\": \"Active\",\n  \"core.plugin.filters.status.items.inactive\": \"Inactive\",\n  \"core.plugin.filters.sort.items.create_time_desc\": \"Latest Installed\",\n  \"core.plugin.filters.sort.items.create_time_asc\": \"Earliest Installed\",\n  \"core.plugin.upload_modal.titles.install\": \"Install plugin\",\n  \"core.plugin.upload_modal.titles.upgrade\": \"Upgrade plugin ({display_name})\",\n  \"core.plugin.upload_modal.tabs.local\": \"Local\",\n  \"core.plugin.upload_modal.tabs.remote.title\": \"Remote\",\n  \"core.plugin.upload_modal.tabs.remote.fields.url\": \"Remote URL\",\n  \"core.plugin.upload_modal.operations.active_after_install.title\": \"Install successful\",\n  \"core.plugin.upload_modal.operations.active_after_install.description\": \"Would you like to activate the currently installed plugin?\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.title\": \"The plugin already exists.\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.description\": \"The currently installed plugin already exists, do you want to upgrade?\",\n  \"core.plugin.detail.title\": \"Plugin detail\",\n  \"core.plugin.detail.header.title\": \"Plugin information\",\n  \"core.plugin.detail.fields.description\": \"Description\",\n  \"core.plugin.detail.fields.version\": \"Version\",\n  \"core.plugin.detail.fields.requires\": \"Requires\",\n  \"core.plugin.detail.fields.author\": \"Author\",\n  \"core.plugin.detail.fields.license\": \"License\",\n  \"core.plugin.detail.fields.role_templates\": \"Role Templates\",\n  \"core.plugin.detail.fields.last_starttime\": \"Last Start Time\",\n  \"core.plugin.detail.fields.homepage\": \"Homepage\",\n  \"core.plugin.detail.fields.repo\": \"Source Repository\",\n  \"core.plugin.detail.fields.load_location\": \"Storage Location\",\n  \"core.plugin.detail.fields.issues\": \"Issues feedback\",\n  \"core.plugin.detail.fields.creation_time\": \"Installation Time\",\n  \"core.plugin.detail.operations.copy_error_message.button\": \"Copy error message\",\n  \"core.plugin.detail.operations.copy_error_message.copied\": \"Copied\",\n  \"core.plugin.detail.operations.view_conditions.button\": \"View recent status\",\n  \"core.plugin.loader.toast.entry_load_failed\": \"Failed to load plugin entry file\",\n  \"core.plugin.loader.toast.style_load_failed\": \"Failed to load plugin stylesheet file\",\n  \"core.plugin.extension_points.editor.providers.default\": \"Default Editor\",\n  \"core.plugin.conditions_modal.title\": \"Recent status\",\n  \"core.plugin.conditions_modal.fields.type\": \"Type\",\n  \"core.plugin.conditions_modal.fields.status\": \"Status\",\n  \"core.plugin.conditions_modal.fields.reason\": \"Reason\",\n  \"core.plugin.conditions_modal.fields.message\": \"Message\",\n  \"core.plugin.conditions_modal.fields.last_transition_time\": \"Last transition time\",\n  \"core.plugin.extension-settings.title\": \"Extension settings\",\n  \"core.plugin.extension-settings.extension-point-definition.title\": \"Extension point definitions\",\n  \"core.plugin.extension-settings.extension-definition.empty.title\": \"There is currently no extension point implemented\",\n  \"core.plugin.extension-settings.extension-definition.operation.use.button\": \"Use\",\n  \"core.plugin.actions.extension-point-settings\": \"Extension settings\",\n  \"core.user.title\": \"Users\",\n  \"core.user.actions.roles\": \"Roles\",\n  \"core.user.actions.identity_authentication\": \"Identity authentication\",\n  \"core.user.empty.title\": \"There are no users that meet the filtering criteria currently.\",\n  \"core.user.empty.message\": \"There are no users that match the filtering criteria at present. You can try refreshing or creating a new user.\",\n  \"core.user.operations.delete.title\": \"Delete user\",\n  \"core.user.operations.delete_in_batch.title\": \"Delete the selected users\",\n  \"core.user.operations.update_profile.title\": \"Update profile\",\n  \"core.user.operations.change_password.title\": \"Change password\",\n  \"core.user.operations.grant_permission.title\": \"Grant permission\",\n  \"core.user.operations.enable.title\": \"Enable\",\n  \"core.user.operations.enable.description\": \"Are you sure you want to enable this user? Once enabled, the user will be able to log back into the system.\",\n  \"core.user.operations.enable_in_batch.title\": \"Enable\",\n  \"core.user.operations.enable_in_batch.description\": \"Are you sure you want to enable the selected users? Once enabled, these users will be able to log back into the system.\",\n  \"core.user.operations.disable.title\": \"Disable\",\n  \"core.user.operations.disable.description\": \"Are you sure you want to disable this user? Once disabled, the user will no longer be able to log in.\",\n  \"core.user.operations.disable_in_batch.title\": \"Disable\",\n  \"core.user.operations.disable_in_batch.description\": \"Are you sure you want to disable the selected users? Once disabled, these users will no longer be able to log in.\",\n  \"core.user.filters.role.label\": \"Role\",\n  \"core.user.filters.role.result\": \"Role: {role}\",\n  \"core.user.filters.sort.items.create_time_desc\": \"Latest Created\",\n  \"core.user.filters.sort.items.create_time_asc\": \"Earliest Created\",\n  \"core.user.editing_modal.titles.update\": \"Edit User\",\n  \"core.user.editing_modal.titles.create\": \"Create User\",\n  \"core.user.editing_modal.groups.general\": \"General\",\n  \"core.user.editing_modal.groups.annotations\": \"Annotations\",\n  \"core.user.editing_modal.fields.username.label\": \"Username\",\n  \"core.user.editing_modal.fields.username.validation\": \"Can only contain numbers, lowercase letters, periods (.), and hyphens (-), and cannot start or end with a period (.) or hyphen (-).\",\n  \"core.user.editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.user.editing_modal.fields.email.label\": \"Email\",\n  \"core.user.editing_modal.fields.phone.label\": \"Phone\",\n  \"core.user.editing_modal.fields.avatar.label\": \"Avatar\",\n  \"core.user.editing_modal.fields.bio.label\": \"Bio\",\n  \"core.user.change_password_modal.title\": \"Change password\",\n  \"core.user.change_password_modal.fields.new_password.label\": \"New password\",\n  \"core.user.change_password_modal.fields.confirm_password.label\": \"Confirm password\",\n  \"core.user.grant_permission_modal.title\": \"Grant permission\",\n  \"core.user.grant_permission_modal.fields.role.label\": \"Role\",\n  \"core.user.grant_permission_modal.fields.role.placeholder\": \"Please select a role\",\n  \"core.user.grant_permission_modal.roles_preview.all\": \"The currently selected role contains all permissions\",\n  \"core.user.grant_permission_modal.roles_preview.includes\": \"The currently selected role contains the following permissions:\",\n  \"core.user.detail.title\": \"User detail\",\n  \"core.user.detail.tabs.detail\": \"Detail\",\n  \"core.user.detail.actions.update_profile.title\": \"Update profile\",\n  \"core.user.detail.actions.change_password.title\": \"Change password\",\n  \"core.user.detail.actions.grant_permission.title\": \"Grant permission\",\n  \"core.user.detail.actions.profile.title\": \"Profile\",\n  \"core.user.detail.fields.display_name\": \"Display name\",\n  \"core.user.detail.fields.username\": \"Username\",\n  \"core.user.detail.fields.email\": \"Email\",\n  \"core.user.detail.fields.roles\": \"Roles\",\n  \"core.user.detail.fields.bio\": \"Bio\",\n  \"core.user.detail.fields.creation_time\": \"Creation time\",\n  \"core.user.detail.fields.email_verified.tooltip\": \"Verified\",\n  \"core.user.detail.fields.email_not_verified.tooltip\": \"Not verified\",\n  \"core.user.fields.disabled\": \"Disabled\",\n  \"core.role.title\": \"Roles\",\n  \"core.role.common.text.contains_all_permissions\": \"Contains all permissions\",\n  \"core.role.common.text.contains_n_permissions\": \"Contains {count} permissions\",\n  \"core.role.common.text.system_reserved\": \"System Reserved\",\n  \"core.role.common.text.custom\": \"Custom\",\n  \"core.role.common.text.dependent_on\": \"Dependent on {roles}\",\n  \"core.role.common.text.provided_by_plugin\": \"Provided by {plugin}\",\n  \"core.role.operations.delete.title\": \"Delete role\",\n  \"core.role.operations.delete.description\": \"After the role is deleted, the associated users will have their role bindings removed and this operation cannot be undone.\",\n  \"core.role.operations.create_based_on_this_role.button\": \"Create based on this role\",\n  \"core.role.detail.title\": \"Role detail\",\n  \"core.role.detail.header.title\": \"Role information\",\n  \"core.role.detail.tabs.detail\": \"Detail\",\n  \"core.role.detail.tabs.permissions\": \"Permissions\",\n  \"core.role.detail.fields.display_name\": \"Display name\",\n  \"core.role.detail.fields.name\": \"Name\",\n  \"core.role.detail.fields.type\": \"Type\",\n  \"core.role.detail.fields.creation_time\": \"Creation time\",\n  \"core.role.permissions_detail.system_reserved_alert.description\": \"The system reserved role does not support modification, it is recommended to create a new role based on this one.\",\n  \"core.role.editing_modal.titles.create\": \"Create role\",\n  \"core.role.editing_modal.titles.update\": \"Update role\",\n  \"core.role.editing_modal.groups.general\": \"General\",\n  \"core.role.editing_modal.groups.permissions\": \"Permissions\",\n  \"core.role.editing_modal.fields.display_name\": \"Display name\",\n  \"core.role.editing_modal.fields.disallow_access_console.label\": \"Disable access to Console\",\n  \"core.role.editing_modal.fields.disallow_access_console.help\": \"Once checked, this role will not be able to access the Console\",\n  \"core.identity_authentication.title\": \"Identity Authentication\",\n  \"core.identity_authentication.tabs.detail\": \"Detail\",\n  \"core.identity_authentication.tabs.setting\": \"Settings\",\n  \"core.identity_authentication.operations.enable.title\": \"Enable identity authentication method\",\n  \"core.identity_authentication.operations.disable.title\": \"Disable identity authentication method\",\n  \"core.identity_authentication.operations.disable_privileged.tooltip\": \"The authentication method reserved by the system cannot be disabled\",\n  \"core.identity_authentication.fields.display_name.local\": \"Login with credentials\",\n  \"core.identity_authentication.fields.description.local\": \"Default login method built into Halo\",\n  \"core.identity_authentication.list.types.form\": \"Basic authentication methods\",\n  \"core.identity_authentication.list.types.oauth2\": \"Third-party authentication methods\",\n  \"core.identity_authentication.detail.title\": \"Identity authentication details\",\n  \"core.identity_authentication.detail.fields.display_name\": \"Display name\",\n  \"core.identity_authentication.detail.fields.description\": \"Description\",\n  \"core.identity_authentication.detail.fields.website\": \"Website\",\n  \"core.identity_authentication.detail.fields.help_page\": \"Help page\",\n  \"core.identity_authentication.detail.fields.authentication_url\": \"Login URL\",\n  \"core.uc_profile.title\": \"Profile\",\n  \"core.uc_profile.tabs.detail\": \"Detail\",\n  \"core.uc_profile.tabs.notification-preferences\": \"Notification Preferences\",\n  \"core.uc_profile.tabs.pat\": \"Personal Access Tokens\",\n  \"core.uc_profile.tabs.authentication\": \"Authentication\",\n  \"core.uc_profile.tabs.devices\": \"Devices\",\n  \"core.uc_profile.actions.update_profile.title\": \"Update profile\",\n  \"core.uc_profile.actions.change_password.title\": \"Change password\",\n  \"core.uc_profile.detail.fields.display_name\": \"Display name\",\n  \"core.uc_profile.detail.fields.username\": \"Username\",\n  \"core.uc_profile.detail.fields.email\": \"Email\",\n  \"core.uc_profile.detail.fields.roles\": \"Roles\",\n  \"core.uc_profile.detail.fields.bio\": \"Bio\",\n  \"core.uc_profile.detail.fields.creation_time\": \"Creation time\",\n  \"core.uc_profile.detail.email_not_set.description\": \"Your email address has not been set yet. Click the button below to set it up.\",\n  \"core.uc_profile.detail.email_not_set.title\": \"Set up email\",\n  \"core.uc_profile.detail.email_not_verified.description\": \"Your email address has not been verified yet, click the button below to verify it\",\n  \"core.uc_profile.detail.email_not_verified.title\": \"Verify email\",\n  \"core.uc_profile.detail.email_verified.tooltip\": \"Verified\",\n  \"core.uc_profile.auth_providers.title\": \"Auth providers\",\n  \"core.uc_profile.auth_providers.operations.bind.button\": \"Bind\",\n  \"core.uc_profile.auth_providers.operations.unbind.button\": \"Unbind\",\n  \"core.uc_profile.auth_providers.operations.unbind.title\": \"Unbind the login method for {display_name}\",\n  \"core.uc_profile.2fa.operations.enable.button\": \"Enable 2FA\",\n  \"core.uc_profile.2fa.operations.enable.title\": \"Enable 2FA\",\n  \"core.uc_profile.2fa.operations.disable.title\": \"Disable 2FA\",\n  \"core.uc_profile.2fa.operations.disable_totp.title\": \"Disable TOTP\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.label\": \"Password\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.help\": \"Login password of the current account\",\n  \"core.uc_profile.2fa.methods.title\": \"Two-factor methods\",\n  \"core.uc_profile.2fa.methods.totp.title\": \"TOTP\",\n  \"core.uc_profile.2fa.methods.totp.description\": \"Configure two-step verification with a TOTP app\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.configured\": \"Configured\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.not_configured\": \"Not configured\",\n  \"core.uc_profile.2fa.methods.totp.operations.reconfigure.button\": \"Reconfigure\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.button\": \"Configure\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.title\": \"TOTP configuration\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.label\": \"Verification code\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.help\": \"6-digit verification code obtained from the authenticator app\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.label\": \"Password\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.help\": \"Login password of the current account\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.qrcode.label\": \"Use the authenticator app to scan the QR code below:\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.label\": \"If you can't scan the QR code, click to view alternative steps.\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.help\": \"Manually configure the authenticator app with the following code:\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.tips\": \"Please keep your two-step verification device safe. If the device is lost or damaged, you will not be able to log in. It is recommended that you install the authenticator app on multiple devices or keep a backup of the key in case the primary device cannot be used.\",\n  \"core.uc_profile.pat.operations.delete.title\": \"Delete Personal Access Token\",\n  \"core.uc_profile.pat.operations.delete.description\": \"Are you sure you want to delete this personal access token?\",\n  \"core.uc_profile.pat.operations.revoke.button\": \"Revoke\",\n  \"core.uc_profile.pat.operations.revoke.title\": \"Revoke Personal Access Token\",\n  \"core.uc_profile.pat.operations.revoke.description\": \"Are you sure you want to revoke this personal access token?\",\n  \"core.uc_profile.pat.operations.revoke.toast_success\": \"Revocation succeeded\",\n  \"core.uc_profile.pat.operations.copy.title\": \"Please copy and save immediately, Token will only be displayed once.\",\n  \"core.uc_profile.pat.operations.restore.button\": \"Restore\",\n  \"core.uc_profile.pat.operations.restore.toast_success\": \"Restore successfully\",\n  \"core.uc_profile.pat.list.empty.title\": \"No personal access tokens have been created\",\n  \"core.uc_profile.pat.list.empty.message\": \"You can try refreshing or creating a new personal access token\",\n  \"core.uc_profile.pat.list.fields.expiresAt.dynamic\": \"Expires on {expiresAt}\",\n  \"core.uc_profile.pat.list.fields.expiresAt.forever\": \"Never expires\",\n  \"core.uc_profile.pat.list.fields.status.normal\": \"Normal\",\n  \"core.uc_profile.pat.list.fields.status.revoked\": \"Revoked\",\n  \"core.uc_profile.pat.list.fields.status.expired\": \"Expired\",\n  \"core.uc_profile.pat.creation_modal.title\": \"Create Personal Access Token\",\n  \"core.uc_profile.pat.creation_modal.groups.general\": \"General\",\n  \"core.uc_profile.pat.creation_modal.groups.permissions\": \"Permissions\",\n  \"core.uc_profile.pat.creation_modal.fields.name.label\": \"Name\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.label\": \"Expiration Time\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.help\": \"Leave empty for no expiration\",\n  \"core.uc_profile.pat.creation_modal.fields.description.label\": \"Description\",\n  \"core.uc_profile.notification-preferences.fields.type\": \"Type\",\n  \"core.uc_profile.editing_modal.title\": \"Edit Profile\",\n  \"core.uc_profile.editing_modal.groups.general\": \"General\",\n  \"core.uc_profile.editing_modal.groups.annotations\": \"Annotations\",\n  \"core.uc_profile.editing_modal.fields.username.label\": \"Username\",\n  \"core.uc_profile.editing_modal.fields.username.validation\": \"Can only contain numbers, lowercase letters, periods (.), and hyphens (-), and cannot start or end with a period (.) or hyphen (-).\",\n  \"core.uc_profile.editing_modal.fields.display_name.label\": \"Display name\",\n  \"core.uc_profile.editing_modal.fields.email.label\": \"Email\",\n  \"core.uc_profile.editing_modal.fields.phone.label\": \"Phone\",\n  \"core.uc_profile.editing_modal.fields.avatar.label\": \"Avatar\",\n  \"core.uc_profile.editing_modal.fields.bio.label\": \"Bio\",\n  \"core.uc_profile.change_password_modal.title\": \"Change password\",\n  \"core.uc_profile.change_password_modal.fields.new_password.label\": \"New password\",\n  \"core.uc_profile.change_password_modal.fields.confirm_password.label\": \"Confirm password\",\n  \"core.uc_profile.change_password_modal.fields.old_password.label\": \"Old password\",\n  \"core.uc_profile.email_verify_modal.fields.code.label\": \"Verification code\",\n  \"core.uc_profile.email_verify_modal.fields.email.label\": \"Email address\",\n  \"core.uc_profile.email_verify_modal.fields.new_email.label\": \"New email address\",\n  \"core.uc_profile.email_verify_modal.fields.password.label\": \"Password\",\n  \"core.uc_profile.email_verify_modal.fields.password.help\": \"The login password for the current account\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.countdown\": \"Resend in {timer} seconds\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.send\": \"Send the verification code\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.sending\": \"Sending\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_email_empty\": \"Please enter your email address\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_success\": \"Verification code sent\",\n  \"core.uc_profile.email_verify_modal.operations.verify.toast_success\": \"Verification successful\",\n  \"core.uc_profile.email_verify_modal.titles.modify\": \"Modify email address\",\n  \"core.uc_profile.email_verify_modal.titles.verify\": \"Verify email\",\n  \"core.uc_profile.device.list.fields.current\": \"Current\",\n  \"core.uc_profile.device.list.fields.last_accessed_time\": \"Last accessed time: {time}\",\n  \"core.uc_profile.device.detail_modal.title\": \"Login device details\",\n  \"core.uc_profile.device.detail_modal.fields.os\": \"OS\",\n  \"core.uc_profile.device.detail_modal.fields.browser\": \"Browser\",\n  \"core.uc_profile.device.detail_modal.fields.creation_timestamp\": \"Creation time\",\n  \"core.uc_profile.device.detail_modal.fields.last_accessed_times\": \"Last accessed time\",\n  \"core.uc_profile.device.detail_modal.fields.last_authenticated_time\": \"Last authenticated time\",\n  \"core.uc_profile.device.operations.revoke.title\": \"Revoke device\",\n  \"core.uc_profile.device.operations.revoke.description\": \"Are you sure you want to revoke this device? After revoking, this device will be logged out\",\n  \"core.uc_profile.device.operations.revoke_others.title\": \"Revoke all other devices\",\n  \"core.uc_profile.device.operations.revoke_others.description\": \"Are you sure you want to revoke all other devices? After you revoke, other devices will be logged out\",\n  \"core.uc_profile.device.operations.revoke_others.toast_success\": \"Login status of other devices has been revoked\",\n  \"core.uc_notification.title\": \"Notifications\",\n  \"core.uc_notification.tabs.unread\": \"Unread\",\n  \"core.uc_notification.tabs.read\": \"Read\",\n  \"core.uc_notification.empty.titles.unread\": \"No unread notifications\",\n  \"core.uc_notification.empty.titles.read\": \"No read notifications\",\n  \"core.uc_notification.operations.mark_as_read.button\": \"Mark as read\",\n  \"core.uc_notification.operations.delete.description\": \"Are you sure you want to delete this notification?\",\n  \"core.uc_notification.operations.delete.title\": \"Delete\",\n  \"core.uc_notification.operations.delete_all.title\": \"Delete all current notifications\",\n  \"core.uc_notification.operations.delete_all.description\": \"Are you sure you want to delete all current notifications? You cannot restore them after deletion.\",\n  \"core.uc_notification.operations.mark_all_as_read.title\": \"Mark all current notifications as read\",\n  \"core.uc_notification.operations.mark_all_as_read.description\": \"Mark all current messages as read. Do you want to continue?\",\n  \"core.setting.title\": \"Settings\",\n  \"core.overview.title\": \"Overview\",\n  \"core.overview.actions.copy.toast_browser_not_supported\": \"The current browser does not support copying\",\n  \"core.overview.header.titles.general\": \"General information\",\n  \"core.overview.header.titles.environment\": \"Environment information\",\n  \"core.overview.fields.external_url\": \"External URL\",\n  \"core.overview.fields.start_time\": \"Start time\",\n  \"core.overview.fields.timezone\": \"Operating system timezone\",\n  \"core.overview.fields.locale\": \"Operating system locale\",\n  \"core.overview.fields.version\": \"Version\",\n  \"core.overview.fields.build_time\": \"Build time\",\n  \"core.overview.fields.database\": \"Database\",\n  \"core.overview.fields.os\": \"Operating system\",\n  \"core.overview.fields.log\": \"System log\",\n  \"core.overview.fields.activated_theme\": \"Activated theme\",\n  \"core.overview.fields.enabled_plugins\": \"Enabled plugins\",\n  \"core.overview.fields_values.external_url.not_setup\": \"Not set up\",\n  \"core.overview.copy_results.external_url\": \"External URL: {external_url}\",\n  \"core.overview.copy_results.start_time\": \"Start time: {start_time}\",\n  \"core.overview.copy_results.version\": \"Version: {version}\",\n  \"core.overview.copy_results.build_time\": \"Build time: {build_time}\",\n  \"core.overview.copy_results.database\": \"Database: {database}\",\n  \"core.overview.copy_results.os\": \"Operating system: {os}\",\n  \"core.overview.alert.external_url_invalid\": \"The detected external access URL is inconsistent with the current access URL, which may cause some links to fail to redirect properly. Please check the external access URL settings.\",\n  \"core.overview.external_url_form.operations.save.title\": \"Modify external URL\",\n  \"core.overview.external_url_form.operations.save.description\": \"Modifying the external access address requires restarting the Halo service. It will automatically restart after the modification is completed. Do you want to continue?\",\n  \"core.overview.external_url_form.tips.restarting\": \"Modification completed, waiting for restart...\",\n  \"core.backup.title\": \"Backup and Restore\",\n  \"core.backup.tabs.backup_list\": \"Backups\",\n  \"core.backup.tabs.restore\": \"Restore\",\n  \"core.backup.empty.title\": \"No backups have been created yet\",\n  \"core.backup.empty.message\": \"You can click the button below to create a backup\",\n  \"core.backup.operations.create.button\": \"Create backup\",\n  \"core.backup.operations.create.title\": \"Create backup\",\n  \"core.backup.operations.create.description\": \"Are you sure you want to create a backup? This operation may last for a long time.\",\n  \"core.backup.operations.create.toast_success\": \"Requested to create a backup\",\n  \"core.backup.operations.delete.title\": \"Delete the backup\",\n  \"core.backup.operations.delete.description\": \"Are you sure you want to delete the backup?\",\n  \"core.backup.operations.restore.title\": \"Restore successful\",\n  \"core.backup.operations.restore.description\": \"After successful restore, you need to restart Halo to load the system resources normally. After clicking OK, we will automatically restart Halo.\",\n  \"core.backup.operations.restart.toast_success\": \"Requested to restart\",\n  \"core.backup.operations.remote_download.button\": \"Download and restore\",\n  \"core.backup.operations.restore_by_backup.button\": \"Restore\",\n  \"core.backup.operations.restore_by_backup.title\": \"Restore from backup file\",\n  \"core.backup.operations.restore_by_backup.description\": \"After clicking OK, data will be restored from the backup file {filename}.\",\n  \"core.backup.list.phases.pending\": \"Pending\",\n  \"core.backup.list.phases.running\": \"Running\",\n  \"core.backup.list.phases.succeeded\": \"Succeeded\",\n  \"core.backup.list.phases.failed\": \"Failed\",\n  \"core.backup.list.fields.expiresAt\": \"Expires {expiresAt}\",\n  \"core.backup.restore.tips.first\": \"1. The restore process may last for a long time, please do not refresh the page during this period.\",\n  \"core.backup.restore.tips.second\": \"2. Before performing the restore, all existing data will be cleared. Please ensure that there is no data that needs to be retained.\",\n  \"core.backup.restore.tips.third\": \"3. After the restore is completed, you need to restart Halo to load the system resources normally.\",\n  \"core.backup.restore.tips.complete\": \"Restore completed, waiting for restart...\",\n  \"core.backup.restore.start\": \"I have read the above instructions, start restore\",\n  \"core.backup.restore.tabs.local.label\": \"Upload\",\n  \"core.backup.restore.tabs.remote.label\": \"Remote\",\n  \"core.backup.restore.tabs.remote.fields.url\": \"Remote URL\",\n  \"core.backup.restore.tabs.backup.label\": \"Restore from backup files\",\n  \"core.backup.restore.tabs.backup.empty.title\": \"No backup files\",\n  \"core.backup.restore.tabs.backup.empty.message\": \"Currently no backup files are scanned. You can manually upload the backup files to the backups directory of the Halo working directory.\",\n  \"core.exception.not_found.message\": \"Page not found\",\n  \"core.exception.forbidden.message\": \"You are not authorized to access this page\",\n  \"core.exception.actions.home\": \"Back to home\",\n  \"core.rbac.Attachments Management\": \"Attachments\",\n  \"core.rbac.Attachment Manage\": \"Attachment Manage\",\n  \"core.rbac.Attachment View\": \"Attachment View\",\n  \"core.rbac.role-template-view-attachments\": \"Attachment View\",\n  \"core.rbac.Comments Management\": \"Comments\",\n  \"core.rbac.Comment Manage\": \"Comment Manage\",\n  \"core.rbac.Comment View\": \"Comment View\",\n  \"core.rbac.role-template-view-comments\": \"Comment View\",\n  \"core.rbac.ConfigMaps Management\": \"ConfigMaps\",\n  \"core.rbac.ConfigMap Manage\": \"ConfigMap Manage\",\n  \"core.rbac.ConfigMap View\": \"ConfigMap View\",\n  \"core.rbac.role-template-view-configmaps\": \"ConfigMap View\",\n  \"core.rbac.Menus Management\": \"Menus\",\n  \"core.rbac.Menu Manage\": \"Menu Manage\",\n  \"core.rbac.Menu View\": \"Menu View\",\n  \"core.rbac.role-template-view-menus\": \"Menu View\",\n  \"core.rbac.Permissions Management\": \"Permissions\",\n  \"core.rbac.Permissions Manage\": \"Permissions Manage\",\n  \"core.rbac.Permissions View\": \"Permissions View\",\n  \"core.rbac.role-template-view-permissions\": \"Permissions View\",\n  \"core.rbac.role-template-manage-permissions\": \"Permissions Manage\",\n  \"core.rbac.Plugins Management\": \"Plugins\",\n  \"core.rbac.Plugin Manage\": \"Plugin Manage\",\n  \"core.rbac.Plugin View\": \"Plugin View\",\n  \"core.rbac.role-template-view-plugins\": \"Plugin View\",\n  \"core.rbac.Posts Management\": \"Posts\",\n  \"core.rbac.Post Manage\": \"Post Manage\",\n  \"core.rbac.Post View\": \"Post View\",\n  \"core.rbac.role-template-view-posts\": \"Post View\",\n  \"core.rbac.role-template-manage-snapshots\": \"Snapshot Manage\",\n  \"core.rbac.role-template-view-snapshots\": \"Snapshot View\",\n  \"core.rbac.role-template-manage-tags\": \"Tag Manage\",\n  \"core.rbac.role-template-view-tags\": \"Tag View\",\n  \"core.rbac.role-template-manage-categories\": \"Category Manage\",\n  \"core.rbac.role-template-view-categories\": \"Category View\",\n  \"core.rbac.role-template-manage-posts\": \"Post Manage\",\n  \"core.rbac.role-template-post-author\": \"Allows you to manage your own posts\",\n  \"core.rbac.role-template-post-contributor\": \"Contributions allowed\",\n  \"core.rbac.role-template-post-publisher\": \"Allow publishing own posts\",\n  \"core.rbac.role-template-post-attachment-manager\": \"Allow images to be uploaded in posts\",\n  \"core.rbac.Roles Management\": \"Roles\",\n  \"core.rbac.Role Manage\": \"Role Manage\",\n  \"core.rbac.Role View\": \"Role View\",\n  \"core.rbac.role-template-view-roles\": \"Role View\",\n  \"core.rbac.Settings Management\": \"Settings\",\n  \"core.rbac.Setting Manage\": \"Setting Manage\",\n  \"core.rbac.Setting View\": \"Setting View\",\n  \"core.rbac.role-template-view-settings\": \"Setting View\",\n  \"core.rbac.SinglePages Management\": \"SinglePages\",\n  \"core.rbac.SinglePage Manage\": \"SinglePage Manage\",\n  \"core.rbac.SinglePage View\": \"SinglePage View\",\n  \"core.rbac.role-template-view-singlepages\": \"SinglePage View\",\n  \"core.rbac.Themes Management\": \"Themes\",\n  \"core.rbac.Theme Manage\": \"Theme Manage\",\n  \"core.rbac.Theme View\": \"Theme View\",\n  \"core.rbac.role-template-view-themes\": \"Theme View\",\n  \"core.rbac.Users Management\": \"Users\",\n  \"core.rbac.User manage\": \"User Manage\",\n  \"core.rbac.User View\": \"User View\",\n  \"core.rbac.Migration Management\": \"Backup and Restore\",\n  \"core.rbac.Migration Manage\": \"Backup and Restore Manage\",\n  \"core.rbac.role-template-view-users\": \"User View\",\n  \"core.rbac.role-template-change-password\": \"Change Password\",\n  \"core.rbac.Actuator Management\": \"System Information\",\n  \"core.rbac.Actuator Manage\": \"Access System Information\",\n  \"core.rbac.Cache Management\": \"Cache\",\n  \"core.rbac.Cache Manage\": \"Cache Manage\",\n  \"core.rbac.Notification Configuration\": \"Notification Configuration\",\n  \"core.rbac.role-template-notifier-config\": \"Configure Notifier\",\n  \"core.rbac.Post Attachment Manager\": \"Allow images to be uploaded in posts\",\n  \"core.rbac.Post Author\": \"Allows you to manage your own posts\",\n  \"core.rbac.Post Contributor\": \"Contributions allowed\",\n  \"core.rbac.Post Publisher\": \"Allow publishing own posts\",\n  \"core.rbac.UC Attachment Manage\": \"Allow managing own attachments\",\n  \"core.rbac.role-template-uc-attachment-manager\": \"Allow managing own attachments\",\n  \"core.rbac.Recycle My Post\": \"Allow deleting own posts\",\n  \"core.rbac.role-template-recycle-my-post\": \"Allow deleting own posts\",\n  \"core.components.submit_button.computed_text\": \"{text} ({shortcut})\",\n  \"core.components.annotations_form.custom_fields.label\": \"Custom\",\n  \"core.components.annotations_form.custom_fields.validation\": \"The current Key is already in use\",\n  \"core.components.annotations_form.buttons.expand\": \"View more\",\n  \"core.components.annotations_form.buttons.collapse\": \"Collapse\",\n  \"core.components.default_editor.tabs.toc.title\": \"TOC\",\n  \"core.components.default_editor.tabs.toc.empty\": \"No TOC available\",\n  \"core.components.default_editor.tabs.detail.title\": \"Detail\",\n  \"core.components.default_editor.tabs.detail.fields.character_count\": \"Character count\",\n  \"core.components.default_editor.tabs.detail.fields.word_count\": \"Word count\",\n  \"core.components.default_editor.tabs.detail.fields.publish_time\": \"Publish time\",\n  \"core.components.default_editor.tabs.detail.fields.draft\": \"Draft\",\n  \"core.components.default_editor.tabs.detail.fields.owner\": \"Owner\",\n  \"core.components.default_editor.tabs.detail.fields.permalink\": \"Permalink\",\n  \"core.components.default_editor.extensions.placeholder.options.placeholder\": \"Enter / to select input type.\",\n  \"core.components.default_editor.toolbox.attachment\": \"Attachment\",\n  \"core.components.default_editor.toolbox.show_hide_sidebar\": \"Show/Hide Sidebar\",\n  \"core.components.default_editor.title_placeholder\": \"Please enter the title\",\n  \"core.components.default_editor.cover.options.add\": \"Add Cover\",\n  \"core.components.default_editor.cover.options.change\": \"Change Cover\",\n  \"core.components.default_editor.cover.options.upload\": \"Upload\",\n  \"core.components.default_editor.cover.options.attachment\": \"Select from attachment library\",\n  \"core.components.default_editor.cover.toast.upload_failed\": \"Failed to upload cover image: {message}\",\n  \"core.components.global_search.placeholder\": \"Enter keywords to search\",\n  \"core.components.global_search.no_results\": \"No search results\",\n  \"core.components.global_search.buttons.select\": \"Select\",\n  \"core.components.global_search.groups.console\": \"Console page\",\n  \"core.components.global_search.groups.user\": \"User\",\n  \"core.components.global_search.groups.plugin\": \"Plugin\",\n  \"core.components.global_search.groups.post\": \"Post\",\n  \"core.components.global_search.groups.category\": \"Category\",\n  \"core.components.global_search.groups.tag\": \"Tag\",\n  \"core.components.global_search.groups.page\": \"Page\",\n  \"core.components.global_search.groups.attachment\": \"Attachment\",\n  \"core.components.global_search.groups.setting\": \"Setting\",\n  \"core.components.global_search.groups.theme_setting\": \"Theme setting\",\n  \"core.components.pagination.page_label\": \"page\",\n  \"core.components.pagination.size_label\": \"items per page\",\n  \"core.components.pagination.total_label\": \"Total {total} items\",\n  \"core.components.app_download_alert.description\": \"Themes and plugins for Halo can be downloaded at the following addresses:\",\n  \"core.components.app_download_alert.sources.app_store\": \"Official App Store: {url}\",\n  \"core.components.app_download_alert.sources.github\": \"GitHub: {url}\",\n  \"core.components.user_avatar.title\": \"Avatar\",\n  \"core.components.user_avatar.toast_upload_failed\": \"Failed to upload avatar\",\n  \"core.components.user_avatar.toast_remove_failed\": \"Failed to delete avatar\",\n  \"core.components.user_avatar.cropper_modal.title\": \"Crop Avatar\",\n  \"core.components.user_avatar.remove.title\": \"Delete avatar\",\n  \"core.components.user_avatar.tooltips.upload\": \"Upload\",\n  \"core.components.user_avatar.tooltips.zoom_in\": \"Zoom In\",\n  \"core.components.user_avatar.tooltips.zoom_out\": \"Zoom Out\",\n  \"core.components.user_avatar.tooltips.flip_horizontal\": \"Flip Horizontal\",\n  \"core.components.user_avatar.tooltips.flip_vertical\": \"Flip Vertical\",\n  \"core.components.user_avatar.tooltips.reset\": \"Reset\",\n  \"core.components.editor_provider_selector.tooltips.disallow\": \"The content format is different and cannot be switched\",\n  \"core.components.uppy.image_editor.revert\": \"Revert\",\n  \"core.components.uppy.image_editor.rotate\": \"Rotate\",\n  \"core.components.uppy.image_editor.zoom_in\": \"Zoom in\",\n  \"core.components.uppy.image_editor.zoom_out\": \"Zoom out\",\n  \"core.components.uppy.image_editor.flip_horizontal\": \"Flip horizontal\",\n  \"core.components.uppy.image_editor.aspect_ratio_square\": \"Crop square\",\n  \"core.components.uppy.image_editor.aspect_ratio_landscape\": \"Crop landscape (16:9)\",\n  \"core.components.uppy.image_editor.aspect_ratio_portrait\": \"Crop portrait (9:16)\",\n  \"core.components.h2_warning_alert.title\": \"Warning: H2 database in use\",\n  \"core.components.h2_warning_alert.description\": \"The H2 database is only suitable for development and testing environments and is not recommended for use in production environments. H2 is very easy to cause data file corruption due to improper operation. If you must use it, please back up the data on time.\",\n  \"core.composables.content_cache.toast_recovered\": \"Recovered unsaved content from cache\",\n  \"core.formkit.color.placeholder\": \"Select color\",\n  \"core.formkit.color.operations.edit\": \"Edit color manually\",\n  \"core.formkit.select.no_data\": \"No data\",\n  \"core.formkit.category_select.creation_label\": \"Create {text} category\",\n  \"core.formkit.tag_select.creation_label\": \"Create {text} tag\",\n  \"core.formkit.validation.trim\": \"Please remove the leading and trailing spaces\",\n  \"core.formkit.validation.password\": \"The password can only use uppercase and lowercase letters (A-Z, a-z), numbers (0-9), and the following special characters: !{'@'}#$%^&*\",\n  \"core.formkit.verification_form.no_action_defined\": \"{label} interface not defined\",\n  \"core.formkit.verification_form.verify_success\": \"{label} successful\",\n  \"core.formkit.verification_form.verify_failed\": \"{label} failed\",\n  \"core.formkit.secret.creation_modal.title\": \"Create secret\",\n  \"core.formkit.secret.edit_modal.title\": \"Edit secret\",\n  \"core.formkit.secret.list_modal.title\": \"Select secret\",\n  \"core.formkit.secret.operations.delete.title\": \"Delete secret\",\n  \"core.formkit.secret.operations.delete.description\": \"Are you sure you want to delete this secret? Please make sure that this secret is not being used anywhere, otherwise you need to reset it in a specific place\",\n  \"core.formkit.secret.form.fields.description\": \"Description\",\n  \"core.formkit.secret.form.fields.string_data\": \"Secret Data\",\n  \"core.formkit.secret.no_permission\": \"You don't have permission to view secrets\",\n  \"core.formkit.secret.no_selected\": \"No secret selected, you can create a new one or select an existing one\",\n  \"core.formkit.secret.missing_keys\": \"The selected secret is missing the following fields: {keys}\",\n  \"core.formkit.secret.includes_keys\": \"Includes fields: {keys}\",\n  \"core.formkit.secret.no_fields\": \"No fields\",\n  \"core.formkit.secret.required_keys_tip\": \"The required key fields for the current form item include:\",\n  \"core.formkit.code.fullscreen.exit\": \"Exit fullscreen\",\n  \"core.formkit.code.fullscreen.enter\": \"Enter fullscreen to edit\",\n  \"core.formkit.iconify.placeholder\": \"Select icon\",\n  \"core.formkit.iconify.tabs.collections\": \"Collections\",\n  \"core.formkit.iconify.tabs.search\": \"Search\",\n  \"core.formkit.iconify.tabs.current\": \"Current icon\",\n  \"core.formkit.iconify.collection_search_placeholder\": \"Search icon collections\",\n  \"core.formkit.iconify.search_placeholder\": \"Search icons\",\n  \"core.formkit.iconify.no_results\": \"No results found, please try again\",\n  \"core.formkit.iconify.option_size\": \"Size\",\n  \"core.formkit.iconify.option_color\": \"Color\",\n  \"core.formkit.iconify.copyright\": \"Icon data from {url}\",\n  \"core.formkit.iconify.operations.edit\": \"Edit icon manually\",\n  \"core.formkit.array.empty_text\": \"No items\",\n  \"core.formkit.array.image_tooltip\": \"View image:{value}\",\n  \"core.formkit.array.item_modal.title\": \"Edit item\",\n  \"core.formkit.attachment.operations.select\": \"Select from attachment library\",\n  \"core.formkit.attachment.operations.input\": \"Input link\",\n  \"core.common.buttons.save\": \"Save\",\n  \"core.common.buttons.close\": \"Close\",\n  \"core.common.buttons.save_and_continue\": \"Save and keep adding\",\n  \"core.common.buttons.close_and_shortcut\": \"Close (Esc)\",\n  \"core.common.buttons.delete\": \"Delete\",\n  \"core.common.buttons.setting\": \"Settings\",\n  \"core.common.buttons.confirm\": \"Confirm\",\n  \"core.common.buttons.cancel\": \"Cancel\",\n  \"core.common.buttons.cancel_and_shortcut\": \"Cancel (Esc)\",\n  \"core.common.buttons.new\": \"New\",\n  \"core.common.buttons.edit\": \"Edit\",\n  \"core.common.buttons.back\": \"Back\",\n  \"core.common.buttons.refresh\": \"Refresh\",\n  \"core.common.buttons.publish\": \"Publish\",\n  \"core.common.buttons.cancel_publish\": \"Cancel publish\",\n  \"core.common.buttons.next\": \"Next\",\n  \"core.common.buttons.previous\": \"Previous\",\n  \"core.common.buttons.install\": \"Install\",\n  \"core.common.buttons.uninstall\": \"Uninstall\",\n  \"core.common.buttons.upgrade\": \"Upgrade\",\n  \"core.common.buttons.reset\": \"Reset\",\n  \"core.common.buttons.preview\": \"Preview\",\n  \"core.common.buttons.restore\": \"Restore\",\n  \"core.common.buttons.delete_permanently\": \"Delete permanently\",\n  \"core.common.buttons.activate\": \"Activate\",\n  \"core.common.buttons.inactivate\": \"Inactivate\",\n  \"core.common.buttons.download\": \"Download\",\n  \"core.common.buttons.copy\": \"Copy\",\n  \"core.common.buttons.upload\": \"Upload\",\n  \"core.common.buttons.cancel_upload\": \"Cancel upload\",\n  \"core.common.buttons.add\": \"Add\",\n  \"core.common.buttons.submit\": \"Submit\",\n  \"core.common.buttons.detail\": \"Detail\",\n  \"core.common.buttons.select\": \"Select\",\n  \"core.common.buttons.view_all\": \"View all\",\n  \"core.common.buttons.verify\": \"Verify\",\n  \"core.common.buttons.modify\": \"Modify\",\n  \"core.common.buttons.access\": \"Access\",\n  \"core.common.buttons.schedule_publish\": \"Schedule publish\",\n  \"core.common.buttons.revoke\": \"Revoke\",\n  \"core.common.buttons.disable\": \"Disable\",\n  \"core.common.buttons.enable\": \"Enable\",\n  \"core.common.buttons.continue\": \"Continue\",\n  \"core.common.buttons.retry\": \"Retry\",\n  \"core.common.buttons.unfold\": \"Unfold\",\n  \"core.common.buttons.fold\": \"Fold\",\n  \"core.common.buttons.remove\": \"Remove\",\n  \"core.common.buttons.replace\": \"Replace\",\n  \"core.common.buttons.more\": \"More\",\n  \"core.common.radio.yes\": \"Yes\",\n  \"core.common.radio.no\": \"No\",\n  \"core.common.select.public\": \"Public\",\n  \"core.common.select.private\": \"Private\",\n  \"core.common.placeholder.search\": \"Enter keywords to search\",\n  \"core.common.toast.operation_success\": \"Operation successful\",\n  \"core.common.toast.delete_success\": \"Deleted successfully\",\n  \"core.common.toast.save_success\": \"Saved successfully\",\n  \"core.common.toast.publish_success\": \"Published successfully\",\n  \"core.common.toast.cancel_publish_success\": \"Unpublished successfully\",\n  \"core.common.toast.recovery_success\": \"Restored successfully\",\n  \"core.common.toast.uninstall_success\": \"Uninstalled successfully\",\n  \"core.common.toast.active_success\": \"Activated successfully\",\n  \"core.common.toast.inactive_success\": \"Deactivated successfully\",\n  \"core.common.toast.upgrade_success\": \"Upgraded successfully\",\n  \"core.common.toast.install_success\": \"Installed successfully\",\n  \"core.common.toast.download_success\": \"Downloaded successfully\",\n  \"core.common.toast.copy_success\": \"Copied successfully\",\n  \"core.common.toast.operation_failed\": \"Operation failed\",\n  \"core.common.toast.download_failed\": \"Failed to download\",\n  \"core.common.toast.save_failed_and_retry\": \"Failed to save, please retry\",\n  \"core.common.toast.publish_failed_and_retry\": \"Failed to publish, please retry\",\n  \"core.common.toast.network_error\": \"Network error, please check your connection\",\n  \"core.common.toast.login_expired\": \"Login expired, please log in again\",\n  \"core.common.toast.forbidden\": \"Access denied\",\n  \"core.common.toast.not_found\": \"Resource not found\",\n  \"core.common.toast.server_internal_error\": \"Internal server error\",\n  \"core.common.toast.unknown_error\": \"Unknown error\",\n  \"core.common.toast.disable_success\": \"Disabled successfully\",\n  \"core.common.toast.enable_success\": \"Enabled successfully\",\n  \"core.common.dialog.titles.tip\": \"Tip\",\n  \"core.common.dialog.titles.warning\": \"Warning\",\n  \"core.common.dialog.titles.login_expired\": \"Login expired\",\n  \"core.common.dialog.descriptions.cannot_be_recovered\": \"This operation is irreversible.\",\n  \"core.common.dialog.descriptions.editor_not_found\": \"No editor found that matches the {raw_type} format. Please check if the editor plugin has been installed.\",\n  \"core.common.dialog.descriptions.login_expired\": \"The current session has expired. Click Confirm to go to the login page. Please ensure that the current content is saved. You can click Cancel to manually copy any unsaved content.\",\n  \"core.common.filters.results.keyword\": \"Keyword: {keyword}\",\n  \"core.common.filters.results.sort\": \"Sort: {sort}\",\n  \"core.common.filters.results.status\": \"Status: {status}\",\n  \"core.common.filters.labels.sort\": \"Sort\",\n  \"core.common.filters.labels.status\": \"Status\",\n  \"core.common.filters.item_labels.all\": \"All\",\n  \"core.common.filters.item_labels.default\": \"Default\",\n  \"core.common.status.deleting\": \"Deleting\",\n  \"core.common.status.loading\": \"Loading\",\n  \"core.common.status.loading_error\": \"Loading error\",\n  \"core.common.status.activated\": \"Activated\",\n  \"core.common.status.not_activated\": \"Not activated\",\n  \"core.common.status.installed\": \"Installed\",\n  \"core.common.status.not_installed\": \"Not installed\",\n  \"core.common.status.starting_up\": \"Starting\",\n  \"core.common.text.none\": \"None\",\n  \"core.common.text.tip\": \"Tip\",\n  \"core.common.text.warning\": \"Warning\",\n  \"core.common.text.system_protection\": \"System protection\",\n  \"core.common.text.all\": \"All\",\n  \"core.common.text.detail\": \"Detail\",\n  \"core.common.tooltips.unpublished_content_tip\": \"There is content that has been saved but not yet published.\",\n  \"core.common.tooltips.publishing\": \"Publishing\",\n  \"core.common.tooltips.recovering\": \"Recovering\",\n  \"core.common.fields.post_count\": \"{count} Posts\",\n  \"core.common.form.validation.slug_unique\": \"The current slug already exists\",\n  \"core.uc_post.creation_modal.title\": \"Create post\",\n  \"core.uc_post.operations.cancel_publish.description\": \"Are you sure you want to cancel publishing?\",\n  \"core.uc_post.operations.cancel_publish.title\": \"Cancel publish\",\n  \"core.uc_post.operations.delete.title\": \"Delete post\",\n  \"core.uc_post.operations.delete.description\": \"This action will move the post to the recycle bin, where it will be managed by the site administrator.\",\n  \"core.uc_post.publish_modal.title\": \"Publish post\",\n  \"core.uc_post.setting_modal.title\": \"Post settings\",\n  \"core.uc_post.title\": \"My posts\",\n  \"core.tool.title\": \"Tools\",\n  \"core.tool.empty.title\": \"There are no tools available\",\n  \"core.tool.empty.message\": \"There are currently no tools available, and system tools may be provided by plugins\",\n  \"core.snapshots.operations.revert.button\": \"Revert\",\n  \"core.snapshots.operations.revert.title\": \"Revert snapshot\",\n  \"core.snapshots.operations.revert.description\": \"Are you sure you want to restore this snapshot? This operation will create a new snapshot based on this one and publish it.\",\n  \"core.snapshots.operations.revert.toast_success\": \"Reverted successfully\",\n  \"core.snapshots.operations.delete.title\": \"Delete snapshot\",\n  \"core.snapshots.operations.delete.description\": \"Are you sure you want to delete this snapshot? This operation is irreversible.\",\n  \"core.snapshots.operations.cleanup.button\": \"Cleanup\",\n  \"core.snapshots.operations.cleanup.title\": \"Cleanup snapshots\",\n  \"core.snapshots.operations.cleanup.description\": \"Are you sure you want to delete all unused snapshots? Only published, base version, and draft versions will be retained.\",\n  \"core.snapshots.operations.cleanup.toast_empty\": \"There are no snapshots to be cleaned up\",\n  \"core.snapshots.operations.cleanup.toast_success\": \"Cleanup completed\",\n  \"core.snapshots.status.released\": \"Released\",\n  \"core.snapshots.status.draft\": \"Draft\",\n  \"core.snapshots.status.base\": \"Base\",\n  \"core.snapshots.diff_mode.toggle.enable\": \"Enable Diff Mode\",\n  \"core.snapshots.diff_mode.toggle.disable\": \"Disable Diff Mode\",\n  \"core.snapshots.diff_mode.title\": \"Diff Mode\",\n  \"core.snapshots.diff_mode.show_diff_only\": \"Show Diff Only\",\n  \"core.snapshots.diff_mode.sync_scroll\": \"Sync Scroll\",\n  \"core.snapshots.diff_mode.select_two_tip\": \"Please select two versions to compare\",\n  \"core.snapshots.diff_mode.old_version\": \"Previous Version (Old)\",\n  \"core.snapshots.diff_mode.new_version\": \"Selected First Version (New)\",\n  \"core.snapshots.diff_mode.diff\": \"Diff\",\n  \"core.snapshots.diff_mode.legend.removed\": \"This line represents deletion\",\n  \"core.snapshots.diff_mode.legend.added\": \"This line represents addition\",\n  \"core.snapshots.diff_mode.legend.modified\": \"This line represents modification\",\n  \"core.post_snapshots.title\": \"Post snapshot history\",\n  \"core.page_snapshots.title\": \"Page snapshot history\",\n  \"core.dashboard.widgets.presets.upvotes_stats.title\": \"Upvotes\"\n}\n"
  },
  {
    "path": "ui/src/locales/es.json",
    "content": "{\n  \"core.sidebar.search.placeholder\": \"Buscar\",\n  \"core.sidebar.menu.groups.content\": \"Contenido\",\n  \"core.sidebar.menu.groups.interface\": \"Interfaz\",\n  \"core.sidebar.menu.groups.system\": \"Sistema\",\n  \"core.sidebar.menu.groups.tool\": \"Herramienta\",\n  \"core.sidebar.menu.items.dashboard\": \"Panel de control\",\n  \"core.sidebar.menu.items.posts\": \"Publicaciones\",\n  \"core.sidebar.menu.items.single_pages\": \"Páginas\",\n  \"core.sidebar.menu.items.comments\": \"Comentarios\",\n  \"core.sidebar.menu.items.attachments\": \"Archivos adjuntos\",\n  \"core.sidebar.menu.items.themes\": \"Temas\",\n  \"core.sidebar.menu.items.menus\": \"Menús\",\n  \"core.sidebar.menu.items.plugins\": \"Complementos\",\n  \"core.sidebar.menu.items.users\": \"Usuarios\",\n  \"core.sidebar.menu.items.settings\": \"Configuraciones\",\n  \"core.sidebar.menu.items.overview\": \"Visión general\",\n  \"core.sidebar.menu.items.backup\": \"Respaldo\",\n  \"core.sidebar.menu.items.home\": \"Inicio\",\n  \"core.sidebar.menu.items.tools\": \"Herramientas\",\n  \"core.sidebar.operations.logout.title\": \"¿Estás seguro de que deseas cerrar sesión?\",\n  \"core.sidebar.operations.logout.tooltip\": \"Cerrar sesión\",\n  \"core.sidebar.operations.logout.description\": \"Al hacer clic en Confirmar se te redirigirá a la página de cierre de sesión. Asegúrate de que el contenido que estás editando esté guardado.\",\n  \"core.sidebar.operations.profile.tooltip\": \"Perfil\",\n  \"core.sidebar.operations.visit_homepage.title\": \"Visitar página de inicio\",\n  \"core.sidebar.profile.aggregate_role\": \"Rol agregado\",\n  \"core.dashboard.title\": \"Panel de control\",\n  \"core.dashboard.actions.setting\": \"Configuración\",\n  \"core.dashboard.widgets.groups.post\": \"Publicación\",\n  \"core.dashboard.widgets.groups.page\": \"Página\",\n  \"core.dashboard.widgets.groups.comment\": \"Comentario\",\n  \"core.dashboard.widgets.groups.user\": \"Usuario\",\n  \"core.dashboard.widgets.groups.other\": \"Otros\",\n  \"core.dashboard.widgets.presets.post_stats.title\": \"Publicaciones\",\n  \"core.dashboard.widgets.presets.page_stats.title\": \"Páginas\",\n  \"core.dashboard.widgets.presets.recent_published.title\": \"Publicaciones Recientes\",\n  \"core.dashboard.widgets.presets.recent_published.visits\": \"{visits} Visitas\",\n  \"core.dashboard.widgets.presets.recent_published.comments\": \"{comments} Comentarios\",\n  \"core.dashboard.widgets.presets.recent_published.empty.title\": \"No hay publicaciones publicadas\",\n  \"core.dashboard.widgets.presets.recent_published.comments-with-pending\": \" ({count} comentarios pendientes)\",\n  \"core.dashboard.widgets.presets.quickaction.title\": \"Acción Rápida\",\n  \"core.dashboard.widgets.presets.quickaction.actions.user_center.title\": \"Perfil de Usuario\",\n  \"core.dashboard.widgets.presets.quickaction.actions.view_site.title\": \"Ver Sitio\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_post.title\": \"Nueva Publicación\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_page.title\": \"Nueva Página\",\n  \"core.dashboard.widgets.presets.quickaction.actions.upload_attachment.title\": \"Subir Archivo Adjunto\",\n  \"core.dashboard.widgets.presets.quickaction.actions.theme_manage.title\": \"Administrar Temas\",\n  \"core.dashboard.widgets.presets.quickaction.actions.plugin_manage.title\": \"Administrar Complementos\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_user.title\": \"Nuevo Usuario\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.title\": \"Actualizar Motor de Búsqueda\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_title\": \"¿Deseas actualizar el índice del motor de búsqueda?\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_content\": \"Esta operación recreará los índices del motor de búsqueda para todas las publicaciones publicadas.\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.success_message\": \"Índice del motor de búsqueda actualizado exitosamente.\",\n  \"core.dashboard.widgets.presets.quickaction.config.fields.enabled_items.label\": \"Elementos habilitados\",\n  \"core.dashboard.widgets.presets.user_stats.title\": \"Usuarios\",\n  \"core.dashboard.widgets.presets.comment_stats.title\": \"Comentarios\",\n  \"core.dashboard.widgets.presets.views_stats.title\": \"Visitas\",\n  \"core.dashboard.widgets.presets.notification.title\": \"Notificaciones\",\n  \"core.dashboard.widgets.presets.notification.empty.title\": \"No hay notificaciones sin leer\",\n  \"core.dashboard.widgets.presets.pending_comments.title\": \"Comentarios pendientes\",\n  \"core.dashboard.widgets.presets.pending_comments.empty.title\": \"No hay comentarios pendientes\",\n  \"core.dashboard.widgets.presets.iframe.title\": \"Iframe\",\n  \"core.dashboard.widgets.presets.iframe.empty.title\": \"Por favor, introduce la URL en la configuración\",\n  \"core.dashboard.widgets.presets.iframe.config.fields.title.label\": \"Título\",\n  \"core.dashboard.widgets.presets.stack.title\": \"Pila de widgets\",\n  \"core.dashboard.widgets.presets.stack.operations.add_widget.button\": \"Añadir widget\",\n  \"core.dashboard.widgets.presets.stack.config_modal.title\": \"Configuración de la pila de widgets\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play.label\": \"Reproducción automática\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play_interval.label\": \"Intervalo de reproducción automática\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.widgets.label\": \"Widgets\",\n  \"core.dashboard.widgets.presets.stack.config_modal.toast.nest_warning\": \"No puedes añadir un widget de pila a otro widget de pila\",\n  \"core.dashboard.widgets.common_form.fields.enable_animation.label\": \"Habilitar animación\",\n  \"core.post.title\": \"Publicaciones\",\n  \"core.post.actions.categories\": \"Categorías\",\n  \"core.post.actions.tags\": \"Etiquetas\",\n  \"core.post.actions.recycle_bin\": \"Papelera de reciclaje\",\n  \"core.post.empty.title\": \"No hay publicaciones actualmente.\",\n  \"core.post.empty.message\": \"Puedes intentar actualizar o crear una nueva publicación.\",\n  \"core.post.operations.delete.title\": \"¿Estás seguro de que deseas eliminar esta publicación?\",\n  \"core.post.operations.delete.description\": \"Esta operación moverá la publicación a la papelera de reciclaje, y podrá ser restaurada desde la papelera de reciclaje posteriormente.\",\n  \"core.post.operations.delete_in_batch.title\": \"¿Estás seguro de que deseas eliminar las publicaciones seleccionadas?\",\n  \"core.post.operations.delete_in_batch.description\": \"Esta operación moverá las publicaciones a la papelera de reciclaje, y podrán ser restauradas desde la papelera de reciclaje posteriormente.\",\n  \"core.post.operations.publish_in_batch.title\": \"Publicar publicaciones\",\n  \"core.post.operations.publish_in_batch.description\": \"Publicar publicaciones por lotes, las publicaciones seleccionadas se establecerán como publicadas\",\n  \"core.post.operations.cancel_publish_in_batch.title\": \"Cancelar publicación de publicaciones\",\n  \"core.post.operations.cancel_publish_in_batch.description\": \"Cancelar publicación por lotes, las publicaciones seleccionadas se establecerán como no publicadas\",\n  \"core.post.operations.batch_setting.button\": \"Ajustes por lotes\",\n  \"core.post.filters.status.items.published\": \"Publicado\",\n  \"core.post.filters.status.items.draft\": \"Borrador\",\n  \"core.post.filters.status.items.scheduling\": \"Programando publicación\",\n  \"core.post.filters.visible.label\": \"Visible\",\n  \"core.post.filters.visible.result\": \"Visible: {visible}\",\n  \"core.post.filters.visible.items.public\": \"Público\",\n  \"core.post.filters.visible.items.private\": \"Privado\",\n  \"core.post.filters.category.label\": \"Categoría\",\n  \"core.post.filters.category.result\": \"Categoría: {category}\",\n  \"core.post.filters.tag.label\": \"Etiqueta\",\n  \"core.post.filters.tag.result\": \"Etiqueta: {tag}\",\n  \"core.post.filters.author.label\": \"Autor\",\n  \"core.post.filters.author.result\": \"Autor: {author}\",\n  \"core.post.filters.sort.items.publish_time_desc\": \"Publicado más reciente\",\n  \"core.post.filters.sort.items.publish_time_asc\": \"Publicado más antiguo\",\n  \"core.post.filters.sort.items.create_time_desc\": \"Creado más reciente\",\n  \"core.post.filters.sort.items.create_time_asc\": \"Creado más antiguo\",\n  \"core.post.filters.sort.items.visit_desc\": \"Máximo de visitas\",\n  \"core.post.filters.sort.items.comment_desc\": \"Número máximo de comentarios\",\n  \"core.post.filters.sort.items.last_modify_time_desc\": \"Actualizado recientemente\",\n  \"core.post.filters.sort.items.last_modify_time_asc\": \"Actualizado más antiguo\",\n  \"core.post.list.fields.categories\": \"Categorías:\",\n  \"core.post.list.fields.visits\": \"{visits} Visitas\",\n  \"core.post.list.fields.comments\": \"{comments} Comentarios\",\n  \"core.post.list.fields.pinned\": \"Fijado\",\n  \"core.post.list.fields.schedule_publish.tooltip\": \"Programar publicación\",\n  \"core.post.list.fields.comments-with-pending\": \" ({count} comentarios pendientes)\",\n  \"core.post.settings.title\": \"Configuraciones\",\n  \"core.post.settings.groups.general\": \"General\",\n  \"core.post.settings.groups.advanced\": \"Avanzado\",\n  \"core.post.settings.groups.annotations\": \"Anotaciones\",\n  \"core.post.settings.fields.title.label\": \"Título\",\n  \"core.post.settings.fields.slug.label\": \"Slug\",\n  \"core.post.settings.fields.slug.help\": \"Usualmente usado para generar el enlace permanente a las publicaciones\",\n  \"core.post.settings.fields.slug.refresh_message\": \"Regenerar slug basado en el título.\",\n  \"core.post.settings.fields.categories.label\": \"Categorías\",\n  \"core.post.settings.fields.tags.label\": \"Etiquetas\",\n  \"core.post.settings.fields.auto_generate_excerpt.label\": \"Generar Extracto Automáticamente\",\n  \"core.post.settings.fields.raw_excerpt.label\": \"Extracto\",\n  \"core.post.settings.fields.allow_comment.label\": \"Permitir Comentarios\",\n  \"core.post.settings.fields.pinned.label\": \"Fijado\",\n  \"core.post.settings.fields.visible.label\": \"Visible\",\n  \"core.post.settings.fields.publish_time.label\": \"Hora de Publicación\",\n  \"core.post.settings.fields.publish_time.help.schedule_publish\": \"Programa una tarea y publícala el {datetime}\",\n  \"core.post.settings.fields.template.label\": \"Plantilla\",\n  \"core.post.settings.fields.cover.label\": \"Portada\",\n  \"core.post.settings.fields.owner.label\": \"Propietario\",\n  \"core.post.tag.filters.sort.items.create_time_desc\": \"Creado más reciente\",\n  \"core.post.tag.filters.sort.items.create_time_asc\": \"Creado más antiguo\",\n  \"core.post.tag.filters.sort.items.display_name_desc\": \"Orden descendente por nombre de etiqueta\",\n  \"core.post.tag.filters.sort.items.display_name_asc\": \"Orden ascendente por nombre de etiqueta\",\n  \"core.post.tag.filters.sort.items.post_desc\": \"Más publicaciones\",\n  \"core.post.batch_setting_modal.title\": \"Ajustes de publicaciones por lotes\",\n  \"core.post.batch_setting_modal.fields.common.enabled\": \"Habilitado\",\n  \"core.post.batch_setting_modal.fields.common.op.label\": \"Operar\",\n  \"core.post.batch_setting_modal.fields.common.op.options.add\": \"Añadir\",\n  \"core.post.batch_setting_modal.fields.common.op.options.replace\": \"Reemplazar\",\n  \"core.post.batch_setting_modal.fields.common.op.options.remove_all\": \"Eliminar todo\",\n  \"core.post.batch_setting_modal.fields.category_group\": \"Categoría\",\n  \"core.post.batch_setting_modal.fields.category_names\": \"Seleccionar categorías\",\n  \"core.post.batch_setting_modal.fields.tag_group\": \"Etiqueta\",\n  \"core.post.batch_setting_modal.fields.tag_names\": \"Seleccionar etiquetas\",\n  \"core.post.batch_setting_modal.fields.visible_group\": \"Visible\",\n  \"core.post.batch_setting_modal.fields.visible_value\": \"Seleccionar opción de visibilidad\",\n  \"core.post.batch_setting_modal.fields.allow_comment_group\": \"Permitir comentarios\",\n  \"core.post.batch_setting_modal.fields.allow_comment_value\": \"Elige si permites comentarios\",\n  \"core.post.batch_setting_modal.fields.owner_group\": \"Propietario\",\n  \"core.post.batch_setting_modal.fields.owner_value\": \"Seleccionar propietario\",\n  \"core.deleted_post.title\": \"Publicaciones eliminadas\",\n  \"core.deleted_post.empty.title\": \"No se han colocado publicaciones en la papelera de reciclaje.\",\n  \"core.deleted_post.empty.message\": \"Puedes intentar actualizar o volver a la página anterior.\",\n  \"core.deleted_post.operations.delete.title\": \"¿Estás seguro de que deseas eliminar permanentemente esta publicación?\",\n  \"core.deleted_post.operations.delete.description\": \"Después de la eliminación, no será posible recuperarla.\",\n  \"core.deleted_post.operations.delete_in_batch.title\": \"¿Estás seguro de que deseas eliminar permanentemente las publicaciones seleccionadas?\",\n  \"core.deleted_post.operations.delete_in_batch.description\": \"Después de la eliminación, no será posible recuperarlas.\",\n  \"core.deleted_post.operations.recovery.title\": \"¿Quieres restaurar esta publicación?\",\n  \"core.deleted_post.operations.recovery.description\": \"Esta operación restaurará la publicación a su estado antes de la eliminación.\",\n  \"core.deleted_post.operations.recovery_in_batch.title\": \"¿Estás seguro de que deseas restaurar las publicaciones seleccionadas?\",\n  \"core.deleted_post.operations.recovery_in_batch.description\": \"Esta operación restaurará las publicaciones a su estado antes de la eliminación.\",\n  \"core.post_editor.title\": \"Edición de publicación\",\n  \"core.post_editor.untitled\": \"Publicación sin título\",\n  \"core.post_editor.actions.snapshots\": \"Instantáneas\",\n  \"core.post_tag.title\": \"Etiquetas de publicación\",\n  \"core.post_tag.header.title\": \"{count} Etiquetas\",\n  \"core.post_tag.empty.title\": \"No hay etiquetas actualmente.\",\n  \"core.post_tag.empty.message\": \"Puedes intentar actualizar o crear una nueva etiqueta.\",\n  \"core.post_tag.operations.delete.title\": \"¿Estás seguro de que deseas eliminar esta etiqueta?\",\n  \"core.post_tag.operations.delete.description\": \"Después de eliminar esta etiqueta, se eliminará la asociación con el artículo correspondiente. Esta operación no se puede deshacer.\",\n  \"core.post_tag.operations.delete_in_batch.title\": \"Eliminar las etiquetas seleccionadas\",\n  \"core.post_tag.editing_modal.titles.update\": \"Actualizar etiqueta de publicación\",\n  \"core.post_tag.editing_modal.titles.create\": \"Crear etiqueta de publicación\",\n  \"core.post_tag.editing_modal.groups.general\": \"General\",\n  \"core.post_tag.editing_modal.groups.annotations\": \"Anotaciones\",\n  \"core.post_tag.editing_modal.fields.display_name.label\": \"Nombre para mostrar\",\n  \"core.post_tag.editing_modal.fields.slug.label\": \"Slug\",\n  \"core.post_tag.editing_modal.fields.slug.help\": \"Usualmente utilizado para generar el enlace permanente de las etiquetas\",\n  \"core.post_tag.editing_modal.fields.slug.refresh_message\": \"Regenerar slug basado en el nombre para mostrar.\",\n  \"core.post_tag.editing_modal.fields.color.label\": \"Color\",\n  \"core.post_tag.editing_modal.fields.color.help\": \"Se requiere adaptación del tema para ser compatible\",\n  \"core.post_tag.editing_modal.fields.cover.label\": \"Portada\",\n  \"core.post_tag.editing_modal.fields.cover.help\": \"Se requiere adaptación del tema para ser compatible\",\n  \"core.post_tag.editing_modal.fields.description.label\": \"Descripción\",\n  \"core.post_tag.editing_modal.fields.description.help\": \"Se requiere adaptación del tema para ser compatible\",\n  \"core.post_category.title\": \"Categorías de publicación\",\n  \"core.post_category.header.title\": \"{count} Categorías\",\n  \"core.post_category.empty.title\": \"No hay categorías actualmente.\",\n  \"core.post_category.empty.message\": \"Puedes intentar actualizar o crear una nueva categoría.\",\n  \"core.post_category.operations.delete.title\": \"¿Estás seguro de que deseas eliminar esta categoría?\",\n  \"core.post_category.operations.delete.description\": \"Después de eliminar esta categoría, se eliminará la asociación con los artículos correspondientes. Esta operación no se puede deshacer.\",\n  \"core.post_category.operations.add_sub_category.button\": \"Agregar subcategoría\",\n  \"core.post_category.editing_modal.titles.update\": \"Actualizar categoría de publicación\",\n  \"core.post_category.editing_modal.titles.create\": \"Crear categoría de publicación\",\n  \"core.post_category.editing_modal.groups.general\": \"General\",\n  \"core.post_category.editing_modal.groups.annotations\": \"Anotaciones\",\n  \"core.post_category.editing_modal.fields.parent.label\": \"Padre\",\n  \"core.post_category.editing_modal.fields.display_name.label\": \"Nombre para mostrar\",\n  \"core.post_category.editing_modal.fields.slug.label\": \"Slug\",\n  \"core.post_category.editing_modal.fields.slug.help\": \"Usualmente utilizado para generar el enlace permanente de las categorías\",\n  \"core.post_category.editing_modal.fields.slug.refresh_message\": \"Regenerar slug basado en el nombre para mostrar.\",\n  \"core.post_category.editing_modal.fields.template.label\": \"Plantilla personalizada\",\n  \"core.post_category.editing_modal.fields.template.help\": \"Personaliza la plantilla de renderizado de la página de archivo de la categoría, lo que requiere soporte del tema\",\n  \"core.post_category.editing_modal.fields.cover.label\": \"Portada\",\n  \"core.post_category.editing_modal.fields.cover.help\": \"Se requiere adaptación del tema para ser compatible\",\n  \"core.post_category.editing_modal.fields.description.label\": \"Descripción\",\n  \"core.post_category.editing_modal.fields.description.help\": \"Se requiere adaptación del tema para ser compatible\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.label\": \"Evitar consulta en cascada de publicación principal\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.help\": \"Si se selecciona, las publicaciones de las subcategorías no se agregarán a la categoría principal\",\n  \"core.post_category.editing_modal.fields.post_template.label\": \"Plantilla de publicación personalizada\",\n  \"core.post_category.editing_modal.fields.post_template.help\": \"Personaliza la plantilla de renderizado de las publicaciones en la categoría actual, lo que requiere soporte del tema\",\n  \"core.post_category.editing_modal.fields.hide_from_list.label\": \"Ocultar de la lista\",\n  \"core.post_category.editing_modal.fields.hide_from_list.help\": \"Después de activar esta opción, esta categoría y sus subcategorías, así como sus publicaciones, no se mostrarán en la lista frontal. Necesitas visitar activamente la página de archivo de la categoría. Esta función solo es efectiva para el directorio de primer nivel.\",\n  \"core.post_category.list.fields.prevent_parent_post_cascade_query\": \"Evitar que la categoría padre incluya esta categoría y sus subcategorías en consultas de publicaciones en cascada\",\n  \"core.post_category.list.fields.hide_from_list\": \"Esta categoría está oculta. Esta categoría y sus subcategorías, así como sus publicaciones, no se mostrarán en la lista frontal. Necesitas visitar activamente la página de archivo de la categoría.\",\n  \"core.page.title\": \"Páginas\",\n  \"core.page.actions.recycle_bin\": \"Papelera de reciclaje\",\n  \"core.page.empty.title\": \"No hay páginas actualmente.\",\n  \"core.page.empty.message\": \"Puedes intentar actualizar o crear una nueva página.\",\n  \"core.page.operations.delete.title\": \"¿Estás seguro de que deseas eliminar esta página?\",\n  \"core.page.operations.delete.description\": \"Esta operación moverá la página a la papelera de reciclaje, y podrá ser restaurada desde la papelera de reciclaje posteriormente.\",\n  \"core.page.operations.delete_in_batch.title\": \"¿Estás seguro de que deseas eliminar las páginas seleccionadas?\",\n  \"core.page.operations.delete_in_batch.description\": \"Esta operación moverá las páginas a la papelera de reciclaje, y podrá ser restaurada desde la papelera de reciclaje posteriormente.\",\n  \"core.page.filters.status.items.published\": \"Publicado\",\n  \"core.page.filters.status.items.draft\": \"Borrador\",\n  \"core.page.filters.visible.label\": \"Visible\",\n  \"core.page.filters.visible.result\": \"Visible: {visible}\",\n  \"core.page.filters.visible.items.public\": \"Público\",\n  \"core.page.filters.visible.items.private\": \"Privado\",\n  \"core.page.filters.author.label\": \"Autor\",\n  \"core.page.filters.author.result\": \"Autor: {author}\",\n  \"core.page.filters.sort.items.publish_time_desc\": \"Publicado más reciente\",\n  \"core.page.filters.sort.items.publish_time_asc\": \"Publicado más antiguo\",\n  \"core.page.filters.sort.items.create_time_desc\": \"Creado más reciente\",\n  \"core.page.filters.sort.items.create_time_asc\": \"Creado más antiguo\",\n  \"core.page.list.fields.visits\": \"{visits} Visitas\",\n  \"core.page.list.fields.comments\": \"{comments} Comentarios\",\n  \"core.page.list.fields.comments-with-pending\": \" ({count} comentarios pendientes)\",\n  \"core.page.settings.title\": \"Configuraciones\",\n  \"core.page.settings.groups.general\": \"General\",\n  \"core.page.settings.groups.advanced\": \"Avanzado\",\n  \"core.page.settings.groups.annotations\": \"Anotaciones\",\n  \"core.page.settings.fields.title.label\": \"Título\",\n  \"core.page.settings.fields.slug.label\": \"Slug\",\n  \"core.page.settings.fields.slug.help\": \"Usualmente utilizado para generar el enlace permanente de las páginas\",\n  \"core.page.settings.fields.slug.refresh_message\": \"Regenerar slug basado en el título.\",\n  \"core.page.settings.fields.auto_generate_excerpt.label\": \"Generar Extracto Automáticamente\",\n  \"core.page.settings.fields.raw_excerpt.label\": \"Extracto\",\n  \"core.page.settings.fields.allow_comment.label\": \"Permitir Comentarios\",\n  \"core.page.settings.fields.pinned.label\": \"Fijado\",\n  \"core.page.settings.fields.visible.label\": \"Visible\",\n  \"core.page.settings.fields.publish_time.label\": \"Hora de Publicación\",\n  \"core.page.settings.fields.template.label\": \"Plantilla\",\n  \"core.page.settings.fields.cover.label\": \"Portada\",\n  \"core.deleted_page.title\": \"Páginas Eliminadas\",\n  \"core.deleted_page.empty.title\": \"No hay páginas en la papelera de reciclaje.\",\n  \"core.deleted_page.empty.message\": \"Puedes intentar actualizar o volver a la página anterior.\",\n  \"core.deleted_page.operations.delete.title\": \"¿Estás seguro de que deseas eliminar permanentemente esta página?\",\n  \"core.deleted_page.operations.delete.description\": \"Después de la eliminación, no será posible recuperarla.\",\n  \"core.deleted_page.operations.delete_in_batch.title\": \"¿Estás seguro de que deseas eliminar permanentemente las páginas seleccionadas?\",\n  \"core.deleted_page.operations.delete_in_batch.description\": \"Después de la eliminación, no será posible recuperarlas.\",\n  \"core.deleted_page.operations.recovery.title\": \"¿Quieres restaurar esta página?\",\n  \"core.deleted_page.operations.recovery.description\": \"Esta operación restaurará la página a su estado antes de la eliminación.\",\n  \"core.deleted_page.operations.recovery_in_batch.title\": \"¿Estás seguro de que deseas restaurar las páginas seleccionadas?\",\n  \"core.deleted_page.operations.recovery_in_batch.description\": \"Esta operación restaurará las páginas a su estado antes de la eliminación.\",\n  \"core.page_editor.title\": \"Edición de Página\",\n  \"core.page_editor.untitled\": \"Página Sin Título\",\n  \"core.page_editor.actions.snapshots\": \"Instantáneas\",\n  \"core.comment.title\": \"Comentarios\",\n  \"core.comment.empty.title\": \"No hay comentarios actualmente.\",\n  \"core.comment.empty.message\": \"Puedes intentar actualizar o modificar los criterios de filtrado.\",\n  \"core.comment.reply_empty.title\": \"No hay respuestas actualmente.\",\n  \"core.comment.reply_empty.message\": \"Puedes intentar actualizar o crear una nueva respuesta.\",\n  \"core.comment.reply_empty.new\": \"Nueva Respuesta\",\n  \"core.comment.operations.delete_comment.title\": \"¿Estás seguro de que deseas eliminar este comentario?\",\n  \"core.comment.operations.delete_comment.description\": \"Todas las respuestas bajo los comentarios se eliminarán al mismo tiempo, y esta operación no se puede deshacer.\",\n  \"core.comment.operations.delete_comment_in_batch.title\": \"¿Estás seguro de que deseas eliminar los comentarios seleccionados?\",\n  \"core.comment.operations.delete_comment_in_batch.description\": \"Todas las respuestas bajo los comentarios se eliminarán al mismo tiempo, y esta operación no se puede deshacer.\",\n  \"core.comment.operations.approve_comment_in_batch.button\": \"Aprobar\",\n  \"core.comment.operations.approve_comment_in_batch.title\": \"¿Estás seguro de que deseas aprobar los comentarios seleccionados para su revisión?\",\n  \"core.comment.operations.approve_applies_in_batch.button\": \"Aprobar todas las respuestas\",\n  \"core.comment.operations.approve_applies_in_batch.title\": \"¿Estás seguro de que deseas aprobar todas las respuestas a este comentario para su revisión?\",\n  \"core.comment.operations.delete_reply.title\": \"¿Estás seguro de que deseas eliminar esta respuesta?\",\n  \"core.comment.operations.reply.button\": \"Responder\",\n  \"core.comment.operations.review.button\": \"Revisar\",\n  \"core.comment.operations.approve.button\": \"Aprobar\",\n  \"core.comment.operations.reply_and_approve.button\": \"Responder y aprobar\",\n  \"core.comment.operations.cancel_approve.button\": \"Cancelar aprobación\",\n  \"core.comment.filters.status.items.approved\": \"Aprobado\",\n  \"core.comment.filters.status.items.pending_review\": \"Pendiente de Revisión\",\n  \"core.comment.filters.owner.label\": \"Propietario\",\n  \"core.comment.filters.owner.result\": \"Propietario: {owner}\",\n  \"core.comment.filters.sort.items.last_reply_time_desc\": \"Respuesta Reciente\",\n  \"core.comment.filters.sort.items.last_reply_time_asc\": \"Respuesta Antigua\",\n  \"core.comment.filters.sort.items.reply_count_desc\": \"Más Respuestas\",\n  \"core.comment.filters.sort.items.reply_count_asc\": \"Menos Respuestas\",\n  \"core.comment.filters.sort.items.create_time_desc\": \"Creado más reciente\",\n  \"core.comment.filters.sort.items.create_time_asc\": \"Creado más antiguo\",\n  \"core.comment.list.fields.reply_count\": \"{count} Respuestas\",\n  \"core.comment.list.fields.has_new_replies\": \"Nuevas respuestas\",\n  \"core.comment.list.fields.pending_review\": \"Pendiente de revisión\",\n  \"core.comment.list.fields.private\": \"Privado\",\n  \"core.comment.subject_refs.post\": \"Publicación\",\n  \"core.comment.subject_refs.page\": \"Página\",\n  \"core.comment.subject_refs.unknown\": \"Desconocido\",\n  \"core.comment.reply_modal.title\": \"Respuesta\",\n  \"core.comment.reply_modal.fields.content.label\": \"Contenido\",\n  \"core.comment.reply_modal.operations.submit.toast_success\": \"Respuesta enviada exitosamente\",\n  \"core.comment.text.commented_on\": \"comentó en\",\n  \"core.comment.text.replied_below\": \"respondió:\",\n  \"core.comment.detail_modal.fields.owner\": \"Comentarista\",\n  \"core.comment.detail_modal.fields.creation_time\": \"Hora del comentario\",\n  \"core.comment.detail_modal.fields.commented_on\": \"Comentó en\",\n  \"core.comment.detail_modal.fields.new_reply\": \"Nueva respuesta\",\n  \"core.comment.detail_modal.fields.user_agent\": \"Agente de usuario\",\n  \"core.comment.comment_detail_modal.title\": \"Detalle del comentario\",\n  \"core.comment.comment_detail_modal.fields.content\": \"Contenido del comentario\",\n  \"core.comment.reply_detail_modal.title\": \"Detalle de la respuesta\",\n  \"core.comment.reply_detail_modal.fields.original_comment\": \"Comentario original\",\n  \"core.comment.reply_detail_modal.fields.content\": \"Contenido de la respuesta\",\n  \"core.attachment.title\": \"Adjuntos\",\n  \"core.attachment.common.text.ungrouped\": \"Sin grupo\",\n  \"core.attachment.common.text.original\": \"Original\",\n  \"core.attachment.actions.storage_policies\": \"Políticas de Almacenamiento\",\n  \"core.attachment.empty.title\": \"No hay adjuntos en el grupo actual.\",\n  \"core.attachment.empty.message\": \"El grupo actual no tiene adjuntos, puedes intentar actualizar o cargar adjuntos.\",\n  \"core.attachment.empty.actions.upload\": \"Cargar Adjunto\",\n  \"core.attachment.operations.delete.title\": \"¿Estás seguro de que deseas eliminar este adjunto?\",\n  \"core.attachment.operations.delete_in_batch.title\": \"¿Estás seguro de que deseas eliminar los adjuntos seleccionados?\",\n  \"core.attachment.operations.deselect_items.button\": \"Deseleccionar ítems\",\n  \"core.attachment.operations.move.button\": \"Mover\",\n  \"core.attachment.operations.move.toast_success\": \"Movimiento exitoso\",\n  \"core.attachment.filters.storage_policy.label\": \"Política de Almacenamiento\",\n  \"core.attachment.filters.storage_policy.result\": \"Política de Almacenamiento: {storage_policy}\",\n  \"core.attachment.filters.owner.label\": \"Propietario\",\n  \"core.attachment.filters.owner.result\": \"Propietario: {owner}\",\n  \"core.attachment.filters.sort.items.create_time_desc\": \"Más recientes\",\n  \"core.attachment.filters.sort.items.create_time_asc\": \"Más antiguos\",\n  \"core.attachment.filters.sort.items.size_desc\": \"Ordenar por tamaño descendente\",\n  \"core.attachment.filters.sort.items.size_asc\": \"Ordenar por tamaño ascendente\",\n  \"core.attachment.filters.sort.items.display_name_asc\": \"Orden ascendente por nombre para mostrar\",\n  \"core.attachment.filters.sort.items.display_name_desc\": \"Orden descendente por nombre para mostrar\",\n  \"core.attachment.filters.view_type.items.grid\": \"Modo Cuadrícula\",\n  \"core.attachment.filters.view_type.items.list\": \"Modo Lista\",\n  \"core.attachment.filters.accept.label\": \"Tipo\",\n  \"core.attachment.filters.accept.items.image\": \"Imagen\",\n  \"core.attachment.filters.accept.items.audio\": \"Audio\",\n  \"core.attachment.filters.accept.items.video\": \"Video\",\n  \"core.attachment.filters.accept.items.file\": \"Archivo\",\n  \"core.attachment.detail_modal.title\": \"Adjunto: {display_name}\",\n  \"core.attachment.detail_modal.fields.preview\": \"Vista Previa\",\n  \"core.attachment.detail_modal.fields.storage_policy\": \"Política de Almacenamiento\",\n  \"core.attachment.detail_modal.fields.group\": \"Grupo\",\n  \"core.attachment.detail_modal.fields.display_name\": \"Nombre para Mostrar\",\n  \"core.attachment.detail_modal.fields.media_type\": \"Tipo de Medio\",\n  \"core.attachment.detail_modal.fields.size\": \"Tamaño\",\n  \"core.attachment.detail_modal.fields.owner\": \"Propietario\",\n  \"core.attachment.detail_modal.fields.creation_time\": \"Hora de Creación\",\n  \"core.attachment.detail_modal.fields.permalink\": \"Enlace Permanente\",\n  \"core.attachment.detail_modal.preview.video_not_support\": \"El navegador actual no admite la reproducción de video.\",\n  \"core.attachment.detail_modal.preview.audio_not_support\": \"El navegador actual no admite la reproducción de audio.\",\n  \"core.attachment.detail_modal.preview.not_support\": \"Este archivo no admite la vista previa.\",\n  \"core.attachment.detail_modal.display_name_form.help\": \"Nombre de adjunto personalizado, solo para visualización en la interfaz de gestión, no afecta al nombre del archivo ni al enlace de acceso\",\n  \"core.attachment.group_editing_modal.titles.create\": \"Crear grupo de adjuntos\",\n  \"core.attachment.group_editing_modal.titles.update\": \"Actualizar grupo de adjuntos\",\n  \"core.attachment.group_editing_modal.fields.display_name.label\": \"Nombre para Mostrar\",\n  \"core.attachment.group_editing_modal.toast.group_name_exists\": \"El nombre del grupo ya existe\",\n  \"core.attachment.group_list.internal_groups.all\": \"Todo\",\n  \"core.attachment.group_list.operations.rename.button\": \"Cambiar nombre\",\n  \"core.attachment.group_list.operations.delete.button\": \"Y mover adjunto a sin grupo\",\n  \"core.attachment.group_list.operations.delete.title\": \"¿Estás seguro de que deseas eliminar este grupo?\",\n  \"core.attachment.group_list.operations.delete.description\": \"El grupo se eliminará, y los adjuntos bajo el grupo se moverán a sin grupo. Esta operación no se puede deshacer.\",\n  \"core.attachment.group_list.operations.delete.toast_success\": \"Eliminación exitosa, {total} adjuntos se han movido a sin grupo\",\n  \"core.attachment.group_list.operations.delete_with_attachments.button\": \"También eliminar adjuntos\",\n  \"core.attachment.group_list.operations.delete_with_attachments.title\": \"¿Estás seguro de que deseas eliminar este grupo?\",\n  \"core.attachment.group_list.operations.delete_with_attachments.description\": \"Al eliminar el grupo y todos los adjuntos dentro de él, esta acción no se puede deshacer.\",\n  \"core.attachment.group_list.operations.delete_with_attachments.toast_success\": \"Eliminación exitosa, {total} adjuntos se han eliminado simultáneamente\",\n  \"core.attachment.policies_modal.title\": \"Políticas de Almacenamiento\",\n  \"core.attachment.policies_modal.empty.title\": \"Actualmente no hay estrategias de almacenamiento disponibles.\",\n  \"core.attachment.policies_modal.empty.message\": \"No hay políticas de almacenamiento disponibles en este momento. Puedes intentar actualizar o crear una nueva política.\",\n  \"core.attachment.policies_modal.operations.delete.title\": \"¿Estás seguro de que deseas eliminar esta política?\",\n  \"core.attachment.policies_modal.operations.delete.description\": \"No hay adjuntos cargados bajo la política actual.\",\n  \"core.attachment.policies_modal.operations.can_not_delete.title\": \"Fallo en la eliminación\",\n  \"core.attachment.policies_modal.operations.can_not_delete.description\": \"Hay adjuntos bajo esta política, que no se pueden eliminar.\",\n  \"core.attachment.policy_editing_modal.titles.create\": \"Nueva política: {policy_template}\",\n  \"core.attachment.policy_editing_modal.titles.update\": \"Editar política: {policy}\",\n  \"core.attachment.policy_editing_modal.fields.display_name.label\": \"Nombre para Mostrar\",\n  \"core.attachment.policy_editing_modal.fields.hidden.label\": \"Hide in upload interface\",\n  \"core.attachment.policy_editing_modal.fields.hidden.help\": \"When enabled, this storage policy will be hidden in the upload interface\",\n  \"core.attachment.policy_editing_modal.fields.config.label\": \"Configuración de la política\",\n  \"core.attachment.policy_editing_modal.fields.priority.label\": \"Prioridad de visualización en la pantalla de carga\",\n  \"core.attachment.policy_editing_modal.fields.priority.help\": \"Establece la prioridad de visualización en la pantalla de carga. Cuanto mayor sea el número, más arriba aparecerá en la lista\",\n  \"core.attachment.policy_editing_modal.toast.policy_name_exists\": \"El nombre de la política de almacenamiento ya existe\",\n  \"core.attachment.upload_modal.title\": \"Cargar adjunto\",\n  \"core.attachment.upload_modal.filters.group.label\": \"Seleccionar grupo:\",\n  \"core.attachment.upload_modal.filters.policy.label\": \"Seleccionar política de almacenamiento:\",\n  \"core.attachment.upload_modal.filters.policy.empty.title\": \"Sin política de almacenamiento\",\n  \"core.attachment.upload_modal.filters.policy.empty.description\": \"Antes de cargar, es necesario crear una nueva política de almacenamiento.\",\n  \"core.attachment.upload_modal.filters.policy.not_select\": \"Por favor, selecciona una política de almacenamiento primero\",\n  \"core.attachment.upload_modal.upload_options.local_upload\": \"Cargar\",\n  \"core.attachment.upload_modal.upload_options.download\": \"Descargar desde URL\",\n  \"core.attachment.upload_modal.download_form.fields.url.label\": \"URL\",\n  \"core.attachment.upload_modal.download_form.toast.success\": \"Descargado con éxito\",\n  \"core.attachment.select_modal.title\": \"Seleccionar adjunto\",\n  \"core.attachment.select_modal.providers.default.label\": \"Adjuntos\",\n  \"core.attachment.select_modal.operations.select.result\": \"({count} elementos seleccionados)\",\n  \"core.attachment.permalink_list.relative\": \"Ruta relativa\",\n  \"core.attachment.permalink_list.absolute\": \"Ruta absoluta\",\n  \"core.theme.title\": \"Temas\",\n  \"core.theme.common.buttons.install\": \"Instalar Tema\",\n  \"core.theme.tabs.detail\": \"Detalles\",\n  \"core.theme.actions.management\": \"Gestión de Temas\",\n  \"core.theme.empty.title\": \"No hay temas activados o seleccionados actualmente.\",\n  \"core.theme.empty.message\": \"Puedes cambiar de tema o instalar nuevos.\",\n  \"core.theme.empty.actions.switch\": \"Cambiar de Tema\",\n  \"core.theme.operations.active.title\": \"¿Estás seguro de activar el tema actual?\",\n  \"core.theme.operations.active.toast_success\": \"Tema activado exitosamente\",\n  \"core.theme.operations.reset.title\": \"¿Estás seguro de que deseas restablecer todas las configuraciones del tema?\",\n  \"core.theme.operations.reset.description\": \"Esta operación eliminará la configuración guardada y la restablecerá a los ajustes predeterminados.\",\n  \"core.theme.operations.reset.toast_success\": \"Configuración restablecida exitosamente\",\n  \"core.theme.operations.reload.button\": \"Recargar\",\n  \"core.theme.operations.reload.title\": \"¿Estás seguro de que deseas recargar todas las configuraciones del tema?\",\n  \"core.theme.operations.reload.description\": \"Esta operación solo recargará la configuración del tema y la definición del formulario de ajustes, y no eliminará ninguna configuración guardada.\",\n  \"core.theme.operations.reload.toast_success\": \"Recarga de configuración exitosa\",\n  \"core.theme.operations.uninstall.title\": \"¿Estás seguro de que deseas desinstalar este tema?\",\n  \"core.theme.operations.uninstall_and_delete_config.button\": \"Desinstalar y eliminar configuración\",\n  \"core.theme.operations.uninstall_and_delete_config.title\": \"¿Estás seguro de que deseas desinstalar este tema y su configuración correspondiente?\",\n  \"core.theme.operations.remote_download.title\": \"Se ha detectado una dirección de descarga remota, ¿deseas descargar?\",\n  \"core.theme.operations.remote_download.description\": \"Por favor, verifica cuidadosamente si esta dirección es confiable: {url}\",\n  \"core.theme.operations.existed_during_installation.title\": \"El tema ya existe.\",\n  \"core.theme.operations.existed_during_installation.description\": \"El tema que intentas instalar ya existe, ¿quieres actualizarlo?\",\n  \"core.theme.operations.clear_templates_cache.button\": \"Limpiar caché de plantillas\",\n  \"core.theme.operations.clear_templates_cache.title\": \"Limpiar caché de plantillas\",\n  \"core.theme.operations.clear_templates_cache.description\": \"Esta función permite refrescar la caché para ver los últimos resultados web tras modificar archivos de plantilla en tiempo de ejecución.\",\n  \"core.theme.operations.export_configuration.button\": \"Exportar configuración del tema\",\n  \"core.theme.operations.import_configuration.button\": \"Importar configuración del tema\",\n  \"core.theme.operations.import_configuration.version_mismatch.title\": \"Versión no coincidente\",\n  \"core.theme.operations.import_configuration.version_mismatch.description\": \"La versión del archivo de configuración importado no coincide con la versión del tema actual, lo que puede causar problemas de compatibilidad. ¿Deseas continuar con la importación?\",\n  \"core.theme.operations.import_configuration.invalid_format\": \"Archivo de configuración de tema inválido\",\n  \"core.theme.operations.import_configuration.mismatched_theme\": \"El archivo de configuración no coincide con el tema seleccionado\",\n  \"core.theme.list_modal.tabs.installed\": \"Instalados\",\n  \"core.theme.list_modal.tabs.not_installed\": \"No Instalados\",\n  \"core.theme.list_modal.tabs.local_upload\": \"Instalación local / actualización\",\n  \"core.theme.list_modal.tabs.remote_download.label\": \"Remoto\",\n  \"core.theme.list_modal.tabs.remote_download.fields.url\": \"URL remota\",\n  \"core.theme.list_modal.empty.title\": \"No hay temas instalados actualmente.\",\n  \"core.theme.list_modal.empty.message\": \"No hay temas instalados actualmente, puedes intentar actualizar o instalar un nuevo tema.\",\n  \"core.theme.list_modal.not_installed_empty.title\": \"No hay temas actualmente no instalados.\",\n  \"core.theme.preview_model.title\": \"Vista Previa del Tema: {display_name}\",\n  \"core.theme.preview_model.actions.switch\": \"Cambiar de tema\",\n  \"core.theme.preview_model.actions.setting\": \"Ajustes\",\n  \"core.theme.preview_model.actions.open\": \"Abrir\",\n  \"core.theme.detail.fields.author\": \"Autor\",\n  \"core.theme.detail.fields.repo\": \"Repositorio Fuente\",\n  \"core.theme.detail.fields.version\": \"Versión\",\n  \"core.theme.detail.fields.requires\": \"Requiere\",\n  \"core.theme.detail.fields.storage_location\": \"Ubicación de Almacenamiento\",\n  \"core.theme.detail.fields.plugin_requires\": \"Requiere Plugin\",\n  \"core.theme.detail.fields.homepage\": \"Sitio web\",\n  \"core.theme.detail.fields.description\": \"Descripción\",\n  \"core.theme.detail.fields.license\": \"Licencia\",\n  \"core.theme.detail.fields.issues\": \"Comentarios sobre problemas\",\n  \"core.theme.settings.title\": \"Ajustes del Tema\",\n  \"core.theme.custom_templates.default\": \"Predeterminado\",\n  \"core.menu.title\": \"Menús\",\n  \"core.menu.empty.title\": \"Actualmente no hay menús.\",\n  \"core.menu.empty.message\": \"Puedes intentar actualizar o crear un nuevo menú.\",\n  \"core.menu.menu_item_empty.title\": \"Actualmente no hay elementos de menú.\",\n  \"core.menu.menu_item_empty.message\": \"Puedes intentar actualizar o crear un nuevo elemento de menú.\",\n  \"core.menu.operations.set_primary.button\": \"Establecer como menú principal\",\n  \"core.menu.operations.set_primary.toast_success\": \"Configuración exitosa\",\n  \"core.menu.operations.delete_menu.title\": \"¿Estás seguro de que deseas eliminar este menú?\",\n  \"core.menu.operations.delete_menu.description\": \"Todos los elementos de menú de este menú se eliminarán al mismo tiempo y esta operación no se puede deshacer.\",\n  \"core.menu.operations.delete_menu_item.title\": \"¿Estás seguro de que deseas eliminar este elemento de menú?\",\n  \"core.menu.operations.delete_menu_item.description\": \"Todos los subelementos de menú se eliminarán al mismo tiempo y no se pueden restaurar después de la eliminación.\",\n  \"core.menu.operations.add_sub_menu_item.button\": \"Agregar subelemento de menú\",\n  \"core.menu.list.fields.primary\": \"Principal\",\n  \"core.menu.list.fields.items_count\": \"{count} elementos\",\n  \"core.menu.menu_editing_modal.titles.create\": \"Crear menú\",\n  \"core.menu.menu_editing_modal.titles.update\": \"Actualizar menú\",\n  \"core.menu.menu_editing_modal.fields.display_name.label\": \"Nombre para mostrar\",\n  \"core.menu.menu_item_editing_modal.titles.create\": \"Crear elemento de menú\",\n  \"core.menu.menu_item_editing_modal.titles.update\": \"Actualizar elemento de menú\",\n  \"core.menu.menu_item_editing_modal.groups.general\": \"General\",\n  \"core.menu.menu_item_editing_modal.groups.annotations\": \"Anotaciones\",\n  \"core.menu.menu_item_editing_modal.fields.parent.label\": \"Padre\",\n  \"core.menu.menu_item_editing_modal.fields.parent.placeholder\": \"Selecciona el elemento de menú padre\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.label\": \"Tipo\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.placeholder\": \"Por favor selecciona {label}\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.custom\": \"Personalizado\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.post\": \"Publicación\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page\": \"Página\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.category\": \"Categoría\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.tag\": \"Etiqueta\",\n  \"core.menu.menu_item_editing_modal.fields.display_name.label\": \"Nombre para mostrar\",\n  \"core.menu.menu_item_editing_modal.fields.href.label\": \"Dirección del enlace\",\n  \"core.menu.menu_item_editing_modal.fields.target.label\": \"Destino\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.self\": \"_self\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.blank\": \"_blank\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.parent\": \"_parent\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.top\": \"_top\",\n  \"core.plugin.title\": \"Plugins\",\n  \"core.plugin.tabs.detail\": \"Detail\",\n  \"core.plugin.empty.title\": \"There are no installed plugins currently.\",\n  \"core.plugin.empty.message\": \"There are no installed plugins currently, you can try refreshing or installing new plugins.\",\n  \"core.plugin.empty.actions.install\": \"Install Plugin\",\n  \"core.plugin.operations.reset.title\": \"Are you sure you want to reset all configurations of the plugin?\",\n  \"core.plugin.operations.reset.description\": \"This operation will delete the saved configuration and reset it to default settings.\",\n  \"core.plugin.operations.reset.toast_success\": \"Reset configuration successfully\",\n  \"core.plugin.operations.uninstall.title\": \"Are you sure you want to uninstall this plugin?\",\n  \"core.plugin.operations.uninstall_and_delete_config.title\": \"Are you sure you want to uninstall this plugin and its corresponding configuration?\",\n  \"core.plugin.operations.uninstall_and_delete_config.button\": \"Desinstalar y borrar configuración\",\n  \"core.plugin.operations.uninstall_when_enabled.confirm_text\": \"Stop running and uninstall\",\n  \"core.plugin.operations.uninstall_when_enabled.description\": \"The current plugin is still in the enabled state and will be uninstalled after it stops running. This operation cannot be undone.\",\n  \"core.plugin.operations.remote_download.title\": \"Remote download address detected, do you want to download?\",\n  \"core.plugin.operations.remote_download.description\": \"Please carefully verify whether this address can be trusted: {url}\",\n  \"core.plugin.operations.uninstall_in_batch.title\": \"Desinstalar los complementos seleccionados\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.button\": \"Desinstalar y borrar configuración\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.title\": \"Desinstalar los complementos seleccionados y sus configuraciones correspondientes\",\n  \"core.plugin.operations.change_status_in_batch.activate_title\": \"Activar los complementos seleccionados\",\n  \"core.plugin.operations.change_status_in_batch.inactivate_title\": \"Desactivar los complementos seleccionados\",\n  \"core.plugin.operations.reload_window.button\": \"Reinicio requerido\",\n  \"core.plugin.filters.status.items.active\": \"Active\",\n  \"core.plugin.filters.status.items.inactive\": \"Inactive\",\n  \"core.plugin.filters.sort.items.create_time_desc\": \"Latest Installed\",\n  \"core.plugin.filters.sort.items.create_time_asc\": \"Earliest Installed\",\n  \"core.plugin.upload_modal.titles.install\": \"Install plugin\",\n  \"core.plugin.upload_modal.titles.upgrade\": \"Upgrade plugin ({display_name})\",\n  \"core.plugin.upload_modal.tabs.local\": \"Local\",\n  \"core.plugin.upload_modal.tabs.remote.title\": \"Remote\",\n  \"core.plugin.upload_modal.tabs.remote.fields.url\": \"Remote URL\",\n  \"core.plugin.upload_modal.operations.active_after_install.title\": \"Install successful\",\n  \"core.plugin.upload_modal.operations.active_after_install.description\": \"Would you like to activate the currently installed plugin?\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.title\": \"The plugin already exists.\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.description\": \"The currently installed plugin already exists, do you want to upgrade?\",\n  \"core.plugin.detail.title\": \"Plugin detail\",\n  \"core.plugin.detail.header.title\": \"Plugin information\",\n  \"core.plugin.detail.fields.description\": \"Description\",\n  \"core.plugin.detail.fields.version\": \"Version\",\n  \"core.plugin.detail.fields.requires\": \"Requires\",\n  \"core.plugin.detail.fields.author\": \"Author\",\n  \"core.plugin.detail.fields.license\": \"License\",\n  \"core.plugin.detail.fields.role_templates\": \"Role Templates\",\n  \"core.plugin.detail.fields.last_starttime\": \"Last Start Time\",\n  \"core.plugin.detail.fields.homepage\": \"Página de inicio\",\n  \"core.plugin.detail.fields.repo\": \"Repositorio de origen\",\n  \"core.plugin.detail.fields.load_location\": \"Ubicación de almacenamiento\",\n  \"core.plugin.detail.fields.issues\": \"Comentarios sobre problemas\",\n  \"core.plugin.detail.fields.creation_time\": \"Tiempo de instalación\",\n  \"core.plugin.detail.operations.view_conditions.button\": \"Ver condiciones recientes\",\n  \"core.plugin.loader.toast.entry_load_failed\": \"{name}: Failed to load plugin entry file\",\n  \"core.plugin.loader.toast.style_load_failed\": \"{name}: Failed to load plugin stylesheet file\",\n  \"core.plugin.extension_points.editor.providers.default\": \"Default Editor\",\n  \"core.plugin.list.fields.system_reserved\": \"Reservado por el sistema\",\n  \"core.plugin.conditions_modal.title\": \"Condiciones recientes\",\n  \"core.plugin.conditions_modal.fields.type\": \"Tipo\",\n  \"core.plugin.conditions_modal.fields.status\": \"Estado\",\n  \"core.plugin.conditions_modal.fields.reason\": \"Razón\",\n  \"core.plugin.conditions_modal.fields.message\": \"Mensaje\",\n  \"core.plugin.conditions_modal.fields.last_transition_time\": \"Tiempo de la última transición\",\n  \"core.plugin.extension-settings.title\": \"Ajustes de extensión\",\n  \"core.plugin.extension-settings.extension-point-definition.title\": \"Definiciones de puntos de extensión\",\n  \"core.plugin.extension-settings.extension-definition.empty.title\": \"Actualmente no hay ningún punto de extensión implementado\",\n  \"core.plugin.extension-settings.extension-definition.operation.use.button\": \"Usar\",\n  \"core.plugin.actions.extension-point-settings\": \"Ajustes de extensión\",\n  \"core.user.title\": \"Usuarios\",\n  \"core.user.actions.roles\": \"Roles\",\n  \"core.user.actions.identity_authentication\": \"Autenticación de Identidad\",\n  \"core.user.empty.title\": \"Actualmente no hay usuarios que cumplan con los criterios de filtrado.\",\n  \"core.user.empty.message\": \"No hay usuarios que coincidan con los criterios de filtrado en este momento. Puedes intentar actualizar o crear un nuevo usuario.\",\n  \"core.user.operations.delete.title\": \"¿Estás seguro de que deseas eliminar a este usuario?\",\n  \"core.user.operations.delete_in_batch.title\": \"¿Estás seguro de que deseas eliminar a los usuarios seleccionados?\",\n  \"core.user.operations.update_profile.title\": \"Actualizar perfil\",\n  \"core.user.operations.change_password.title\": \"Cambiar contraseña\",\n  \"core.user.operations.grant_permission.title\": \"Conceder permiso\",\n  \"core.user.operations.enable.title\": \"Habilitar\",\n  \"core.user.operations.enable.description\": \"¿Estás seguro de que quieres habilitar a este usuario? Una vez habilitado, el usuario podrá volver a entrar en el sistema.\",\n  \"core.user.operations.enable_in_batch.title\": \"Habilitar\",\n  \"core.user.operations.enable_in_batch.description\": \"¿Estás seguro de que quieres habilitar a los usuarios seleccionados? Una vez habilitados, podrán volver a entrar en el sistema.\",\n  \"core.user.operations.disable.title\": \"Deshabilitar\",\n  \"core.user.operations.disable.description\": \"¿Estás seguro de que quieres deshabilitar a este usuario? Una vez deshabilitado, ya no podrá entrar.\",\n  \"core.user.operations.disable_in_batch.title\": \"Deshabilitar\",\n  \"core.user.operations.disable_in_batch.description\": \"¿Estás seguro de que quieres deshabilitar a los usuarios seleccionados? Una vez deshabilitados, ya no podrán entrar.\",\n  \"core.user.filters.role.label\": \"Rol\",\n  \"core.user.filters.role.result\": \"Rol: {role}\",\n  \"core.user.filters.sort.items.create_time_desc\": \"Últimos creados\",\n  \"core.user.filters.sort.items.create_time_asc\": \"Más antiguos creados\",\n  \"core.user.editing_modal.titles.update\": \"Editar usuario\",\n  \"core.user.editing_modal.titles.create\": \"Crear usuario\",\n  \"core.user.editing_modal.groups.general\": \"General\",\n  \"core.user.editing_modal.groups.annotations\": \"Anotaciones\",\n  \"core.user.editing_modal.fields.username.label\": \"Nombre de usuario\",\n  \"core.user.editing_modal.fields.username.validation\": \"Por favor, introduce un nombre de usuario válido.\",\n  \"core.user.editing_modal.fields.display_name.label\": \"Nombre para mostrar\",\n  \"core.user.editing_modal.fields.email.label\": \"Correo electrónico\",\n  \"core.user.editing_modal.fields.phone.label\": \"Teléfono\",\n  \"core.user.editing_modal.fields.avatar.label\": \"Avatar\",\n  \"core.user.editing_modal.fields.bio.label\": \"Biografía\",\n  \"core.user.change_password_modal.title\": \"Cambiar contraseña\",\n  \"core.user.change_password_modal.fields.new_password.label\": \"Nueva contraseña\",\n  \"core.user.change_password_modal.fields.confirm_password.label\": \"Confirmar contraseña\",\n  \"core.user.grant_permission_modal.title\": \"Conceder permiso\",\n  \"core.user.grant_permission_modal.fields.role.label\": \"Rol\",\n  \"core.user.grant_permission_modal.fields.role.placeholder\": \"Por favor, selecciona un rol\",\n  \"core.user.grant_permission_modal.roles_preview.all\": \"El rol seleccionado actualmente contiene todos los permisos\",\n  \"core.user.grant_permission_modal.roles_preview.includes\": \"El rol seleccionado actualmente contiene los siguientes permisos:\",\n  \"core.user.detail.title\": \"Detalles del usuario\",\n  \"core.user.detail.tabs.detail\": \"Detalle\",\n  \"core.user.detail.actions.update_profile.title\": \"Actualizar perfil\",\n  \"core.user.detail.actions.change_password.title\": \"Cambiar contraseña\",\n  \"core.user.detail.actions.grant_permission.title\": \"Conceder permiso\",\n  \"core.user.detail.actions.profile.title\": \"Perfil\",\n  \"core.user.detail.fields.display_name\": \"Nombre para mostrar\",\n  \"core.user.detail.fields.username\": \"Nombre de usuario\",\n  \"core.user.detail.fields.email\": \"Correo electrónico\",\n  \"core.user.detail.fields.roles\": \"Roles\",\n  \"core.user.detail.fields.bio\": \"Biografía\",\n  \"core.user.detail.fields.creation_time\": \"Fecha de creación\",\n  \"core.user.detail.fields.email_verified.tooltip\": \"Verificado\",\n  \"core.user.detail.fields.email_not_verified.tooltip\": \"No verificado\",\n  \"core.user.fields.disabled\": \"Deshabilitado\",\n  \"core.role.title\": \"Roles\",\n  \"core.role.common.text.contains_all_permissions\": \"Contiene todos los permisos\",\n  \"core.role.common.text.contains_n_permissions\": \"Contiene {count} permisos\",\n  \"core.role.common.text.system_reserved\": \"Reservado del Sistema\",\n  \"core.role.common.text.custom\": \"Personalizado\",\n  \"core.role.common.text.dependent_on\": \"Dependiente de {roles}\",\n  \"core.role.common.text.provided_by_plugin\": \"Proporcionado por {plugin}\",\n  \"core.role.operations.delete.title\": \"¿Estás seguro de que deseas eliminar este rol?\",\n  \"core.role.operations.delete.description\": \"Una vez eliminado el rol, se eliminarán las asignaciones de rol de los usuarios asociados y esta operación no se puede deshacer.\",\n  \"core.role.operations.create_based_on_this_role.button\": \"Crear basado en este rol\",\n  \"core.role.detail.title\": \"Detalle del rol\",\n  \"core.role.detail.header.title\": \"Información del rol\",\n  \"core.role.detail.tabs.detail\": \"Detalle\",\n  \"core.role.detail.tabs.permissions\": \"Permisos\",\n  \"core.role.detail.fields.display_name\": \"Nombre para mostrar\",\n  \"core.role.detail.fields.name\": \"Nombre\",\n  \"core.role.detail.fields.type\": \"Tipo\",\n  \"core.role.detail.fields.creation_time\": \"Fecha de creación\",\n  \"core.role.permissions_detail.system_reserved_alert.description\": \"El rol reservado del sistema no admite modificaciones. Se recomienda crear un nuevo rol basado en este.\",\n  \"core.role.editing_modal.titles.create\": \"Crear rol\",\n  \"core.role.editing_modal.titles.update\": \"Actualizar rol\",\n  \"core.role.editing_modal.groups.general\": \"General\",\n  \"core.role.editing_modal.groups.permissions\": \"Permisos\",\n  \"core.role.editing_modal.fields.display_name\": \"Nombre para mostrar\",\n  \"core.role.editing_modal.fields.disallow_access_console.label\": \"Deshabilitar acceso a la Consola\",\n  \"core.role.editing_modal.fields.disallow_access_console.help\": \"Una vez marcado, este rol no podrá acceder a la Consola\",\n  \"core.identity_authentication.title\": \"Autenticación de Identidad\",\n  \"core.identity_authentication.tabs.detail\": \"Detalle\",\n  \"core.identity_authentication.tabs.setting\": \"Configuración\",\n  \"core.identity_authentication.operations.enable.title\": \"¿Estás seguro de que deseas habilitar este método de autenticación de identidad?\",\n  \"core.identity_authentication.operations.disable.title\": \"¿Estás seguro de que deseas deshabilitar este método de autenticación de identidad?\",\n  \"core.identity_authentication.operations.disable_privileged.tooltip\": \"El método de autenticación reservado por el sistema no se puede deshabilitar\",\n  \"core.identity_authentication.detail.title\": \"Detalle de la autenticación de identidad\",\n  \"core.identity_authentication.detail.fields.display_name\": \"Nombre para mostrar\",\n  \"core.identity_authentication.detail.fields.description\": \"Descripción\",\n  \"core.identity_authentication.detail.fields.website\": \"Sitio web\",\n  \"core.identity_authentication.detail.fields.help_page\": \"Página de ayuda\",\n  \"core.identity_authentication.detail.fields.authentication_url\": \"URL de inicio de sesión\",\n  \"core.identity_authentication.fields.display_name.local\": \"Iniciar sesión con credenciales\",\n  \"core.identity_authentication.fields.description.local\": \"Método de inicio de sesión predeterminado integrado en Halo\",\n  \"core.identity_authentication.list.types.form\": \"Método de autenticación básica\",\n  \"core.identity_authentication.list.types.oauth2\": \"Método de autenticación de terceros\",\n  \"core.setting.title\": \"Configuraciones\",\n  \"core.overview.title\": \"Visión general\",\n  \"core.overview.actions.copy.toast_browser_not_supported\": \"El navegador actual no admite la función de copiado\",\n  \"core.overview.header.titles.general\": \"Información general\",\n  \"core.overview.header.titles.environment\": \"Información del entorno\",\n  \"core.overview.fields.external_url\": \"URL externa\",\n  \"core.overview.fields.start_time\": \"Hora de inicio\",\n  \"core.overview.fields.timezone\": \"Zona horaria\",\n  \"core.overview.fields.locale\": \"Idioma\",\n  \"core.overview.fields.version\": \"Versión\",\n  \"core.overview.fields.build_time\": \"Fecha de compilación\",\n  \"core.overview.fields.database\": \"Base de datos\",\n  \"core.overview.fields.os\": \"Sistema operativo\",\n  \"core.overview.fields.log\": \"Registro del sistema\",\n  \"core.overview.fields.activated_theme\": \"Tema activado\",\n  \"core.overview.fields.enabled_plugins\": \"Complementos habilitados\",\n  \"core.overview.fields_values.external_url.not_setup\": \"No configurado\",\n  \"core.overview.copy_results.external_url\": \"URL externa: {external_url}\",\n  \"core.overview.copy_results.start_time\": \"Hora de inicio: {start_time}\",\n  \"core.overview.copy_results.version\": \"Versión: {version}\",\n  \"core.overview.copy_results.build_time\": \"Fecha de compilación: {build_time}\",\n  \"core.overview.copy_results.database\": \"Base de datos: {database}\",\n  \"core.overview.copy_results.os\": \"Sistema operativo: {os}\",\n  \"core.overview.alert.external_url_invalid\": \"La URL de acceso externo detectada no coincide con la URL de acceso actual, lo que podría causar que algunos enlaces no se redirijan correctamente. Por favor, verifica la configuración de la URL de acceso externo.\",\n  \"core.overview.external_url_form.operations.save.title\": \"Modificar URL externa\",\n  \"core.overview.external_url_form.operations.save.description\": \"Modificar la dirección de acceso externa requiere reiniciar el servicio Halo. Se reiniciará automáticamente tras completar la modificación. ¿Deseas continuar?\",\n  \"core.overview.external_url_form.tips.restarting\": \"Modificación completada, esperando el reinicio...\",\n  \"core.backup.title\": \"Copia de Seguridad y Restauración\",\n  \"core.backup.tabs.backup_list\": \"Copias de Seguridad\",\n  \"core.backup.tabs.restore\": \"Restaurar\",\n  \"core.backup.empty.title\": \"Aún no se han creado copias de seguridad\",\n  \"core.backup.empty.message\": \"Puedes hacer clic en el botón debajo para crear una copia de seguridad\",\n  \"core.backup.operations.create.button\": \"Crear copia de seguridad\",\n  \"core.backup.operations.create.title\": \"Crear copia de seguridad\",\n  \"core.backup.operations.create.description\": \"¿Estás seguro de que deseas crear una copia de seguridad? Esta operación puede tomar un tiempo.\",\n  \"core.backup.operations.create.toast_success\": \"Solicitud de creación de copia de seguridad realizada\",\n  \"core.backup.operations.delete.title\": \"Eliminar copia de seguridad\",\n  \"core.backup.operations.delete.description\": \"¿Estás seguro de que deseas eliminar esta copia de seguridad?\",\n  \"core.backup.operations.restore.title\": \"Restauración exitosa\",\n  \"core.backup.operations.restore.description\": \"Después de una restauración exitosa, necesitas reiniciar Halo para cargar los recursos del sistema normalmente. Después de hacer clic en OK, reiniciaremos automáticamente Halo.\",\n  \"core.backup.operations.restart.toast_success\": \"Solicitud de reinicio realizada\",\n  \"core.backup.operations.remote_download.button\": \"Descargar y restaurar\",\n  \"core.backup.operations.restore_by_backup.button\": \"Restaurar\",\n  \"core.backup.operations.restore_by_backup.title\": \"Restaurar desde archivo de respaldo\",\n  \"core.backup.operations.restore_by_backup.description\": \"Tras hacer clic en Aceptar, los datos se restaurarán desde el archivo de respaldo {filename}.\",\n  \"core.backup.list.phases.pending\": \"Pendiente\",\n  \"core.backup.list.phases.running\": \"En Progreso\",\n  \"core.backup.list.phases.succeeded\": \"Exitosa\",\n  \"core.backup.list.phases.failed\": \"Fallida\",\n  \"core.backup.list.fields.expiresAt\": \"Expira el {expiresAt}\",\n  \"core.backup.restore.tips.first\": \"1. El proceso de restauración puede tomar un tiempo, por favor no actualices la página durante este período.\",\n  \"core.backup.restore.tips.second\": \"2. Durante el proceso de restauración, aunque los datos existentes no se eliminarán, si hay un conflicto, los datos se sobrescribirán.\",\n  \"core.backup.restore.tips.third\": \"3. Después de completar la restauración, necesitas reiniciar Halo para cargar los recursos del sistema normalmente.\",\n  \"core.backup.restore.tips.complete\": \"Restauración completada, esperando reinicio...\",\n  \"core.backup.restore.start\": \"Iniciar Restauración\",\n  \"core.backup.restore.tabs.local.label\": \"Cargar\",\n  \"core.backup.restore.tabs.remote.label\": \"Remoto\",\n  \"core.backup.restore.tabs.remote.fields.url\": \"URL remota\",\n  \"core.backup.restore.tabs.backup.label\": \"Restaurar desde archivos de respaldo\",\n  \"core.backup.restore.tabs.backup.empty.title\": \"No hay archivos de respaldo\",\n  \"core.backup.restore.tabs.backup.empty.message\": \"Actualmente no hay archivos de respaldo escaneados. Puedes cargar manualmente los archivos de respaldo al directorio backups del directorio de trabajo de Halo.\",\n  \"core.exception.not_found.message\": \"Página no encontrada\",\n  \"core.exception.forbidden.message\": \"Acceso no autorizado a esta página\",\n  \"core.exception.actions.home\": \"Ir a la página de inicio\",\n  \"core.rbac.Attachments Management\": \"Gestión de archivos adjuntos\",\n  \"core.rbac.Attachment Manage\": \"Gestor de adjuntos\",\n  \"core.rbac.Attachment View\": \"Vista de adjuntos\",\n  \"core.rbac.role-template-view-attachments\": \"Vista de adjuntos\",\n  \"core.rbac.Comments Management\": \"Comentarios\",\n  \"core.rbac.Comment Manage\": \"Gestor de comentarios\",\n  \"core.rbac.Comment View\": \"Vista de comentarios\",\n  \"core.rbac.role-template-view-comments\": \"Vista de comentarios\",\n  \"core.rbac.ConfigMaps Management\": \"ConfigMaps\",\n  \"core.rbac.ConfigMap Manage\": \"Gestor de ConfigMaps\",\n  \"core.rbac.ConfigMap View\": \"Vista de ConfigMaps\",\n  \"core.rbac.role-template-view-configmaps\": \"Vista de ConfigMaps\",\n  \"core.rbac.Menus Management\": \"Menús\",\n  \"core.rbac.Menu Manage\": \"Gestor de menús\",\n  \"core.rbac.Menu View\": \"Vista de menús\",\n  \"core.rbac.role-template-view-menus\": \"Vista de menús\",\n  \"core.rbac.Permissions Management\": \"Permisos\",\n  \"core.rbac.Permissions Manage\": \"Gestor de permisos\",\n  \"core.rbac.Permissions View\": \"Vista de permisos\",\n  \"core.rbac.role-template-view-permissions\": \"Vista de permisos\",\n  \"core.rbac.role-template-manage-permissions\": \"Gestor de permisos\",\n  \"core.rbac.Plugins Management\": \"Plugins\",\n  \"core.rbac.Plugin Manage\": \"Gestor de plugins\",\n  \"core.rbac.Plugin View\": \"Vista de plugins\",\n  \"core.rbac.role-template-view-plugins\": \"Vista de plugins\",\n  \"core.rbac.Posts Management\": \"Publicaciones\",\n  \"core.rbac.Post Manage\": \"Gestor de publicaciones\",\n  \"core.rbac.Post View\": \"Vista de publicaciones\",\n  \"core.rbac.role-template-view-posts\": \"Vista de publicaciones\",\n  \"core.rbac.role-template-manage-snapshots\": \"Gestor de snapshots\",\n  \"core.rbac.role-template-view-snapshots\": \"Vista de snapshots\",\n  \"core.rbac.role-template-manage-tags\": \"Gestor de etiquetas\",\n  \"core.rbac.role-template-view-tags\": \"Vista de etiquetas\",\n  \"core.rbac.role-template-manage-categories\": \"Gestor de categorías\",\n  \"core.rbac.role-template-view-categories\": \"Vista de categorías\",\n  \"core.rbac.Roles Management\": \"Roles\",\n  \"core.rbac.Role Manage\": \"Gestor de roles\",\n  \"core.rbac.Role View\": \"Vista de roles\",\n  \"core.rbac.role-template-view-roles\": \"Vista de roles\",\n  \"core.rbac.Settings Management\": \"Configuración\",\n  \"core.rbac.Setting Manage\": \"Gestor de configuración\",\n  \"core.rbac.Setting View\": \"Vista de configuración\",\n  \"core.rbac.role-template-view-settings\": \"Vista de configuración\",\n  \"core.rbac.SinglePages Management\": \"SinglePages\",\n  \"core.rbac.SinglePage Manage\": \"Gestor de SinglePages\",\n  \"core.rbac.SinglePage View\": \"Vista de SinglePages\",\n  \"core.rbac.role-template-view-singlepages\": \"Vista de SinglePages\",\n  \"core.rbac.Themes Management\": \"Temas\",\n  \"core.rbac.Theme Manage\": \"Gestor de temas\",\n  \"core.rbac.Theme View\": \"Vista de temas\",\n  \"core.rbac.role-template-view-themes\": \"Vista de temas\",\n  \"core.rbac.Users Management\": \"Usuarios\",\n  \"core.rbac.User manage\": \"Gestor de usuarios\",\n  \"core.rbac.User View\": \"Vista de usuarios\",\n  \"core.rbac.Migration Management\": \"Copia de seguridad y restauración\",\n  \"core.rbac.Migration Manage\": \"Gestor de copia de seguridad y restauración\",\n  \"core.rbac.role-template-view-users\": \"Vista de usuarios\",\n  \"core.rbac.role-template-change-password\": \"Cambiar contraseña\",\n  \"core.rbac.role-template-manage-posts\": \"Gestión de publicaciones\",\n  \"core.rbac.role-template-post-author\": \"Te permite gestionar tus propias publicaciones\",\n  \"core.rbac.role-template-post-contributor\": \"Contribuciones permitidas\",\n  \"core.rbac.role-template-post-publisher\": \"Permite publicar publicaciones propias\",\n  \"core.rbac.role-template-post-attachment-manager\": \"Permite cargar imágenes en las publicaciones\",\n  \"core.rbac.Actuator Management\": \"Información del sistema\",\n  \"core.rbac.Actuator Manage\": \"Acceso a la información del sistema\",\n  \"core.rbac.Cache Management\": \"Caché\",\n  \"core.rbac.Cache Manage\": \"Gestión de caché\",\n  \"core.rbac.Notification Configuration\": \"Configuración de notificaciones\",\n  \"core.rbac.role-template-notifier-config\": \"Configurar notificador\",\n  \"core.rbac.Post Attachment Manager\": \"Permite cargar imágenes en las publicaciones\",\n  \"core.rbac.Post Author\": \"Te permite gestionar tus propias publicaciones\",\n  \"core.rbac.Post Contributor\": \"Contribuciones permitidas\",\n  \"core.rbac.Post Publisher\": \"Permite publicar publicaciones propias\",\n  \"core.rbac.UC Attachment Manage\": \"Permite gestionar los propios adjuntos\",\n  \"core.rbac.role-template-uc-attachment-manager\": \"Permite gestionar los propios adjuntos\",\n  \"core.rbac.Recycle My Post\": \"Reciclar mi publicación\",\n  \"core.rbac.role-template-recycle-my-post\": \"Reciclar mi publicación\",\n  \"core.components.submit_button.computed_text\": \"{text} ({shortcut})\",\n  \"core.components.annotations_form.custom_fields.label\": \"Personalizado\",\n  \"core.components.annotations_form.custom_fields.validation\": \"La clave actual ya está en uso\",\n  \"core.components.annotations_form.buttons.expand\": \"Ver más\",\n  \"core.components.annotations_form.buttons.collapse\": \"Colapsar\",\n  \"core.components.default_editor.tabs.toc.title\": \"Índice\",\n  \"core.components.default_editor.tabs.toc.empty\": \"No hay índice disponible\",\n  \"core.components.default_editor.tabs.detail.title\": \"Detalle\",\n  \"core.components.default_editor.tabs.detail.fields.character_count\": \"Conteo de caracteres\",\n  \"core.components.default_editor.tabs.detail.fields.word_count\": \"Conteo de palabras\",\n  \"core.components.default_editor.tabs.detail.fields.publish_time\": \"Hora de publicación\",\n  \"core.components.default_editor.tabs.detail.fields.draft\": \"Borrador\",\n  \"core.components.default_editor.tabs.detail.fields.owner\": \"Propietario\",\n  \"core.components.default_editor.tabs.detail.fields.permalink\": \"Enlace permanente\",\n  \"core.components.default_editor.extensions.placeholder.options.placeholder\": \"Ingresa / para seleccionar el tipo de entrada.\",\n  \"core.components.default_editor.toolbox.attachment\": \"Adjunto\",\n  \"core.components.default_editor.toolbox.show_hide_sidebar\": \"Mostrar/Ocultar barra lateral\",\n  \"core.components.default_editor.title_placeholder\": \"Por favor, introduce el título\",\n  \"core.components.default_editor.cover.options.add\": \"Añadir portada\",\n  \"core.components.default_editor.cover.options.change\": \"Cambiar portada\",\n  \"core.components.default_editor.cover.options.upload\": \"Cargar\",\n  \"core.components.default_editor.cover.options.attachment\": \"Seleccionar de la biblioteca de adjuntos\",\n  \"core.components.default_editor.cover.toast.upload_failed\": \"Error al cargar la imagen de portada: {message}\",\n  \"core.components.global_search.placeholder\": \"Ingresa palabras clave para buscar\",\n  \"core.components.global_search.no_results\": \"Sin resultados de búsqueda\",\n  \"core.components.global_search.buttons.select\": \"Seleccionar\",\n  \"core.components.global_search.groups.console\": \"Página de la consola\",\n  \"core.components.global_search.groups.user\": \"Usuario\",\n  \"core.components.global_search.groups.plugin\": \"Plugin\",\n  \"core.components.global_search.groups.post\": \"Publicación\",\n  \"core.components.global_search.groups.category\": \"Categoría\",\n  \"core.components.global_search.groups.tag\": \"Etiqueta\",\n  \"core.components.global_search.groups.page\": \"Página\",\n  \"core.components.global_search.groups.attachment\": \"Adjunto\",\n  \"core.components.global_search.groups.setting\": \"Configuración\",\n  \"core.components.global_search.groups.theme_setting\": \"Configuración del tema\",\n  \"core.components.pagination.page_label\": \"página\",\n  \"core.components.pagination.size_label\": \"elementos por página\",\n  \"core.components.pagination.total_label\": \"Total de {total} elementos\",\n  \"core.components.app_download_alert.description\": \"Los temas y complementos para Halo se pueden descargar en las siguientes direcciones:\",\n  \"core.components.app_download_alert.sources.app_store\": \"Tienda de aplicaciones oficial: {url}\",\n  \"core.components.app_download_alert.sources.github\": \"GitHub: {url}\",\n  \"core.components.user_avatar.title\": \"Avatar\",\n  \"core.components.user_avatar.toast_upload_failed\": \"Error al cargar el avatar\",\n  \"core.components.user_avatar.toast_remove_failed\": \"Error al eliminar el avatar\",\n  \"core.components.user_avatar.cropper_modal.title\": \"Recortar avatar\",\n  \"core.components.user_avatar.remove.title\": \"Eliminar avatar\",\n  \"core.components.user_avatar.tooltips.upload\": \"Cargar\",\n  \"core.components.user_avatar.tooltips.zoom_in\": \"Acercar\",\n  \"core.components.user_avatar.tooltips.zoom_out\": \"Alejar\",\n  \"core.components.user_avatar.tooltips.flip_horizontal\": \"Volteo horizontal\",\n  \"core.components.user_avatar.tooltips.flip_vertical\": \"Volteo vertical\",\n  \"core.components.user_avatar.tooltips.reset\": \"Restablecer\",\n  \"core.components.editor_provider_selector.tooltips.disallow\": \"El formato de contenido es diferente y no se puede cambiar\",\n  \"core.components.uppy.image_editor.revert\": \"Revertir\",\n  \"core.components.uppy.image_editor.rotate\": \"Rotar\",\n  \"core.components.uppy.image_editor.zoom_in\": \"Acercar\",\n  \"core.components.uppy.image_editor.zoom_out\": \"Alejar\",\n  \"core.components.uppy.image_editor.flip_horizontal\": \"Volteo horizontal\",\n  \"core.components.uppy.image_editor.aspect_ratio_square\": \"Recorte cuadrado\",\n  \"core.components.uppy.image_editor.aspect_ratio_landscape\": \"Recorte horizontal (16:9)\",\n  \"core.components.uppy.image_editor.aspect_ratio_portrait\": \"Recorte vertical (9:16)\",\n  \"core.components.h2_warning_alert.title\": \"Advertencia: Base de datos H2 en uso\",\n  \"core.components.h2_warning_alert.description\": \"La base de datos H2 solo es apta para entornos de desarrollo y pruebas y no se recomienda para entornos de producción. H2 es muy propenso a corromper los archivos de datos por un uso inadecuado. Si debes usarla, por favor respalda los datos a tiempo.\",\n  \"core.composables.content_cache.toast_recovered\": \"Contenido no guardado recuperado de la caché\",\n  \"core.formkit.category_select.creation_label\": \"Crear categoría {text}\",\n  \"core.formkit.tag_select.creation_label\": \"Crear etiqueta {text}\",\n  \"core.formkit.validation.trim\": \"Por favor, elimina los espacios al inicio y al final\",\n  \"core.formkit.validation.password\": \"La contraseña solo puede usar letras mayúsculas y minúsculas (A-Z, a-z), números (0-9) y los siguientes caracteres especiales: !{'@'}#$%^&*\",\n  \"core.formkit.color.placeholder\": \"Seleccionar color\",\n  \"core.formkit.color.operations.edit\": \"Editar color manualmente\",\n  \"core.formkit.select.no_data\": \"Sin datos\",\n  \"core.formkit.verification_form.no_action_defined\": \"Interfaz {label} no definida\",\n  \"core.formkit.verification_form.verify_success\": \"{label} exitosa\",\n  \"core.formkit.verification_form.verify_failed\": \"{label} fallida\",\n  \"core.formkit.secret.creation_modal.title\": \"Crear secreto\",\n  \"core.formkit.secret.edit_modal.title\": \"Editar secreto\",\n  \"core.formkit.secret.list_modal.title\": \"Seleccionar secreto\",\n  \"core.formkit.secret.operations.delete.title\": \"Eliminar secreto\",\n  \"core.formkit.secret.operations.delete.description\": \"¿Estás seguro de que quieres eliminar este secreto? Por favor, asegúrate de que este secreto no se esté usando en ningún sitio, de lo contrario tendrás que restablecerlo en un lugar específico\",\n  \"core.formkit.secret.form.fields.description\": \"Descripción\",\n  \"core.formkit.secret.form.fields.string_data\": \"Datos de secreto\",\n  \"core.formkit.code.fullscreen.exit\": \"Salir de pantalla completa\",\n  \"core.formkit.code.fullscreen.enter\": \"Entrar en pantalla completa para editar\",\n  \"core.formkit.iconify.placeholder\": \"Seleccionar icono\",\n  \"core.formkit.iconify.tabs.collections\": \"Colecciones\",\n  \"core.formkit.iconify.tabs.search\": \"Buscar\",\n  \"core.formkit.iconify.tabs.current\": \"Icono actual\",\n  \"core.formkit.iconify.collection_search_placeholder\": \"Buscar colecciones de iconos\",\n  \"core.formkit.iconify.search_placeholder\": \"Buscar iconos\",\n  \"core.formkit.iconify.no_results\": \"No se han encontrado resultados, por favor inténtalo de nuevo\",\n  \"core.formkit.iconify.option_size\": \"Tamaño\",\n  \"core.formkit.iconify.option_color\": \"Color\",\n  \"core.formkit.iconify.copyright\": \"Datos de iconos de {url}\",\n  \"core.formkit.iconify.operations.edit\": \"Editar icono manualmente\",\n  \"core.formkit.array.empty_text\": \"Sin elementos\",\n  \"core.formkit.array.image_tooltip\": \"Ver imagen: {value}\",\n  \"core.formkit.array.item_modal.title\": \"Editar elemento\",\n  \"core.formkit.attachment.operations.select\": \"Seleccionar de la biblioteca de adjuntos\",\n  \"core.formkit.attachment.operations.input\": \"Enlace de entrada\",\n  \"core.common.buttons.save\": \"Guardar\",\n  \"core.common.buttons.close\": \"Cerrar\",\n  \"core.common.buttons.save_and_continue\": \"Guardar y seguir añadiendo\",\n  \"core.common.buttons.close_and_shortcut\": \"Cerrar (Esc)\",\n  \"core.common.buttons.delete\": \"Borrar\",\n  \"core.common.buttons.setting\": \"Configuración\",\n  \"core.common.buttons.confirm\": \"Confirmar\",\n  \"core.common.buttons.cancel\": \"Cancelar\",\n  \"core.common.buttons.cancel_and_shortcut\": \"Cancelar (Esc)\",\n  \"core.common.buttons.new\": \"Nuevo\",\n  \"core.common.buttons.edit\": \"Editar\",\n  \"core.common.buttons.back\": \"Volver\",\n  \"core.common.buttons.refresh\": \"Actualizar\",\n  \"core.common.buttons.publish\": \"Publicar\",\n  \"core.common.buttons.cancel_publish\": \"Cancelar Publicación\",\n  \"core.common.buttons.next\": \"Siguiente\",\n  \"core.common.buttons.previous\": \"Anterior\",\n  \"core.common.buttons.install\": \"Instalar\",\n  \"core.common.buttons.uninstall\": \"Desinstalar\",\n  \"core.common.buttons.upgrade\": \"Actualizar\",\n  \"core.common.buttons.reset\": \"Reiniciar\",\n  \"core.common.buttons.preview\": \"Vista previa\",\n  \"core.common.buttons.restore\": \"Recuperar\",\n  \"core.common.buttons.delete_permanently\": \"Borrar permanentemente\",\n  \"core.common.buttons.download\": \"Descargar\",\n  \"core.common.buttons.copy\": \"Copiar\",\n  \"core.common.buttons.upload\": \"Subir\",\n  \"core.common.buttons.add\": \"Agregar\",\n  \"core.common.buttons.submit\": \"Enviar\",\n  \"core.common.buttons.detail\": \"Detalle\",\n  \"core.common.buttons.activate\": \"Activar\",\n  \"core.common.buttons.inactivate\": \"Desactivar\",\n  \"core.common.buttons.cancel_upload\": \"Cancelar carga\",\n  \"core.common.buttons.select\": \"Seleccionar\",\n  \"core.common.buttons.view_all\": \"Ver todo\",\n  \"core.common.buttons.verify\": \"Verificar\",\n  \"core.common.buttons.modify\": \"Modificar\",\n  \"core.common.buttons.access\": \"Acceder\",\n  \"core.common.buttons.schedule_publish\": \"Programar publicación\",\n  \"core.common.buttons.revoke\": \"Revocar\",\n  \"core.common.buttons.disable\": \"Deshabilitar\",\n  \"core.common.buttons.enable\": \"Habilitar\",\n  \"core.common.buttons.continue\": \"Continuar\",\n  \"core.common.buttons.retry\": \"Reintentar\",\n  \"core.common.buttons.unfold\": \"Desplegar\",\n  \"core.common.buttons.fold\": \"Plegar\",\n  \"core.common.buttons.remove\": \"Eliminar\",\n  \"core.common.buttons.replace\": \"Reemplazar\",\n  \"core.common.buttons.more\": \"Más\",\n  \"core.common.radio.yes\": \"Sí\",\n  \"core.common.radio.no\": \"No\",\n  \"core.common.select.public\": \"Público\",\n  \"core.common.select.private\": \"Privado\",\n  \"core.common.placeholder.search\": \"Ingresa palabras clave para buscar\",\n  \"core.common.toast.operation_success\": \"Operación realizada con éxito\",\n  \"core.common.toast.delete_success\": \"Eliminado exitosamente\",\n  \"core.common.toast.save_success\": \"Guardado exitosamente\",\n  \"core.common.toast.publish_success\": \"Publicado exitosamente\",\n  \"core.common.toast.cancel_publish_success\": \"Publicación cancelada exitosamente\",\n  \"core.common.toast.recovery_success\": \"Recuperado exitosamente\",\n  \"core.common.toast.uninstall_success\": \"Desinstalado exitosamente\",\n  \"core.common.toast.active_success\": \"Activado exitosamente\",\n  \"core.common.toast.inactive_success\": \"Desactivado exitosamente\",\n  \"core.common.toast.upgrade_success\": \"Actualizado exitosamente\",\n  \"core.common.toast.install_success\": \"Instalado exitosamente\",\n  \"core.common.toast.download_success\": \"Descargado exitosamente\",\n  \"core.common.toast.copy_success\": \"Copiado exitosamente\",\n  \"core.common.toast.operation_failed\": \"Fallo en la operación\",\n  \"core.common.toast.download_failed\": \"Fallo en la descarga\",\n  \"core.common.toast.save_failed_and_retry\": \"Fallo al guardar, por favor intenta nuevamente\",\n  \"core.common.toast.publish_failed_and_retry\": \"Fallo al publicar, por favor intenta nuevamente\",\n  \"core.common.toast.network_error\": \"Error de red, por favor verifica tu conexión\",\n  \"core.common.toast.login_expired\": \"Sesión expirada, por favor inicia sesión nuevamente\",\n  \"core.common.toast.forbidden\": \"Acceso denegado\",\n  \"core.common.toast.not_found\": \"Recurso no encontrado\",\n  \"core.common.toast.server_internal_error\": \"Error interno del servidor\",\n  \"core.common.toast.unknown_error\": \"Error desconocido\",\n  \"core.common.toast.disable_success\": \"Deshabilitado con éxito\",\n  \"core.common.toast.enable_success\": \"Habilitado con éxito\",\n  \"core.common.dialog.titles.tip\": \"Consejo\",\n  \"core.common.dialog.titles.warning\": \"Advertencia\",\n  \"core.common.dialog.titles.login_expired\": \"Sesión expirada\",\n  \"core.common.dialog.descriptions.cannot_be_recovered\": \"Esta operación es irreversible.\",\n  \"core.common.dialog.descriptions.editor_not_found\": \"No se encontró ningún editor que coincida con el formato {raw_type}. Por favor verifica si el complemento del editor ha sido instalado.\",\n  \"core.common.dialog.descriptions.login_expired\": \"La sesión actual ha expirado. Haz clic en Confirmar para ir a la página de inicio de sesión. Por favor, asegúrate de que el contenido actual esté guardado. Puedes hacer clic en Cancelar para copiar manualmente cualquier contenido no guardado.\",\n  \"core.common.filters.results.keyword\": \"Palabra clave: {keyword}\",\n  \"core.common.filters.results.sort\": \"Ordenar: {sort}\",\n  \"core.common.filters.results.status\": \"Estado: {status}\",\n  \"core.common.filters.labels.sort\": \"Ordenar\",\n  \"core.common.filters.labels.status\": \"Estado\",\n  \"core.common.filters.item_labels.all\": \"Todo\",\n  \"core.common.filters.item_labels.default\": \"Por defecto\",\n  \"core.common.status.deleting\": \"Borrando\",\n  \"core.common.status.loading\": \"Cargando\",\n  \"core.common.status.loading_error\": \"Error al cargar\",\n  \"core.common.status.activated\": \"Activado\",\n  \"core.common.status.not_activated\": \"No activado\",\n  \"core.common.status.installed\": \"Instalado\",\n  \"core.common.status.not_installed\": \"No instalado\",\n  \"core.common.status.starting_up\": \"Iniciando\",\n  \"core.common.text.none\": \"Ninguno\",\n  \"core.common.text.tip\": \"Consejo\",\n  \"core.common.text.warning\": \"Advertencia\",\n  \"core.common.text.system_protection\": \"Protección del sistema\",\n  \"core.common.text.all\": \"Todo\",\n  \"core.common.text.detail\": \"Detalle\",\n  \"core.common.tooltips.unpublished_content_tip\": \"Hay contenido que ha sido guardado pero aún no ha sido publicado.\",\n  \"core.common.tooltips.publishing\": \"Publicando\",\n  \"core.common.tooltips.recovering\": \"Recuperando\",\n  \"core.common.fields.post_count\": \"{count} Publicaciones\",\n  \"core.common.form.validation.slug_unique\": \"El slug actual ya existe\",\n  \"core.uc_sidebar.menu.items.profile\": \"Perfil\",\n  \"core.uc_sidebar.menu.items.notification\": \"Notificaciones\",\n  \"core.uc_sidebar.menu.items.posts\": \"Publicaciones\",\n  \"core.uc_sidebar.operations.console.tooltip\": \"Consola\",\n  \"core.uc_sidebar.profile.aggregate_role\": \"Rol agregado\",\n  \"core.dashboard_designer.title\": \"Editar panel de control\",\n  \"core.dashboard_designer.actions.add_widget\": \"Añadir widget\",\n  \"core.dashboard_designer.widgets_modal.title\": \"Widgets\",\n  \"core.dashboard_designer.config_modal.title\": \"Configuración del widget\",\n  \"core.dashboard_designer.breakpoints.lg\": \"Escritorio\",\n  \"core.dashboard_designer.breakpoints.md\": \"Tableta (Horizontal)\",\n  \"core.dashboard_designer.breakpoints.sm\": \"Tableta\",\n  \"core.dashboard_designer.breakpoints.xs\": \"Móvil\",\n  \"core.dashboard_designer.operations.back.title\": \"Diseño no guardado\",\n  \"core.dashboard_designer.operations.back.description\": \"El diseño actual no se ha guardado, si sales, se perderá. ¿Deseas continuar?\",\n  \"core.dashboard_designer.operations.back.confirm_text\": \"Salir\",\n  \"core.dashboard_designer.operations.change_breakpoint.tips_not_saved\": \"Por favor, guarda el diseño actual primero\",\n  \"core.dashboard_designer.operations.copy_from_layout.button\": \"Copiar desde el diseño\",\n  \"core.dashboard_designer.operations.open_widgets_hub.button\": \"Centro de widgets\",\n  \"core.uc_attachment.empty.title\": \"No hay adjuntos.\",\n  \"core.uc_attachment.empty.message\": \"No hay adjuntos, puedes intentar refrescar o cargar adjuntos.\",\n  \"core.uc_attachment.empty.actions.upload\": \"Cargar adjunto\",\n  \"core.uc_attachment.filters.sort.items.create_time_desc\": \"Más recientes\",\n  \"core.uc_attachment.filters.sort.items.create_time_asc\": \"Más antiguos\",\n  \"core.uc_attachment.filters.sort.items.size_desc\": \"Orden descendente por tamaño de archivo\",\n  \"core.uc_attachment.filters.sort.items.size_asc\": \"Orden ascendente por tamaño de archivo\",\n  \"core.uc_attachment.filters.sort.items.display_name_asc\": \"Orden ascendente por nombre para mostrar\",\n  \"core.uc_attachment.filters.sort.items.display_name_desc\": \"Orden descendente por nombre para mostrar\",\n  \"core.uc_attachment.filters.view_type.items.grid\": \"Modo cuadrícula\",\n  \"core.uc_attachment.filters.view_type.items.list\": \"Modo lista\",\n  \"core.uc_attachment.detail_modal.title\": \"Adjunto: {display_name}\",\n  \"core.uc_attachment.detail_modal.fields.preview\": \"Vista previa\",\n  \"core.uc_attachment.detail_modal.fields.display_name\": \"Nombre para mostrar\",\n  \"core.uc_attachment.detail_modal.fields.media_type\": \"Tipo de medio\",\n  \"core.uc_attachment.detail_modal.fields.size\": \"Tamaño\",\n  \"core.uc_attachment.detail_modal.fields.owner\": \"Propietario\",\n  \"core.uc_attachment.detail_modal.fields.creation_time\": \"Hora de creación\",\n  \"core.uc_attachment.detail_modal.fields.permalink\": \"Enlace permanente\",\n  \"core.uc_attachment.detail_modal.preview.click_to_exit\": \"Haz clic para salir de la vista previa\",\n  \"core.uc_attachment.detail_modal.preview.video_not_support\": \"El navegador actual no soporta la reproducción de vídeo.\",\n  \"core.uc_attachment.detail_modal.preview.audio_not_support\": \"El navegador actual no soporta la reproducción de audio.\",\n  \"core.uc_attachment.detail_modal.preview.not_support\": \"Este archivo no soporta la vista previa.\",\n  \"core.uc_attachment.upload_modal.title\": \"Cargar adjunto\",\n  \"core.uc_attachment.select_modal.title\": \"Seleccionar adjunto\",\n  \"core.uc_attachment.select_modal.providers.default.label\": \"Adjuntos\",\n  \"core.uc_attachment.select_modal.operations.select.result\": \"({count} elementos seleccionados)\",\n  \"core.uc_profile.title\": \"Perfil\",\n  \"core.uc_profile.tabs.detail\": \"Detalle\",\n  \"core.uc_profile.tabs.notification-preferences\": \"Preferencias de notificación\",\n  \"core.uc_profile.tabs.pat\": \"Tokens de acceso personal (PAT)\",\n  \"core.uc_profile.tabs.authentication\": \"Autenticación\",\n  \"core.uc_profile.tabs.devices\": \"Dispositivos\",\n  \"core.uc_profile.actions.update_profile.title\": \"Actualizar perfil\",\n  \"core.uc_profile.actions.change_password.title\": \"Cambiar contraseña\",\n  \"core.uc_profile.detail.fields.display_name\": \"Nombre para mostrar\",\n  \"core.uc_profile.detail.fields.username\": \"Nombre de usuario\",\n  \"core.uc_profile.detail.fields.email\": \"Correo electrónico\",\n  \"core.uc_profile.detail.fields.roles\": \"Roles\",\n  \"core.uc_profile.detail.fields.bio\": \"Biografía\",\n  \"core.uc_profile.detail.fields.creation_time\": \"Hora de creación\",\n  \"core.uc_profile.detail.email_not_set.description\": \"Tu dirección de correo electrónico aún no se ha configurado. Haz clic en el botón de abajo para configurarla.\",\n  \"core.uc_profile.detail.email_not_set.title\": \"Configurar correo electrónico\",\n  \"core.uc_profile.detail.email_not_verified.description\": \"Tu dirección de correo electrónico aún no ha sido verificada, haz clic en el botón de abajo para verificarla\",\n  \"core.uc_profile.detail.email_not_verified.title\": \"Verificar correo electrónico\",\n  \"core.uc_profile.detail.email_verified.tooltip\": \"Verificado\",\n  \"core.uc_profile.auth_providers.title\": \"Proveedores de autenticación\",\n  \"core.uc_profile.auth_providers.operations.bind.button\": \"Vincular\",\n  \"core.uc_profile.auth_providers.operations.unbind.button\": \"Desvincular\",\n  \"core.uc_profile.auth_providers.operations.unbind.title\": \"Desvincular el método de inicio de sesión para {display_name}\",\n  \"core.uc_profile.2fa.operations.enable.button\": \"Habilitar 2FA\",\n  \"core.uc_profile.2fa.operations.enable.title\": \"Habilitar 2FA\",\n  \"core.uc_profile.2fa.operations.disable.title\": \"Deshabilitar 2FA\",\n  \"core.uc_profile.2fa.operations.disable_totp.title\": \"Deshabilitar TOTP\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.label\": \"Contraseña\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.help\": \"Contraseña de inicio de sesión de la cuenta actual\",\n  \"core.uc_profile.2fa.methods.title\": \"Métodos de dos factores\",\n  \"core.uc_profile.2fa.methods.totp.title\": \"TOTP\",\n  \"core.uc_profile.2fa.methods.totp.description\": \"Configura la verificación en dos pasos con una aplicación TOTP\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.configured\": \"Configurado\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.not_configured\": \"No configurado\",\n  \"core.uc_profile.2fa.methods.totp.operations.reconfigure.button\": \"Reconfigurar\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.button\": \"Configurar\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.title\": \"Configuración TOTP\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.label\": \"Código de verificación\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.help\": \"Código de verificación de 6 dígitos obtenido de la aplicación validadora\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.label\": \"Contraseña\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.help\": \"Contraseña de inicio de sesión de la cuenta actual\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.qrcode.label\": \"Usa la aplicación validadora para escanear el código QR de abajo:\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.label\": \"Si no puedes escanear el código QR, haz clic para ver los pasos alternativos.\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.help\": \"Configura manualmente la aplicación validadora con el siguiente código:\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.tips\": \"Por favor, mantén seguro tu dispositivo de verificación en dos pasos. Si el dispositivo se pierde o se daña, no podrás iniciar sesión. Se recomienda instalar la aplicación de autenticación en varios dispositivos o guardar una copia de seguridad de la clave por si el dispositivo principal no pudiera utilizarse.\",\n  \"core.uc_profile.pat.operations.delete.title\": \"Eliminar token de acceso personal\",\n  \"core.uc_profile.pat.operations.delete.description\": \"¿Estás seguro de que quieres eliminar este token de acceso personal?\",\n  \"core.uc_profile.pat.operations.revoke.button\": \"Revocar\",\n  \"core.uc_profile.pat.operations.revoke.title\": \"Revocar token de acceso personal\",\n  \"core.uc_profile.pat.operations.revoke.description\": \"¿Estás seguro de que quieres revocar este token de acceso personal?\",\n  \"core.uc_profile.pat.operations.revoke.toast_success\": \"Revocación exitosa\",\n  \"core.uc_profile.pat.operations.copy.title\": \"Por favor, cópialo y guárdalo inmediatamente, el token solo se mostrará una vez.\",\n  \"core.uc_profile.pat.operations.restore.button\": \"Restaurar\",\n  \"core.uc_profile.pat.operations.restore.toast_success\": \"Restaurado con éxito\",\n  \"core.uc_profile.pat.list.empty.title\": \"No se han creado tokens de acceso personal\",\n  \"core.uc_profile.pat.list.empty.message\": \"Puedes intentar refrescar o crear un nuevo token de acceso personal\",\n  \"core.uc_profile.pat.list.fields.expiresAt.dynamic\": \"Expira el {expiresAt}\",\n  \"core.uc_profile.pat.list.fields.expiresAt.forever\": \"Nunca expira\",\n  \"core.uc_profile.pat.list.fields.status.normal\": \"Normal\",\n  \"core.uc_profile.pat.list.fields.status.revoked\": \"Revocado\",\n  \"core.uc_profile.pat.list.fields.status.expired\": \"Expirado\",\n  \"core.uc_profile.pat.creation_modal.title\": \"Crear token de acceso personal\",\n  \"core.uc_profile.pat.creation_modal.groups.general\": \"General\",\n  \"core.uc_profile.pat.creation_modal.groups.permissions\": \"Permisos\",\n  \"core.uc_profile.pat.creation_modal.fields.name.label\": \"Nombre\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.label\": \"Tiempo de expiración\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.help\": \"Dejar vacío para que no expire\",\n  \"core.uc_profile.pat.creation_modal.fields.description.label\": \"Descripción\",\n  \"core.uc_profile.notification-preferences.fields.type\": \"Tipo\",\n  \"core.uc_profile.editing_modal.title\": \"Editar perfil\",\n  \"core.uc_profile.editing_modal.groups.general\": \"General\",\n  \"core.uc_profile.editing_modal.groups.annotations\": \"Anotaciones\",\n  \"core.uc_profile.editing_modal.fields.username.label\": \"Nombre de usuario\",\n  \"core.uc_profile.editing_modal.fields.username.validation\": \"Solo puede contener números, letras minúsculas, puntos (.) y guiones (-), y no puede empezar ni terminar con punto (.) ni guion (-).\",\n  \"core.uc_profile.editing_modal.fields.display_name.label\": \"Nombre para mostrar\",\n  \"core.uc_profile.editing_modal.fields.email.label\": \"Correo electrónico\",\n  \"core.uc_profile.editing_modal.fields.phone.label\": \"Teléfono\",\n  \"core.uc_profile.editing_modal.fields.avatar.label\": \"Avatar\",\n  \"core.uc_profile.editing_modal.fields.bio.label\": \"Biografía\",\n  \"core.uc_profile.change_password_modal.title\": \"Cambiar contraseña\",\n  \"core.uc_profile.change_password_modal.fields.new_password.label\": \"Nueva contraseña\",\n  \"core.uc_profile.change_password_modal.fields.confirm_password.label\": \"Confirmar contraseña\",\n  \"core.uc_profile.change_password_modal.fields.old_password.label\": \"Contraseña antigua\",\n  \"core.uc_profile.email_verify_modal.fields.code.label\": \"Código de verificación\",\n  \"core.uc_profile.email_verify_modal.fields.email.label\": \"Dirección de correo electrónico\",\n  \"core.uc_profile.email_verify_modal.fields.new_email.label\": \"Nueva dirección de correo electrónico\",\n  \"core.uc_profile.email_verify_modal.fields.password.label\": \"Contraseña\",\n  \"core.uc_profile.email_verify_modal.fields.password.help\": \"La contraseña de inicio de sesión de la cuenta actual\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.countdown\": \"Reenviar en {timer} segundos\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.send\": \"Enviar el código de verificación\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.sending\": \"enviando\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_email_empty\": \"Por favor, introduce tu dirección de correo electrónico\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_success\": \"Código de verificación enviado\",\n  \"core.uc_profile.email_verify_modal.operations.verify.toast_success\": \"Verificación exitosa\",\n  \"core.uc_profile.email_verify_modal.titles.modify\": \"Modificar dirección de correo electrónico\",\n  \"core.uc_profile.email_verify_modal.titles.verify\": \"Verificar correo electrónico\",\n  \"core.uc_profile.device.list.fields.current\": \"Actual\",\n  \"core.uc_profile.device.list.fields.last_accessed_time\": \"Última vez accedido: {time}\",\n  \"core.uc_profile.device.detail_modal.title\": \"Detalles del dispositivo de inicio de sesión\",\n  \"core.uc_profile.device.detail_modal.fields.os\": \"SO\",\n  \"core.uc_profile.device.detail_modal.fields.browser\": \"Navegador\",\n  \"core.uc_profile.device.detail_modal.fields.creation_timestamp\": \"Hora de creación\",\n  \"core.uc_profile.device.detail_modal.fields.last_accessed_times\": \"Última vez accedido\",\n  \"core.uc_profile.device.detail_modal.fields.last_authenticated_time\": \"Última vez autenticado\",\n  \"core.uc_profile.device.operations.revoke.title\": \"Revocar dispositivo\",\n  \"core.uc_profile.device.operations.revoke.description\": \"¿Estás seguro de que quieres revocar este dispositivo? Tras revocarlo, se cerrará la sesión en este dispositivo\",\n  \"core.uc_profile.device.operations.revoke_others.title\": \"Revocar todos los demás dispositivos\",\n  \"core.uc_profile.device.operations.revoke_others.description\": \"¿Estás seguro de que quieres revocar todos los demás dispositivos? Tras revocar, se cerrará la sesión en los otros dispositivos\",\n  \"core.uc_profile.device.operations.revoke_others.toast_success\": \"El estado de inicio de sesión de los otros dispositivos ha sido revocado\",\n  \"core.uc_notification.title\": \"Notificaciones\",\n  \"core.uc_notification.tabs.unread\": \"Sin leer\",\n  \"core.uc_notification.tabs.read\": \"Leídas\",\n  \"core.uc_notification.empty.titles.unread\": \"No hay notificaciones sin leer\",\n  \"core.uc_notification.empty.titles.read\": \"No hay notificaciones leídas\",\n  \"core.uc_notification.operations.mark_as_read.button\": \"Marcar como leído\",\n  \"core.uc_notification.operations.delete.description\": \"¿Estás seguro de que quieres eliminar esta notificación?\",\n  \"core.uc_notification.operations.delete.title\": \"Eliminar\",\n  \"core.uc_notification.operations.delete_all.title\": \"Eliminar todas las notificaciones actuales\",\n  \"core.uc_notification.operations.delete_all.description\": \"¿Estás seguro de que quieres eliminar todas las notificaciones actuales? No podrás restaurarlas tras la eliminación.\",\n  \"core.uc_notification.operations.mark_all_as_read.title\": \"Marcar todas las notificaciones actuales como leídas\",\n  \"core.uc_notification.operations.mark_all_as_read.description\": \"Marcar todos los mensajes actuales como leídos. ¿Deseas continuar?\",\n  \"core.uc_post.creation_modal.title\": \"Crear publicación\",\n  \"core.uc_post.operations.cancel_publish.description\": \"¿Estás seguro de que quieres cancelar la publicación?\",\n  \"core.uc_post.operations.cancel_publish.title\": \"Cancelar publicación\",\n  \"core.uc_post.operations.delete.title\": \"Eliminar publicación\",\n  \"core.uc_post.operations.delete.description\": \"Esta acción moverá la publicación a la papelera de reciclaje, donde será gestionada por el administrador del sitio.\",\n  \"core.uc_post.publish_modal.title\": \"Publicar publicación\",\n  \"core.uc_post.setting_modal.title\": \"Ajustes de publicación\",\n  \"core.uc_post.title\": \"Mis publicaciones\",\n  \"core.tool.title\": \"Herramientas\",\n  \"core.tool.empty.title\": \"No hay herramientas disponibles\",\n  \"core.tool.empty.message\": \"Actualmente no hay herramientas disponibles, y las herramientas del sistema pueden ser proporcionadas por complementos\",\n  \"core.snapshots.operations.revert.button\": \"Revertir\",\n  \"core.snapshots.operations.revert.title\": \"Revertir instantánea\",\n  \"core.snapshots.operations.revert.description\": \"¿Estás seguro de que quieres restaurar esta instantánea? Esta operación creará una nueva instantánea basada en ésta y la publicará.\",\n  \"core.snapshots.operations.revert.toast_success\": \"Revertido con éxito\",\n  \"core.snapshots.operations.delete.title\": \"Eliminar instantánea\",\n  \"core.snapshots.operations.delete.description\": \"¿Estás seguro de que quieres eliminar esta instantánea? Esta operación es irreversible.\",\n  \"core.snapshots.operations.cleanup.button\": \"Limpieza\",\n  \"core.snapshots.operations.cleanup.title\": \"Limpiar instantáneas\",\n  \"core.snapshots.operations.cleanup.description\": \"¿Estás seguro de que quieres eliminar todas las instantáneas no utilizadas? Solo se conservarán las versiones publicada, base y borrador.\",\n  \"core.snapshots.operations.cleanup.toast_empty\": \"No hay instantáneas para limpiar\",\n  \"core.snapshots.operations.cleanup.toast_success\": \"Limpieza completada\",\n  \"core.snapshots.status.released\": \"Publicada\",\n  \"core.snapshots.status.draft\": \"Borrador\",\n  \"core.snapshots.status.base\": \"Base\",\n  \"core.snapshots.diff_mode.toggle.enable\": \"Habilitar modo comparación\",\n  \"core.snapshots.diff_mode.toggle.disable\": \"Deshabilitar modo comparación\",\n  \"core.snapshots.diff_mode.title\": \"Modo comparación\",\n  \"core.snapshots.diff_mode.show_diff_only\": \"Mostrar solo diferencias\",\n  \"core.snapshots.diff_mode.sync_scroll\": \"Desplazamiento sincronizado\",\n  \"core.snapshots.diff_mode.select_two_tip\": \"Por favor, seleccione dos versiones para comparar\",\n  \"core.snapshots.diff_mode.old_version\": \"Versión anterior (antigua)\",\n  \"core.snapshots.diff_mode.new_version\": \"Primera versión seleccionada (nueva)\",\n  \"core.snapshots.diff_mode.diff\": \"Diferencias\",\n  \"core.snapshots.diff_mode.legend.removed\": \"Esta línea representa eliminación\",\n  \"core.snapshots.diff_mode.legend.added\": \"Esta línea representa adición\",\n  \"core.snapshots.diff_mode.legend.modified\": \"Esta línea representa modificación\",\n  \"core.post_snapshots.title\": \"Instantáneas de publicación\",\n  \"core.page_snapshots.title\": \"Instantáneas de página\",\n  \"core.formkit.secret.no_permission\": \"No tienes permiso para ver secretos\",\n  \"core.formkit.secret.no_selected\": \"No has seleccionado ningún secreto, puedes crear uno nuevo o seleccionar uno existente\",\n  \"core.formkit.secret.missing_keys\": \"El secreto seleccionado falta los siguientes campos: {keys}\",\n  \"core.formkit.secret.includes_keys\": \"Incluye campos: {keys}\",\n  \"core.formkit.secret.no_fields\": \"No hay campos\",\n  \"core.formkit.secret.required_keys_tip\": \"Los campos requeridos para el elemento del formulario actual incluyen:\",\n  \"core.dashboard.widgets.presets.upvotes_stats.title\": \"Me gusta\",\n  \"core.plugin.detail.operations.copy_error_message.button\": \"Copiar mensaje de error\",\n  \"core.plugin.detail.operations.copy_error_message.copied\": \"Copiado\"\n}\n"
  },
  {
    "path": "ui/src/locales/index.ts",
    "content": "import { utils } from \"@halo-dev/ui-shared\";\nimport type { App } from \"vue\";\nimport { createI18n } from \"vue-i18n\";\nimport { getCookie } from \"@/utils/cookie\";\n\ninterface LocaleConfig {\n  code: string[];\n  file: string;\n}\n\nexport const SUPPORTED_LOCALES: LocaleConfig[] = [\n  {\n    code: [\"en\"],\n    file: \"en.json\",\n  },\n  {\n    code: [\"es\"],\n    file: \"es.json\",\n  },\n  {\n    code: [\"zh-CN\", \"zh\"],\n    file: \"zh-CN.json\",\n  },\n  {\n    code: [\"zh-TW\"],\n    file: \"zh-TW.json\",\n  },\n];\n\nconst localeModules = import.meta.glob<{ default: Record<string, unknown> }>(\n  [\"./*.json\", \"!**/_missing_translations_*.json\"],\n  { eager: false }\n);\n\nconst i18n = createI18n({\n  legacy: false,\n  fallbackLocale: \"en\",\n  messages: {},\n});\n\nexport function getEnvironmentLanguage(): string {\n  return getCookie(\"language\") || navigator.language;\n}\n\nexport function getLocaleDefinition(\n  language: string\n): LocaleConfig | undefined {\n  const locale = SUPPORTED_LOCALES.find((locale) =>\n    locale.code.includes(language)\n  );\n\n  if (locale) {\n    return locale;\n  }\n\n  const code = language.split(\"-\")[0];\n  return SUPPORTED_LOCALES.find((locale) => locale.code.includes(code));\n}\n\nexport async function setLanguage(_language?: string): Promise<void> {\n  const language = _language || getEnvironmentLanguage();\n\n  if (!i18n.global.availableLocales.includes(language)) {\n    const locale = getLocaleDefinition(language);\n    if (locale) {\n      try {\n        const localeLoader = localeModules[`./${locale.file}`];\n        if (!localeLoader) {\n          throw new Error(`Locale file ${locale.file} not found`);\n        }\n        const messages = await localeLoader();\n        i18n.global.setLocaleMessage(language, messages.default || messages);\n      } catch (error) {\n        console.error(`Failed to load locale file for ${language}:`, error);\n      }\n    } else {\n      console.warn(`Locale not found for ${language}, using fallback`);\n    }\n  }\n\n  i18n.global.locale.value = language;\n  utils.date.setLocale(language);\n\n  await loadFallbackLocale();\n}\n\nasync function loadFallbackLocale(): Promise<void> {\n  const fallback = i18n.global.fallbackLocale.value as string;\n\n  if (!i18n.global.availableLocales.includes(fallback)) {\n    const fallbackLocale = getLocaleDefinition(fallback);\n    if (fallbackLocale) {\n      try {\n        const localeLoader = localeModules[`./${fallbackLocale.file}`];\n        if (!localeLoader) {\n          throw new Error(\n            `Fallback locale file ${fallbackLocale.file} not found`\n          );\n        }\n        const messages = await localeLoader();\n        i18n.global.setLocaleMessage(fallback, messages.default || messages);\n      } catch (error) {\n        console.error(`Failed to load fallback locale file:`, error);\n      }\n    }\n  }\n}\n\nexport function setupI18n(app: App): void {\n  app.use(i18n);\n}\n\nexport { i18n };\n"
  },
  {
    "path": "ui/src/locales/zh-CN.json",
    "content": "{\n  \"core.sidebar.search.placeholder\": \"搜索\",\n  \"core.sidebar.menu.groups.content\": \"内容\",\n  \"core.sidebar.menu.groups.interface\": \"外观\",\n  \"core.sidebar.menu.groups.system\": \"系统\",\n  \"core.sidebar.menu.groups.tool\": \"工具\",\n  \"core.sidebar.menu.items.dashboard\": \"仪表盘\",\n  \"core.sidebar.menu.items.posts\": \"文章\",\n  \"core.sidebar.menu.items.single_pages\": \"页面\",\n  \"core.sidebar.menu.items.comments\": \"评论\",\n  \"core.sidebar.menu.items.attachments\": \"附件\",\n  \"core.sidebar.menu.items.themes\": \"主题\",\n  \"core.sidebar.menu.items.menus\": \"菜单\",\n  \"core.sidebar.menu.items.plugins\": \"插件\",\n  \"core.sidebar.menu.items.users\": \"用户\",\n  \"core.sidebar.menu.items.settings\": \"设置\",\n  \"core.sidebar.menu.items.overview\": \"概览\",\n  \"core.sidebar.menu.items.backup\": \"备份\",\n  \"core.sidebar.menu.items.tools\": \"工具\",\n  \"core.sidebar.menu.items.home\": \"首页\",\n  \"core.sidebar.operations.logout.tooltip\": \"退出登录\",\n  \"core.sidebar.operations.logout.title\": \"退出登录\",\n  \"core.sidebar.operations.logout.description\": \"点击确定将跳转至退出登录页面，请确保正在编辑的内容已保存。\",\n  \"core.sidebar.operations.profile.tooltip\": \"个人中心\",\n  \"core.sidebar.operations.visit_homepage.title\": \"访问首页\",\n  \"core.sidebar.profile.aggregate_role\": \"聚合角色\",\n  \"core.uc_sidebar.menu.items.profile\": \"我的\",\n  \"core.uc_sidebar.menu.items.notification\": \"消息\",\n  \"core.uc_sidebar.menu.items.posts\": \"文章\",\n  \"core.uc_sidebar.operations.console.tooltip\": \"管理控制台\",\n  \"core.uc_sidebar.profile.aggregate_role\": \"聚合角色\",\n  \"core.dashboard.title\": \"仪表盘\",\n  \"core.dashboard.actions.setting\": \"设置\",\n  \"core.dashboard.widgets.groups.post\": \"文章\",\n  \"core.dashboard.widgets.groups.page\": \"页面\",\n  \"core.dashboard.widgets.groups.comment\": \"评论\",\n  \"core.dashboard.widgets.groups.user\": \"用户\",\n  \"core.dashboard.widgets.groups.other\": \"其他\",\n  \"core.dashboard.widgets.presets.post_stats.title\": \"文章\",\n  \"core.dashboard.widgets.presets.page_stats.title\": \"页面\",\n  \"core.dashboard.widgets.presets.recent_published.title\": \"最近文章\",\n  \"core.dashboard.widgets.presets.recent_published.visits\": \"访问量 {visits}\",\n  \"core.dashboard.widgets.presets.recent_published.comments\": \"评论 {comments}\",\n  \"core.dashboard.widgets.presets.recent_published.empty.title\": \"暂无已发布文章\",\n  \"core.dashboard.widgets.presets.recent_published.comments-with-pending\": \" ({count} 条待审核)\",\n  \"core.dashboard.widgets.presets.notification.title\": \"通知\",\n  \"core.dashboard.widgets.presets.notification.empty.title\": \"当前没有未读的消息\",\n  \"core.dashboard.widgets.presets.quickaction.title\": \"快捷访问\",\n  \"core.dashboard.widgets.presets.quickaction.actions.user_center.title\": \"个人中心\",\n  \"core.dashboard.widgets.presets.quickaction.actions.view_site.title\": \"查看站点\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_post.title\": \"创建文章\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_page.title\": \"创建页面\",\n  \"core.dashboard.widgets.presets.quickaction.actions.upload_attachment.title\": \"附件上传\",\n  \"core.dashboard.widgets.presets.quickaction.actions.theme_manage.title\": \"主题管理\",\n  \"core.dashboard.widgets.presets.quickaction.actions.plugin_manage.title\": \"插件管理\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_user.title\": \"新建用户\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.title\": \"刷新搜索引擎\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_title\": \"刷新搜索引擎索引\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_content\": \"此操作会对所有已发布的文章重新创建本地搜索引擎的索引。\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.success_message\": \"刷新成功\",\n  \"core.dashboard.widgets.presets.quickaction.config.fields.enabled_items.label\": \"启用的功能\",\n  \"core.dashboard.widgets.presets.user_stats.title\": \"用户\",\n  \"core.dashboard.widgets.presets.comment_stats.title\": \"评论\",\n  \"core.dashboard.widgets.presets.views_stats.title\": \"浏览量\",\n  \"core.dashboard.widgets.presets.pending_comments.title\": \"新评论\",\n  \"core.dashboard.widgets.presets.pending_comments.empty.title\": \"暂无待审核评论\",\n  \"core.dashboard.widgets.presets.iframe.title\": \"网页嵌入\",\n  \"core.dashboard.widgets.presets.iframe.empty.title\": \"请在配置中输入 URL\",\n  \"core.dashboard.widgets.presets.iframe.config.fields.title.label\": \"标题\",\n  \"core.dashboard.widgets.presets.stack.title\": \"堆叠部件\",\n  \"core.dashboard.widgets.presets.stack.operations.add_widget.button\": \"添加部件\",\n  \"core.dashboard.widgets.presets.stack.config_modal.title\": \"堆叠部件配置\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play.label\": \"自动切换\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play_interval.label\": \"自动切换间隔\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.widgets.label\": \"部件\",\n  \"core.dashboard.widgets.presets.stack.config_modal.toast.nest_warning\": \"不能将堆叠部件添加到堆叠部件中\",\n  \"core.dashboard.widgets.common_form.fields.enable_animation.label\": \"启用动画\",\n  \"core.dashboard_designer.title\": \"编辑仪表盘\",\n  \"core.dashboard_designer.actions.add_widget\": \"添加部件\",\n  \"core.dashboard_designer.widgets_modal.title\": \"小部件\",\n  \"core.dashboard_designer.config_modal.title\": \"部件配置\",\n  \"core.dashboard_designer.operations.back.title\": \"布局未保存\",\n  \"core.dashboard_designer.operations.back.description\": \"当前编辑的布局还未保存，放弃后，当前布局将丢失，是否继续？\",\n  \"core.dashboard_designer.operations.back.confirm_text\": \"放弃\",\n  \"core.dashboard_designer.operations.change_breakpoint.tips_not_saved\": \"请先保存当前布局\",\n  \"core.dashboard_designer.operations.copy_from_layout.button\": \"从其他布局复制\",\n  \"core.dashboard_designer.operations.open_widgets_hub.button\": \"小部件中心\",\n  \"core.dashboard_designer.breakpoints.lg\": \"桌面端\",\n  \"core.dashboard_designer.breakpoints.md\": \"平板端（横向）\",\n  \"core.dashboard_designer.breakpoints.sm\": \"平板端\",\n  \"core.dashboard_designer.breakpoints.xs\": \"手机端\",\n  \"core.post.title\": \"文章\",\n  \"core.post.actions.categories\": \"分类\",\n  \"core.post.actions.tags\": \"标签\",\n  \"core.post.actions.recycle_bin\": \"回收站\",\n  \"core.post.empty.title\": \"当前没有文章\",\n  \"core.post.empty.message\": \"你可以尝试刷新或者新建文章\",\n  \"core.post.operations.delete.title\": \"删除文章\",\n  \"core.post.operations.delete.description\": \"该操作会将文章放入回收站，后续可以从回收站恢复。\",\n  \"core.post.operations.delete_in_batch.title\": \"删除所选文章\",\n  \"core.post.operations.delete_in_batch.description\": \"该操作会将文章放入回收站，后续可以从回收站恢复。\",\n  \"core.post.operations.publish_in_batch.title\": \"发布文章\",\n  \"core.post.operations.publish_in_batch.description\": \"批量发布文章，所选文章会被设置为发布状态\",\n  \"core.post.operations.cancel_publish_in_batch.title\": \"取消发布文章\",\n  \"core.post.operations.cancel_publish_in_batch.description\": \"批量取消发布文章，所选文章会被设置为未发布状态\",\n  \"core.post.operations.batch_setting.button\": \"批量设置\",\n  \"core.post.filters.status.items.published\": \"已发布\",\n  \"core.post.filters.status.items.draft\": \"未发布\",\n  \"core.post.filters.status.items.scheduling\": \"定时发布\",\n  \"core.post.filters.visible.label\": \"可见性\",\n  \"core.post.filters.visible.result\": \"可见性：{visible}\",\n  \"core.post.filters.visible.items.public\": \"公开\",\n  \"core.post.filters.visible.items.private\": \"私有\",\n  \"core.post.filters.category.label\": \"分类\",\n  \"core.post.filters.category.result\": \"分类：{category}\",\n  \"core.post.filters.tag.label\": \"标签\",\n  \"core.post.filters.tag.result\": \"标签：{tag}\",\n  \"core.post.filters.author.label\": \"作者\",\n  \"core.post.filters.author.result\": \"作者：{author}\",\n  \"core.post.filters.sort.items.publish_time_desc\": \"较近发布\",\n  \"core.post.filters.sort.items.publish_time_asc\": \"较早发布\",\n  \"core.post.filters.sort.items.create_time_desc\": \"较近创建\",\n  \"core.post.filters.sort.items.create_time_asc\": \"较早创建\",\n  \"core.post.filters.sort.items.visit_desc\": \"最多访问量\",\n  \"core.post.filters.sort.items.comment_desc\": \"最多评论量\",\n  \"core.post.filters.sort.items.last_modify_time_desc\": \"最近更新\",\n  \"core.post.filters.sort.items.last_modify_time_asc\": \"较早更新\",\n  \"core.post.list.fields.categories\": \"分类：\",\n  \"core.post.list.fields.visits\": \"访问量 {visits}\",\n  \"core.post.list.fields.comments\": \"评论 {comments}\",\n  \"core.post.list.fields.pinned\": \"已置顶\",\n  \"core.post.list.fields.schedule_publish.tooltip\": \"定时发布\",\n  \"core.post.list.fields.comments-with-pending\": \"（{count} 条待审核）\",\n  \"core.post.settings.title\": \"文章设置\",\n  \"core.post.settings.groups.general\": \"常规设置\",\n  \"core.post.settings.groups.advanced\": \"高级设置\",\n  \"core.post.settings.groups.annotations\": \"元数据\",\n  \"core.post.settings.fields.title.label\": \"标题\",\n  \"core.post.settings.fields.slug.label\": \"别名\",\n  \"core.post.settings.fields.slug.help\": \"通常用于生成文章的固定链接\",\n  \"core.post.settings.fields.slug.refresh_message\": \"根据标题重新生成别名\",\n  \"core.post.settings.fields.categories.label\": \"分类目录\",\n  \"core.post.settings.fields.tags.label\": \"标签\",\n  \"core.post.settings.fields.auto_generate_excerpt.label\": \"自动生成摘要\",\n  \"core.post.settings.fields.raw_excerpt.label\": \"自定义摘要\",\n  \"core.post.settings.fields.allow_comment.label\": \"允许评论\",\n  \"core.post.settings.fields.pinned.label\": \"是否置顶\",\n  \"core.post.settings.fields.visible.label\": \"可见性\",\n  \"core.post.settings.fields.publish_time.label\": \"发表时间\",\n  \"core.post.settings.fields.publish_time.help.schedule_publish\": \"将设置定时任务，并于 {datetime} 发布\",\n  \"core.post.settings.fields.template.label\": \"自定义模板\",\n  \"core.post.settings.fields.cover.label\": \"封面图\",\n  \"core.post.settings.fields.owner.label\": \"作者\",\n  \"core.post.tag.filters.sort.items.create_time_desc\": \"较近创建\",\n  \"core.post.tag.filters.sort.items.create_time_asc\": \"较早创建\",\n  \"core.post.tag.filters.sort.items.display_name_desc\": \"标签名降序\",\n  \"core.post.tag.filters.sort.items.display_name_asc\": \"标签名升序\",\n  \"core.post.tag.filters.sort.items.post_desc\": \"最多文章量\",\n  \"core.post.batch_setting_modal.title\": \"文章批量设置\",\n  \"core.post.batch_setting_modal.fields.common.enabled\": \"启用\",\n  \"core.post.batch_setting_modal.fields.common.op.label\": \"设置方式\",\n  \"core.post.batch_setting_modal.fields.common.op.options.add\": \"追加\",\n  \"core.post.batch_setting_modal.fields.common.op.options.replace\": \"替换\",\n  \"core.post.batch_setting_modal.fields.common.op.options.remove_all\": \"移除全部\",\n  \"core.post.batch_setting_modal.fields.category_group\": \"分类\",\n  \"core.post.batch_setting_modal.fields.category_names\": \"选择分类\",\n  \"core.post.batch_setting_modal.fields.tag_group\": \"标签\",\n  \"core.post.batch_setting_modal.fields.tag_names\": \"选择标签\",\n  \"core.post.batch_setting_modal.fields.visible_group\": \"可见性\",\n  \"core.post.batch_setting_modal.fields.visible_value\": \"选择可见性\",\n  \"core.post.batch_setting_modal.fields.allow_comment_group\": \"允许评论\",\n  \"core.post.batch_setting_modal.fields.allow_comment_value\": \"选择是否允许评论\",\n  \"core.post.batch_setting_modal.fields.owner_group\": \"作者\",\n  \"core.post.batch_setting_modal.fields.owner_value\": \"设置作者\",\n  \"core.deleted_post.title\": \"文章回收站\",\n  \"core.deleted_post.empty.title\": \"没有文章被放入回收站\",\n  \"core.deleted_post.empty.message\": \"你可以尝试刷新或者返回文章管理\",\n  \"core.deleted_post.operations.delete.title\": \"删除文章\",\n  \"core.deleted_post.operations.delete.description\": \"删除之后将无法恢复。\",\n  \"core.deleted_post.operations.delete_in_batch.title\": \"删除所选文章\",\n  \"core.deleted_post.operations.delete_in_batch.description\": \"删除之后将无法恢复。\",\n  \"core.deleted_post.operations.recovery.title\": \"恢复文章\",\n  \"core.deleted_post.operations.recovery.description\": \"该操作会将文章恢复到被删除之前的状态。\",\n  \"core.deleted_post.operations.recovery_in_batch.title\": \"恢复所选文章\",\n  \"core.deleted_post.operations.recovery_in_batch.description\": \"该操作会将文章恢复到被删除之前的状态。\",\n  \"core.uc_post.title\": \"我的文章\",\n  \"core.uc_post.setting_modal.title\": \"文章设置\",\n  \"core.uc_post.creation_modal.title\": \"创建文章\",\n  \"core.uc_post.publish_modal.title\": \"发布文章\",\n  \"core.uc_post.operations.cancel_publish.title\": \"取消发布\",\n  \"core.uc_post.operations.cancel_publish.description\": \"确定要取消发布吗？\",\n  \"core.uc_post.operations.delete.title\": \"删除文章\",\n  \"core.uc_post.operations.delete.description\": \"该操作会将文章放入回收站，后续由网站管理员进行管理。\",\n  \"core.post_editor.title\": \"文章编辑\",\n  \"core.post_editor.untitled\": \"未命名文章\",\n  \"core.post_editor.actions.snapshots\": \"版本历史\",\n  \"core.post_tag.title\": \"文章标签\",\n  \"core.post_tag.header.title\": \"{count} 个标签\",\n  \"core.post_tag.empty.title\": \"当前没有标签\",\n  \"core.post_tag.empty.message\": \"你可以尝试刷新或者新建标签\",\n  \"core.post_tag.operations.delete.title\": \"删除标签\",\n  \"core.post_tag.operations.delete.description\": \"删除此标签之后，对应文章的关联将被解除。该操作不可恢复。\",\n  \"core.post_tag.operations.delete_in_batch.title\": \"删除所选标签\",\n  \"core.post_tag.editing_modal.titles.update\": \"编辑文章标签\",\n  \"core.post_tag.editing_modal.titles.create\": \"新增文章标签\",\n  \"core.post_tag.editing_modal.groups.general\": \"常规\",\n  \"core.post_tag.editing_modal.groups.annotations\": \"元数据\",\n  \"core.post_tag.editing_modal.fields.display_name.label\": \"名称\",\n  \"core.post_tag.editing_modal.fields.slug.label\": \"别名\",\n  \"core.post_tag.editing_modal.fields.slug.help\": \"通常用于生成标签的固定链接\",\n  \"core.post_tag.editing_modal.fields.slug.refresh_message\": \"根据名称重新生成别名\",\n  \"core.post_tag.editing_modal.fields.color.label\": \"颜色\",\n  \"core.post_tag.editing_modal.fields.color.help\": \"需要主题适配以支持\",\n  \"core.post_tag.editing_modal.fields.cover.label\": \"封面图\",\n  \"core.post_tag.editing_modal.fields.cover.help\": \"需要主题适配以支持\",\n  \"core.post_tag.editing_modal.fields.description.label\": \"描述\",\n  \"core.post_tag.editing_modal.fields.description.help\": \"该描述会自动添加到页面的 meta description 中用于 SEO，其他展示用途需要主题适配\",\n  \"core.post_category.title\": \"文章分类\",\n  \"core.post_category.header.title\": \"{count} 个分类\",\n  \"core.post_category.empty.title\": \"当前没有分类\",\n  \"core.post_category.empty.message\": \"你可以尝试刷新或者新建分类\",\n  \"core.post_category.operations.delete.title\": \"删除分类\",\n  \"core.post_category.operations.delete.description\": \"删除此分类之后，对应文章的关联将被解除。该操作不可恢复。\",\n  \"core.post_category.operations.add_sub_category.button\": \"新增子分类\",\n  \"core.post_category.editing_modal.titles.update\": \"编辑文章分类\",\n  \"core.post_category.editing_modal.titles.create\": \"新增文章分类\",\n  \"core.post_category.editing_modal.groups.general\": \"常规\",\n  \"core.post_category.editing_modal.groups.annotations\": \"元数据\",\n  \"core.post_category.editing_modal.fields.parent.label\": \"上级分类\",\n  \"core.post_category.editing_modal.fields.display_name.label\": \"名称\",\n  \"core.post_category.editing_modal.fields.slug.label\": \"别名\",\n  \"core.post_category.editing_modal.fields.slug.help\": \"通常用于生成分类的固定链接\",\n  \"core.post_category.editing_modal.fields.slug.refresh_message\": \"根据名称重新生成别名\",\n  \"core.post_category.editing_modal.fields.template.label\": \"自定义模板\",\n  \"core.post_category.editing_modal.fields.template.help\": \"自定义分类归档页面的渲染模板，需要主题提供支持\",\n  \"core.post_category.editing_modal.fields.cover.label\": \"封面图\",\n  \"core.post_category.editing_modal.fields.cover.help\": \"需要主题适配以支持\",\n  \"core.post_category.editing_modal.fields.description.label\": \"描述\",\n  \"core.post_category.editing_modal.fields.description.help\": \"描述会自动添加到页面用于 SEO 的描述标签中，其他显示用途需要主题适配\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.label\": \"阻止文章级联查询\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.help\": \"阻止父级分类在级联文章查询中包含此分类及其子分类\",\n  \"core.post_category.editing_modal.fields.post_template.label\": \"自定义文章模板\",\n  \"core.post_category.editing_modal.fields.post_template.help\": \"自定义当前分类下文章的渲染模板，需要主题提供支持\",\n  \"core.post_category.editing_modal.fields.hide_from_list.label\": \"在列表中隐藏\",\n  \"core.post_category.editing_modal.fields.hide_from_list.help\": \"开启此选项后，此分类和其下子分类，以及其下文章将不会显示在前台的列表中，需要主动访问分类归档页面，此功能仅对第一级目录生效\",\n  \"core.post_category.list.fields.prevent_parent_post_cascade_query\": \"阻止父级分类在级联文章查询中包含此分类及其子分类\",\n  \"core.post_category.list.fields.hide_from_list\": \"已隐藏，此分类和其下子分类，以及其下文章将不会显示在前台的列表中\",\n  \"core.page.title\": \"页面\",\n  \"core.page.actions.recycle_bin\": \"回收站\",\n  \"core.page.empty.title\": \"当前没有页面\",\n  \"core.page.empty.message\": \"你可以尝试刷新或者新建页面\",\n  \"core.page.operations.delete.title\": \"删除页面\",\n  \"core.page.operations.delete.description\": \"该操作会将自定义页面放入回收站，后续可以从回收站恢复。\",\n  \"core.page.operations.delete_in_batch.title\": \"删除所选页面\",\n  \"core.page.operations.delete_in_batch.description\": \"该操作会将自定义页面放入回收站，后续可以从回收站恢复。\",\n  \"core.page.filters.status.items.published\": \"已发布\",\n  \"core.page.filters.status.items.draft\": \"未发布\",\n  \"core.page.filters.visible.label\": \"可见性\",\n  \"core.page.filters.visible.result\": \"可见性：{visible}\",\n  \"core.page.filters.visible.items.public\": \"公开\",\n  \"core.page.filters.visible.items.private\": \"私有\",\n  \"core.page.filters.author.label\": \"作者\",\n  \"core.page.filters.author.result\": \"作者：{author}\",\n  \"core.page.filters.sort.items.publish_time_desc\": \"较近发布\",\n  \"core.page.filters.sort.items.publish_time_asc\": \"较早发布\",\n  \"core.page.filters.sort.items.create_time_desc\": \"较近创建\",\n  \"core.page.filters.sort.items.create_time_asc\": \"较早创建\",\n  \"core.page.list.fields.visits\": \"访问量 {visits}\",\n  \"core.page.list.fields.comments\": \"评论 {comments}\",\n  \"core.page.list.fields.comments-with-pending\": \"（{count} 条待审核）\",\n  \"core.page.settings.title\": \"页面设置\",\n  \"core.page.settings.groups.general\": \"常规设置\",\n  \"core.page.settings.groups.advanced\": \"高级设置\",\n  \"core.page.settings.groups.annotations\": \"元数据\",\n  \"core.page.settings.fields.title.label\": \"标题\",\n  \"core.page.settings.fields.slug.label\": \"别名\",\n  \"core.page.settings.fields.slug.help\": \"通常用于生成页面的固定链接\",\n  \"core.page.settings.fields.slug.refresh_message\": \"根据标题重新生成别名\",\n  \"core.page.settings.fields.auto_generate_excerpt.label\": \"自动生成摘要\",\n  \"core.page.settings.fields.raw_excerpt.label\": \"自定义摘要\",\n  \"core.page.settings.fields.allow_comment.label\": \"允许评论\",\n  \"core.page.settings.fields.pinned.label\": \"是否置顶\",\n  \"core.page.settings.fields.visible.label\": \"可见性\",\n  \"core.page.settings.fields.publish_time.label\": \"发表时间\",\n  \"core.page.settings.fields.template.label\": \"自定义模板\",\n  \"core.page.settings.fields.cover.label\": \"封面图\",\n  \"core.deleted_page.title\": \"页面回收站\",\n  \"core.deleted_page.empty.title\": \"没有自定义页面被放入回收站\",\n  \"core.deleted_page.empty.message\": \"你可以尝试刷新或者返回自定义页面管理\",\n  \"core.deleted_page.operations.delete.title\": \"删除页面\",\n  \"core.deleted_page.operations.delete.description\": \"删除之后将无法恢复。\",\n  \"core.deleted_page.operations.delete_in_batch.title\": \"删除所选页面\",\n  \"core.deleted_page.operations.delete_in_batch.description\": \"删除之后将无法恢复。\",\n  \"core.deleted_page.operations.recovery.title\": \"恢复页面\",\n  \"core.deleted_page.operations.recovery.description\": \"该操作会将自定义页面恢复到被删除之前的状态。\",\n  \"core.deleted_page.operations.recovery_in_batch.title\": \"恢复所选页面\",\n  \"core.deleted_page.operations.recovery_in_batch.description\": \"该操作会将自定义页面恢复到被删除之前的状态。\",\n  \"core.page_editor.title\": \"页面编辑\",\n  \"core.page_editor.untitled\": \"未命名页面\",\n  \"core.page_editor.actions.snapshots\": \"版本历史\",\n  \"core.comment.title\": \"评论\",\n  \"core.comment.empty.title\": \"当前没有评论\",\n  \"core.comment.empty.message\": \"你可以尝试刷新或者修改筛选条件\",\n  \"core.comment.reply_empty.title\": \"当前没有回复\",\n  \"core.comment.reply_empty.message\": \"你可以尝试刷新或者创建新回复\",\n  \"core.comment.reply_empty.new\": \"创建新回复\",\n  \"core.comment.operations.delete_comment.title\": \"删除评论\",\n  \"core.comment.operations.delete_comment.description\": \"将同时删除所有评论下的回复，该操作不可恢复。\",\n  \"core.comment.operations.delete_comment_in_batch.title\": \"删除所选评论\",\n  \"core.comment.operations.delete_comment_in_batch.description\": \"将同时删除所有评论下的回复，该操作不可恢复。\",\n  \"core.comment.operations.approve_comment_in_batch.button\": \"审核通过\",\n  \"core.comment.operations.approve_comment_in_batch.title\": \"确定要审核通过所选的评论吗？\",\n  \"core.comment.operations.approve_applies_in_batch.button\": \"审核通过所有回复\",\n  \"core.comment.operations.approve_applies_in_batch.title\": \"确定要审核通过该评论的所有回复吗？\",\n  \"core.comment.operations.delete_reply.title\": \"删除回复\",\n  \"core.comment.operations.review.button\": \"审核\",\n  \"core.comment.operations.reply.button\": \"回复\",\n  \"core.comment.operations.approve.button\": \"通过\",\n  \"core.comment.operations.reply_and_approve.button\": \"回复并通过\",\n  \"core.comment.operations.cancel_approve.button\": \"取消通过\",\n  \"core.comment.filters.status.items.approved\": \"已审核\",\n  \"core.comment.filters.status.items.pending_review\": \"待审核\",\n  \"core.comment.filters.owner.label\": \"评论者\",\n  \"core.comment.filters.owner.result\": \"评论者：{owner}\",\n  \"core.comment.filters.sort.items.last_reply_time_desc\": \"较近回复\",\n  \"core.comment.filters.sort.items.last_reply_time_asc\": \"较早回复\",\n  \"core.comment.filters.sort.items.reply_count_desc\": \"较多回复数\",\n  \"core.comment.filters.sort.items.reply_count_asc\": \"较少回复数\",\n  \"core.comment.filters.sort.items.create_time_desc\": \"较近创建\",\n  \"core.comment.filters.sort.items.create_time_asc\": \"较早创建\",\n  \"core.comment.list.fields.reply_count\": \"{count} 条回复\",\n  \"core.comment.list.fields.has_new_replies\": \"有新的回复\",\n  \"core.comment.list.fields.pending_review\": \"待审核\",\n  \"core.comment.list.fields.private\": \"私密\",\n  \"core.comment.subject_refs.post\": \"文章\",\n  \"core.comment.subject_refs.page\": \"页面\",\n  \"core.comment.subject_refs.unknown\": \"未知\",\n  \"core.comment.reply_modal.title\": \"回复\",\n  \"core.comment.reply_modal.fields.content.label\": \"内容\",\n  \"core.comment.reply_modal.operations.submit.toast_success\": \"回复成功\",\n  \"core.comment.text.commented_on\": \"评论在\",\n  \"core.comment.text.replied_below\": \"回复：\",\n  \"core.comment.detail_modal.fields.owner\": \"评论者\",\n  \"core.comment.detail_modal.fields.creation_time\": \"评论时间\",\n  \"core.comment.detail_modal.fields.commented_on\": \"评论在\",\n  \"core.comment.detail_modal.fields.new_reply\": \"新回复\",\n  \"core.comment.detail_modal.fields.user_agent\": \"用户代理（UA）\",\n  \"core.comment.comment_detail_modal.title\": \"评论详情\",\n  \"core.comment.comment_detail_modal.fields.content\": \"评论内容\",\n  \"core.comment.reply_detail_modal.title\": \"回复详情\",\n  \"core.comment.reply_detail_modal.fields.original_comment\": \"原评论\",\n  \"core.comment.reply_detail_modal.fields.content\": \"回复内容\",\n  \"core.attachment.title\": \"附件库\",\n  \"core.attachment.common.text.ungrouped\": \"未分组\",\n  \"core.attachment.common.text.original\": \"原始\",\n  \"core.attachment.actions.storage_policies\": \"存储策略\",\n  \"core.attachment.empty.title\": \"当前分组没有附件\",\n  \"core.attachment.empty.message\": \"当前分组没有附件，你可以尝试刷新或者上传附件\",\n  \"core.attachment.empty.actions.upload\": \"上传附件\",\n  \"core.attachment.operations.delete.title\": \"删除附件\",\n  \"core.attachment.operations.delete_in_batch.title\": \"删除所选附件\",\n  \"core.attachment.operations.deselect_items.button\": \"取消选择\",\n  \"core.attachment.operations.move.button\": \"移动\",\n  \"core.attachment.operations.move.toast_success\": \"移动成功\",\n  \"core.attachment.filters.storage_policy.label\": \"存储策略\",\n  \"core.attachment.filters.storage_policy.result\": \"存储策略：{storage_policy}\",\n  \"core.attachment.filters.owner.label\": \"上传者\",\n  \"core.attachment.filters.owner.result\": \"上传者：{owner}\",\n  \"core.attachment.filters.sort.items.create_time_desc\": \"较近上传\",\n  \"core.attachment.filters.sort.items.create_time_asc\": \"较早上传\",\n  \"core.attachment.filters.sort.items.size_desc\": \"文件大小降序\",\n  \"core.attachment.filters.sort.items.size_asc\": \"文件大小升序\",\n  \"core.attachment.filters.sort.items.display_name_asc\": \"文件名升序\",\n  \"core.attachment.filters.sort.items.display_name_desc\": \"文件名降序\",\n  \"core.attachment.filters.view_type.items.grid\": \"网格模式\",\n  \"core.attachment.filters.view_type.items.list\": \"列表模式\",\n  \"core.attachment.filters.accept.label\": \"类型\",\n  \"core.attachment.filters.accept.items.image\": \"图片\",\n  \"core.attachment.filters.accept.items.audio\": \"音频\",\n  \"core.attachment.filters.accept.items.video\": \"视频\",\n  \"core.attachment.filters.accept.items.file\": \"文件\",\n  \"core.attachment.detail_modal.title\": \"附件：{display_name}\",\n  \"core.attachment.detail_modal.fields.preview\": \"预览\",\n  \"core.attachment.detail_modal.fields.storage_policy\": \"存储策略\",\n  \"core.attachment.detail_modal.fields.group\": \"所在分组\",\n  \"core.attachment.detail_modal.fields.display_name\": \"附件名称\",\n  \"core.attachment.detail_modal.fields.media_type\": \"文件类型\",\n  \"core.attachment.detail_modal.fields.size\": \"文件大小\",\n  \"core.attachment.detail_modal.fields.owner\": \"上传者\",\n  \"core.attachment.detail_modal.fields.creation_time\": \"上传时间\",\n  \"core.attachment.detail_modal.fields.permalink\": \"链接\",\n  \"core.attachment.detail_modal.display_name_form.help\": \"自定义附件名称，仅用于管理界面显示，不会影响文件本身名称以及访问链接\",\n  \"core.attachment.detail_modal.preview.video_not_support\": \"当前浏览器不支持该视频播放\",\n  \"core.attachment.detail_modal.preview.audio_not_support\": \"当前浏览器不支持该音频播放\",\n  \"core.attachment.detail_modal.preview.not_support\": \"此文件不支持预览\",\n  \"core.attachment.group_editing_modal.titles.create\": \"新增附件分组\",\n  \"core.attachment.group_editing_modal.titles.update\": \"编辑附件分组\",\n  \"core.attachment.group_editing_modal.fields.display_name.label\": \"名称\",\n  \"core.attachment.group_editing_modal.toast.group_name_exists\": \"分组名称已存在\",\n  \"core.attachment.group_list.internal_groups.all\": \"全部\",\n  \"core.attachment.group_list.operations.rename.button\": \"重命名\",\n  \"core.attachment.group_list.operations.delete.button\": \"删除并将附件移动至未分组\",\n  \"core.attachment.group_list.operations.delete.title\": \"删除附件分组\",\n  \"core.attachment.group_list.operations.delete.description\": \"将删除分组，并将分组下的附件移动至未分组，该操作不可恢复。\",\n  \"core.attachment.group_list.operations.delete.toast_success\": \"删除成功，{total} 个附件已移动至未分组\",\n  \"core.attachment.group_list.operations.delete_with_attachments.button\": \"删除并同时删除附件\",\n  \"core.attachment.group_list.operations.delete_with_attachments.title\": \"删除附件分组\",\n  \"core.attachment.group_list.operations.delete_with_attachments.description\": \"将删除分组以及分组下的所有附件，该操作不可恢复。\",\n  \"core.attachment.group_list.operations.delete_with_attachments.toast_success\": \"删除成功，{total} 个附件已被同时删除\",\n  \"core.attachment.policies_modal.title\": \"存储策略\",\n  \"core.attachment.policies_modal.empty.title\": \"当前没有可用的存储策略\",\n  \"core.attachment.policies_modal.empty.message\": \"当前没有可用的存储策略，你可以尝试刷新或者新建策略\",\n  \"core.attachment.policies_modal.operations.delete.title\": \"删除附件策略\",\n  \"core.attachment.policies_modal.operations.delete.description\": \"当前策略下没有已上传的附件。\",\n  \"core.attachment.policies_modal.operations.can_not_delete.title\": \"删除失败\",\n  \"core.attachment.policies_modal.operations.can_not_delete.description\": \"该策略下存在附件，无法删除。\",\n  \"core.attachment.policy_editing_modal.titles.create\": \"新增策略：{policy_template}\",\n  \"core.attachment.policy_editing_modal.titles.update\": \"编辑策略：{policy}\",\n  \"core.attachment.policy_editing_modal.fields.display_name.label\": \"名称\",\n  \"core.attachment.policy_editing_modal.fields.config.label\": \"策略配置\",\n  \"core.attachment.policy_editing_modal.fields.hidden.label\": \"在上传界面隐藏\",\n  \"core.attachment.policy_editing_modal.fields.hidden.help\": \"开启后，会在上传界面隐藏该存储策略\",\n  \"core.attachment.policy_editing_modal.fields.priority.help\": \"设置在上传界面的显示优先级，数字越大，排序越靠前\",\n  \"core.attachment.policy_editing_modal.fields.priority.label\": \"上传界面显示优先级\",\n  \"core.attachment.policy_editing_modal.toast.policy_name_exists\": \"存储策略名称已存在\",\n  \"core.attachment.upload_modal.title\": \"上传附件\",\n  \"core.attachment.upload_modal.filters.group.label\": \"分组：\",\n  \"core.attachment.upload_modal.filters.policy.label\": \"存储策略：\",\n  \"core.attachment.upload_modal.filters.policy.empty.title\": \"没有存储策略\",\n  \"core.attachment.upload_modal.filters.policy.empty.description\": \"在上传之前，需要新建一个存储策略\",\n  \"core.attachment.upload_modal.filters.policy.not_select\": \"请先选择存储策略\",\n  \"core.attachment.upload_modal.upload_options.local_upload\": \"本地上传\",\n  \"core.attachment.upload_modal.upload_options.download\": \"通过链接下载\",\n  \"core.attachment.upload_modal.download_form.fields.url.label\": \"链接地址\",\n  \"core.attachment.upload_modal.download_form.toast.success\": \"下载成功\",\n  \"core.attachment.select_modal.title\": \"选择附件\",\n  \"core.attachment.select_modal.providers.default.label\": \"附件库\",\n  \"core.attachment.select_modal.operations.select.result\": \"（已选择 {count} 项）\",\n  \"core.attachment.permalink_list.relative\": \"相对路径\",\n  \"core.attachment.permalink_list.absolute\": \"完整路径\",\n  \"core.uc_attachment.empty.title\": \"当前没有附件\",\n  \"core.uc_attachment.empty.message\": \"当前没有附件，你可以尝试刷新或者上传附件\",\n  \"core.uc_attachment.empty.actions.upload\": \"上传附件\",\n  \"core.uc_attachment.filters.sort.items.create_time_desc\": \"较近上传\",\n  \"core.uc_attachment.filters.sort.items.create_time_asc\": \"较早上传\",\n  \"core.uc_attachment.filters.sort.items.size_desc\": \"文件大小降序\",\n  \"core.uc_attachment.filters.sort.items.size_asc\": \"文件大小升序\",\n  \"core.uc_attachment.filters.sort.items.display_name_asc\": \"文件名升序\",\n  \"core.uc_attachment.filters.sort.items.display_name_desc\": \"文件名降序\",\n  \"core.uc_attachment.filters.view_type.items.grid\": \"网格模式\",\n  \"core.uc_attachment.filters.view_type.items.list\": \"列表模式\",\n  \"core.uc_attachment.detail_modal.title\": \"附件：{display_name}\",\n  \"core.uc_attachment.detail_modal.fields.preview\": \"预览\",\n  \"core.uc_attachment.detail_modal.fields.display_name\": \"文件名称\",\n  \"core.uc_attachment.detail_modal.fields.media_type\": \"文件类型\",\n  \"core.uc_attachment.detail_modal.fields.size\": \"文件大小\",\n  \"core.uc_attachment.detail_modal.fields.owner\": \"上传者\",\n  \"core.uc_attachment.detail_modal.fields.creation_time\": \"上传时间\",\n  \"core.uc_attachment.detail_modal.fields.permalink\": \"链接\",\n  \"core.uc_attachment.detail_modal.preview.click_to_exit\": \"点击退出预览\",\n  \"core.uc_attachment.detail_modal.preview.video_not_support\": \"当前浏览器不支持该视频播放\",\n  \"core.uc_attachment.detail_modal.preview.audio_not_support\": \"当前浏览器不支持该音频播放\",\n  \"core.uc_attachment.detail_modal.preview.not_support\": \"此文件不支持预览\",\n  \"core.uc_attachment.upload_modal.title\": \"上传附件\",\n  \"core.uc_attachment.select_modal.title\": \"选择附件\",\n  \"core.uc_attachment.select_modal.providers.default.label\": \"附件库\",\n  \"core.uc_attachment.select_modal.operations.select.result\": \"（已选择 {count} 项）\",\n  \"core.theme.title\": \"主题\",\n  \"core.theme.common.buttons.install\": \"安装主题\",\n  \"core.theme.tabs.detail\": \"详情\",\n  \"core.theme.actions.management\": \"主题管理\",\n  \"core.theme.empty.title\": \"当前没有已激活或已选择的主题\",\n  \"core.theme.empty.message\": \"当前没有已激活或者选择的主题，你可以切换主题或者安装新主题\",\n  \"core.theme.empty.actions.switch\": \"切换主题\",\n  \"core.theme.operations.active.title\": \"启用主题\",\n  \"core.theme.operations.active.toast_success\": \"启用成功\",\n  \"core.theme.operations.reset.title\": \"重置主题配置\",\n  \"core.theme.operations.reset.description\": \"该操作会删除已保存的配置，重置为默认配置。\",\n  \"core.theme.operations.reset.toast_success\": \"重置配置成功\",\n  \"core.theme.operations.reload.button\": \"重载主题配置\",\n  \"core.theme.operations.reload.title\": \"重载主题配置\",\n  \"core.theme.operations.reload.description\": \"该操作仅会重载主题配置和设置表单定义，不会删除已保存的配置。\",\n  \"core.theme.operations.reload.toast_success\": \"重载配置成功\",\n  \"core.theme.operations.uninstall.title\": \"卸载主题\",\n  \"core.theme.operations.uninstall_and_delete_config.button\": \"卸载并删除配置\",\n  \"core.theme.operations.uninstall_and_delete_config.title\": \"卸载主题以及对应的配置\",\n  \"core.theme.operations.remote_download.title\": \"检测到了远程下载地址，是否需要下载？\",\n  \"core.theme.operations.remote_download.description\": \"请仔细鉴别此地址是否可信：{url}\",\n  \"core.theme.operations.existed_during_installation.title\": \"主题已存在\",\n  \"core.theme.operations.existed_during_installation.description\": \"当前安装的主题已存在，是否升级？\",\n  \"core.theme.operations.clear_templates_cache.button\": \"清理模板缓存\",\n  \"core.theme.operations.clear_templates_cache.title\": \"清除模板缓存\",\n  \"core.theme.operations.clear_templates_cache.description\": \"此功能适用于在运行时修改模板文件后，刷新缓存以查看最新网页结果。\",\n  \"core.theme.operations.export_configuration.button\": \"导出主题配置\",\n  \"core.theme.operations.import_configuration.button\": \"导入主题配置\",\n  \"core.theme.operations.import_configuration.version_mismatch.title\": \"版本不匹配\",\n  \"core.theme.operations.import_configuration.version_mismatch.description\": \"导入的配置文件版本与当前主题版本不匹配，这可能会导致兼容性问题。是否继续导入？\",\n  \"core.theme.operations.import_configuration.invalid_format\": \"无效的主题配置文件\",\n  \"core.theme.operations.import_configuration.mismatched_theme\": \"配置文件与所选主题不匹配\",\n  \"core.theme.list_modal.tabs.installed\": \"已安装\",\n  \"core.theme.list_modal.tabs.not_installed\": \"本地未安装\",\n  \"core.theme.list_modal.tabs.local_upload\": \"上传安装 / 升级\",\n  \"core.theme.list_modal.tabs.remote_download.label\": \"远程下载\",\n  \"core.theme.list_modal.tabs.remote_download.fields.url\": \"下载地址\",\n  \"core.theme.list_modal.empty.title\": \"当前没有已安装的主题\",\n  \"core.theme.list_modal.empty.message\": \"当前没有已安装的主题，你可以尝试刷新或者安装新主题\",\n  \"core.theme.list_modal.not_installed_empty.title\": \"当前没有未安装的主题\",\n  \"core.theme.preview_model.title\": \"预览主题：{display_name}\",\n  \"core.theme.preview_model.actions.switch\": \"切换主题\",\n  \"core.theme.preview_model.actions.setting\": \"主题设置\",\n  \"core.theme.preview_model.actions.open\": \"新窗口打开\",\n  \"core.theme.detail.fields.author\": \"作者\",\n  \"core.theme.detail.fields.homepage\": \"网站\",\n  \"core.theme.detail.fields.repo\": \"源码仓库\",\n  \"core.theme.detail.fields.version\": \"版本\",\n  \"core.theme.detail.fields.requires\": \"Halo 版本要求\",\n  \"core.theme.detail.fields.storage_location\": \"存储位置\",\n  \"core.theme.detail.fields.plugin_requires\": \"插件依赖\",\n  \"core.theme.detail.fields.description\": \"描述\",\n  \"core.theme.detail.fields.license\": \"协议\",\n  \"core.theme.detail.fields.issues\": \"问题反馈\",\n  \"core.theme.settings.title\": \"主题设置\",\n  \"core.theme.custom_templates.default\": \"默认模板\",\n  \"core.menu.title\": \"菜单\",\n  \"core.menu.empty.title\": \"当前没有菜单\",\n  \"core.menu.empty.message\": \"你可以尝试刷新或者新建菜单\",\n  \"core.menu.menu_item_empty.title\": \"当前没有菜单项\",\n  \"core.menu.menu_item_empty.message\": \"你可以尝试刷新或者新建菜单项\",\n  \"core.menu.operations.set_primary.button\": \"设置为主菜单\",\n  \"core.menu.operations.set_primary.toast_success\": \"设置成功\",\n  \"core.menu.operations.delete_menu.title\": \"删除菜单\",\n  \"core.menu.operations.delete_menu.description\": \"将同时删除该菜单下的所有菜单项，该操作不可恢复。\",\n  \"core.menu.operations.delete_menu_item.title\": \"删除菜单项\",\n  \"core.menu.operations.delete_menu_item.description\": \"将同时删除所有子菜单项，删除后将无法恢复。\",\n  \"core.menu.operations.add_sub_menu_item.button\": \"新增子菜单项\",\n  \"core.menu.list.fields.primary\": \"主菜单\",\n  \"core.menu.list.fields.items_count\": \"{count} 个菜单项\",\n  \"core.menu.menu_editing_modal.titles.create\": \"新增菜单\",\n  \"core.menu.menu_editing_modal.titles.update\": \"编辑菜单\",\n  \"core.menu.menu_editing_modal.fields.display_name.label\": \"菜单名称\",\n  \"core.menu.menu_item_editing_modal.titles.create\": \"新增菜单项\",\n  \"core.menu.menu_item_editing_modal.titles.update\": \"编辑菜单项\",\n  \"core.menu.menu_item_editing_modal.groups.general\": \"常规\",\n  \"core.menu.menu_item_editing_modal.groups.annotations\": \"元数据\",\n  \"core.menu.menu_item_editing_modal.fields.parent.label\": \"上级菜单项\",\n  \"core.menu.menu_item_editing_modal.fields.parent.placeholder\": \"选择上级菜单项\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.label\": \"类型\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.placeholder\": \"请选择{label}\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.custom\": \"自定义链接\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.post\": \"文章\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page\": \"自定义页面\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.category\": \"分类\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.tag\": \"标签\",\n  \"core.menu.menu_item_editing_modal.fields.display_name.label\": \"名称\",\n  \"core.menu.menu_item_editing_modal.fields.href.label\": \"链接地址\",\n  \"core.menu.menu_item_editing_modal.fields.target.label\": \"打开方式\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.self\": \"当前窗口\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.blank\": \"新窗口\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.parent\": \"父窗口\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.top\": \"顶级窗口\",\n  \"core.plugin.title\": \"插件\",\n  \"core.plugin.tabs.detail\": \"详情\",\n  \"core.plugin.empty.title\": \"当前没有已安装的插件\",\n  \"core.plugin.empty.message\": \"当前没有已安装的插件，你可以尝试刷新或者安装新插件\",\n  \"core.plugin.empty.actions.install\": \"安装插件\",\n  \"core.plugin.operations.reset.title\": \"重置插件配置\",\n  \"core.plugin.operations.reset.description\": \"该操作会删除已保存的配置，重置为默认配置。\",\n  \"core.plugin.operations.reset.toast_success\": \"重置配置成功\",\n  \"core.plugin.operations.uninstall.title\": \"卸载插件\",\n  \"core.plugin.operations.uninstall_in_batch.title\": \"卸载所选插件\",\n  \"core.plugin.operations.uninstall_and_delete_config.button\": \"卸载并删除配置\",\n  \"core.plugin.operations.uninstall_and_delete_config.title\": \"卸载插件以及对应的配置\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.button\": \"卸载并删除配置\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.title\": \"卸载所选插件以及对应的配置\",\n  \"core.plugin.operations.uninstall_when_enabled.confirm_text\": \"停止运行并卸载\",\n  \"core.plugin.operations.uninstall_when_enabled.description\": \"当前插件还在启用状态，将在停止运行后卸载，该操作不可恢复。\",\n  \"core.plugin.operations.change_status_in_batch.activate_title\": \"启用所选插件\",\n  \"core.plugin.operations.change_status_in_batch.inactivate_title\": \"停用所选插件\",\n  \"core.plugin.operations.remote_download.title\": \"检测到了远程下载地址，是否需要下载？\",\n  \"core.plugin.operations.remote_download.description\": \"请仔细鉴别此地址是否可信：{url}\",\n  \"core.plugin.operations.reload_window.button\": \"需要重载页面\",\n  \"core.plugin.filters.status.items.active\": \"已启用\",\n  \"core.plugin.filters.status.items.inactive\": \"未启用\",\n  \"core.plugin.filters.sort.items.create_time_desc\": \"较近安装\",\n  \"core.plugin.filters.sort.items.create_time_asc\": \"较早安装\",\n  \"core.plugin.upload_modal.titles.install\": \"安装插件\",\n  \"core.plugin.upload_modal.titles.upgrade\": \"升级插件（{display_name}）\",\n  \"core.plugin.upload_modal.tabs.local\": \"本地上传\",\n  \"core.plugin.upload_modal.tabs.remote.title\": \"远程下载\",\n  \"core.plugin.upload_modal.tabs.remote.fields.url\": \"下载地址\",\n  \"core.plugin.upload_modal.operations.active_after_install.title\": \"安装成功\",\n  \"core.plugin.upload_modal.operations.active_after_install.description\": \"是否启动当前安装的插件？\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.title\": \"插件已存在\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.description\": \"当前安装的插件已存在，是否升级？\",\n  \"core.plugin.detail.title\": \"插件详情\",\n  \"core.plugin.detail.header.title\": \"插件信息\",\n  \"core.plugin.detail.fields.description\": \"描述\",\n  \"core.plugin.detail.fields.version\": \"版本\",\n  \"core.plugin.detail.fields.requires\": \"Halo 版本要求\",\n  \"core.plugin.detail.fields.author\": \"作者\",\n  \"core.plugin.detail.fields.license\": \"协议\",\n  \"core.plugin.detail.fields.role_templates\": \"权限模板\",\n  \"core.plugin.detail.fields.last_starttime\": \"最近一次启动\",\n  \"core.plugin.detail.fields.homepage\": \"网站\",\n  \"core.plugin.detail.fields.repo\": \"源码仓库\",\n  \"core.plugin.detail.fields.load_location\": \"存储位置\",\n  \"core.plugin.detail.fields.issues\": \"问题反馈\",\n  \"core.plugin.detail.fields.creation_time\": \"安装时间\",\n  \"core.plugin.detail.operations.copy_error_message.button\": \"复制错误信息\",\n  \"core.plugin.detail.operations.copy_error_message.copied\": \"已复制\",\n  \"core.plugin.detail.operations.view_conditions.button\": \"查看最近状态\",\n  \"core.plugin.loader.toast.entry_load_failed\": \"加载插件入口文件失败\",\n  \"core.plugin.loader.toast.style_load_failed\": \"加载插件样式文件失败\",\n  \"core.plugin.extension_points.editor.providers.default\": \"默认编辑器\",\n  \"core.plugin.conditions_modal.title\": \"插件最近状态\",\n  \"core.plugin.conditions_modal.fields.type\": \"类型\",\n  \"core.plugin.conditions_modal.fields.status\": \"状态\",\n  \"core.plugin.conditions_modal.fields.reason\": \"原因\",\n  \"core.plugin.conditions_modal.fields.message\": \"信息\",\n  \"core.plugin.conditions_modal.fields.last_transition_time\": \"时间\",\n  \"core.plugin.extension-settings.title\": \"扩展配置\",\n  \"core.plugin.extension-settings.extension-point-definition.title\": \"扩展点定义\",\n  \"core.plugin.extension-settings.extension-definition.empty.title\": \"当前没有扩展点实现\",\n  \"core.plugin.extension-settings.extension-definition.operation.use.button\": \"使用\",\n  \"core.plugin.actions.extension-point-settings\": \"扩展配置\",\n  \"core.plugin.list.fields.system_reserved\": \"系统保留\",\n  \"core.user.title\": \"用户\",\n  \"core.user.actions.roles\": \"角色管理\",\n  \"core.user.actions.identity_authentication\": \"身份认证\",\n  \"core.user.empty.title\": \"当前没有符合筛选条件的用户\",\n  \"core.user.empty.message\": \"当前没有符合筛选条件的用户，你可以尝试刷新或者创建新用户\",\n  \"core.user.operations.delete.title\": \"删除用户\",\n  \"core.user.operations.delete_in_batch.title\": \"删除所选用户\",\n  \"core.user.operations.update_profile.title\": \"修改资料\",\n  \"core.user.operations.change_password.title\": \"修改密码\",\n  \"core.user.operations.grant_permission.title\": \"分配角色\",\n  \"core.user.operations.enable.title\": \"启用\",\n  \"core.user.operations.enable.description\": \"确定要启用该用户吗？启用之后该用户将可以重新登录系统\",\n  \"core.user.operations.enable_in_batch.title\": \"启用\",\n  \"core.user.operations.enable_in_batch.description\": \"确定要启用所选用户吗？启用后这些用户将可以重新登录系统\",\n  \"core.user.operations.disable.title\": \"禁用\",\n  \"core.user.operations.disable.description\": \"确定要禁用该用户吗？禁用后该用户将无法登录系统\",\n  \"core.user.operations.disable_in_batch.title\": \"禁用\",\n  \"core.user.operations.disable_in_batch.description\": \"确定要禁用所选用户吗？禁用后这些用户将无法登录系统\",\n  \"core.user.filters.role.label\": \"角色\",\n  \"core.user.filters.role.result\": \"角色：{role}\",\n  \"core.user.filters.sort.items.create_time_desc\": \"较近创建\",\n  \"core.user.filters.sort.items.create_time_asc\": \"较早创建\",\n  \"core.user.editing_modal.titles.update\": \"编辑用户\",\n  \"core.user.editing_modal.titles.create\": \"新增用户\",\n  \"core.user.editing_modal.groups.general\": \"常规\",\n  \"core.user.editing_modal.groups.annotations\": \"元数据\",\n  \"core.user.editing_modal.fields.username.label\": \"用户名\",\n  \"core.user.editing_modal.fields.username.validation\": \"只能包含数字、小写字母、. 和 - 符号，且不能以 . 和 - 符号开头和结尾\",\n  \"core.user.editing_modal.fields.display_name.label\": \"显示名称\",\n  \"core.user.editing_modal.fields.email.label\": \"电子邮箱\",\n  \"core.user.editing_modal.fields.phone.label\": \"手机号\",\n  \"core.user.editing_modal.fields.avatar.label\": \"头像\",\n  \"core.user.editing_modal.fields.bio.label\": \"描述\",\n  \"core.user.change_password_modal.title\": \"密码修改\",\n  \"core.user.change_password_modal.fields.new_password.label\": \"新密码\",\n  \"core.user.change_password_modal.fields.confirm_password.label\": \"确认密码\",\n  \"core.user.grant_permission_modal.title\": \"分配角色\",\n  \"core.user.grant_permission_modal.fields.role.label\": \"角色\",\n  \"core.user.grant_permission_modal.fields.role.placeholder\": \"请选择角色\",\n  \"core.user.grant_permission_modal.roles_preview.all\": \"当前所选角色包含所有权限\",\n  \"core.user.grant_permission_modal.roles_preview.includes\": \"当前所选角色包含的权限：\",\n  \"core.user.detail.title\": \"用户详情\",\n  \"core.user.detail.tabs.detail\": \"详情\",\n  \"core.user.detail.actions.update_profile.title\": \"修改资料\",\n  \"core.user.detail.actions.change_password.title\": \"修改密码\",\n  \"core.user.detail.actions.grant_permission.title\": \"分配角色\",\n  \"core.user.detail.actions.profile.title\": \"个人中心\",\n  \"core.user.detail.fields.display_name\": \"显示名称\",\n  \"core.user.detail.fields.username\": \"用户名\",\n  \"core.user.detail.fields.email\": \"电子邮箱\",\n  \"core.user.detail.fields.roles\": \"角色\",\n  \"core.user.detail.fields.bio\": \"描述\",\n  \"core.user.detail.fields.creation_time\": \"注册时间\",\n  \"core.user.detail.fields.email_verified.tooltip\": \"已验证\",\n  \"core.user.detail.fields.email_not_verified.tooltip\": \"未验证\",\n  \"core.user.fields.disabled\": \"已禁用\",\n  \"core.role.title\": \"角色\",\n  \"core.role.common.text.contains_all_permissions\": \"包含所有权限\",\n  \"core.role.common.text.contains_n_permissions\": \"包含 {count} 个权限\",\n  \"core.role.common.text.system_reserved\": \"系统保留\",\n  \"core.role.common.text.custom\": \"自定义\",\n  \"core.role.common.text.dependent_on\": \"依赖于 {roles}\",\n  \"core.role.common.text.provided_by_plugin\": \"由 {plugin} 提供\",\n  \"core.role.operations.delete.title\": \"删除角色\",\n  \"core.role.operations.delete.description\": \"该角色删除后，相关联的用户将被删除角色绑定，该操作不可恢复。\",\n  \"core.role.operations.create_based_on_this_role.button\": \"基于此角色创建\",\n  \"core.role.detail.title\": \"角色详情\",\n  \"core.role.detail.header.title\": \"角色信息\",\n  \"core.role.detail.tabs.detail\": \"详情\",\n  \"core.role.detail.tabs.permissions\": \"权限设置\",\n  \"core.role.detail.fields.display_name\": \"名称\",\n  \"core.role.detail.fields.name\": \"别名\",\n  \"core.role.detail.fields.type\": \"类型\",\n  \"core.role.detail.fields.creation_time\": \"创建时间\",\n  \"core.role.permissions_detail.system_reserved_alert.description\": \"系统保留的角色不支持修改，推荐基于此角色创建一个新的角色。\",\n  \"core.role.editing_modal.titles.create\": \"创建角色\",\n  \"core.role.editing_modal.titles.update\": \"编辑角色\",\n  \"core.role.editing_modal.groups.general\": \"基本信息\",\n  \"core.role.editing_modal.groups.permissions\": \"权限\",\n  \"core.role.editing_modal.fields.display_name\": \"名称\",\n  \"core.role.editing_modal.fields.disallow_access_console.label\": \"禁止访问 Console\",\n  \"core.role.editing_modal.fields.disallow_access_console.help\": \"勾选之后，该角色将无法访问 Console\",\n  \"core.identity_authentication.title\": \"身份认证\",\n  \"core.identity_authentication.tabs.detail\": \"详情\",\n  \"core.identity_authentication.tabs.setting\": \"设置\",\n  \"core.identity_authentication.operations.enable.title\": \"启用认证方式\",\n  \"core.identity_authentication.operations.disable.title\": \"停用认证方式\",\n  \"core.identity_authentication.operations.disable_privileged.tooltip\": \"系统保留的认证方式，无法禁用\",\n  \"core.identity_authentication.fields.display_name.local\": \"账号密码登录\",\n  \"core.identity_authentication.fields.description.local\": \"Halo 内置的默认登录方式\",\n  \"core.identity_authentication.list.types.form\": \"基础认证方式\",\n  \"core.identity_authentication.list.types.oauth2\": \"三方认证方式\",\n  \"core.identity_authentication.detail.title\": \"身份认证详情\",\n  \"core.identity_authentication.detail.fields.display_name\": \"名称\",\n  \"core.identity_authentication.detail.fields.description\": \"描述\",\n  \"core.identity_authentication.detail.fields.website\": \"网站\",\n  \"core.identity_authentication.detail.fields.help_page\": \"帮助页面\",\n  \"core.identity_authentication.detail.fields.authentication_url\": \"登录入口\",\n  \"core.uc_profile.title\": \"我的\",\n  \"core.uc_profile.tabs.detail\": \"详情\",\n  \"core.uc_profile.tabs.notification-preferences\": \"通知配置\",\n  \"core.uc_profile.tabs.pat\": \"个人令牌\",\n  \"core.uc_profile.tabs.devices\": \"登录设备\",\n  \"core.uc_profile.tabs.authentication\": \"身份认证\",\n  \"core.uc_profile.actions.update_profile.title\": \"修改资料\",\n  \"core.uc_profile.actions.change_password.title\": \"修改密码\",\n  \"core.uc_profile.detail.fields.display_name\": \"显示名称\",\n  \"core.uc_profile.detail.fields.username\": \"用户名\",\n  \"core.uc_profile.detail.fields.email\": \"电子邮箱\",\n  \"core.uc_profile.detail.fields.roles\": \"角色\",\n  \"core.uc_profile.detail.fields.bio\": \"描述\",\n  \"core.uc_profile.detail.fields.creation_time\": \"注册时间\",\n  \"core.uc_profile.detail.email_not_set.title\": \"设置电子邮箱\",\n  \"core.uc_profile.detail.email_not_set.description\": \"电子邮箱地址还未设置，点击下方按钮进行设置\",\n  \"core.uc_profile.detail.email_not_verified.title\": \"验证电子邮箱\",\n  \"core.uc_profile.detail.email_not_verified.description\": \"电子邮箱地址还未验证，点击下方按钮进行验证\",\n  \"core.uc_profile.detail.email_verified.tooltip\": \"已验证\",\n  \"core.uc_profile.auth_providers.title\": \"登录方式\",\n  \"core.uc_profile.auth_providers.operations.bind.button\": \"绑定\",\n  \"core.uc_profile.auth_providers.operations.unbind.button\": \"解绑\",\n  \"core.uc_profile.auth_providers.operations.unbind.title\": \"取消绑定 {display_name} 登录方式\",\n  \"core.uc_profile.2fa.operations.enable.button\": \"启用两步验证\",\n  \"core.uc_profile.2fa.operations.enable.title\": \"启用两步验证\",\n  \"core.uc_profile.2fa.operations.disable.title\": \"禁用两步验证\",\n  \"core.uc_profile.2fa.operations.disable_totp.title\": \"停用 TOTP\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.label\": \"验证密码\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.help\": \"当前账号的登录密码\",\n  \"core.uc_profile.2fa.methods.title\": \"两步验证\",\n  \"core.uc_profile.2fa.methods.totp.title\": \"TOTP\",\n  \"core.uc_profile.2fa.methods.totp.description\": \"使用 TOTP 应用程序配置两步验证\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.configured\": \"已配置\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.not_configured\": \"未配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.reconfigure.button\": \"重新配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.button\": \"配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.title\": \"TOTP 配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.label\": \"验证码\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.help\": \"从验证器应用获得的 6 位验证码\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.label\": \"验证密码\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.help\": \"当前账号的登录密码\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.qrcode.label\": \"使用验证器应用扫描下方二维码：\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.label\": \"如果无法扫描二维码，点击查看代替步骤\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.help\": \"使用以下代码手动配置验证器应用：\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.tips\": \"请妥善保管您的两步验证设备，如果设备丢失或损坏，你将无法登录系统。建议你在多个设备上安装验证器应用，或保存好密钥的备份，以防主要设备无法使用。\",\n  \"core.uc_profile.pat.operations.delete.title\": \"删除个人令牌\",\n  \"core.uc_profile.pat.operations.delete.description\": \"确定要删除该个人令牌吗？\",\n  \"core.uc_profile.pat.operations.revoke.button\": \"撤销\",\n  \"core.uc_profile.pat.operations.revoke.title\": \"撤销个人令牌\",\n  \"core.uc_profile.pat.operations.revoke.description\": \"撤销个人令牌\",\n  \"core.uc_profile.pat.operations.revoke.toast_success\": \"撤销成功\",\n  \"core.uc_profile.pat.operations.copy.title\": \"请立即复制并保存，Token 将仅显示一次。\",\n  \"core.uc_profile.pat.operations.restore.button\": \"恢复\",\n  \"core.uc_profile.pat.operations.restore.toast_success\": \"恢复成功\",\n  \"core.uc_profile.pat.list.empty.title\": \"当前没有创建个人令牌\",\n  \"core.uc_profile.pat.list.empty.message\": \"你可以尝试刷新或者新建个人令牌\",\n  \"core.uc_profile.pat.list.fields.expiresAt.dynamic\": \"{expiresAt}失效\",\n  \"core.uc_profile.pat.list.fields.expiresAt.forever\": \"永久有效\",\n  \"core.uc_profile.pat.list.fields.status.normal\": \"正常\",\n  \"core.uc_profile.pat.list.fields.status.revoked\": \"已撤销\",\n  \"core.uc_profile.pat.list.fields.status.expired\": \"已过期\",\n  \"core.uc_profile.pat.creation_modal.title\": \"创建个人令牌\",\n  \"core.uc_profile.pat.creation_modal.groups.general\": \"基本信息\",\n  \"core.uc_profile.pat.creation_modal.groups.permissions\": \"权限\",\n  \"core.uc_profile.pat.creation_modal.fields.name.label\": \"名称\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.label\": \"过期时间\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.help\": \"不设置代表永不过期\",\n  \"core.uc_profile.pat.creation_modal.fields.description.label\": \"描述\",\n  \"core.uc_profile.notification-preferences.fields.type\": \"通知类型\",\n  \"core.uc_profile.editing_modal.title\": \"编辑资料\",\n  \"core.uc_profile.editing_modal.groups.general\": \"常规\",\n  \"core.uc_profile.editing_modal.groups.annotations\": \"元数据\",\n  \"core.uc_profile.editing_modal.fields.username.label\": \"用户名\",\n  \"core.uc_profile.editing_modal.fields.username.validation\": \"只能包含数字、小写字母、. 和 - 符号，且不能以 . 和 - 符号开头和结尾\",\n  \"core.uc_profile.editing_modal.fields.display_name.label\": \"显示名称\",\n  \"core.uc_profile.editing_modal.fields.email.label\": \"电子邮箱\",\n  \"core.uc_profile.editing_modal.fields.phone.label\": \"手机号\",\n  \"core.uc_profile.editing_modal.fields.avatar.label\": \"头像\",\n  \"core.uc_profile.editing_modal.fields.bio.label\": \"描述\",\n  \"core.uc_profile.change_password_modal.title\": \"密码修改\",\n  \"core.uc_profile.change_password_modal.fields.new_password.label\": \"新密码\",\n  \"core.uc_profile.change_password_modal.fields.confirm_password.label\": \"确认密码\",\n  \"core.uc_profile.change_password_modal.fields.old_password.label\": \"旧密码\",\n  \"core.uc_profile.email_verify_modal.titles.modify\": \"修改电子邮箱\",\n  \"core.uc_profile.email_verify_modal.titles.verify\": \"验证电子邮箱\",\n  \"core.uc_profile.email_verify_modal.fields.new_email.label\": \"新电子邮箱\",\n  \"core.uc_profile.email_verify_modal.fields.email.label\": \"电子邮箱\",\n  \"core.uc_profile.email_verify_modal.fields.code.label\": \"验证码\",\n  \"core.uc_profile.email_verify_modal.fields.password.label\": \"验证密码\",\n  \"core.uc_profile.email_verify_modal.fields.password.help\": \"当前账号的登录密码\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.sending\": \"发送中\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.send\": \"发送验证码\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.countdown\": \"{timer} 秒后重发\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_success\": \"验证码已发送\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_email_empty\": \"请输入电子邮箱\",\n  \"core.uc_profile.email_verify_modal.operations.verify.toast_success\": \"验证成功\",\n  \"core.uc_profile.device.list.fields.current\": \"当前设备\",\n  \"core.uc_profile.device.list.fields.last_accessed_time\": \"上次访问：{time}\",\n  \"core.uc_profile.device.detail_modal.title\": \"登录设备详情\",\n  \"core.uc_profile.device.detail_modal.fields.os\": \"操作系统\",\n  \"core.uc_profile.device.detail_modal.fields.browser\": \"浏览器\",\n  \"core.uc_profile.device.detail_modal.fields.creation_timestamp\": \"创建时间\",\n  \"core.uc_profile.device.detail_modal.fields.last_accessed_times\": \"上次访问时间\",\n  \"core.uc_profile.device.detail_modal.fields.last_authenticated_time\": \"上次登录时间\",\n  \"core.uc_profile.device.operations.revoke.title\": \"撤销设备\",\n  \"core.uc_profile.device.operations.revoke.description\": \"确定要撤销该设备吗？撤销之后，此设备会退出登录\",\n  \"core.uc_profile.device.operations.revoke_others.title\": \"撤销其他所有设备\",\n  \"core.uc_profile.device.operations.revoke_others.description\": \"确定要撤销其他所有设备吗？撤销之后，其他设备会退出登录\",\n  \"core.uc_profile.device.operations.revoke_others.toast_success\": \"已撤销其他设备的登录状态\",\n  \"core.uc_notification.title\": \"消息\",\n  \"core.uc_notification.tabs.unread\": \"未读\",\n  \"core.uc_notification.tabs.read\": \"已读\",\n  \"core.uc_notification.empty.titles.unread\": \"当前没有未读的消息\",\n  \"core.uc_notification.empty.titles.read\": \"当前没有已读的消息\",\n  \"core.uc_notification.operations.mark_as_read.button\": \"标记为已读\",\n  \"core.uc_notification.operations.delete.title\": \"删除消息\",\n  \"core.uc_notification.operations.delete.description\": \"确定要删除该消息吗？\",\n  \"core.uc_notification.operations.delete_all.title\": \"删除当前所有通知\",\n  \"core.uc_notification.operations.delete_all.description\": \"确定要删除当前所有通知吗？删除后将无法恢复。\",\n  \"core.uc_notification.operations.mark_all_as_read.title\": \"将当前所有通知标记为已读\",\n  \"core.uc_notification.operations.mark_all_as_read.description\": \"将当前所有通知标记为已读，是否继续？\",\n  \"core.setting.title\": \"设置\",\n  \"core.overview.title\": \"系统概览\",\n  \"core.overview.actions.copy.toast_browser_not_supported\": \"当前浏览器不支持复制\",\n  \"core.overview.header.titles.general\": \"基本信息\",\n  \"core.overview.header.titles.environment\": \"环境信息\",\n  \"core.overview.fields.external_url\": \"外部访问地址\",\n  \"core.overview.fields.start_time\": \"启动时间\",\n  \"core.overview.fields.timezone\": \"操作系统时区\",\n  \"core.overview.fields.locale\": \"操作系统语言\",\n  \"core.overview.fields.version\": \"版本\",\n  \"core.overview.fields.build_time\": \"构建时间\",\n  \"core.overview.fields.database\": \"数据库\",\n  \"core.overview.fields.os\": \"操作系统\",\n  \"core.overview.fields.log\": \"运行日志\",\n  \"core.overview.fields.activated_theme\": \"已激活主题\",\n  \"core.overview.fields.enabled_plugins\": \"已启动插件\",\n  \"core.overview.fields_values.external_url.not_setup\": \"未设置\",\n  \"core.overview.copy_results.external_url\": \"外部访问地址：{external_url}\",\n  \"core.overview.copy_results.start_time\": \"启动时间：{start_time}\",\n  \"core.overview.copy_results.version\": \"Halo 版本：{version}\",\n  \"core.overview.copy_results.build_time\": \"构建时间：{build_time}\",\n  \"core.overview.copy_results.database\": \"数据库：{database}\",\n  \"core.overview.copy_results.os\": \"操作系统：{os}\",\n  \"core.overview.alert.external_url_invalid\": \"检测到外部访问地址与当前访问地址不一致，可能会导致部分链接无法正常跳转，请检查外部访问地址设置。\",\n  \"core.overview.external_url_form.operations.save.title\": \"修改外部访问地址\",\n  \"core.overview.external_url_form.operations.save.description\": \"修改外部访问地址需要重启 Halo 服务，修改完成之后会自动进行重启，是否继续？\",\n  \"core.overview.external_url_form.tips.restarting\": \"修改完成，等待重启...\",\n  \"core.backup.title\": \"备份与恢复\",\n  \"core.backup.tabs.backup_list\": \"备份\",\n  \"core.backup.tabs.restore\": \"恢复\",\n  \"core.backup.empty.title\": \"没有备份\",\n  \"core.backup.empty.message\": \"当前没有已创建的备份，你可以点击刷新或者创建新的备份\",\n  \"core.backup.operations.create.button\": \"创建备份\",\n  \"core.backup.operations.create.title\": \"创建备份\",\n  \"core.backup.operations.create.description\": \"确定要创建备份吗，此操作可能会持续较长时间。\",\n  \"core.backup.operations.create.toast_success\": \"已请求创建备份\",\n  \"core.backup.operations.delete.title\": \"删除备份\",\n  \"core.backup.operations.delete.description\": \"确定要删除该备份吗？\",\n  \"core.backup.operations.restore.title\": \"恢复成功\",\n  \"core.backup.operations.restore.description\": \"恢复成功之后，需要重启一下 Halo 才能够正常加载系统资源，点击确定之后我们会自动重启 Halo。\",\n  \"core.backup.operations.restart.toast_success\": \"已请求重启\",\n  \"core.backup.operations.remote_download.button\": \"下载并恢复\",\n  \"core.backup.operations.restore_by_backup.button\": \"恢复\",\n  \"core.backup.operations.restore_by_backup.title\": \"从备份文件恢复\",\n  \"core.backup.operations.restore_by_backup.description\": \"点击确定后，将从备份文件 {filename} 恢复数据。\",\n  \"core.backup.list.phases.pending\": \"准备中\",\n  \"core.backup.list.phases.running\": \"备份中\",\n  \"core.backup.list.phases.succeeded\": \"备份完成\",\n  \"core.backup.list.phases.failed\": \"备份失败\",\n  \"core.backup.list.fields.expiresAt\": \"{expiresAt}失效\",\n  \"core.backup.restore.tips.first\": \"1. 恢复过程可能会持续较长时间，期间请勿刷新页面。\",\n  \"core.backup.restore.tips.second\": \"2. 在执行恢复之前，会清空现有的所有数据，请确保当前没有需要保留的数据。\",\n  \"core.backup.restore.tips.third\": \"3. 恢复完成之后需要重启 Halo 才能够正常加载系统资源。\",\n  \"core.backup.restore.tips.complete\": \"恢复完成，等待重启...\",\n  \"core.backup.restore.start\": \"我已阅读上方提示，开始恢复\",\n  \"core.backup.restore.tabs.local.label\": \"上传\",\n  \"core.backup.restore.tabs.remote.label\": \"远程恢复\",\n  \"core.backup.restore.tabs.remote.fields.url\": \"下载地址\",\n  \"core.backup.restore.tabs.backup.label\": \"从备份文件恢复\",\n  \"core.backup.restore.tabs.backup.empty.title\": \"没有备份文件\",\n  \"core.backup.restore.tabs.backup.empty.message\": \"当前没有扫描到备份文件，你可以手动将备份文件上传到 Halo 工作目录的 backups 目录。\",\n  \"core.exception.not_found.message\": \"没有找到该页面\",\n  \"core.exception.forbidden.message\": \"没有权限访问此页面\",\n  \"core.exception.actions.home\": \"返回首页\",\n  \"core.rbac.Attachments Management\": \"附件\",\n  \"core.rbac.Attachment Manage\": \"附件管理\",\n  \"core.rbac.Attachment View\": \"附件查看\",\n  \"core.rbac.role-template-view-attachments\": \"附件查看\",\n  \"core.rbac.Comments Management\": \"评论\",\n  \"core.rbac.Comment Manage\": \"评论管理\",\n  \"core.rbac.Comment View\": \"评论查看\",\n  \"core.rbac.role-template-view-comments\": \"评论查看\",\n  \"core.rbac.ConfigMaps Management\": \"配置\",\n  \"core.rbac.ConfigMap Manage\": \"配置管理\",\n  \"core.rbac.ConfigMap View\": \"配置查看\",\n  \"core.rbac.role-template-view-configmaps\": \"配置查看\",\n  \"core.rbac.Menus Management\": \"菜单\",\n  \"core.rbac.Menu Manage\": \"菜单管理\",\n  \"core.rbac.Menu View\": \"菜单查看\",\n  \"core.rbac.role-template-view-menus\": \"菜单查看\",\n  \"core.rbac.Permissions Management\": \"权限\",\n  \"core.rbac.Permissions Manage\": \"权限管理\",\n  \"core.rbac.Permissions View\": \"权限查看\",\n  \"core.rbac.role-template-view-permissions\": \"权限查看\",\n  \"core.rbac.role-template-manage-permissions\": \"权限管理\",\n  \"core.rbac.Plugins Management\": \"插件\",\n  \"core.rbac.Plugin Manage\": \"插件管理\",\n  \"core.rbac.Plugin View\": \"插件查看\",\n  \"core.rbac.role-template-view-plugins\": \"插件查看\",\n  \"core.rbac.Posts Management\": \"文章\",\n  \"core.rbac.Post Manage\": \"文章管理\",\n  \"core.rbac.Post View\": \"文章查看\",\n  \"core.rbac.role-template-view-posts\": \"文章查看\",\n  \"core.rbac.role-template-manage-snapshots\": \"版本管理\",\n  \"core.rbac.role-template-view-snapshots\": \"版本查看\",\n  \"core.rbac.role-template-manage-tags\": \"标签管理\",\n  \"core.rbac.role-template-view-tags\": \"标签查看\",\n  \"core.rbac.role-template-manage-categories\": \"分类管理\",\n  \"core.rbac.role-template-view-categories\": \"分类查看\",\n  \"core.rbac.role-template-manage-posts\": \"文章管理\",\n  \"core.rbac.role-template-post-author\": \"允许管理自己的文章\",\n  \"core.rbac.role-template-post-contributor\": \"允许投稿\",\n  \"core.rbac.role-template-post-publisher\": \"允许发布自己的文章\",\n  \"core.rbac.role-template-post-attachment-manager\": \"允许在文章中上传图片\",\n  \"core.rbac.Roles Management\": \"角色\",\n  \"core.rbac.Role Manage\": \"角色管理\",\n  \"core.rbac.Role View\": \"角色查看\",\n  \"core.rbac.role-template-view-roles\": \"角色查看\",\n  \"core.rbac.Settings Management\": \"设置表单\",\n  \"core.rbac.Setting Manage\": \"设置表单管理\",\n  \"core.rbac.Setting View\": \"设置表单查看\",\n  \"core.rbac.role-template-view-settings\": \"设置表单查看\",\n  \"core.rbac.SinglePages Management\": \"页面\",\n  \"core.rbac.SinglePage Manage\": \"页面管理\",\n  \"core.rbac.SinglePage View\": \"页面查看\",\n  \"core.rbac.role-template-view-singlepages\": \"页面查看\",\n  \"core.rbac.Themes Management\": \"主题\",\n  \"core.rbac.Theme Manage\": \"主题管理\",\n  \"core.rbac.Theme View\": \"主题查看\",\n  \"core.rbac.role-template-view-themes\": \"主题查看\",\n  \"core.rbac.Users Management\": \"用户\",\n  \"core.rbac.User manage\": \"用户管理\",\n  \"core.rbac.User View\": \"用户查看\",\n  \"core.rbac.Migration Management\": \"备份与恢复\",\n  \"core.rbac.Migration Manage\": \"备份与恢复管理\",\n  \"core.rbac.role-template-view-users\": \"用户查看\",\n  \"core.rbac.role-template-change-password\": \"修改密码\",\n  \"core.rbac.Actuator Management\": \"系统指标\",\n  \"core.rbac.Actuator Manage\": \"访问系统指标\",\n  \"core.rbac.Cache Management\": \"缓存\",\n  \"core.rbac.Cache Manage\": \"缓存管理\",\n  \"core.rbac.Notification Configuration\": \"通知配置\",\n  \"core.rbac.role-template-notifier-config\": \"配置通知器\",\n  \"core.rbac.Post Contributor\": \"允许投稿\",\n  \"core.rbac.Post Author\": \"允许管理自己的文章\",\n  \"core.rbac.Post Attachment Manager\": \"允许在文章中上传图片\",\n  \"core.rbac.Post Publisher\": \"允许发布自己的文章\",\n  \"core.rbac.UC Attachment Manage\": \"允许管理自己的附件\",\n  \"core.rbac.role-template-uc-attachment-manager\": \"允许管理自己的附件\",\n  \"core.rbac.Recycle My Post\": \"允许删除自己的文章\",\n  \"core.rbac.role-template-recycle-my-post\": \"允许删除自己的文章\",\n  \"core.components.submit_button.computed_text\": \"{text}（{shortcut}）\",\n  \"core.components.annotations_form.custom_fields.label\": \"自定义\",\n  \"core.components.annotations_form.custom_fields.validation\": \"当前 Key 已被占用\",\n  \"core.components.annotations_form.buttons.expand\": \"查看更多\",\n  \"core.components.annotations_form.buttons.collapse\": \"收起\",\n  \"core.components.default_editor.tabs.toc.title\": \"大纲\",\n  \"core.components.default_editor.tabs.toc.empty\": \"暂无大纲\",\n  \"core.components.default_editor.tabs.detail.title\": \"详情\",\n  \"core.components.default_editor.tabs.detail.fields.character_count\": \"字符数\",\n  \"core.components.default_editor.tabs.detail.fields.word_count\": \"词数\",\n  \"core.components.default_editor.tabs.detail.fields.publish_time\": \"发布时间\",\n  \"core.components.default_editor.tabs.detail.fields.draft\": \"未发布\",\n  \"core.components.default_editor.tabs.detail.fields.owner\": \"创建者\",\n  \"core.components.default_editor.tabs.detail.fields.permalink\": \"访问链接\",\n  \"core.components.default_editor.extensions.placeholder.options.placeholder\": \"输入 / 以选择输入类型\",\n  \"core.components.default_editor.toolbox.attachment\": \"选择附件\",\n  \"core.components.default_editor.toolbox.show_hide_sidebar\": \"显示 / 隐藏侧边栏\",\n  \"core.components.default_editor.title_placeholder\": \"请输入标题\",\n  \"core.components.default_editor.cover.options.add\": \"添加封面图\",\n  \"core.components.default_editor.cover.options.change\": \"更换\",\n  \"core.components.default_editor.cover.options.upload\": \"上传\",\n  \"core.components.default_editor.cover.options.attachment\": \"从附件库选择\",\n  \"core.components.default_editor.cover.toast.upload_failed\": \"上传封面失败: {message}\",\n  \"core.components.global_search.placeholder\": \"输入关键词以搜索\",\n  \"core.components.global_search.no_results\": \"没有搜索结果\",\n  \"core.components.global_search.buttons.select\": \"选择\",\n  \"core.components.global_search.groups.console\": \"后台页面\",\n  \"core.components.global_search.groups.user\": \"用户\",\n  \"core.components.global_search.groups.plugin\": \"插件\",\n  \"core.components.global_search.groups.post\": \"文章\",\n  \"core.components.global_search.groups.category\": \"分类\",\n  \"core.components.global_search.groups.tag\": \"标签\",\n  \"core.components.global_search.groups.page\": \"自定义页面\",\n  \"core.components.global_search.groups.attachment\": \"附件\",\n  \"core.components.global_search.groups.setting\": \"设置\",\n  \"core.components.global_search.groups.theme_setting\": \"主题设置\",\n  \"core.components.pagination.page_label\": \"页\",\n  \"core.components.pagination.size_label\": \"条 / 页\",\n  \"core.components.pagination.total_label\": \"共 {total} 项数据\",\n  \"core.components.app_download_alert.description\": \"Halo 的主题和插件可以在以下地址下载：\",\n  \"core.components.app_download_alert.sources.app_store\": \"官方应用市场：{url}\",\n  \"core.components.app_download_alert.sources.github\": \"GitHub：{url}\",\n  \"core.components.user_avatar.title\": \"头像\",\n  \"core.components.user_avatar.toast_upload_failed\": \"上传头像失败\",\n  \"core.components.user_avatar.toast_remove_failed\": \"删除头像失败\",\n  \"core.components.user_avatar.cropper_modal.title\": \"裁剪头像\",\n  \"core.components.user_avatar.remove.title\": \"删除头像\",\n  \"core.components.user_avatar.tooltips.upload\": \"上传\",\n  \"core.components.user_avatar.tooltips.zoom_in\": \"放大\",\n  \"core.components.user_avatar.tooltips.zoom_out\": \"缩小\",\n  \"core.components.user_avatar.tooltips.flip_horizontal\": \"水平翻转\",\n  \"core.components.user_avatar.tooltips.flip_vertical\": \"垂直翻转\",\n  \"core.components.user_avatar.tooltips.reset\": \"重置\",\n  \"core.components.editor_provider_selector.tooltips.disallow\": \"内容格式不同，无法切换\",\n  \"core.components.uppy.image_editor.revert\": \"恢复\",\n  \"core.components.uppy.image_editor.rotate\": \"旋转\",\n  \"core.components.uppy.image_editor.zoom_in\": \"放大\",\n  \"core.components.uppy.image_editor.zoom_out\": \"缩小\",\n  \"core.components.uppy.image_editor.flip_horizontal\": \"水平翻转\",\n  \"core.components.uppy.image_editor.aspect_ratio_square\": \"裁剪为正方形\",\n  \"core.components.uppy.image_editor.aspect_ratio_landscape\": \"裁剪为横向 (16:9)\",\n  \"core.components.uppy.image_editor.aspect_ratio_portrait\": \"裁剪为纵向 (9:16)\",\n  \"core.components.h2_warning_alert.title\": \"警告：正在使用 H2 数据库\",\n  \"core.components.h2_warning_alert.description\": \"H2 数据库仅适用于开发环境和测试环境，不推荐在生产环境中使用，H2 非常容易因为操作不当导致数据文件损坏。如果必须要使用，请按时进行数据备份。\",\n  \"core.composables.content_cache.toast_recovered\": \"已从缓存中恢复未保存的内容\",\n  \"core.formkit.select.no_data\": \"暂无数据\",\n  \"core.formkit.category_select.creation_label\": \"创建 {text} 分类\",\n  \"core.formkit.tag_select.creation_label\": \"创建 {text} 标签\",\n  \"core.formkit.validation.trim\": \"不能以空格开头或结尾\",\n  \"core.formkit.validation.password\": \"密码只能使用大小写字母 (A-Z, a-z)、数字 (0-9)，以及以下特殊字符: !{'@'}#$%^&*\",\n  \"core.formkit.verification_form.no_action_defined\": \"未定义{label}接口\",\n  \"core.formkit.verification_form.verify_success\": \"{label}成功\",\n  \"core.formkit.verification_form.verify_failed\": \"{label}失败\",\n  \"core.formkit.secret.creation_modal.title\": \"创建密钥\",\n  \"core.formkit.secret.edit_modal.title\": \"编辑密钥\",\n  \"core.formkit.secret.list_modal.title\": \"选择密钥\",\n  \"core.formkit.secret.operations.delete.title\": \"删除密钥\",\n  \"core.formkit.secret.operations.delete.description\": \"确定删除此密钥吗？请确保没有地方正在使用此密钥，否则需要在具体的地方重新设置\",\n  \"core.formkit.secret.form.fields.description\": \"备注\",\n  \"core.formkit.secret.form.fields.string_data\": \"密钥数据\",\n  \"core.formkit.code.fullscreen.exit\": \"退出全屏\",\n  \"core.formkit.code.fullscreen.enter\": \"全屏编辑\",\n  \"core.formkit.iconify.placeholder\": \"选择图标\",\n  \"core.formkit.iconify.tabs.collections\": \"图标集\",\n  \"core.formkit.iconify.tabs.search\": \"全局搜索\",\n  \"core.formkit.iconify.tabs.current\": \"当前图标\",\n  \"core.formkit.iconify.collection_search_placeholder\": \"搜索图标集合\",\n  \"core.formkit.iconify.search_placeholder\": \"搜索图标\",\n  \"core.formkit.iconify.no_results\": \"没有找到结果，请尝试其他关键词\",\n  \"core.formkit.iconify.option_size\": \"大小\",\n  \"core.formkit.iconify.option_color\": \"颜色\",\n  \"core.formkit.iconify.copyright\": \"图标数据来自 {url}\",\n  \"core.formkit.iconify.operations.edit\": \"手动编辑图标\",\n  \"core.formkit.array.empty_text\": \"没有条目\",\n  \"core.formkit.array.image_tooltip\": \"查看图片：{value}\",\n  \"core.formkit.array.item_modal.title\": \"编辑条目\",\n  \"core.formkit.color.placeholder\": \"选择颜色\",\n  \"core.formkit.color.operations.edit\": \"手动编辑颜色\",\n  \"core.formkit.attachment.operations.select\": \"从附件库选择\",\n  \"core.formkit.attachment.operations.input\": \"输入链接\",\n  \"core.common.buttons.save\": \"保存\",\n  \"core.common.buttons.close\": \"关闭\",\n  \"core.common.buttons.save_and_continue\": \"保存并继续添加\",\n  \"core.common.buttons.close_and_shortcut\": \"关闭（Esc）\",\n  \"core.common.buttons.delete\": \"删除\",\n  \"core.common.buttons.setting\": \"设置\",\n  \"core.common.buttons.confirm\": \"确定\",\n  \"core.common.buttons.cancel\": \"取消\",\n  \"core.common.buttons.cancel_and_shortcut\": \"取消（Esc）\",\n  \"core.common.buttons.new\": \"新建\",\n  \"core.common.buttons.edit\": \"编辑\",\n  \"core.common.buttons.back\": \"返回\",\n  \"core.common.buttons.refresh\": \"刷新\",\n  \"core.common.buttons.publish\": \"发布\",\n  \"core.common.buttons.cancel_publish\": \"取消发布\",\n  \"core.common.buttons.next\": \"下一项\",\n  \"core.common.buttons.previous\": \"上一项\",\n  \"core.common.buttons.install\": \"安装\",\n  \"core.common.buttons.uninstall\": \"卸载\",\n  \"core.common.buttons.upgrade\": \"升级\",\n  \"core.common.buttons.reset\": \"重置\",\n  \"core.common.buttons.preview\": \"预览\",\n  \"core.common.buttons.restore\": \"恢复\",\n  \"core.common.buttons.delete_permanently\": \"永久删除\",\n  \"core.common.buttons.activate\": \"启用\",\n  \"core.common.buttons.inactivate\": \"停用\",\n  \"core.common.buttons.download\": \"下载\",\n  \"core.common.buttons.copy\": \"复制\",\n  \"core.common.buttons.upload\": \"上传\",\n  \"core.common.buttons.add\": \"添加\",\n  \"core.common.buttons.submit\": \"提交\",\n  \"core.common.buttons.detail\": \"详情\",\n  \"core.common.buttons.select\": \"选择\",\n  \"core.common.buttons.view_all\": \"查看全部\",\n  \"core.common.buttons.verify\": \"验证\",\n  \"core.common.buttons.modify\": \"修改\",\n  \"core.common.buttons.access\": \"访问\",\n  \"core.common.buttons.schedule_publish\": \"定时发布\",\n  \"core.common.buttons.revoke\": \"撤销\",\n  \"core.common.buttons.disable\": \"禁用\",\n  \"core.common.buttons.enable\": \"启用\",\n  \"core.common.buttons.continue\": \"继续\",\n  \"core.common.buttons.retry\": \"重试\",\n  \"core.common.buttons.unfold\": \"展开\",\n  \"core.common.buttons.fold\": \"收起\",\n  \"core.common.buttons.remove\": \"移除\",\n  \"core.common.buttons.replace\": \"替换\",\n  \"core.common.buttons.cancel_upload\": \"取消上传\",\n  \"core.common.buttons.more\": \"更多\",\n  \"core.common.radio.yes\": \"是\",\n  \"core.common.radio.no\": \"否\",\n  \"core.common.select.public\": \"公开\",\n  \"core.common.select.private\": \"私有\",\n  \"core.common.placeholder.search\": \"输入关键词搜索\",\n  \"core.common.toast.operation_success\": \"操作成功\",\n  \"core.common.toast.delete_success\": \"删除成功\",\n  \"core.common.toast.save_success\": \"保存成功\",\n  \"core.common.toast.publish_success\": \"发布成功\",\n  \"core.common.toast.cancel_publish_success\": \"取消发布成功\",\n  \"core.common.toast.recovery_success\": \"恢复成功\",\n  \"core.common.toast.uninstall_success\": \"卸载成功\",\n  \"core.common.toast.active_success\": \"启用成功\",\n  \"core.common.toast.inactive_success\": \"停用成功\",\n  \"core.common.toast.upgrade_success\": \"升级成功\",\n  \"core.common.toast.install_success\": \"安装成功\",\n  \"core.common.toast.download_success\": \"下载成功\",\n  \"core.common.toast.copy_success\": \"复制成功\",\n  \"core.common.toast.operation_failed\": \"操作失败\",\n  \"core.common.toast.download_failed\": \"下载失败\",\n  \"core.common.toast.save_failed_and_retry\": \"保存失败，请重试\",\n  \"core.common.toast.publish_failed_and_retry\": \"发布失败，请重试\",\n  \"core.common.toast.network_error\": \"网络错误，请检查网络连接\",\n  \"core.common.toast.login_expired\": \"登录已过期，请重新登录\",\n  \"core.common.toast.forbidden\": \"无权限访问\",\n  \"core.common.toast.not_found\": \"资源不存在\",\n  \"core.common.toast.server_internal_error\": \"服务器内部错误\",\n  \"core.common.toast.unknown_error\": \"未知错误\",\n  \"core.common.toast.disable_success\": \"禁用成功\",\n  \"core.common.toast.enable_success\": \"启用成功\",\n  \"core.common.dialog.titles.tip\": \"提示\",\n  \"core.common.dialog.titles.warning\": \"警告\",\n  \"core.common.dialog.titles.login_expired\": \"登录已过期\",\n  \"core.common.dialog.descriptions.cannot_be_recovered\": \"该操作不可恢复。\",\n  \"core.common.dialog.descriptions.editor_not_found\": \"未找到符合 {raw_type} 格式的编辑器，请检查是否已安装编辑器插件。\",\n  \"core.common.dialog.descriptions.login_expired\": \"当前登录已过期，点击确定跳转到登录页面，请确保当前内容已保存，你可以点击取消之后手动复制未保存的内容。\",\n  \"core.common.filters.results.keyword\": \"关键词：{keyword}\",\n  \"core.common.filters.results.sort\": \"排序：{sort}\",\n  \"core.common.filters.results.status\": \"状态：{status}\",\n  \"core.common.filters.labels.sort\": \"排序\",\n  \"core.common.filters.labels.status\": \"状态\",\n  \"core.common.filters.item_labels.all\": \"全部\",\n  \"core.common.filters.item_labels.default\": \"默认\",\n  \"core.common.status.deleting\": \"删除中\",\n  \"core.common.status.loading\": \"加载中\",\n  \"core.common.status.loading_error\": \"加载异常\",\n  \"core.common.status.activated\": \"已启用\",\n  \"core.common.status.not_activated\": \"未启用\",\n  \"core.common.status.installed\": \"已安装\",\n  \"core.common.status.not_installed\": \"未安装\",\n  \"core.common.status.starting_up\": \"启动中\",\n  \"core.common.text.none\": \"无\",\n  \"core.common.text.tip\": \"提示\",\n  \"core.common.text.warning\": \"警告\",\n  \"core.common.text.system_protection\": \"系统保留\",\n  \"core.common.text.all\": \"全部\",\n  \"core.common.text.detail\": \"详情\",\n  \"core.common.tooltips.unpublished_content_tip\": \"当前有内容已保存，但还未发布。\",\n  \"core.common.tooltips.publishing\": \"发布中\",\n  \"core.common.tooltips.recovering\": \"恢复中\",\n  \"core.common.fields.post_count\": \"{count} 篇文章\",\n  \"core.common.form.validation.slug_unique\": \"当前别名已存在\",\n  \"core.tool.title\": \"工具\",\n  \"core.tool.empty.title\": \"没有可用工具\",\n  \"core.tool.empty.message\": \"当前没有可用的工具，系统工具可能由插件提供\",\n  \"core.snapshots.operations.revert.button\": \"恢复\",\n  \"core.snapshots.operations.revert.title\": \"恢复快照\",\n  \"core.snapshots.operations.revert.description\": \"确定要恢复该快照吗？此操作将根据这个快照创建一个新的快照并发布。\",\n  \"core.snapshots.operations.revert.toast_success\": \"恢复成功\",\n  \"core.snapshots.operations.delete.title\": \"删除快照\",\n  \"core.snapshots.operations.delete.description\": \"确定要删除该快照吗？此操作无法恢复。\",\n  \"core.snapshots.operations.cleanup.button\": \"清理\",\n  \"core.snapshots.operations.cleanup.title\": \"清理快照\",\n  \"core.snapshots.operations.cleanup.description\": \"确定要清理所有不再使用的快照吗？只会保留已发布、基础和草稿版本。\",\n  \"core.snapshots.operations.cleanup.toast_empty\": \"没有需要清理的快照\",\n  \"core.snapshots.operations.cleanup.toast_success\": \"清理完成\",\n  \"core.snapshots.status.released\": \"已发布\",\n  \"core.snapshots.status.draft\": \"草稿\",\n  \"core.snapshots.status.base\": \"基础\",\n  \"core.snapshots.diff_mode.toggle.enable\": \"开启对比模式\",\n  \"core.snapshots.diff_mode.toggle.disable\": \"关闭对比模式\",\n  \"core.snapshots.diff_mode.title\": \"对比模式\",\n  \"core.snapshots.diff_mode.show_diff_only\": \"只显示差异\",\n  \"core.snapshots.diff_mode.sync_scroll\": \"同步滚动\",\n  \"core.snapshots.diff_mode.select_two_tip\": \"请选择两个版本进行对比\",\n  \"core.snapshots.diff_mode.old_version\": \"前一个版本（旧）\",\n  \"core.snapshots.diff_mode.new_version\": \"所选第一个版本（新）\",\n  \"core.snapshots.diff_mode.diff\": \"差异\",\n  \"core.snapshots.diff_mode.legend.removed\": \"该行代表被删除\",\n  \"core.snapshots.diff_mode.legend.added\": \"该行代表被添加\",\n  \"core.snapshots.diff_mode.legend.modified\": \"该行代表被修改\",\n  \"core.post_snapshots.title\": \"文章版本历史\",\n  \"core.page_snapshots.title\": \"页面版本历史\",\n  \"core.formkit.secret.no_permission\": \"无权限查看密钥\",\n  \"core.formkit.secret.no_selected\": \"当前没有选择密钥，你可以新建或者选择一个已有的密钥\",\n  \"core.formkit.secret.missing_keys\": \"所选密钥还缺少字段：{keys}\",\n  \"core.formkit.secret.includes_keys\": \"包含字段：{keys}\",\n  \"core.formkit.secret.no_fields\": \"没有字段\",\n  \"core.formkit.secret.required_keys_tip\": \"当前表单项所需的密钥字段包括：\",\n  \"core.dashboard.widgets.presets.upvotes_stats.title\": \"点赞\"\n}\n"
  },
  {
    "path": "ui/src/locales/zh-TW.json",
    "content": "{\n  \"core.sidebar.search.placeholder\": \"搜尋\",\n  \"core.sidebar.menu.groups.content\": \"內容\",\n  \"core.sidebar.menu.groups.interface\": \"外觀\",\n  \"core.sidebar.menu.groups.system\": \"系統\",\n  \"core.sidebar.menu.groups.tool\": \"工具\",\n  \"core.sidebar.menu.items.dashboard\": \"儀表盤\",\n  \"core.sidebar.menu.items.posts\": \"文章\",\n  \"core.sidebar.menu.items.single_pages\": \"頁面\",\n  \"core.sidebar.menu.items.comments\": \"留言\",\n  \"core.sidebar.menu.items.attachments\": \"附件\",\n  \"core.sidebar.menu.items.themes\": \"主題\",\n  \"core.sidebar.menu.items.menus\": \"選單\",\n  \"core.sidebar.menu.items.plugins\": \"插件\",\n  \"core.sidebar.menu.items.users\": \"用戶\",\n  \"core.sidebar.menu.items.settings\": \"設置\",\n  \"core.sidebar.menu.items.overview\": \"概覽\",\n  \"core.sidebar.menu.items.backup\": \"備份\",\n  \"core.sidebar.menu.items.tools\": \"工具\",\n  \"core.sidebar.menu.items.home\": \"首页\",\n  \"core.sidebar.operations.logout.tooltip\": \"登出\",\n  \"core.sidebar.operations.logout.title\": \"登出\",\n  \"core.sidebar.operations.logout.description\": \"點擊確定將跳轉至退出登入頁面，請確保正在編輯的內容已保存。\",\n  \"core.sidebar.operations.profile.tooltip\": \"個人中心\",\n  \"core.sidebar.operations.visit_homepage.title\": \"訪問首頁\",\n  \"core.sidebar.profile.aggregate_role\": \"聚合角色\",\n  \"core.uc_sidebar.menu.items.profile\": \"我的\",\n  \"core.uc_sidebar.menu.items.notification\": \"消息\",\n  \"core.uc_sidebar.menu.items.posts\": \"文章\",\n  \"core.uc_sidebar.operations.console.tooltip\": \"管理控制台\",\n  \"core.uc_sidebar.profile.aggregate_role\": \"聚合角色\",\n  \"core.dashboard.title\": \"儀表盤\",\n  \"core.dashboard.actions.setting\": \"設置\",\n  \"core.dashboard.widgets.groups.post\": \"文章\",\n  \"core.dashboard.widgets.groups.page\": \"頁面\",\n  \"core.dashboard.widgets.groups.comment\": \"留言\",\n  \"core.dashboard.widgets.groups.user\": \"用戶\",\n  \"core.dashboard.widgets.groups.other\": \"其他\",\n  \"core.dashboard.widgets.presets.post_stats.title\": \"文章\",\n  \"core.dashboard.widgets.presets.page_stats.title\": \"頁面\",\n  \"core.dashboard.widgets.presets.recent_published.title\": \"最近文章\",\n  \"core.dashboard.widgets.presets.recent_published.visits\": \"訪問量 {visits}\",\n  \"core.dashboard.widgets.presets.recent_published.comments\": \"留言 {comments}\",\n  \"core.dashboard.widgets.presets.recent_published.empty.title\": \"沒有已發布文章\",\n  \"core.dashboard.widgets.presets.recent_published.comments-with-pending\": \" ({count} 條待審核)\",\n  \"core.dashboard.widgets.presets.notification.title\": \"通知\",\n  \"core.dashboard.widgets.presets.notification.empty.title\": \"當前沒有未讀的消息\",\n  \"core.dashboard.widgets.presets.quickaction.title\": \"快捷訪問\",\n  \"core.dashboard.widgets.presets.quickaction.actions.user_center.title\": \"個人中心\",\n  \"core.dashboard.widgets.presets.quickaction.actions.view_site.title\": \"查看站點\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_post.title\": \"創建文章\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_page.title\": \"創建頁面\",\n  \"core.dashboard.widgets.presets.quickaction.actions.upload_attachment.title\": \"附件上傳\",\n  \"core.dashboard.widgets.presets.quickaction.actions.theme_manage.title\": \"主題管理\",\n  \"core.dashboard.widgets.presets.quickaction.actions.plugin_manage.title\": \"插件管理\",\n  \"core.dashboard.widgets.presets.quickaction.actions.new_user.title\": \"新建用戶\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.title\": \"刷新搜尋引擎\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_title\": \"刷新搜尋引擎索引\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.dialog_content\": \"此操作會對所有已發佈的文章重新創建本地搜尋引擎的索引。\",\n  \"core.dashboard.widgets.presets.quickaction.actions.refresh_search_engine.success_message\": \"刷新成功\",\n  \"core.dashboard.widgets.presets.quickaction.config.fields.enabled_items.label\": \"啟用的功能\",\n  \"core.dashboard.widgets.presets.user_stats.title\": \"用戶\",\n  \"core.dashboard.widgets.presets.comment_stats.title\": \"留言\",\n  \"core.dashboard.widgets.presets.views_stats.title\": \"瀏覽量\",\n  \"core.dashboard.widgets.presets.pending_comments.title\": \"新評論\",\n  \"core.dashboard.widgets.presets.pending_comments.empty.title\": \"沒有待審核評論\",\n  \"core.dashboard.widgets.presets.iframe.title\": \"網頁嵌入\",\n  \"core.dashboard.widgets.presets.iframe.empty.title\": \"請在配置中輸入 URL\",\n  \"core.dashboard.widgets.presets.iframe.config.fields.title.label\": \"標題\",\n  \"core.dashboard.widgets.presets.stack.title\": \"堆疊部件\",\n  \"core.dashboard.widgets.presets.stack.operations.add_widget.button\": \"添加部件\",\n  \"core.dashboard.widgets.presets.stack.config_modal.title\": \"堆疊部件配置\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play.label\": \"自動切換\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.auto_play_interval.label\": \"自動切換間隔\",\n  \"core.dashboard.widgets.presets.stack.config_modal.fields.widgets.label\": \"部件\",\n  \"core.dashboard.widgets.presets.stack.config_modal.toast.nest_warning\": \"不能將堆疊部件添加到堆疊部件中\",\n  \"core.dashboard.widgets.common_form.fields.enable_animation.label\": \"啟用動畫\",\n  \"core.dashboard_designer.title\": \"編輯儀表盤\",\n  \"core.dashboard_designer.actions.add_widget\": \"添加部件\",\n  \"core.dashboard_designer.widgets_modal.title\": \"小部件\",\n  \"core.dashboard_designer.config_modal.title\": \"部件配置\",\n  \"core.dashboard_designer.operations.back.title\": \"佈局未儲存\",\n  \"core.dashboard_designer.operations.back.description\": \"當前編輯的佈局還未儲存，放棄後，當前佈局將遺失，是否繼續？\",\n  \"core.dashboard_designer.operations.back.confirm_text\": \"放棄\",\n  \"core.dashboard_designer.operations.change_breakpoint.tips_not_saved\": \"請先儲存當前佈局\",\n  \"core.dashboard_designer.operations.copy_from_layout.button\": \"從其他布局複製\",\n  \"core.dashboard_designer.operations.open_widgets_hub.button\": \"小部件中心\",\n  \"core.dashboard_designer.breakpoints.lg\": \"桌面端\",\n  \"core.dashboard_designer.breakpoints.md\": \"平板端（橫向）\",\n  \"core.dashboard_designer.breakpoints.sm\": \"平板端\",\n  \"core.dashboard_designer.breakpoints.xs\": \"手機端\",\n  \"core.post.title\": \"文章\",\n  \"core.post.actions.categories\": \"分類\",\n  \"core.post.actions.tags\": \"標籤\",\n  \"core.post.actions.recycle_bin\": \"回收站\",\n  \"core.post.empty.title\": \"當前沒有文章\",\n  \"core.post.empty.message\": \"你可以嘗試刷新或者新建文章\",\n  \"core.post.operations.delete.title\": \"刪除文章\",\n  \"core.post.operations.delete.description\": \"該操作會將文章放入回收站，後續可以從回收站恢復。\",\n  \"core.post.operations.delete_in_batch.title\": \"刪除所選文章\",\n  \"core.post.operations.delete_in_batch.description\": \"該操作會將文章放入回收站，後續可以從回收站恢復。\",\n  \"core.post.operations.publish_in_batch.title\": \"發布文章\",\n  \"core.post.operations.publish_in_batch.description\": \"批量發佈文章，所選文章會被設置為發佈狀態\",\n  \"core.post.operations.cancel_publish_in_batch.title\": \"取消發佈文章\",\n  \"core.post.operations.cancel_publish_in_batch.description\": \"批量取消發佈文章，所選文章會被設置為未發布狀態\",\n  \"core.post.operations.batch_setting.button\": \"批量設置\",\n  \"core.post.filters.status.items.published\": \"已發布\",\n  \"core.post.filters.status.items.draft\": \"未發布\",\n  \"core.post.filters.status.items.scheduling\": \"定時發佈\",\n  \"core.post.filters.visible.label\": \"可見性\",\n  \"core.post.filters.visible.result\": \"可見性：{visible}\",\n  \"core.post.filters.visible.items.public\": \"公開\",\n  \"core.post.filters.visible.items.private\": \"私有\",\n  \"core.post.filters.category.label\": \"分類\",\n  \"core.post.filters.category.result\": \"分類：{category}\",\n  \"core.post.filters.tag.label\": \"標籤\",\n  \"core.post.filters.tag.result\": \"標籤：{tag}\",\n  \"core.post.filters.author.label\": \"作者\",\n  \"core.post.filters.author.result\": \"作者：{author}\",\n  \"core.post.filters.sort.items.publish_time_desc\": \"較近發布\",\n  \"core.post.filters.sort.items.publish_time_asc\": \"較早發布\",\n  \"core.post.filters.sort.items.create_time_desc\": \"較近創建\",\n  \"core.post.filters.sort.items.create_time_asc\": \"較早創建\",\n  \"core.post.filters.sort.items.visit_desc\": \"最多訪問量\",\n  \"core.post.filters.sort.items.comment_desc\": \"最多評論量\",\n  \"core.post.filters.sort.items.last_modify_time_asc\": \"較早更新\",\n  \"core.post.filters.sort.items.last_modify_time_desc\": \"最近更新\",\n  \"core.post.list.fields.categories\": \"分類：\",\n  \"core.post.list.fields.visits\": \"訪問量 {visits}\",\n  \"core.post.list.fields.comments\": \"留言 {comments}\",\n  \"core.post.list.fields.pinned\": \"已置頂\",\n  \"core.post.list.fields.schedule_publish.tooltip\": \"定時發佈\",\n  \"core.post.list.fields.comments-with-pending\": \"({count} 條待審核)\",\n  \"core.post.settings.title\": \"文章設置\",\n  \"core.post.settings.groups.general\": \"常規設置\",\n  \"core.post.settings.groups.advanced\": \"高級設置\",\n  \"core.post.settings.groups.annotations\": \"元數據\",\n  \"core.post.settings.fields.title.label\": \"標題\",\n  \"core.post.settings.fields.slug.label\": \"別名\",\n  \"core.post.settings.fields.slug.help\": \"通常用於生成文章的固定連結\",\n  \"core.post.settings.fields.slug.refresh_message\": \"根據標題重新生成別名\",\n  \"core.post.settings.fields.categories.label\": \"分類目錄\",\n  \"core.post.settings.fields.tags.label\": \"標籤\",\n  \"core.post.settings.fields.auto_generate_excerpt.label\": \"自動生成摘要\",\n  \"core.post.settings.fields.raw_excerpt.label\": \"自定義摘要\",\n  \"core.post.settings.fields.allow_comment.label\": \"允許留言\",\n  \"core.post.settings.fields.pinned.label\": \"是否置頂\",\n  \"core.post.settings.fields.visible.label\": \"可見性\",\n  \"core.post.settings.fields.publish_time.label\": \"發表時間\",\n  \"core.post.settings.fields.publish_time.help.schedule_publish\": \"將設定定時任務，並於 {datetime} 發佈\",\n  \"core.post.settings.fields.template.label\": \"自定義模板\",\n  \"core.post.settings.fields.cover.label\": \"封面圖\",\n  \"core.post.settings.fields.owner.label\": \"作者\",\n  \"core.post.batch_setting_modal.title\": \"文章批量設置\",\n  \"core.post.batch_setting_modal.fields.common.enabled\": \"啟用\",\n  \"core.post.batch_setting_modal.fields.common.op.label\": \"設置方式\",\n  \"core.post.batch_setting_modal.fields.common.op.options.add\": \"追加\",\n  \"core.post.batch_setting_modal.fields.common.op.options.replace\": \"替换\",\n  \"core.post.batch_setting_modal.fields.common.op.options.remove_all\": \"移除全部\",\n  \"core.post.batch_setting_modal.fields.category_group\": \"分类\",\n  \"core.post.batch_setting_modal.fields.category_names\": \"選擇分類\",\n  \"core.post.batch_setting_modal.fields.tag_group\": \"標籤\",\n  \"core.post.batch_setting_modal.fields.tag_names\": \"選擇標籤\",\n  \"core.post.batch_setting_modal.fields.visible_group\": \"可見性\",\n  \"core.post.batch_setting_modal.fields.visible_value\": \"選擇可見性\",\n  \"core.post.batch_setting_modal.fields.allow_comment_group\": \"允許評論\",\n  \"core.post.batch_setting_modal.fields.allow_comment_value\": \"選擇是否允許評論\",\n  \"core.post.batch_setting_modal.fields.owner_group\": \"作者\",\n  \"core.post.batch_setting_modal.fields.owner_value\": \"設置作者\",\n  \"core.post.tag.filters.sort.items.create_time_desc\": \"較近建立\",\n  \"core.post.tag.filters.sort.items.create_time_asc\": \"較早建立\",\n  \"core.post.tag.filters.sort.items.display_name_desc\": \"標籤名稱降序\",\n  \"core.post.tag.filters.sort.items.display_name_asc\": \"標籤名稱升序\",\n  \"core.post.tag.filters.sort.items.post_desc\": \"最多文章量\",\n  \"core.deleted_post.title\": \"文章回收站\",\n  \"core.deleted_post.empty.title\": \"沒有文章被放入回收站\",\n  \"core.deleted_post.empty.message\": \"你可以嘗試刷新或者返回文章管理\",\n  \"core.deleted_post.operations.delete.title\": \"刪除文章\",\n  \"core.deleted_post.operations.delete.description\": \"刪除之後將無法恢復。\",\n  \"core.deleted_post.operations.delete_in_batch.title\": \"刪除所選文章\",\n  \"core.deleted_post.operations.delete_in_batch.description\": \"刪除之後將無法恢復。\",\n  \"core.deleted_post.operations.recovery.title\": \"恢復文章\",\n  \"core.deleted_post.operations.recovery.description\": \"該操作會將文章恢復到被刪除之前的狀態。\",\n  \"core.deleted_post.operations.recovery_in_batch.title\": \"恢復所選文章\",\n  \"core.deleted_post.operations.recovery_in_batch.description\": \"該操作會將文章恢復到被刪除之前的狀態。\",\n  \"core.post_editor.title\": \"文章編輯\",\n  \"core.post_editor.untitled\": \"未命名文章\",\n  \"core.post_editor.actions.snapshots\": \"版本歷史\",\n  \"core.post_tag.title\": \"文章標籤\",\n  \"core.post_tag.header.title\": \"{count} 個標籤\",\n  \"core.post_tag.empty.title\": \"當前沒有標籤\",\n  \"core.post_tag.empty.message\": \"你可以嘗試刷新或者新建標籤\",\n  \"core.post_tag.operations.delete.title\": \"刪除標籤\",\n  \"core.post_tag.operations.delete.description\": \"刪除此標籤之後，對應文章的關聯將被解除。該操作不可恢復。\",\n  \"core.post_tag.operations.delete_in_batch.title\": \"刪除所選標籤\",\n  \"core.post_tag.editing_modal.titles.update\": \"編輯文章標籤\",\n  \"core.post_tag.editing_modal.titles.create\": \"新增文章標籤\",\n  \"core.post_tag.editing_modal.groups.general\": \"常規\",\n  \"core.post_tag.editing_modal.groups.annotations\": \"元數據\",\n  \"core.post_tag.editing_modal.fields.display_name.label\": \"名稱\",\n  \"core.post_tag.editing_modal.fields.slug.label\": \"別名\",\n  \"core.post_tag.editing_modal.fields.slug.help\": \"通常用於生成標籤的固定連結\",\n  \"core.post_tag.editing_modal.fields.slug.refresh_message\": \"根據名稱重新生成別名\",\n  \"core.post_tag.editing_modal.fields.color.label\": \"顏色\",\n  \"core.post_tag.editing_modal.fields.color.help\": \"需要主題適配以支持\",\n  \"core.post_tag.editing_modal.fields.cover.label\": \"封面圖\",\n  \"core.post_tag.editing_modal.fields.cover.help\": \"需要主題適配以支持\",\n  \"core.post_tag.editing_modal.fields.description.label\": \"描述\",\n  \"core.post_tag.editing_modal.fields.description.help\": \"描述會自動添加到頁面用於 SEO 的描述標籤中，其他顯示用途需要主題適配\",\n  \"core.post_category.title\": \"文章分類\",\n  \"core.post_category.header.title\": \"{count} 個分類\",\n  \"core.post_category.empty.title\": \"當前沒有分類\",\n  \"core.post_category.empty.message\": \"你可以嘗試刷新或者新建分類\",\n  \"core.post_category.operations.delete.title\": \"刪除分類\",\n  \"core.post_category.operations.delete.description\": \"刪除此分類之後，對應文章的關聯將被解除。該操作不可恢復。\",\n  \"core.post_category.operations.add_sub_category.button\": \"新增子分類\",\n  \"core.post_category.editing_modal.titles.update\": \"編輯文章分類\",\n  \"core.post_category.editing_modal.titles.create\": \"新增文章分類\",\n  \"core.post_category.editing_modal.groups.general\": \"常規\",\n  \"core.post_category.editing_modal.groups.annotations\": \"元數據\",\n  \"core.post_category.editing_modal.fields.parent.label\": \"上級分類\",\n  \"core.post_category.editing_modal.fields.display_name.label\": \"名稱\",\n  \"core.post_category.editing_modal.fields.slug.label\": \"別名\",\n  \"core.post_category.editing_modal.fields.slug.help\": \"通常用於生成分類的固定連結\",\n  \"core.post_category.editing_modal.fields.slug.refresh_message\": \"根據名稱重新生成別名\",\n  \"core.post_category.editing_modal.fields.template.label\": \"自定義模板\",\n  \"core.post_category.editing_modal.fields.template.help\": \"自定義分類歸檔頁面的渲染模板，需要主題提供支持\",\n  \"core.post_category.editing_modal.fields.cover.label\": \"封面圖\",\n  \"core.post_category.editing_modal.fields.cover.help\": \"需要主題適配以支持\",\n  \"core.post_category.editing_modal.fields.description.label\": \"描述\",\n  \"core.post_category.editing_modal.fields.description.help\": \"描述會自動添加到頁面用於 SEO 的描述標籤中，其他顯示用途需要主題適配\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.label\": \"防止父級聯查詢\",\n  \"core.post_category.editing_modal.fields.prevent_parent_post_cascade_query.help\": \"阻止父級分類在級聯文章查詢中包含此分類及其子分類\",\n  \"core.post_category.editing_modal.fields.post_template.label\": \"自定義文章模板\",\n  \"core.post_category.editing_modal.fields.post_template.help\": \"自定義當前分類下文章的渲染模板，需要主題提供支持\",\n  \"core.post_category.editing_modal.fields.hide_from_list.label\": \"在列表中隱藏\",\n  \"core.post_category.editing_modal.fields.hide_from_list.help\": \"開啟此選項後，此分類和其下子分類，以及其下文章將不會顯示在前臺的列表中，需要主動訪問分類歸檔頁面，此功能僅對第一級目錄生效\",\n  \"core.post_category.list.fields.prevent_parent_post_cascade_query\": \"阻止父級分類在級聯文章查詢中包含此分類及其子分類\",\n  \"core.post_category.list.fields.hide_from_list\": \"已隱藏，此分類和其下子分類，以及其下文章將不會顯示在前臺的列表中，需要主動訪問分類歸檔頁面\",\n  \"core.page.title\": \"頁面\",\n  \"core.page.actions.recycle_bin\": \"回收站\",\n  \"core.page.empty.title\": \"當前沒有頁面\",\n  \"core.page.empty.message\": \"你可以嘗試刷新或者新建頁面\",\n  \"core.page.operations.delete.title\": \"刪除頁面\",\n  \"core.page.operations.delete.description\": \"該操作會將自定義頁面放入回收站，後續可以從回收站恢復。\",\n  \"core.page.operations.delete_in_batch.title\": \"刪除所選頁面\",\n  \"core.page.operations.delete_in_batch.description\": \"該操作會將自定義頁面放入回收站，後續可以從回收站恢復。\",\n  \"core.page.filters.status.items.published\": \"已發布\",\n  \"core.page.filters.status.items.draft\": \"未發布\",\n  \"core.page.filters.visible.label\": \"可見性\",\n  \"core.page.filters.visible.result\": \"可見性：{visible}\",\n  \"core.page.filters.visible.items.public\": \"公開\",\n  \"core.page.filters.visible.items.private\": \"私有\",\n  \"core.page.filters.author.label\": \"作者\",\n  \"core.page.filters.author.result\": \"作者：{author}\",\n  \"core.page.filters.sort.items.publish_time_desc\": \"較近發布\",\n  \"core.page.filters.sort.items.publish_time_asc\": \"較早發布\",\n  \"core.page.filters.sort.items.create_time_desc\": \"較近創建\",\n  \"core.page.filters.sort.items.create_time_asc\": \"較早創建\",\n  \"core.page.list.fields.visits\": \"訪問量 {visits}\",\n  \"core.page.list.fields.comments\": \"留言 {comments}\",\n  \"core.page.list.fields.comments-with-pending\": \" ({count} 條待審核)\",\n  \"core.page.settings.title\": \"頁面設置\",\n  \"core.page.settings.groups.general\": \"常規設置\",\n  \"core.page.settings.groups.advanced\": \"高級設置\",\n  \"core.page.settings.groups.annotations\": \"元數據\",\n  \"core.page.settings.fields.title.label\": \"標題\",\n  \"core.page.settings.fields.slug.label\": \"別名\",\n  \"core.page.settings.fields.slug.help\": \"通常用於生成頁面的固定連結\",\n  \"core.page.settings.fields.slug.refresh_message\": \"根據標題重新生成別名\",\n  \"core.page.settings.fields.auto_generate_excerpt.label\": \"自動生成摘要\",\n  \"core.page.settings.fields.raw_excerpt.label\": \"自定義摘要\",\n  \"core.page.settings.fields.allow_comment.label\": \"允許留言\",\n  \"core.page.settings.fields.pinned.label\": \"是否置頂\",\n  \"core.page.settings.fields.visible.label\": \"可見性\",\n  \"core.page.settings.fields.publish_time.label\": \"發表時間\",\n  \"core.page.settings.fields.template.label\": \"自定義模板\",\n  \"core.page.settings.fields.cover.label\": \"封面圖\",\n  \"core.deleted_page.title\": \"頁面回收站\",\n  \"core.deleted_page.empty.title\": \"沒有自定義頁面被放入回收站\",\n  \"core.deleted_page.empty.message\": \"你可以嘗試刷新或者返回自定義頁面管理\",\n  \"core.deleted_page.operations.delete.title\": \"刪除頁面\",\n  \"core.deleted_page.operations.delete.description\": \"刪除之後將無法恢復。\",\n  \"core.deleted_page.operations.delete_in_batch.title\": \"刪除所選頁面\",\n  \"core.deleted_page.operations.delete_in_batch.description\": \"刪除之後將無法恢復。\",\n  \"core.deleted_page.operations.recovery.title\": \"恢復頁面\",\n  \"core.deleted_page.operations.recovery.description\": \"該操作會將自定義頁面恢復到被刪除之前的狀態。\",\n  \"core.deleted_page.operations.recovery_in_batch.title\": \"恢復所選頁面\",\n  \"core.deleted_page.operations.recovery_in_batch.description\": \"該操作會將自定義頁面恢復到被刪除之前的狀態。\",\n  \"core.page_editor.title\": \"頁面編輯\",\n  \"core.page_editor.untitled\": \"Untitled page\",\n  \"core.page_editor.actions.snapshots\": \"版本歷史\",\n  \"core.comment.title\": \"留言\",\n  \"core.comment.empty.title\": \"當前沒有留言\",\n  \"core.comment.empty.message\": \"你可以嘗試刷新或者修改篩選條件\",\n  \"core.comment.reply_empty.title\": \"當前沒有回覆\",\n  \"core.comment.reply_empty.message\": \"你可以嘗試刷新或者創建新回覆\",\n  \"core.comment.reply_empty.new\": \"創建新回覆\",\n  \"core.comment.operations.delete_comment.title\": \"刪除留言\",\n  \"core.comment.operations.delete_comment.description\": \"將同時刪除所有留言下的回覆，該操作不可恢復。\",\n  \"core.comment.operations.delete_comment_in_batch.title\": \"刪除所選留言\",\n  \"core.comment.operations.delete_comment_in_batch.description\": \"將同時刪除所有留言下的回覆，該操作不可恢復。\",\n  \"core.comment.operations.approve_comment_in_batch.button\": \"審核通過\",\n  \"core.comment.operations.approve_comment_in_batch.title\": \"確定要審核通過所選的留言嗎？\",\n  \"core.comment.operations.approve_applies_in_batch.button\": \"審核通過所有回覆\",\n  \"core.comment.operations.approve_applies_in_batch.title\": \"確定要審核通過該留言的所有回覆嗎？\",\n  \"core.comment.operations.delete_reply.title\": \"刪除回覆\",\n  \"core.comment.operations.review.button\": \"審核\",\n  \"core.comment.operations.reply.button\": \"回覆\",\n  \"core.comment.operations.approve.button\": \"通過\",\n  \"core.comment.operations.reply_and_approve.button\": \"回覆並通過\",\n  \"core.comment.operations.cancel_approve.button\": \"取消通過\",\n  \"core.comment.filters.status.items.approved\": \"已審核\",\n  \"core.comment.filters.status.items.pending_review\": \"待審核\",\n  \"core.comment.filters.owner.label\": \"留言者\",\n  \"core.comment.filters.owner.result\": \"留言者：{owner}\",\n  \"core.comment.filters.sort.items.last_reply_time_desc\": \"較近回覆\",\n  \"core.comment.filters.sort.items.last_reply_time_asc\": \"較早回覆\",\n  \"core.comment.filters.sort.items.reply_count_desc\": \"較多回覆數\",\n  \"core.comment.filters.sort.items.reply_count_asc\": \"較少回覆數\",\n  \"core.comment.filters.sort.items.create_time_desc\": \"較近創建\",\n  \"core.comment.filters.sort.items.create_time_asc\": \"較早創建\",\n  \"core.comment.list.fields.reply_count\": \"{count} 條回覆\",\n  \"core.comment.list.fields.has_new_replies\": \"有新的回覆\",\n  \"core.comment.list.fields.pending_review\": \"待審核\",\n  \"core.comment.list.fields.private\": \"私密\",\n  \"core.comment.subject_refs.post\": \"文章\",\n  \"core.comment.subject_refs.page\": \"頁面\",\n  \"core.comment.subject_refs.unknown\": \"未知\",\n  \"core.comment.reply_modal.title\": \"回覆\",\n  \"core.comment.reply_modal.fields.content.label\": \"內容\",\n  \"core.comment.reply_modal.operations.submit.toast_success\": \"回覆成功\",\n  \"core.comment.text.commented_on\": \"評論在\",\n  \"core.comment.text.replied_below\": \"回覆：\",\n  \"core.comment.detail_modal.fields.owner\": \"評論者\",\n  \"core.comment.detail_modal.fields.creation_time\": \"評論時間\",\n  \"core.comment.detail_modal.fields.commented_on\": \"評論在\",\n  \"core.comment.detail_modal.fields.new_reply\": \"新回覆\",\n  \"core.comment.detail_modal.fields.user_agent\": \"用戶代理（UA）\",\n  \"core.comment.comment_detail_modal.title\": \"評論詳情\",\n  \"core.comment.comment_detail_modal.fields.content\": \"評論內容\",\n  \"core.comment.reply_detail_modal.title\": \"回覆詳情\",\n  \"core.comment.reply_detail_modal.fields.original_comment\": \"原評論\",\n  \"core.comment.reply_detail_modal.fields.content\": \"回覆內容\",\n  \"core.attachment.title\": \"附件庫\",\n  \"core.attachment.common.text.ungrouped\": \"未分組\",\n  \"core.attachment.common.text.original\": \"原始\",\n  \"core.attachment.actions.storage_policies\": \"存儲策略\",\n  \"core.attachment.empty.title\": \"當前分組沒有附件\",\n  \"core.attachment.empty.message\": \"當前分組沒有附件，你可以嘗試重整或者上傳附件\",\n  \"core.attachment.empty.actions.upload\": \"上傳附件\",\n  \"core.attachment.operations.delete.title\": \"刪除附件\",\n  \"core.attachment.operations.delete_in_batch.title\": \"刪除所選附件\",\n  \"core.attachment.operations.deselect_items.button\": \"取消選擇\",\n  \"core.attachment.operations.move.button\": \"移動\",\n  \"core.attachment.operations.move.toast_success\": \"移動成功\",\n  \"core.attachment.filters.storage_policy.label\": \"存儲策略\",\n  \"core.attachment.filters.storage_policy.result\": \"存儲策略：{storage_policy}\",\n  \"core.attachment.filters.owner.label\": \"上傳者\",\n  \"core.attachment.filters.owner.result\": \"上傳者：{owner}\",\n  \"core.attachment.filters.sort.items.create_time_desc\": \"較近上傳\",\n  \"core.attachment.filters.sort.items.create_time_asc\": \"較早上傳\",\n  \"core.attachment.filters.sort.items.size_desc\": \"文件大小降序\",\n  \"core.attachment.filters.sort.items.size_asc\": \"文件大小升序\",\n  \"core.attachment.filters.sort.items.display_name_asc\": \"文件名升序\",\n  \"core.attachment.filters.sort.items.display_name_desc\": \"文件名降序\",\n  \"core.attachment.filters.view_type.items.grid\": \"網格模式\",\n  \"core.attachment.filters.view_type.items.list\": \"列表模式\",\n  \"core.attachment.filters.accept.label\": \"類型\",\n  \"core.attachment.filters.accept.items.image\": \"圖片\",\n  \"core.attachment.filters.accept.items.audio\": \"音頻\",\n  \"core.attachment.filters.accept.items.video\": \"視頻\",\n  \"core.attachment.filters.accept.items.file\": \"文件\",\n  \"core.attachment.detail_modal.title\": \"附件：{display_name}\",\n  \"core.attachment.detail_modal.fields.preview\": \"預覽\",\n  \"core.attachment.detail_modal.fields.storage_policy\": \"存儲策略\",\n  \"core.attachment.detail_modal.fields.group\": \"所在分組\",\n  \"core.attachment.detail_modal.fields.display_name\": \"附件名稱\",\n  \"core.attachment.detail_modal.fields.media_type\": \"文件類型\",\n  \"core.attachment.detail_modal.fields.size\": \"文件大小\",\n  \"core.attachment.detail_modal.fields.owner\": \"上傳者\",\n  \"core.attachment.detail_modal.fields.creation_time\": \"上傳時間\",\n  \"core.attachment.detail_modal.fields.permalink\": \"連結\",\n  \"core.attachment.detail_modal.display_name_form.help\": \"自定義附件名稱，僅用於管理介面顯示，不會影響文件本身名稱以及訪問連結\",\n  \"core.attachment.detail_modal.preview.video_not_support\": \"當前瀏覽器不支援該影片播放\",\n  \"core.attachment.detail_modal.preview.audio_not_support\": \"當前瀏覽器不支援該音頻播放\",\n  \"core.attachment.detail_modal.preview.not_support\": \"此文件不支援預覽\",\n  \"core.attachment.group_editing_modal.titles.create\": \"新增附件分組\",\n  \"core.attachment.group_editing_modal.titles.update\": \"編輯附件分組\",\n  \"core.attachment.group_editing_modal.fields.display_name.label\": \"名稱\",\n  \"core.attachment.group_editing_modal.toast.group_name_exists\": \"分組名稱已存在\",\n  \"core.attachment.group_list.internal_groups.all\": \"全部\",\n  \"core.attachment.group_list.operations.rename.button\": \"重命名\",\n  \"core.attachment.group_list.operations.delete.button\": \"刪除並將附件移動至未分組\",\n  \"core.attachment.group_list.operations.delete.title\": \"刪除附件分組\",\n  \"core.attachment.group_list.operations.delete.description\": \"將刪除分組，並將分組下的附件移動至未分組，該操作不可恢復。\",\n  \"core.attachment.group_list.operations.delete.toast_success\": \"刪除成功，{total} 個附件已移動至未分組\",\n  \"core.attachment.group_list.operations.delete_with_attachments.button\": \"刪除並同時刪除附件\",\n  \"core.attachment.group_list.operations.delete_with_attachments.title\": \"刪除附件分組\",\n  \"core.attachment.group_list.operations.delete_with_attachments.description\": \"將刪除分組以及分組下的所有附件，該操作不可恢復。\",\n  \"core.attachment.group_list.operations.delete_with_attachments.toast_success\": \"刪除成功，{total} 個附件已被同時刪除\",\n  \"core.attachment.policies_modal.title\": \"存儲策略\",\n  \"core.attachment.policies_modal.empty.title\": \"當前沒有可用的存儲策略\",\n  \"core.attachment.policies_modal.empty.message\": \"當前沒有可用的存儲策略，你可以嘗試刷新或者新建策略\",\n  \"core.attachment.policies_modal.operations.delete.title\": \"刪除附件策略\",\n  \"core.attachment.policies_modal.operations.delete.description\": \"當前策略下沒有已上傳的附件。\",\n  \"core.attachment.policies_modal.operations.can_not_delete.title\": \"刪除失敗\",\n  \"core.attachment.policies_modal.operations.can_not_delete.description\": \"該策略下存在附件，無法刪除。\",\n  \"core.attachment.policy_editing_modal.titles.create\": \"新增策略：{policy_template}\",\n  \"core.attachment.policy_editing_modal.titles.update\": \"編輯策略：{policy}\",\n  \"core.attachment.policy_editing_modal.fields.display_name.label\": \"名稱\",\n  \"core.attachment.policy_editing_modal.fields.config.label\": \"策略配置\",\n  \"core.attachment.policy_editing_modal.fields.hidden.label\": \"在上傳介面隱藏\",\n  \"core.attachment.policy_editing_modal.fields.hidden.help\": \"開啟後，會在上傳介面隱藏該儲存策略\",\n  \"core.attachment.policy_editing_modal.fields.priority.label\": \"上傳介面顯示優先權\",\n  \"core.attachment.policy_editing_modal.fields.priority.help\": \"設置在上傳界面的顯示優先權，數字越大，排序越靠前\",\n  \"core.attachment.policy_editing_modal.toast.policy_name_exists\": \"儲存策略名稱已存在\",\n  \"core.attachment.upload_modal.title\": \"上傳附件\",\n  \"core.attachment.upload_modal.filters.group.label\": \"分組：\",\n  \"core.attachment.upload_modal.filters.policy.label\": \"存儲策略：\",\n  \"core.attachment.upload_modal.filters.policy.empty.title\": \"沒有存儲策略\",\n  \"core.attachment.upload_modal.filters.policy.empty.description\": \"在上傳之前，需要新建一個存儲策略\",\n  \"core.attachment.upload_modal.filters.policy.not_select\": \"請先選擇存儲策略\",\n  \"core.attachment.upload_modal.upload_options.local_upload\": \"本地上傳\",\n  \"core.attachment.upload_modal.upload_options.download\": \"通過鏈接下載\",\n  \"core.attachment.upload_modal.download_form.fields.url.label\": \"鏈接地址\",\n  \"core.attachment.upload_modal.download_form.toast.success\": \"下載成功\",\n  \"core.attachment.select_modal.title\": \"選擇附件\",\n  \"core.attachment.select_modal.providers.default.label\": \"附件庫\",\n  \"core.attachment.select_modal.operations.select.result\": \"（已選擇 {count} 項）\",\n  \"core.attachment.permalink_list.relative\": \"相對路徑\",\n  \"core.attachment.permalink_list.absolute\": \"完整路徑\",\n  \"core.uc_attachment.empty.title\": \"當前沒有附件\",\n  \"core.uc_attachment.empty.message\": \"當前沒有附件，你可以嘗試重整或者上傳附件\",\n  \"core.uc_attachment.empty.actions.upload\": \"上傳附件\",\n  \"core.uc_attachment.filters.sort.items.create_time_desc\": \"較近上傳\",\n  \"core.uc_attachment.filters.sort.items.create_time_asc\": \"較早上傳\",\n  \"core.uc_attachment.filters.sort.items.size_desc\": \"文件大小降序\",\n  \"core.uc_attachment.filters.sort.items.size_asc\": \"文件大小升序\",\n  \"core.uc_attachment.filters.sort.items.display_name_asc\": \"文件名升序\",\n  \"core.uc_attachment.filters.sort.items.display_name_desc\": \"文件名降序\",\n  \"core.uc_attachment.filters.view_type.items.grid\": \"網格模式\",\n  \"core.uc_attachment.filters.view_type.items.list\": \"列表模式\",\n  \"core.uc_attachment.detail_modal.title\": \"附件：{display_name}\",\n  \"core.uc_attachment.detail_modal.fields.preview\": \"預覽\",\n  \"core.uc_attachment.detail_modal.fields.display_name\": \"文件名稱\",\n  \"core.uc_attachment.detail_modal.fields.media_type\": \"文件類型\",\n  \"core.uc_attachment.detail_modal.fields.size\": \"文件大小\",\n  \"core.uc_attachment.detail_modal.fields.owner\": \"上傳者\",\n  \"core.uc_attachment.detail_modal.fields.creation_time\": \"上傳時間\",\n  \"core.uc_attachment.detail_modal.fields.permalink\": \"連結\",\n  \"core.uc_attachment.detail_modal.preview.click_to_exit\": \"點擊離開預覽\",\n  \"core.uc_attachment.detail_modal.preview.video_not_support\": \"當前瀏覽器不支援該影片播放\",\n  \"core.uc_attachment.detail_modal.preview.audio_not_support\": \"當前瀏覽器不支援該音頻播放\",\n  \"core.uc_attachment.detail_modal.preview.not_support\": \"此文件不支援預覽\",\n  \"core.uc_attachment.upload_modal.title\": \"上傳附件\",\n  \"core.uc_attachment.select_modal.title\": \"選擇附件\",\n  \"core.uc_attachment.select_modal.providers.default.label\": \"附件庫\",\n  \"core.uc_attachment.select_modal.operations.select.result\": \"（已選擇 {count} 項）\",\n  \"core.theme.title\": \"主題\",\n  \"core.theme.common.buttons.install\": \"安裝主題\",\n  \"core.theme.tabs.detail\": \"詳情\",\n  \"core.theme.actions.management\": \"主題管理\",\n  \"core.theme.empty.title\": \"當前沒有已啟用或已選擇的主題\",\n  \"core.theme.empty.message\": \"當前沒有已啟用或者選擇的主題，你可以切換主題或者安裝新主題\",\n  \"core.theme.empty.actions.switch\": \"切換主題\",\n  \"core.theme.operations.active.title\": \"啟用主題\",\n  \"core.theme.operations.active.toast_success\": \"啟用成功\",\n  \"core.theme.operations.reset.title\": \"重置主題配置\",\n  \"core.theme.operations.reset.description\": \"該操作會刪除已保存的配置，重置為預設配置。\",\n  \"core.theme.operations.reset.toast_success\": \"重置配置成功\",\n  \"core.theme.operations.reload.button\": \"重載主題配置\",\n  \"core.theme.operations.reload.title\": \"重載主題配置\",\n  \"core.theme.operations.reload.description\": \"該操作僅會重載主題配置和設置表單定義，不會刪除已保存的配置。\",\n  \"core.theme.operations.reload.toast_success\": \"重載配置成功\",\n  \"core.theme.operations.uninstall.title\": \"卸載主題\",\n  \"core.theme.operations.uninstall_and_delete_config.button\": \"卸載並刪除配置\",\n  \"core.theme.operations.uninstall_and_delete_config.title\": \"卸載主題以及對應的配置\",\n  \"core.theme.operations.remote_download.title\": \"偵測到遠端下載地址，是否需要下載？\",\n  \"core.theme.operations.remote_download.description\": \"請仔細鑑別此地址是否可信：{url}\",\n  \"core.theme.operations.existed_during_installation.title\": \"主題已存在\",\n  \"core.theme.operations.existed_during_installation.description\": \"當前安裝的主題已存在，是否升級？\",\n  \"core.theme.operations.clear_templates_cache.button\": \"清除模板快取\",\n  \"core.theme.operations.clear_templates_cache.title\": \"清除模板快取\",\n  \"core.theme.operations.clear_templates_cache.description\": \"此功能適用於在運行時修改模板檔案後，刷新快取以查看最新網頁結果。\",\n  \"core.theme.operations.export_configuration.button\": \"匯出主題配置\",\n  \"core.theme.operations.import_configuration.button\": \"匯入主題配置\",\n  \"core.theme.operations.import_configuration.version_mismatch.title\": \"版本不匹配\",\n  \"core.theme.operations.import_configuration.version_mismatch.description\": \"匯入的配置檔版本與目前主題版本不匹配，這可能會導致相容性問題。是否繼續匯入？\",\n  \"core.theme.operations.import_configuration.invalid_format\": \"無效的主題配置文件\",\n  \"core.theme.operations.import_configuration.mismatched_theme\": \"配置文件與所選主題不匹配\",\n  \"core.theme.list_modal.tabs.installed\": \"已安裝\",\n  \"core.theme.list_modal.tabs.not_installed\": \"本地未安裝\",\n  \"core.theme.list_modal.tabs.local_upload\": \"上傳安裝 / 升級\",\n  \"core.theme.list_modal.tabs.remote_download.label\": \"遠端下載\",\n  \"core.theme.list_modal.tabs.remote_download.fields.url\": \"下載地址\",\n  \"core.theme.list_modal.empty.title\": \"當前沒有已安裝的主題\",\n  \"core.theme.list_modal.empty.message\": \"當前沒有已安裝的主題，你可以嘗試刷新或者安裝新主題\",\n  \"core.theme.list_modal.not_installed_empty.title\": \"當前沒有未安裝的主題\",\n  \"core.theme.preview_model.title\": \"預覽主題：{display_name}\",\n  \"core.theme.preview_model.actions.switch\": \"切換主題\",\n  \"core.theme.preview_model.actions.setting\": \"主題設置\",\n  \"core.theme.preview_model.actions.open\": \"新窗口打開\",\n  \"core.theme.detail.fields.author\": \"作者\",\n  \"core.theme.detail.fields.homepage\": \"網站\",\n  \"core.theme.detail.fields.repo\": \"源碼倉庫\",\n  \"core.theme.detail.fields.version\": \"版本\",\n  \"core.theme.detail.fields.requires\": \"Halo 版本要求\",\n  \"core.theme.detail.fields.storage_location\": \"存儲位置\",\n  \"core.theme.detail.fields.plugin_requires\": \"插件依賴\",\n  \"core.theme.detail.fields.description\": \"描述\",\n  \"core.theme.detail.fields.license\": \"協議\",\n  \"core.theme.detail.fields.issues\": \"問題回饋\",\n  \"core.theme.settings.title\": \"主題設置\",\n  \"core.theme.custom_templates.default\": \"預設模板\",\n  \"core.menu.title\": \"選單\",\n  \"core.menu.empty.title\": \"當前沒有選單\",\n  \"core.menu.empty.message\": \"你可以嘗試刷新或者新建選單\",\n  \"core.menu.menu_item_empty.title\": \"當前沒有選單項\",\n  \"core.menu.menu_item_empty.message\": \"你可以嘗試刷新或者新建選單項\",\n  \"core.menu.operations.set_primary.button\": \"設置為主選單\",\n  \"core.menu.operations.set_primary.toast_success\": \"設置成功\",\n  \"core.menu.operations.delete_menu.title\": \"刪除選單\",\n  \"core.menu.operations.delete_menu.description\": \"將同時刪除該選單下的所有選單項，該操作不可恢復。\",\n  \"core.menu.operations.delete_menu_item.title\": \"刪除選單項\",\n  \"core.menu.operations.delete_menu_item.description\": \"將同時刪除所有子選單項，刪除後將無法恢復。\",\n  \"core.menu.operations.add_sub_menu_item.button\": \"新增子選單項\",\n  \"core.menu.list.fields.primary\": \"主選單\",\n  \"core.menu.list.fields.items_count\": \"{count} 個選單項\",\n  \"core.menu.menu_editing_modal.titles.create\": \"新增選單\",\n  \"core.menu.menu_editing_modal.titles.update\": \"編輯選單\",\n  \"core.menu.menu_editing_modal.fields.display_name.label\": \"選單名稱\",\n  \"core.menu.menu_item_editing_modal.titles.create\": \"新增選單項\",\n  \"core.menu.menu_item_editing_modal.titles.update\": \"編輯選單項\",\n  \"core.menu.menu_item_editing_modal.groups.general\": \"常規\",\n  \"core.menu.menu_item_editing_modal.groups.annotations\": \"元數據\",\n  \"core.menu.menu_item_editing_modal.fields.parent.label\": \"上級選單項\",\n  \"core.menu.menu_item_editing_modal.fields.parent.placeholder\": \"選擇上級選單項\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.label\": \"類型\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.placeholder\": \"請選擇{label}\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.custom\": \"自定義連結\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.post\": \"文章\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page\": \"自定義頁面\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.category\": \"分類\",\n  \"core.menu.menu_item_editing_modal.fields.ref_kind.options.tag\": \"標籤\",\n  \"core.menu.menu_item_editing_modal.fields.display_name.label\": \"名稱\",\n  \"core.menu.menu_item_editing_modal.fields.href.label\": \"連結地址\",\n  \"core.menu.menu_item_editing_modal.fields.target.label\": \"打開方式\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.self\": \"當前窗口\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.blank\": \"新窗口\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.parent\": \"父窗口\",\n  \"core.menu.menu_item_editing_modal.fields.target.options.top\": \"頂級窗口\",\n  \"core.plugin.title\": \"插件\",\n  \"core.plugin.tabs.detail\": \"詳情\",\n  \"core.plugin.empty.title\": \"當前沒有已安裝的插件\",\n  \"core.plugin.empty.message\": \"當前沒有已安裝的插件，你可以嘗試重整或者安裝新插件\",\n  \"core.plugin.empty.actions.install\": \"安裝插件\",\n  \"core.plugin.operations.reset.title\": \"重置插件配置\",\n  \"core.plugin.operations.reset.description\": \"該操作會刪除已保存的配置，重置為默認配置。\",\n  \"core.plugin.operations.reset.toast_success\": \"重置配置成功\",\n  \"core.plugin.operations.uninstall.title\": \"卸載插件\",\n  \"core.plugin.operations.uninstall_in_batch.title\": \"卸載所選插件\",\n  \"core.plugin.operations.uninstall_and_delete_config.button\": \"卸載並刪除配置\",\n  \"core.plugin.operations.uninstall_and_delete_config.title\": \"卸載插件以及對應的配置\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.button\": \"卸載並刪除配置\",\n  \"core.plugin.operations.uninstall_and_delete_config_in_batch.title\": \"卸載所選插件以及對應的配置\",\n  \"core.plugin.operations.uninstall_when_enabled.confirm_text\": \"停止運行並卸載\",\n  \"core.plugin.operations.uninstall_when_enabled.description\": \"當前插件還在啟用狀態，將在停止運行後卸載，該操作不可恢復。\",\n  \"core.plugin.operations.change_status_in_batch.activate_title\": \"啟用所選插件\",\n  \"core.plugin.operations.change_status_in_batch.inactivate_title\": \"停用所選插件\",\n  \"core.plugin.operations.remote_download.title\": \"偵測到遠端下載地址，是否需要下載？\",\n  \"core.plugin.operations.remote_download.description\": \"請仔細鑑別此地址是否可信：{url}\",\n  \"core.plugin.operations.reload_window.button\": \"需要重載頁面\",\n  \"core.plugin.filters.status.items.active\": \"已啟用\",\n  \"core.plugin.filters.status.items.inactive\": \"未啟用\",\n  \"core.plugin.filters.sort.items.create_time_desc\": \"較近安裝\",\n  \"core.plugin.filters.sort.items.create_time_asc\": \"較早安裝\",\n  \"core.plugin.upload_modal.titles.install\": \"安裝插件\",\n  \"core.plugin.upload_modal.titles.upgrade\": \"升級插件（{display_name}）\",\n  \"core.plugin.upload_modal.tabs.local\": \"本地上傳\",\n  \"core.plugin.upload_modal.tabs.remote.title\": \"遠端下載\",\n  \"core.plugin.upload_modal.tabs.remote.fields.url\": \"下載地址\",\n  \"core.plugin.upload_modal.operations.active_after_install.title\": \"安裝成功\",\n  \"core.plugin.upload_modal.operations.active_after_install.description\": \"是否啟動當前安裝的插件？\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.title\": \"插件已存在\",\n  \"core.plugin.upload_modal.operations.existed_during_installation.description\": \"當前安裝的插件已存在，是否升級？\",\n  \"core.plugin.detail.title\": \"插件詳情\",\n  \"core.plugin.detail.header.title\": \"插件信息\",\n  \"core.plugin.detail.fields.description\": \"描述\",\n  \"core.plugin.detail.fields.version\": \"版本\",\n  \"core.plugin.detail.fields.requires\": \"Halo 版本要求\",\n  \"core.plugin.detail.fields.author\": \"作者\",\n  \"core.plugin.detail.fields.license\": \"協議\",\n  \"core.plugin.detail.fields.role_templates\": \"權限模板\",\n  \"core.plugin.detail.fields.last_starttime\": \"最近一次啟動\",\n  \"core.plugin.detail.fields.homepage\": \"網站\",\n  \"core.plugin.detail.fields.repo\": \"源碼倉庫\",\n  \"core.plugin.detail.fields.load_location\": \"存儲位置\",\n  \"core.plugin.detail.fields.issues\": \"問題回饋\",\n  \"core.plugin.detail.fields.creation_time\": \"安裝時間\",\n  \"core.plugin.detail.operations.view_conditions.button\": \"查看最近狀態\",\n  \"core.plugin.loader.toast.entry_load_failed\": \"讀取插件入口文件失敗\",\n  \"core.plugin.loader.toast.style_load_failed\": \"讀取插件樣式文件失敗\",\n  \"core.plugin.extension_points.editor.providers.default\": \"預設編輯器\",\n  \"core.plugin.conditions_modal.title\": \"插件最近狀態\",\n  \"core.plugin.conditions_modal.fields.type\": \"類型\",\n  \"core.plugin.conditions_modal.fields.status\": \"狀態\",\n  \"core.plugin.conditions_modal.fields.reason\": \"原因\",\n  \"core.plugin.conditions_modal.fields.message\": \"訊息\",\n  \"core.plugin.conditions_modal.fields.last_transition_time\": \"時間\",\n  \"core.plugin.extension-settings.title\": \"擴展配置\",\n  \"core.plugin.extension-settings.extension-point-definition.title\": \"擴展點定義\",\n  \"core.plugin.extension-settings.extension-definition.empty.title\": \"當前沒有擴展點實現\",\n  \"core.plugin.extension-settings.extension-definition.operation.use.button\": \"使用\",\n  \"core.plugin.actions.extension-point-settings\": \"擴展配置\",\n  \"core.plugin.list.fields.system_reserved\": \"系統保留\",\n  \"core.user.title\": \"用戶\",\n  \"core.user.actions.roles\": \"角色管理\",\n  \"core.user.actions.identity_authentication\": \"身份認證\",\n  \"core.user.empty.title\": \"當前沒有符合篩選條件的用戶\",\n  \"core.user.empty.message\": \"當前沒有符合篩選條件的用戶，你可以嘗試刷新或者創建新用戶\",\n  \"core.user.operations.delete.title\": \"刪除用戶\",\n  \"core.user.operations.delete_in_batch.title\": \"刪除所選用戶\",\n  \"core.user.operations.update_profile.title\": \"修改資料\",\n  \"core.user.operations.change_password.title\": \"修改密碼\",\n  \"core.user.operations.grant_permission.title\": \"分配角色\",\n  \"core.user.operations.enable.title\": \"啟用\",\n  \"core.user.operations.enable.description\": \"確定要啟用該用戶嗎？啟用後該用戶將可以重新登入系統\",\n  \"core.user.operations.enable_in_batch.title\": \"启用\",\n  \"core.user.operations.enable_in_batch.description\": \"確定要啟用所選用戶嗎？啟用後這些用戶將可以重新登入系統\",\n  \"core.user.operations.disable.title\": \"停用\",\n  \"core.user.operations.disable.description\": \"確定要停用該用戶嗎？停用後該用戶將無法登入系統\",\n  \"core.user.operations.disable_in_batch.title\": \"停用\",\n  \"core.user.operations.disable_in_batch.description\": \"確定要停用所選用戶嗎？停用後這些用戶將無法登入系統\",\n  \"core.user.filters.role.label\": \"角色\",\n  \"core.user.filters.role.result\": \"角色：{role}\",\n  \"core.user.filters.sort.items.create_time_desc\": \"較近創建\",\n  \"core.user.filters.sort.items.create_time_asc\": \"較早創建\",\n  \"core.user.editing_modal.titles.update\": \"編輯用戶\",\n  \"core.user.editing_modal.titles.create\": \"新增用戶\",\n  \"core.user.editing_modal.groups.general\": \"常規\",\n  \"core.user.editing_modal.groups.annotations\": \"元數據\",\n  \"core.user.editing_modal.fields.username.label\": \"用戶名\",\n  \"core.user.editing_modal.fields.username.validation\": \"只能包含數字、小寫字母、. 和 - 符號，且不能以 . 和 - 符號開頭和結尾。\",\n  \"core.user.editing_modal.fields.display_name.label\": \"顯示名稱\",\n  \"core.user.editing_modal.fields.email.label\": \"電子郵箱\",\n  \"core.user.editing_modal.fields.phone.label\": \"手機號\",\n  \"core.user.editing_modal.fields.avatar.label\": \"頭像\",\n  \"core.user.editing_modal.fields.bio.label\": \"描述\",\n  \"core.user.change_password_modal.title\": \"密碼修改\",\n  \"core.user.change_password_modal.fields.new_password.label\": \"新密碼\",\n  \"core.user.change_password_modal.fields.confirm_password.label\": \"確認密碼\",\n  \"core.user.grant_permission_modal.title\": \"分配角色\",\n  \"core.user.grant_permission_modal.fields.role.label\": \"角色\",\n  \"core.user.grant_permission_modal.fields.role.placeholder\": \"請選擇角色\",\n  \"core.user.grant_permission_modal.roles_preview.all\": \"目前所選角色包含所有權限\",\n  \"core.user.grant_permission_modal.roles_preview.includes\": \"目前選定角色所包含的權限：\",\n  \"core.user.detail.title\": \"用戶詳情\",\n  \"core.user.detail.tabs.detail\": \"詳情\",\n  \"core.user.detail.actions.update_profile.title\": \"修改資料\",\n  \"core.user.detail.actions.change_password.title\": \"修改密碼\",\n  \"core.user.detail.actions.grant_permission.title\": \"分配角色\",\n  \"core.user.detail.actions.profile.title\": \"個人中心\",\n  \"core.user.detail.fields.display_name\": \"顯示名稱\",\n  \"core.user.detail.fields.username\": \"用戶名\",\n  \"core.user.detail.fields.email\": \"電子郵箱\",\n  \"core.user.detail.fields.roles\": \"角色\",\n  \"core.user.detail.fields.bio\": \"描述\",\n  \"core.user.detail.fields.creation_time\": \"註冊時間\",\n  \"core.user.detail.fields.email_verified.tooltip\": \"已驗證\",\n  \"core.user.detail.fields.email_not_verified.tooltip\": \"未驗證\",\n  \"core.user.fields.disabled\": \"已停用\",\n  \"core.role.title\": \"角色\",\n  \"core.role.common.text.contains_all_permissions\": \"包含所有權限\",\n  \"core.role.common.text.contains_n_permissions\": \"包含 {count} 個權限\",\n  \"core.role.common.text.system_reserved\": \"系統保留\",\n  \"core.role.common.text.custom\": \"自定義\",\n  \"core.role.common.text.dependent_on\": \"依賴於 {roles}\",\n  \"core.role.common.text.provided_by_plugin\": \"由 {plugin} 提供\",\n  \"core.role.operations.delete.title\": \"刪除角色\",\n  \"core.role.operations.delete.description\": \"該角色刪除後，相關聯的用戶將被刪除角色綁定，該操作不可恢復。\",\n  \"core.role.operations.create_based_on_this_role.button\": \"基於此角色創建\",\n  \"core.role.detail.title\": \"角色詳情\",\n  \"core.role.detail.header.title\": \"角色信息\",\n  \"core.role.detail.tabs.detail\": \"詳情\",\n  \"core.role.detail.tabs.permissions\": \"權限設置\",\n  \"core.role.detail.fields.display_name\": \"名稱\",\n  \"core.role.detail.fields.name\": \"別名\",\n  \"core.role.detail.fields.type\": \"類型\",\n  \"core.role.detail.fields.creation_time\": \"創建時間\",\n  \"core.role.permissions_detail.system_reserved_alert.description\": \"系統保留的角色不支持修改，推薦基於此角色創建一個新的角色。\",\n  \"core.role.editing_modal.titles.create\": \"創建角色\",\n  \"core.role.editing_modal.titles.update\": \"編輯角色\",\n  \"core.role.editing_modal.groups.general\": \"基本資訊\",\n  \"core.role.editing_modal.groups.permissions\": \"權限\",\n  \"core.role.editing_modal.fields.display_name\": \"名稱\",\n  \"core.role.editing_modal.fields.disallow_access_console.label\": \"禁止訪問 Console\",\n  \"core.role.editing_modal.fields.disallow_access_console.help\": \"勾選之後，該角色將無法存取 Console\",\n  \"core.identity_authentication.title\": \"身份認證\",\n  \"core.identity_authentication.tabs.detail\": \"詳情\",\n  \"core.identity_authentication.tabs.setting\": \"設置\",\n  \"core.identity_authentication.operations.enable.title\": \"啟用認證方式\",\n  \"core.identity_authentication.operations.disable.title\": \"停用認證方式\",\n  \"core.identity_authentication.operations.disable_privileged.tooltip\": \"系統保留的認證方式，無法禁用\",\n  \"core.identity_authentication.fields.display_name.local\": \"帳號密碼登入\",\n  \"core.identity_authentication.fields.description.local\": \"Halo 內建的默認登入方式\",\n  \"core.identity_authentication.list.types.form\": \"基礎認證方式\",\n  \"core.identity_authentication.list.types.oauth2\": \"三方認證方式\",\n  \"core.identity_authentication.detail.title\": \"身份認證詳情\",\n  \"core.identity_authentication.detail.fields.display_name\": \"名稱\",\n  \"core.identity_authentication.detail.fields.description\": \"描述\",\n  \"core.identity_authentication.detail.fields.website\": \"網站\",\n  \"core.identity_authentication.detail.fields.help_page\": \"幫助頁面\",\n  \"core.identity_authentication.detail.fields.authentication_url\": \"登入入口\",\n  \"core.uc_profile.title\": \"我的\",\n  \"core.uc_profile.tabs.detail\": \"詳情\",\n  \"core.uc_profile.tabs.notification-preferences\": \"通知配置\",\n  \"core.uc_profile.tabs.pat\": \"個人令牌\",\n  \"core.uc_profile.tabs.devices\": \"登錄設備\",\n  \"core.uc_profile.tabs.authentication\": \"身份認證\",\n  \"core.uc_profile.actions.update_profile.title\": \"修改資料\",\n  \"core.uc_profile.actions.change_password.title\": \"修改密碼\",\n  \"core.uc_profile.detail.fields.display_name\": \"顯示名稱\",\n  \"core.uc_profile.detail.fields.username\": \"用戶名\",\n  \"core.uc_profile.detail.fields.email\": \"電子郵箱\",\n  \"core.uc_profile.detail.fields.roles\": \"角色\",\n  \"core.uc_profile.detail.fields.bio\": \"描述\",\n  \"core.uc_profile.detail.fields.creation_time\": \"註冊時間\",\n  \"core.uc_profile.detail.email_not_set.description\": \"電子郵件地址尚未設置，點擊下方按鈕進行設置\",\n  \"core.uc_profile.detail.email_not_set.title\": \"設定電子郵件信箱\",\n  \"core.uc_profile.detail.email_not_verified.description\": \"電子郵件地址尚未驗證，點擊下方按鈕進行驗證\",\n  \"core.uc_profile.detail.email_not_verified.title\": \"驗證電子郵件信箱\",\n  \"core.uc_profile.detail.email_verified.tooltip\": \"已驗證\",\n  \"core.uc_profile.auth_providers.title\": \"登入方式\",\n  \"core.uc_profile.auth_providers.operations.bind.button\": \"綁定\",\n  \"core.uc_profile.auth_providers.operations.unbind.button\": \"解綁\",\n  \"core.uc_profile.auth_providers.operations.unbind.title\": \"取消綁定 {display_name} 登入方式\",\n  \"core.uc_profile.2fa.operations.enable.button\": \"启用两步验证\",\n  \"core.uc_profile.2fa.operations.enable.title\": \"启用两步验证\",\n  \"core.uc_profile.2fa.operations.disable.title\": \"禁用两步验证\",\n  \"core.uc_profile.2fa.operations.disable_totp.title\": \"停用 TOTP\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.label\": \"验证密碼\",\n  \"core.uc_profile.2fa.password_validation_form.fields.password.help\": \"目前帳號的登入密碼\",\n  \"core.uc_profile.2fa.methods.title\": \"兩步驗證\",\n  \"core.uc_profile.2fa.methods.totp.title\": \"TOTP\",\n  \"core.uc_profile.2fa.methods.totp.description\": \"使用 TOTP 應用程式設定兩步驟驗證\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.configured\": \"已配置\",\n  \"core.uc_profile.2fa.methods.totp.fields.status.not_configured\": \"未配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.reconfigure.button\": \"重新配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.button\": \"配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.title\": \"TOTP 配置\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.label\": \"驗證碼\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.code.help\": \"從驗證器應用程式取得的 6 位驗證碼\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.label\": \"验证密碼\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.password.help\": \"目前帳號的登入密碼\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.qrcode.label\": \"使用驗證器應用程式掃描下面的二維碼：\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.label\": \"如果您無法掃描二維碼，請按一下查看替代步驟。\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.help\": \"使用以下程式碼手動配置驗證器應用程式：\",\n  \"core.uc_profile.2fa.methods.totp.operations.configure.tips\": \"請妥善保管您的兩步驟驗證裝置，如果裝置遺失或損壞，您將無法登入系統。建議您在多個裝置上安裝驗證器應用程式，或保存好金鑰的備份，以防主要裝置無法使用。\",\n  \"core.uc_profile.pat.operations.delete.title\": \"刪除個人令牌\",\n  \"core.uc_profile.pat.operations.delete.description\": \"刪除個人令牌\",\n  \"core.uc_profile.pat.operations.revoke.button\": \"撤銷\",\n  \"core.uc_profile.pat.operations.revoke.title\": \"撤銷個人令牌\",\n  \"core.uc_profile.pat.operations.revoke.description\": \"撤銷個人令牌\",\n  \"core.uc_profile.pat.operations.revoke.toast_success\": \"撤銷成功\",\n  \"core.uc_profile.pat.operations.copy.title\": \"請立即複製並保存，令牌僅顯示一次。\",\n  \"core.uc_profile.pat.operations.restore.button\": \"還原\",\n  \"core.uc_profile.pat.operations.restore.toast_success\": \"還原成功\",\n  \"core.uc_profile.pat.list.empty.title\": \"目前尚未建立個人令牌\",\n  \"core.uc_profile.pat.list.empty.message\": \"您可以嘗試重新整理或建立新的個人令牌\",\n  \"core.uc_profile.pat.list.fields.expiresAt.dynamic\": \"到期於 {expiresAt}\",\n  \"core.uc_profile.pat.list.fields.expiresAt.forever\": \"永久有效\",\n  \"core.uc_profile.pat.list.fields.status.normal\": \"正常\",\n  \"core.uc_profile.pat.list.fields.status.revoked\": \"已撤銷\",\n  \"core.uc_profile.pat.list.fields.status.expired\": \"已過期\",\n  \"core.uc_profile.pat.creation_modal.title\": \"建立個人令牌\",\n  \"core.uc_profile.pat.creation_modal.groups.general\": \"基本資訊\",\n  \"core.uc_profile.pat.creation_modal.groups.permissions\": \"權限\",\n  \"core.uc_profile.pat.creation_modal.fields.name.label\": \"名稱\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.label\": \"到期時間\",\n  \"core.uc_profile.pat.creation_modal.fields.expiresAt.help\": \"留空代表永不過期\",\n  \"core.uc_profile.pat.creation_modal.fields.description.label\": \"描述\",\n  \"core.uc_profile.notification-preferences.fields.type\": \"通知類型\",\n  \"core.uc_profile.editing_modal.title\": \"編輯資料\",\n  \"core.uc_profile.editing_modal.groups.general\": \"常規\",\n  \"core.uc_profile.editing_modal.groups.annotations\": \"元數據\",\n  \"core.uc_profile.editing_modal.fields.username.label\": \"用戶名\",\n  \"core.uc_profile.editing_modal.fields.username.validation\": \"只能包含數字、小寫字母、. 和 - 符號，且不能以 . 和 - 符號開頭和結尾。\",\n  \"core.uc_profile.editing_modal.fields.display_name.label\": \"顯示名稱\",\n  \"core.uc_profile.editing_modal.fields.email.label\": \"電子郵箱\",\n  \"core.uc_profile.editing_modal.fields.phone.label\": \"手機號\",\n  \"core.uc_profile.editing_modal.fields.avatar.label\": \"頭像\",\n  \"core.uc_profile.editing_modal.fields.bio.label\": \"描述\",\n  \"core.uc_profile.change_password_modal.title\": \"密碼修改\",\n  \"core.uc_profile.change_password_modal.fields.new_password.label\": \"新密碼\",\n  \"core.uc_profile.change_password_modal.fields.confirm_password.label\": \"確認密碼\",\n  \"core.uc_profile.change_password_modal.fields.old_password.label\": \"舊密碼\",\n  \"core.uc_profile.email_verify_modal.fields.code.label\": \"驗證碼\",\n  \"core.uc_profile.email_verify_modal.fields.email.label\": \"電子郵件信箱\",\n  \"core.uc_profile.email_verify_modal.fields.new_email.label\": \"新電子郵件信箱\",\n  \"core.uc_profile.email_verify_modal.fields.password.label\": \"驗證密碼\",\n  \"core.uc_profile.email_verify_modal.fields.password.help\": \"目前帳號的登入密碼\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.countdown\": \"{timer} 秒後重發\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.send\": \"發送驗證碼\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.buttons.sending\": \"發送中\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_email_empty\": \"請輸入電子郵件信箱\",\n  \"core.uc_profile.email_verify_modal.operations.send_code.toast_success\": \"驗證碼已發送\",\n  \"core.uc_profile.email_verify_modal.operations.verify.toast_success\": \"驗證成功\",\n  \"core.uc_profile.email_verify_modal.titles.modify\": \"修改電子郵件信箱\",\n  \"core.uc_profile.email_verify_modal.titles.verify\": \"驗證電子郵件信箱\",\n  \"core.uc_profile.device.list.fields.last_accessed_time\": \"上次訪問：{time}\",\n  \"core.uc_profile.device.list.fields.current\": \"目前設備\",\n  \"core.uc_profile.device.detail_modal.title\": \"登入設備詳情\",\n  \"core.uc_profile.device.detail_modal.fields.os\": \"操作系統\",\n  \"core.uc_profile.device.detail_modal.fields.browser\": \"瀏覽器\",\n  \"core.uc_profile.device.detail_modal.fields.creation_timestamp\": \"創建時間\",\n  \"core.uc_profile.device.detail_modal.fields.last_accessed_times\": \"上次訪問時間\",\n  \"core.uc_profile.device.detail_modal.fields.last_authenticated_time\": \"上次登錄時間\",\n  \"core.uc_profile.device.operations.revoke.title\": \"撤銷設備\",\n  \"core.uc_profile.device.operations.revoke.description\": \"確定要撤銷該設備嗎？撤銷之後，此裝置會退出登錄\",\n  \"core.uc_profile.device.operations.revoke_others.title\": \"撤銷其他所有設備\",\n  \"core.uc_profile.device.operations.revoke_others.toast_success\": \"已撤銷其他設備的登入狀態\",\n  \"core.uc_profile.device.operations.revoke_others.description\": \"確定要撤銷其他所有設備嗎？撤銷之後，其他設備會登出登入\",\n  \"core.uc_notification.title\": \"訊息\",\n  \"core.uc_notification.tabs.unread\": \"未讀\",\n  \"core.uc_notification.tabs.read\": \"已讀\",\n  \"core.uc_notification.empty.titles.unread\": \"目前沒有未讀的訊息\",\n  \"core.uc_notification.empty.titles.read\": \"目前沒有已讀的訊息\",\n  \"core.uc_notification.operations.mark_as_read.button\": \"標記為已讀\",\n  \"core.uc_notification.operations.delete.description\": \"確定要刪除該訊息嗎？\",\n  \"core.uc_notification.operations.delete.title\": \"刪除訊息\",\n  \"core.uc_notification.operations.delete_all.title\": \"刪除目前所有通知\",\n  \"core.uc_notification.operations.delete_all.description\": \"確定要刪除目前所有通知嗎？刪除後將無法復原。\",\n  \"core.uc_notification.operations.mark_all_as_read.title\": \"將目前所有通知標示為已讀\",\n  \"core.uc_notification.operations.mark_all_as_read.description\": \"將目前所有通知標示為已讀，是否繼續？\",\n  \"core.setting.title\": \"設置\",\n  \"core.overview.title\": \"系統概覽\",\n  \"core.overview.actions.copy.toast_browser_not_supported\": \"當前瀏覽器不支持複製\",\n  \"core.overview.header.titles.general\": \"基本資訊\",\n  \"core.overview.header.titles.environment\": \"環境資訊\",\n  \"core.overview.fields.external_url\": \"外部訪問地址\",\n  \"core.overview.fields.start_time\": \"啟動時間\",\n  \"core.overview.fields.timezone\": \"操作系統時區\",\n  \"core.overview.fields.locale\": \"操作系統語言\",\n  \"core.overview.fields.version\": \"版本\",\n  \"core.overview.fields.build_time\": \"構建時間\",\n  \"core.overview.fields.database\": \"資料庫\",\n  \"core.overview.fields.os\": \"操作系統\",\n  \"core.overview.fields.log\": \"運行日誌\",\n  \"core.overview.fields.activated_theme\": \"已啟動主題\",\n  \"core.overview.fields.enabled_plugins\": \"已啟動插件\",\n  \"core.overview.fields_values.external_url.not_setup\": \"未設置\",\n  \"core.overview.copy_results.external_url\": \"外部訪問地址：{external_url}\",\n  \"core.overview.copy_results.start_time\": \"啟動時間：{start_time}\",\n  \"core.overview.copy_results.version\": \"Halo 版本：{version}\",\n  \"core.overview.copy_results.build_time\": \"構建時間：{build_time}\",\n  \"core.overview.copy_results.database\": \"資料庫：{database}\",\n  \"core.overview.copy_results.os\": \"操作系統：{os}\",\n  \"core.overview.alert.external_url_invalid\": \"檢測到外部訪問地址與當前訪問地址不一致，可能會導致部分連結無法正常跳轉，請檢查外部訪問地址設置。\",\n  \"core.overview.external_url_form.operations.save.title\": \"修改外部訪問地址\",\n  \"core.overview.external_url_form.operations.save.description\": \"修改外部訪問地址需要重啟 Halo 服務，修改完成之後會自動進行重啟，是否繼續？\",\n  \"core.overview.external_url_form.tips.restarting\": \"修改完成，等待重啟...\",\n  \"core.backup.title\": \"備份與還原\",\n  \"core.backup.tabs.backup_list\": \"備份\",\n  \"core.backup.tabs.restore\": \"還原\",\n  \"core.backup.empty.title\": \"沒有備份\",\n  \"core.backup.empty.message\": \"目前沒有已建立的備份，您可以點擊重新整理或建立新的備份。\",\n  \"core.backup.operations.create.button\": \"建立備份\",\n  \"core.backup.operations.create.title\": \"建立備份\",\n  \"core.backup.operations.create.description\": \"確定要建立備份嗎？此操作可能需要較長時間。\",\n  \"core.backup.operations.create.toast_success\": \"已請求建立備份\",\n  \"core.backup.operations.delete.title\": \"刪除備份\",\n  \"core.backup.operations.delete.description\": \"確定要刪除此備份嗎？\",\n  \"core.backup.operations.restore.title\": \"還原成功\",\n  \"core.backup.operations.restore.description\": \"還原成功後，需要重新啟動 Halo 才能正常載入系統資源，點擊確定之後，我們會自動重啟 Halo。\",\n  \"core.backup.operations.restart.toast_success\": \"已請求重啟\",\n  \"core.backup.operations.remote_download.button\": \"下載並還原\",\n  \"core.backup.operations.restore_by_backup.button\": \"還原\",\n  \"core.backup.operations.restore_by_backup.title\": \"從備份檔案恢復\",\n  \"core.backup.operations.restore_by_backup.description\": \"點選確定後，將從備份檔案 {filename} 還原資料。\",\n  \"core.backup.list.phases.pending\": \"準備中\",\n  \"core.backup.list.phases.running\": \"備份中\",\n  \"core.backup.list.phases.succeeded\": \"備份完成\",\n  \"core.backup.list.phases.failed\": \"備份失敗\",\n  \"core.backup.list.fields.expiresAt\": \"{expiresAt}失效\",\n  \"core.backup.restore.tips.first\": \"1. 還原過程可能需要較長時間，期間請勿重新整理頁面。\",\n  \"core.backup.restore.tips.second\": \"2. 在執行恢復之前，會清空現有的所有數據，請確保當前沒有需要保留的數據。\",\n  \"core.backup.restore.tips.third\": \"3. 還原完成後需要重新啟動 Halo 才能正常載入系統資源。\",\n  \"core.backup.restore.tips.complete\": \"還原完成，等待重啟...\",\n  \"core.backup.restore.start\": \"我已閱讀上方提示，開始恢復\",\n  \"core.backup.restore.tabs.local.label\": \"上傳\",\n  \"core.backup.restore.tabs.remote.label\": \"遠程還原\",\n  \"core.backup.restore.tabs.remote.fields.url\": \"下載地址\",\n  \"core.backup.restore.tabs.backup.label\": \"從備份檔案恢復\",\n  \"core.backup.restore.tabs.backup.empty.title\": \"沒有備份文件\",\n  \"core.backup.restore.tabs.backup.empty.message\": \"目前沒有掃描到備份文件，你可以手動將備份文件上傳到 Halo 工作目錄的 backups 目錄。\",\n  \"core.exception.not_found.message\": \"沒有找到該頁面\",\n  \"core.exception.forbidden.message\": \"沒有權限訪問此頁面\",\n  \"core.exception.actions.home\": \"返回首頁\",\n  \"core.rbac.Attachments Management\": \"附件\",\n  \"core.rbac.Attachment Manage\": \"附件管理\",\n  \"core.rbac.Attachment View\": \"附件查看\",\n  \"core.rbac.role-template-view-attachments\": \"附件查看\",\n  \"core.rbac.Comments Management\": \"留言\",\n  \"core.rbac.Comment Manage\": \"留言管理\",\n  \"core.rbac.Comment View\": \"留言查看\",\n  \"core.rbac.role-template-view-comments\": \"留言查看\",\n  \"core.rbac.ConfigMaps Management\": \"配置\",\n  \"core.rbac.ConfigMap Manage\": \"配置管理\",\n  \"core.rbac.ConfigMap View\": \"配置查看\",\n  \"core.rbac.role-template-view-configmaps\": \"配置查看\",\n  \"core.rbac.Menus Management\": \"選單\",\n  \"core.rbac.Menu Manage\": \"選單管理\",\n  \"core.rbac.Menu View\": \"選單查看\",\n  \"core.rbac.role-template-view-menus\": \"選單查看\",\n  \"core.rbac.Permissions Management\": \"權限\",\n  \"core.rbac.Permissions Manage\": \"權限管理\",\n  \"core.rbac.Permissions View\": \"權限查看\",\n  \"core.rbac.role-template-view-permissions\": \"權限查看\",\n  \"core.rbac.role-template-manage-permissions\": \"權限管理\",\n  \"core.rbac.Plugins Management\": \"插件\",\n  \"core.rbac.Plugin Manage\": \"插件管理\",\n  \"core.rbac.Plugin View\": \"插件查看\",\n  \"core.rbac.role-template-view-plugins\": \"插件查看\",\n  \"core.rbac.Posts Management\": \"文章\",\n  \"core.rbac.Post Manage\": \"文章管理\",\n  \"core.rbac.Post View\": \"文章查看\",\n  \"core.rbac.role-template-view-posts\": \"文章查看\",\n  \"core.rbac.role-template-manage-snapshots\": \"版本管理\",\n  \"core.rbac.role-template-view-snapshots\": \"版本查看\",\n  \"core.rbac.role-template-manage-tags\": \"標籤管理\",\n  \"core.rbac.role-template-view-tags\": \"標籤查看\",\n  \"core.rbac.role-template-manage-categories\": \"分類管理\",\n  \"core.rbac.role-template-view-categories\": \"分類查看\",\n  \"core.rbac.role-template-manage-posts\": \"文章管理\",\n  \"core.rbac.role-template-post-author\": \"允許管理自己的文章\",\n  \"core.rbac.role-template-post-contributor\": \"允許投稿\",\n  \"core.rbac.role-template-post-publisher\": \"允許發布自己的文章\",\n  \"core.rbac.role-template-post-attachment-manager\": \"允許在文章中上傳圖片\",\n  \"core.rbac.Roles Management\": \"角色\",\n  \"core.rbac.Role Manage\": \"角色管理\",\n  \"core.rbac.Role View\": \"角色查看\",\n  \"core.rbac.role-template-view-roles\": \"角色查看\",\n  \"core.rbac.Settings Management\": \"設置表單\",\n  \"core.rbac.Setting Manage\": \"設置表單管理\",\n  \"core.rbac.Setting View\": \"設置表單查看\",\n  \"core.rbac.role-template-view-settings\": \"設置表單查看\",\n  \"core.rbac.SinglePages Management\": \"頁面\",\n  \"core.rbac.SinglePage Manage\": \"頁面管理\",\n  \"core.rbac.SinglePage View\": \"頁面查看\",\n  \"core.rbac.role-template-view-singlepages\": \"頁面查看\",\n  \"core.rbac.Themes Management\": \"主題\",\n  \"core.rbac.Theme Manage\": \"主題管理\",\n  \"core.rbac.Theme View\": \"主題查看\",\n  \"core.rbac.role-template-view-themes\": \"主題查看\",\n  \"core.rbac.Users Management\": \"用戶\",\n  \"core.rbac.User manage\": \"用戶管理\",\n  \"core.rbac.User View\": \"用戶查看\",\n  \"core.rbac.Migration Management\": \"備份與還原\",\n  \"core.rbac.Migration Manage\": \"備份與還原管理\",\n  \"core.rbac.role-template-view-users\": \"用戶查看\",\n  \"core.rbac.role-template-change-password\": \"修改密碼\",\n  \"core.rbac.Actuator Management\": \"系統指標\",\n  \"core.rbac.Actuator Manage\": \"訪問系統指標\",\n  \"core.rbac.Cache Management\": \"緩存\",\n  \"core.rbac.Cache Manage\": \"緩存管理\",\n  \"core.rbac.role-template-notifier-config\": \"配置通知器\",\n  \"core.rbac.Notification Configuration\": \"通知配置\",\n  \"core.rbac.Post Attachment Manager\": \"允許在文章中上傳圖片\",\n  \"core.rbac.Post Author\": \"允許管理自己的文章\",\n  \"core.rbac.Post Contributor\": \"允許投稿\",\n  \"core.rbac.Post Publisher\": \"允許發布自己的文章\",\n  \"core.rbac.UC Attachment Manage\": \"允許管理自己的附件\",\n  \"core.rbac.role-template-uc-attachment-manager\": \"允許管理自己的附件\",\n  \"core.rbac.Recycle My Post\": \"允許刪除自己的文章\",\n  \"core.rbac.role-template-recycle-my-post\": \"允許刪除自己的文章\",\n  \"core.components.submit_button.computed_text\": \"{text}（{shortcut}）\",\n  \"core.components.annotations_form.custom_fields.label\": \"自定義\",\n  \"core.components.annotations_form.custom_fields.validation\": \"當前 Key 已被占用\",\n  \"core.components.annotations_form.buttons.expand\": \"查看更多\",\n  \"core.components.annotations_form.buttons.collapse\": \"收起\",\n  \"core.components.default_editor.tabs.toc.title\": \"大綱\",\n  \"core.components.default_editor.tabs.toc.empty\": \"暫無大綱\",\n  \"core.components.default_editor.tabs.detail.title\": \"詳情\",\n  \"core.components.default_editor.tabs.detail.fields.character_count\": \"字符數\",\n  \"core.components.default_editor.tabs.detail.fields.word_count\": \"詞數\",\n  \"core.components.default_editor.tabs.detail.fields.publish_time\": \"發布時間\",\n  \"core.components.default_editor.tabs.detail.fields.draft\": \"未發布\",\n  \"core.components.default_editor.tabs.detail.fields.owner\": \"創建者\",\n  \"core.components.default_editor.tabs.detail.fields.permalink\": \"訪問連結\",\n  \"core.components.default_editor.extensions.placeholder.options.placeholder\": \"輸入 / 以選擇輸入類型\",\n  \"core.components.default_editor.toolbox.attachment\": \"選擇附件\",\n  \"core.components.default_editor.toolbox.show_hide_sidebar\": \"顯示 / 隱藏側邊欄\",\n  \"core.components.default_editor.title_placeholder\": \"請輸入標題\",\n  \"core.components.default_editor.cover.options.add\": \"添加封面圖\",\n  \"core.components.default_editor.cover.options.change\": \"更換\",\n  \"core.components.default_editor.cover.options.upload\": \"上傳\",\n  \"core.components.default_editor.cover.options.attachment\": \"從附件庫選擇\",\n  \"core.components.default_editor.cover.toast.upload_failed\": \"上傳封面圖失敗: {message}\",\n  \"core.components.global_search.placeholder\": \"輸入關鍵字以搜尋\",\n  \"core.components.global_search.no_results\": \"沒有搜尋結果\",\n  \"core.components.global_search.buttons.select\": \"選擇\",\n  \"core.components.global_search.groups.console\": \"後臺頁面\",\n  \"core.components.global_search.groups.user\": \"用戶\",\n  \"core.components.global_search.groups.plugin\": \"插件\",\n  \"core.components.global_search.groups.post\": \"文章\",\n  \"core.components.global_search.groups.category\": \"分類\",\n  \"core.components.global_search.groups.tag\": \"標籤\",\n  \"core.components.global_search.groups.page\": \"自定義頁面\",\n  \"core.components.global_search.groups.attachment\": \"附件\",\n  \"core.components.global_search.groups.setting\": \"設置\",\n  \"core.components.global_search.groups.theme_setting\": \"主題設置\",\n  \"core.components.pagination.page_label\": \"頁\",\n  \"core.components.pagination.size_label\": \"條 / 頁\",\n  \"core.components.pagination.total_label\": \"共 {total} 項資料\",\n  \"core.components.app_download_alert.description\": \"Halo 的主題和插件可以在以下地址下載：\",\n  \"core.components.app_download_alert.sources.app_store\": \"官方應用市場：{url}\",\n  \"core.components.app_download_alert.sources.github\": \"GitHub：{url}\",\n  \"core.components.user_avatar.title\": \"頭像\",\n  \"core.components.user_avatar.toast_upload_failed\": \"上傳頭像失敗\",\n  \"core.components.user_avatar.toast_remove_failed\": \"刪除頭像失敗\",\n  \"core.components.user_avatar.cropper_modal.title\": \"裁剪頭像\",\n  \"core.components.user_avatar.remove.title\": \"刪除頭像\",\n  \"core.components.user_avatar.tooltips.upload\": \"上傳\",\n  \"core.components.user_avatar.tooltips.zoom_in\": \"放大\",\n  \"core.components.user_avatar.tooltips.zoom_out\": \"縮小\",\n  \"core.components.user_avatar.tooltips.flip_horizontal\": \"水平翻轉\",\n  \"core.components.user_avatar.tooltips.flip_vertical\": \"垂直翻轉\",\n  \"core.components.user_avatar.tooltips.reset\": \"重置\",\n  \"core.components.editor_provider_selector.tooltips.disallow\": \"內容格式不同，無法切換\",\n  \"core.components.uppy.image_editor.revert\": \"還原\",\n  \"core.components.uppy.image_editor.rotate\": \"旋轉\",\n  \"core.components.uppy.image_editor.zoom_in\": \"放大\",\n  \"core.components.uppy.image_editor.zoom_out\": \"縮小\",\n  \"core.components.uppy.image_editor.flip_horizontal\": \"水平翻轉\",\n  \"core.components.uppy.image_editor.aspect_ratio_square\": \"裁剪為正方形\",\n  \"core.components.uppy.image_editor.aspect_ratio_landscape\": \"裁剪為橫向 (16:9)\",\n  \"core.components.uppy.image_editor.aspect_ratio_portrait\": \"裁剪為縱向 (9:16)\",\n  \"core.components.h2_warning_alert.title\": \"警告：正在使用 H2 資料庫\",\n  \"core.components.h2_warning_alert.description\": \"H2 資料庫僅適用於開發環境和測試環境，不建議在生產環境中使用，H2 非常容易因為操作不當而導致資料檔案損壞。如果必須要使用，請按時進行資料備份。\",\n  \"core.composables.content_cache.toast_recovered\": \"已從緩存中恢復未保存的內容\",\n  \"core.formkit.select.no_data\": \"暫無數據\",\n  \"core.formkit.category_select.creation_label\": \"創建 {text} 分類\",\n  \"core.formkit.tag_select.creation_label\": \"創建 {text} 標籤\",\n  \"core.formkit.validation.trim\": \"不能以空格開頭或結尾\",\n  \"core.formkit.validation.password\": \"密碼只能使用大小寫字母 (A-Z, a-z)、數字 (0-9)，以及以下特殊字符: !{'@'}#$%^&*\",\n  \"core.formkit.verification_form.no_action_defined\": \"未定義{label}介面\",\n  \"core.formkit.verification_form.verify_success\": \"{label}成功\",\n  \"core.formkit.verification_form.verify_failed\": \"{label}失敗\",\n  \"core.formkit.secret.creation_modal.title\": \"創建密鈅\",\n  \"core.formkit.secret.edit_modal.title\": \"編輯密鈅\",\n  \"core.formkit.secret.list_modal.title\": \"選擇密鈅\",\n  \"core.formkit.secret.operations.delete.title\": \"刪除密鈅\",\n  \"core.formkit.secret.operations.delete.description\": \"確定刪除此密鈅嗎？請確保沒有地方正在使用此密鈅，否則需要在具體的地方重新設置\",\n  \"core.formkit.secret.form.fields.description\": \"備註\",\n  \"core.formkit.secret.form.fields.string_data\": \"密鈅數據\",\n  \"core.formkit.code.fullscreen.exit\": \"退出全屏\",\n  \"core.formkit.code.fullscreen.enter\": \"全屏编辑\",\n  \"core.formkit.iconify.placeholder\": \"選擇圖標\",\n  \"core.formkit.iconify.tabs.collections\": \"圖標集\",\n  \"core.formkit.iconify.tabs.search\": \"全局搜索\",\n  \"core.formkit.iconify.tabs.current\": \"當前圖標\",\n  \"core.formkit.iconify.collection_search_placeholder\": \"搜索圖標集合\",\n  \"core.formkit.iconify.search_placeholder\": \"搜索圖標\",\n  \"core.formkit.iconify.no_results\": \"沒有找到結果，請嘗試其他關鍵詞\",\n  \"core.formkit.iconify.option_size\": \"大小\",\n  \"core.formkit.iconify.option_color\": \"顏色\",\n  \"core.formkit.iconify.copyright\": \"圖標數據來自 {url}\",\n  \"core.formkit.iconify.operations.edit\": \"手動編輯圖標\",\n  \"core.formkit.color.placeholder\": \"選擇顏色\",\n  \"core.formkit.color.operations.edit\": \"手動編輯顏色\",\n  \"core.formkit.array.empty_text\": \"沒有條目\",\n  \"core.formkit.array.image_tooltip\": \"查看圖片：{value}\",\n  \"core.formkit.array.item_modal.title\": \"編輯條目\",\n  \"core.formkit.attachment.operations.select\": \"從附件庫選擇\",\n  \"core.formkit.attachment.operations.input\": \"輸入鏈接\",\n  \"core.common.buttons.save\": \"保存\",\n  \"core.common.buttons.close\": \"關閉\",\n  \"core.common.buttons.save_and_continue\": \"保存並繼續添加\",\n  \"core.common.buttons.close_and_shortcut\": \"關閉（Esc）\",\n  \"core.common.buttons.delete\": \"刪除\",\n  \"core.common.buttons.setting\": \"設置\",\n  \"core.common.buttons.confirm\": \"確定\",\n  \"core.common.buttons.cancel\": \"取消\",\n  \"core.common.buttons.cancel_and_shortcut\": \"取消（Esc）\",\n  \"core.common.buttons.new\": \"新建\",\n  \"core.common.buttons.edit\": \"編輯\",\n  \"core.common.buttons.back\": \"返回\",\n  \"core.common.buttons.refresh\": \"刷新\",\n  \"core.common.buttons.publish\": \"發布\",\n  \"core.common.buttons.cancel_publish\": \"取消發布\",\n  \"core.common.buttons.next\": \"下一項\",\n  \"core.common.buttons.previous\": \"上一項\",\n  \"core.common.buttons.install\": \"安裝\",\n  \"core.common.buttons.uninstall\": \"卸載\",\n  \"core.common.buttons.upgrade\": \"升級\",\n  \"core.common.buttons.reset\": \"重置\",\n  \"core.common.buttons.preview\": \"預覽\",\n  \"core.common.buttons.restore\": \"恢復\",\n  \"core.common.buttons.delete_permanently\": \"永久刪除\",\n  \"core.common.buttons.activate\": \"啟用\",\n  \"core.common.buttons.inactivate\": \"停用\",\n  \"core.common.buttons.download\": \"下載\",\n  \"core.common.buttons.copy\": \"複製\",\n  \"core.common.buttons.upload\": \"上傳\",\n  \"core.common.buttons.add\": \"添加\",\n  \"core.common.buttons.submit\": \"提交\",\n  \"core.common.buttons.detail\": \"詳情\",\n  \"core.common.buttons.select\": \"選擇\",\n  \"core.common.buttons.view_all\": \"查看全部\",\n  \"core.common.buttons.modify\": \"修改\",\n  \"core.common.buttons.verify\": \"驗證\",\n  \"core.common.buttons.access\": \"訪問\",\n  \"core.common.buttons.schedule_publish\": \"定時發佈\",\n  \"core.common.buttons.revoke\": \"撤銷\",\n  \"core.common.buttons.disable\": \"禁用\",\n  \"core.common.buttons.enable\": \"启用\",\n  \"core.common.buttons.continue\": \"繼續\",\n  \"core.common.buttons.retry\": \"重試\",\n  \"core.common.buttons.unfold\": \"展開\",\n  \"core.common.buttons.fold\": \"收起\",\n  \"core.common.buttons.remove\": \"移除\",\n  \"core.common.buttons.replace\": \"替換\",\n  \"core.common.buttons.cancel_upload\": \"取消上傳\",\n  \"core.common.buttons.more\": \"更多\",\n  \"core.common.radio.yes\": \"是\",\n  \"core.common.radio.no\": \"否\",\n  \"core.common.select.public\": \"公開\",\n  \"core.common.select.private\": \"私有\",\n  \"core.common.placeholder.search\": \"輸入關鍵字搜尋\",\n  \"core.common.toast.operation_success\": \"操作成功\",\n  \"core.common.toast.delete_success\": \"刪除成功\",\n  \"core.common.toast.save_success\": \"保存成功\",\n  \"core.common.toast.publish_success\": \"發布成功\",\n  \"core.common.toast.cancel_publish_success\": \"取消發布成功\",\n  \"core.common.toast.recovery_success\": \"恢復成功\",\n  \"core.common.toast.uninstall_success\": \"卸載成功\",\n  \"core.common.toast.active_success\": \"啟用成功\",\n  \"core.common.toast.inactive_success\": \"停用成功\",\n  \"core.common.toast.upgrade_success\": \"升級成功\",\n  \"core.common.toast.install_success\": \"安裝成功\",\n  \"core.common.toast.download_success\": \"下載成功\",\n  \"core.common.toast.copy_success\": \"複製成功\",\n  \"core.common.toast.operation_failed\": \"操作失敗\",\n  \"core.common.toast.download_failed\": \"下載失敗\",\n  \"core.common.toast.save_failed_and_retry\": \"保存失敗，請重試\",\n  \"core.common.toast.publish_failed_and_retry\": \"發布失敗，請重試\",\n  \"core.common.toast.network_error\": \"網絡錯誤，請檢查網絡連接\",\n  \"core.common.toast.login_expired\": \"登入已過期，請重新登入\",\n  \"core.common.toast.forbidden\": \"無權限訪問\",\n  \"core.common.toast.not_found\": \"資源不存在\",\n  \"core.common.toast.server_internal_error\": \"伺服器內部錯誤\",\n  \"core.common.toast.unknown_error\": \"未知錯誤\",\n  \"core.common.toast.disable_success\": \"禁用成功\",\n  \"core.common.toast.enable_success\": \"啟用成功\",\n  \"core.common.dialog.titles.tip\": \"提示\",\n  \"core.common.dialog.titles.warning\": \"警告\",\n  \"core.common.dialog.titles.login_expired\": \"登入已過期\",\n  \"core.common.dialog.descriptions.cannot_be_recovered\": \"該操作不可恢復。\",\n  \"core.common.dialog.descriptions.editor_not_found\": \"未找到符合 {raw_type} 格式的編輯器，請檢查是否已安裝編輯器插件。\",\n  \"core.common.dialog.descriptions.login_expired\": \"當前登入已過期，點擊確定跳轉到登入頁面，請確保當前內容已保存，你可以點擊取消之後手動複製未保存的內容。\",\n  \"core.common.filters.results.keyword\": \"關鍵字：{keyword}\",\n  \"core.common.filters.results.sort\": \"排序：{sort}\",\n  \"core.common.filters.results.status\": \"狀態：{status}\",\n  \"core.common.filters.labels.sort\": \"排序\",\n  \"core.common.filters.labels.status\": \"狀態\",\n  \"core.common.filters.item_labels.all\": \"全部\",\n  \"core.common.filters.item_labels.default\": \"預設\",\n  \"core.common.status.deleting\": \"刪除中\",\n  \"core.common.status.loading\": \"加載中\",\n  \"core.common.status.loading_error\": \"加載異常\",\n  \"core.common.status.activated\": \"已啟用\",\n  \"core.common.status.not_activated\": \"未啟用\",\n  \"core.common.status.installed\": \"已安裝\",\n  \"core.common.status.not_installed\": \"未安裝\",\n  \"core.common.status.starting_up\": \"启动中\",\n  \"core.common.text.none\": \"無\",\n  \"core.common.text.tip\": \"提示\",\n  \"core.common.text.warning\": \"警告\",\n  \"core.common.text.system_protection\": \"系統保留\",\n  \"core.common.text.all\": \"全部\",\n  \"core.common.text.detail\": \"詳情\",\n  \"core.common.tooltips.unpublished_content_tip\": \"當前有內容已保存，但還未發布。\",\n  \"core.common.tooltips.publishing\": \"發布中\",\n  \"core.common.tooltips.recovering\": \"還原中\",\n  \"core.common.fields.post_count\": \"{count} 篇文章\",\n  \"core.common.form.validation.slug_unique\": \"當前別名已存在\",\n  \"core.uc_post.creation_modal.title\": \"創建文章\",\n  \"core.uc_post.operations.cancel_publish.description\": \"確定要取消發布嗎？\",\n  \"core.uc_post.operations.cancel_publish.title\": \"取消發布\",\n  \"core.uc_post.operations.delete.title\": \"刪除文章\",\n  \"core.uc_post.operations.delete.description\": \"該操作會將文章放入回收站，後續由網站管理員進行管理。\",\n  \"core.uc_post.publish_modal.title\": \"發布文章\",\n  \"core.uc_post.setting_modal.title\": \"文章設定\",\n  \"core.uc_post.title\": \"我的文章\",\n  \"core.tool.title\": \"工具\",\n  \"core.tool.empty.title\": \"沒有可用工具\",\n  \"core.tool.empty.message\": \"目前沒有可用的工具，系統工具可能由外掛提供\",\n  \"core.snapshots.operations.revert.button\": \"恢復\",\n  \"core.snapshots.operations.revert.title\": \"恢復快照\",\n  \"core.snapshots.operations.revert.description\": \"確定要恢復該快照嗎？此操作將根據這個快照創建一個新的快照並發佈。\",\n  \"core.snapshots.operations.revert.toast_success\": \"恢復成功\",\n  \"core.snapshots.operations.delete.title\": \"刪除快照\",\n  \"core.snapshots.operations.delete.description\": \"確定要刪除該快照嗎？此操作無法恢復。\",\n  \"core.snapshots.operations.cleanup.button\": \"清理\",\n  \"core.snapshots.operations.cleanup.title\": \"清理快照\",\n  \"core.snapshots.operations.cleanup.description\": \"確定要清理所有不再使用的快照嗎？只會保留已發佈、基礎和草稿版本。\",\n  \"core.snapshots.operations.cleanup.toast_empty\": \"沒有需要清理的快照\",\n  \"core.snapshots.operations.cleanup.toast_success\": \"清理完成\",\n  \"core.snapshots.status.released\": \"已發布\",\n  \"core.snapshots.status.draft\": \"草稿\",\n  \"core.snapshots.status.base\": \"基礎\",\n  \"core.snapshots.diff_mode.toggle.enable\": \"開啟對比模式\",\n  \"core.snapshots.diff_mode.toggle.disable\": \"關閉對比模式\",\n  \"core.snapshots.diff_mode.title\": \"對比模式\",\n  \"core.snapshots.diff_mode.show_diff_only\": \"只顯示差異\",\n  \"core.snapshots.diff_mode.sync_scroll\": \"同步滾動\",\n  \"core.snapshots.diff_mode.select_two_tip\": \"請選擇兩個版本進行對比\",\n  \"core.snapshots.diff_mode.old_version\": \"前一個版本（舊）\",\n  \"core.snapshots.diff_mode.new_version\": \"所選第一個版本（新）\",\n  \"core.snapshots.diff_mode.diff\": \"差異\",\n  \"core.snapshots.diff_mode.legend.removed\": \"該行代表被刪除\",\n  \"core.snapshots.diff_mode.legend.added\": \"該行代表被添加\",\n  \"core.snapshots.diff_mode.legend.modified\": \"該行代表被修改\",\n  \"core.post_snapshots.title\": \"文章版本歷史\",\n  \"core.page_snapshots.title\": \"頁面版本歷史\",\n  \"core.formkit.secret.no_permission\": \"無權限查看密鈅\",\n  \"core.formkit.secret.no_selected\": \"當前沒有選擇密鈅，你可以新建或者選擇一個已有的密鈅\",\n  \"core.formkit.secret.missing_keys\": \"所選密鈅還缺少字段：{keys}\",\n  \"core.formkit.secret.includes_keys\": \"包含字段：{keys}\",\n  \"core.formkit.secret.no_fields\": \"沒有字段\",\n  \"core.formkit.secret.required_keys_tip\": \"當前表單項所需的密鈅字段包括：\",\n  \"core.dashboard.widgets.presets.upvotes_stats.title\": \"點讚\",\n  \"core.plugin.detail.operations.copy_error_message.button\": \"複製錯誤訊息\",\n  \"core.plugin.detail.operations.copy_error_message.copied\": \"已複製\"\n}\n"
  },
  {
    "path": "ui/src/router/process-bar.ts",
    "content": "import nprogress from \"nprogress\";\nimport \"nprogress/nprogress.css\";\nimport type { Router } from \"vue-router\";\n\nnprogress.configure({\n  showSpinner: false,\n});\n\nlet progressTimer: ReturnType<typeof setTimeout> | null = null;\n\nexport function setupProcessBarGuard(router: Router) {\n  router.beforeEach((_to, _from, next) => {\n    progressTimer = setTimeout(() => {\n      nprogress.start();\n      progressTimer = null;\n    }, 200);\n    next();\n  });\n  router.afterEach(() => {\n    if (progressTimer) {\n      clearTimeout(progressTimer);\n      progressTimer = null;\n    } else {\n      nprogress.done();\n    }\n  });\n}\n"
  },
  {
    "path": "ui/src/setup/setupApiClient.ts",
    "content": "import { axiosInstance } from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport type { AxiosError } from \"axios\";\nimport objectHash from \"object-hash\";\nimport { h } from \"vue\";\nimport { i18n } from \"@/locales\";\nimport { createHTMLContentModal } from \"@/utils/modal\";\n\nexport interface ProblemDetail {\n  detail: string;\n  instance: string;\n  status: number;\n  title: string;\n  type?: string;\n}\n\nexport function setupApiClient() {\n  axiosInstance.interceptors.response.use(\n    (response) => {\n      return response;\n    },\n    async (error: AxiosError) => {\n      if (error.code === \"ERR_CANCELED\") {\n        return Promise.reject(error);\n      }\n\n      if (/Network Error/.test(error.message)) {\n        // @ts-ignore\n        Toast.error(i18n.global.t(\"core.common.toast.network_error\"));\n        return Promise.reject(error);\n      }\n\n      const errorResponse = error.response;\n\n      if (!errorResponse) {\n        Toast.error(i18n.global.t(\"core.common.toast.network_error\"));\n        return Promise.reject(error);\n      }\n\n      // Don't show error toast\n      // see https://github.com/halo-dev/halo/issues/2836\n      if (errorResponse.config.mute) {\n        return Promise.reject(error);\n      }\n\n      const { status } = errorResponse;\n      const { title, detail } = errorResponse.data as ProblemDetail;\n\n      if (status === 401) {\n        Dialog.warning({\n          title: i18n.global.t(\"core.common.dialog.titles.login_expired\"),\n          description: i18n.global.t(\n            \"core.common.dialog.descriptions.login_expired\"\n          ),\n          confirmType: \"secondary\",\n          confirmText: i18n.global.t(\"core.common.buttons.confirm\"),\n          cancelText: i18n.global.t(\"core.common.buttons.cancel\"),\n          uniqueId: \"login_expired\",\n          onConfirm: () => {\n            const currentPath = `${location.pathname}${location.search}`;\n            location.href = `/login?redirect_uri=${encodeURIComponent(\n              currentPath\n            )}`;\n          },\n        });\n\n        return Promise.reject(error);\n      }\n\n      // Catch error requests where the response is text/html,\n      // which usually comes from a reverse proxy or WAF\n\n      const contentType = error.response?.headers?.[\"content-type\"];\n\n      if (contentType?.toLowerCase().includes(\"text/html\")) {\n        createHTMLContentModal({\n          uniqueId: objectHash(error.response?.data || \"\"),\n          title: error.response?.status.toString(),\n          width: 700,\n          height: \"calc(100vh - 20px)\",\n          centered: true,\n          content: h(\"iframe\", {\n            srcdoc: error.response?.data?.toString(),\n            sandbox: \"\",\n            referrerpolicy: \"no-referrer\",\n            loading: \"lazy\",\n            style: {\n              width: \"100%\",\n              height: \"100%\",\n            },\n          }),\n        });\n\n        return Promise.reject(error);\n      }\n\n      if (title || detail) {\n        Toast.error(detail || title);\n        return Promise.reject(error);\n      }\n\n      // Final fallback\n      if (errorResponse.status) {\n        const { status, statusText } = errorResponse;\n        Toast.error([status, statusText].filter(Boolean).join(\": \"));\n        return Promise.reject(error);\n      }\n\n      Toast.error(i18n.global.t(\"core.common.toast.unknown_error\"));\n\n      return Promise.reject(error);\n    }\n  );\n}\n"
  },
  {
    "path": "ui/src/setup/setupComponents.ts",
    "content": "import { defaultConfig, plugin as FormKit } from \"@formkit/vue\";\nimport { vClosePopper, VLoading, vTooltip } from \"@halo-dev/components\";\nimport { defineAsyncComponent, type App } from \"vue\";\nimport VueGridLayout from \"vue-grid-layout\";\nimport FilterCleanButton from \"@/components/filter/FilterCleanButton.vue\";\nimport FilterDropdown from \"@/components/filter/FilterDropdown.vue\";\nimport AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport SearchInput from \"@/components/input/SearchInput.vue\";\nimport HasPermission from \"@/components/permission/HasPermission.vue\";\nimport FormKitConfig from \"@/formkit/formkit.config\";\n\nexport function setupComponents(app: App) {\n  app.use(VueGridLayout);\n  app.use(\n    FormKit,\n    defaultConfig({\n      ...FormKitConfig,\n    })\n  );\n\n  app.directive(\"tooltip\", vTooltip);\n  app.directive(\"close-popper\", vClosePopper);\n  app.component(\n    \"VCodemirror\",\n    defineAsyncComponent(() => import(\"@/components/codemirror/Codemirror.vue\"))\n  );\n\n  // Console components\n  app.component(\"FilterDropdown\", FilterDropdown);\n  app.component(\"FilterCleanButton\", FilterCleanButton);\n  app.component(\"SearchInput\", SearchInput);\n  app.component(\"AnnotationsForm\", AnnotationsForm);\n  app.component(\n    \"AttachmentFileTypeIcon\",\n    defineAsyncComponent(\n      () => import(\"@/components/icon/AttachmentFileTypeIcon.vue\")\n    )\n  );\n  app.component(\"HasPermission\", HasPermission);\n  app.component(\n    \"UppyUpload\",\n    defineAsyncComponent({\n      loader: () => import(\"@/components/upload/UppyUpload.vue\"),\n      loadingComponent: VLoading,\n    })\n  );\n}\n\ndeclare module \"vue\" {\n  interface GlobalComponents {\n    VCodemirror: (typeof import(\"@/components/codemirror/Codemirror.vue\"))[\"default\"];\n    FilterDropdown: (typeof import(\"@/components/filter/FilterDropdown.vue\"))[\"default\"];\n    FilterCleanButton: (typeof import(\"@/components/filter/FilterCleanButton.vue\"))[\"default\"];\n    SearchInput: (typeof import(\"@/components/input/SearchInput.vue\"))[\"default\"];\n    AnnotationsForm: (typeof import(\"@/components/form/AnnotationsForm.vue\"))[\"default\"];\n    AttachmentFileTypeIcon: (typeof import(\"@/components/icon/AttachmentFileTypeIcon.vue\"))[\"default\"];\n    HasPermission: (typeof import(\"@/components/permission/HasPermission.vue\"))[\"default\"];\n    UppyUpload: (typeof import(\"@/components/upload/UppyUpload.vue\"))[\"default\"];\n  }\n}\n"
  },
  {
    "path": "ui/src/setup/setupModules.ts",
    "content": "import { Toast } from \"@halo-dev/components\";\nimport type { PluginModule, RouteRecordAppend } from \"@halo-dev/ui-shared\";\nimport { useScriptTag } from \"@vueuse/core\";\nimport type { App } from \"vue\";\nimport type { Router, RouteRecordRaw } from \"vue-router\";\nimport { i18n } from \"@/locales\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport { loadStyle } from \"@/utils/load-style\";\n\nexport type Platform = \"console\" | \"uc\";\n\nexport function setupCoreModules({\n  app,\n  router,\n  platform,\n  modules,\n}: {\n  app: App;\n  router: Router;\n  platform: Platform;\n  modules: Record<string, PluginModule>;\n}) {\n  for (const [key, module] of Object.entries(modules)) {\n    initJsModule({\n      app,\n      router,\n      platform,\n      name: key,\n      jsModule: module,\n      core: true,\n    });\n  }\n}\n\nexport async function setupPluginModules({\n  app,\n  router,\n  platform,\n}: {\n  app: App;\n  router: Router;\n  platform: Platform;\n}) {\n  try {\n    await loadPluginBundle();\n\n    const enabledPlugins = window[\"enabledPlugins\"] as {\n      name: string;\n      value: string;\n    }[];\n\n    for (const plugin of enabledPlugins || []) {\n      const module = window[plugin.name];\n      if (!module) {\n        continue;\n      }\n      initJsModule({\n        app,\n        router,\n        platform,\n        name: plugin.name,\n        jsModule: module,\n        core: false,\n      });\n    }\n\n    await loadPluginStyles();\n  } catch (error) {\n    const message =\n      error instanceof Error && error.message.includes(\"style\")\n        ? i18n.global.t(\"core.plugin.loader.toast.style_load_failed\")\n        : i18n.global.t(\"core.plugin.loader.toast.entry_load_failed\");\n\n    console.error(message, error);\n    Toast.error(message);\n  }\n}\n\nasync function loadPluginBundle() {\n  const { load } = useScriptTag(\n    `/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js?t=${Date.now()}`\n  );\n  await load();\n}\n\nasync function loadPluginStyles() {\n  await loadStyle(\n    `/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css?t=${Date.now()}`\n  );\n}\n\nfunction initJsModule(options: {\n  app: App;\n  router: Router;\n  platform: Platform;\n  name: string;\n  jsModule: PluginModule;\n  core: boolean;\n}) {\n  const { app, router, platform, name, jsModule, core } = options;\n\n  const pluginModuleStore = usePluginModuleStore();\n\n  pluginModuleStore.registerPluginModule(name, jsModule);\n\n  if (jsModule.components) {\n    for (const [key, component] of Object.entries(jsModule.components)) {\n      if (component) {\n        app.component(key, component);\n      }\n    }\n  }\n\n  let routes: RouteRecordRaw[] | RouteRecordAppend[] | undefined;\n\n  switch (platform) {\n    case \"console\":\n      routes = jsModule.routes;\n      break;\n    case \"uc\":\n      routes = jsModule.ucRoutes;\n      break;\n    default:\n      throw new Error(`Invalid platform: ${platform}`);\n  }\n\n  if (!routes) {\n    return;\n  }\n\n  if (!Array.isArray(routes)) {\n    return;\n  }\n\n  resetRouteMeta(routes);\n\n  for (const route of routes) {\n    if (\"parentName\" in route) {\n      const parentRoute = router\n        .getRoutes()\n        .find((item) => item.name === route.parentName);\n      if (parentRoute) {\n        router.removeRoute(route.parentName);\n        parentRoute.children = [...parentRoute.children, route.route];\n        router.addRoute(parentRoute);\n      }\n    } else {\n      router.addRoute(route);\n    }\n  }\n\n  function resetRouteMeta(routes: RouteRecordRaw[] | RouteRecordAppend[]) {\n    for (const route of routes) {\n      if (\"parentName\" in route) {\n        if (route.route.meta?.menu) {\n          route.route.meta = {\n            ...route.route.meta,\n            core,\n          };\n        }\n        if (route.route.children) {\n          resetRouteMeta(route.route.children);\n        }\n      } else {\n        if (route.meta?.menu) {\n          route.meta = {\n            ...route.meta,\n            core,\n          };\n        }\n        if (route.children) {\n          resetRouteMeta(route.children);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ui/src/setup/setupStyles.ts",
    "content": "import \"@halo-dev/richtext-editor/dist/style.css\";\nimport \"@halo-dev/components/dist/style.css\";\nimport \"@/styles/tailwind.css\";\nimport \"@/styles/index.css\";\nimport \"overlayscrollbars/overlayscrollbars.css\";\n"
  },
  {
    "path": "ui/src/setup/setupUserPermissions.ts",
    "content": "import { consoleApiClient } from \"@halo-dev/api-client\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport type { App, DirectiveBinding } from \"vue\";\nimport { useRoleStore } from \"@/stores/role\";\n\nexport async function setupUserPermissions(app: App) {\n  const { data: currentPermissions } =\n    await consoleApiClient.user.getPermissions({\n      name: \"-\",\n    });\n  const roleStore = useRoleStore();\n  roleStore.$patch({\n    permissions: currentPermissions,\n  });\n\n  // Set permissions in shared utils\n  utils.permission.setUserPermissions(currentPermissions.uiPermissions);\n\n  app.directive(\n    \"permission\",\n    (el: HTMLElement, binding: DirectiveBinding<string[]>) => {\n      const { value } = binding;\n      const { any } = binding.modifiers;\n\n      if (utils.permission.has(value, any ?? false)) {\n        return;\n      }\n\n      el?.remove?.();\n    }\n  );\n}\n"
  },
  {
    "path": "ui/src/setup/setupVueQuery.ts",
    "content": "import {\n  VueQueryPlugin,\n  type VueQueryPluginOptions,\n} from \"@tanstack/vue-query\";\nimport type { App } from \"vue\";\n\nconst options: VueQueryPluginOptions = {\n  queryClientConfig: {\n    defaultOptions: {\n      queries: {\n        refetchOnWindowFocus: false,\n        networkMode: \"always\",\n      },\n      mutations: {\n        networkMode: \"always\",\n      },\n    },\n  },\n};\n\nexport function setupVueQuery(app: App) {\n  app.use(VueQueryPlugin, options);\n}\n"
  },
  {
    "path": "ui/src/stores/plugin.ts",
    "content": "import type { PluginModule } from \"@halo-dev/ui-shared\";\nimport { defineStore } from \"pinia\";\nimport { computed, shallowRef } from \"vue\";\n\nexport const usePluginModuleStore = defineStore(\"plugin\", () => {\n  const pluginModuleMap = shallowRef<Record<string, PluginModule>>({});\n\n  function registerPluginModule(name: string, pluginModule: PluginModule) {\n    pluginModuleMap.value = {\n      ...pluginModuleMap.value,\n      [name]: pluginModule,\n    };\n  }\n\n  const pluginModules = computed(() => {\n    return Object.values(pluginModuleMap.value);\n  });\n\n  return { pluginModuleMap, pluginModules, registerPluginModule };\n});\n"
  },
  {
    "path": "ui/src/stores/role.ts",
    "content": "import type { UserPermission } from \"@halo-dev/api-client\";\nimport { defineStore } from \"pinia\";\nimport { ref } from \"vue\";\n\nexport const useRoleStore = defineStore(\"role\", () => {\n  const permissions = ref<UserPermission>({\n    roles: [],\n    permissions: [],\n    uiPermissions: [],\n  });\n\n  return { permissions };\n});\n"
  },
  {
    "path": "ui/src/styles/index.css",
    "content": ".fade-enter-active {\n  @apply duration-200 ease-out;\n}\n.fade-enter-from {\n  @apply opacity-0;\n}\n.fade-enter-to {\n  @apply opacity-100;\n}\n.fade-leave-active {\n  @apply duration-200 ease-in;\n}\n.fade-leave-from {\n  @apply opacity-100;\n}\n.fade-leave-to {\n  @apply opacity-0;\n}\n\n/* https://learn.microsoft.com/en-us/microsoft-edge/web-platform/password-reveal#remove-the-password-reveal-control */\n::-ms-reveal {\n  display: none;\n}\n"
  },
  {
    "path": "ui/src/styles/tailwind.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* override @tailwindcss/forms styles */\ninput[type=\"text\"],\ninput[type=\"password\"],\ninput[type=\"email\"],\ninput[type=\"number\"],\ninput[type=\"url\"],\ninput[type=\"date\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"],\ninput[type=\"week\"],\ninput[type=\"time\"],\ninput[type=\"search\"],\ninput[type=\"tel\"],\ninput:where(:not([type])),\nselect,\ntextarea {\n  @apply border-none py-0 focus:outline-0 focus:ring-0;\n}\n\ninput[type=\"checkbox\"] {\n  @apply rounded-sm border-gray-500 disabled:cursor-not-allowed disabled:border-gray-400 disabled:opacity-50 disabled:checked:border-none;\n}\n"
  },
  {
    "path": "ui/src/utils/__tests__/media-type.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { matchMediaType, matchMediaTypes } from \"../media-type\";\n\ndescribe(\"matchMediaType\", () => {\n  it('should match all image types for \"image/*\"', () => {\n    expect(matchMediaType(\"image/png\", \"image/*\")).toBe(true);\n    expect(matchMediaType(\"image/jpeg\", \"image/*\")).toBe(true);\n    expect(matchMediaType(\"image/gif\", \"image/*\")).toBe(true);\n    expect(matchMediaType(\"image/webp\", \"image/*\")).toBe(true);\n  });\n\n  it('should only match image/png for \"image/png\"', () => {\n    expect(matchMediaType(\"image/png\", \"image/png\")).toBe(true);\n    expect(matchMediaType(\"image/jpeg\", \"image/png\")).toBe(false);\n    expect(matchMediaType(\"image/gif\", \"image/png\")).toBe(false);\n    expect(matchMediaType(\"image/webp\", \"image/png\")).toBe(false);\n  });\n\n  it('should match any type for \"*/*\"', () => {\n    expect(matchMediaType(\"image/png\", \"*/*\")).toBe(true);\n    expect(matchMediaType(\"application/json\", \"*/*\")).toBe(true);\n    expect(matchMediaType(\"video/mp4\", \"*/*\")).toBe(true);\n  });\n\n  it(\"should not match if type does not match accept\", () => {\n    expect(matchMediaType(\"image/png\", \"video/*\")).toBe(false);\n    expect(matchMediaType(\"video/mp4\", \"image/*\")).toBe(false);\n  });\n\n  it(\"should match with case-insensitive comparison\", () => {\n    expect(matchMediaType(\"image/png\", \"IMAGE/*\")).toBe(true);\n    expect(matchMediaType(\"application/json\", \"APPLICATION/*\")).toBe(true);\n  });\n});\n\ndescribe(\"matchMediaTypes\", () => {\n  it(\"multi accepts\", () => {\n    expect(matchMediaTypes(\"image/png\", [\"image/*\", \"video/*\"])).toBe(true);\n    expect(matchMediaTypes(\"image/jpg\", [\"image/jpg\", \"video/*\"])).toBe(true);\n    expect(matchMediaTypes(\"image/png\", [\"video/mp4\", \"application/*\"])).toBe(\n      false\n    );\n  });\n});\n"
  },
  {
    "path": "ui/src/utils/cookie.ts",
    "content": "export function getCookie(name: string) {\n  const match = document.cookie.match(new RegExp(\"(^| )\" + name + \"=([^;]+)\"));\n  return match ? match[2] : null;\n}\n"
  },
  {
    "path": "ui/src/utils/device.ts",
    "content": "export const isMac = /macintosh|mac os x/i.test(navigator.userAgent);\n"
  },
  {
    "path": "ui/src/utils/image.ts",
    "content": "export const imageTypes: string[] = [\n  \"image/jpeg\",\n  \"image/jpg\",\n  \"image/png\",\n  \"image/gif\",\n  \"image/webp\",\n  \"image/svg+xml\",\n  \"image/avif\",\n];\n\nexport function isImage(mediaType: string | undefined | null): boolean {\n  if (!mediaType) {\n    return false;\n  }\n  return imageTypes.includes(mediaType);\n}\n"
  },
  {
    "path": "ui/src/utils/load-style.ts",
    "content": "export function loadStyle(href: string) {\n  return new Promise(function (resolve, reject) {\n    let shouldAppend = false;\n    let el: HTMLLinkElement | null = document.querySelector(\n      'link[href=\"' + href + '\"]'\n    );\n    if (!el) {\n      el = document.createElement(\"link\");\n      el.rel = \"stylesheet\";\n      el.type = \"text/css\";\n      el.href = href;\n      shouldAppend = true;\n    } else if (el.hasAttribute(\"data-loaded\")) {\n      resolve(el);\n      return;\n    }\n\n    el.addEventListener(\"error\", reject);\n    el.addEventListener(\"abort\", reject);\n    el.addEventListener(\"load\", function loadStyleHandler() {\n      el?.setAttribute(\"data-loaded\", \"true\");\n      resolve(el);\n    });\n\n    if (shouldAppend) document.head.prepend(el);\n  });\n}\n"
  },
  {
    "path": "ui/src/utils/media-type.ts",
    "content": "export function matchMediaType(mediaType: string, accept: string) {\n  const regex = new RegExp(accept.toLowerCase().replace(/\\*/g, \".*\"));\n\n  return regex.test(mediaType);\n}\n\nexport function matchMediaTypes(mediaType: string, accepts: string[]) {\n  return accepts.some((accept) => matchMediaType(mediaType, accept));\n}\n"
  },
  {
    "path": "ui/src/utils/modal.ts",
    "content": "import { VButton, VModal } from \"@halo-dev/components\";\nimport { type Component, createApp, h } from \"vue\";\nimport { i18n } from \"@/locales\";\n\ninterface ModalOptions {\n  uniqueId?: string;\n  title?: string;\n  width?: number;\n  height?: string;\n  centered?: boolean;\n  content: Component;\n}\n\nexport function createHTMLContentModal(options: ModalOptions) {\n  if (options.uniqueId) {\n    const existingModal = document.getElementById(`modal-${options.uniqueId}`);\n    if (existingModal) {\n      return;\n    }\n  }\n\n  const container = document.createElement(\"div\");\n  if (options.uniqueId) {\n    container.id = `modal-${options.uniqueId}`;\n  }\n\n  document.body.appendChild(container);\n\n  const app = createApp({\n    setup() {\n      const handleClose = () => {\n        app.unmount();\n        container.remove();\n      };\n\n      return () =>\n        h(\n          VModal,\n          {\n            title: options.title,\n            width: options.width || 500,\n            height: options.height,\n            centered: options.centered ?? true,\n            onClose: handleClose,\n            \"onUpdate:visible\": (value: boolean) => {\n              if (!value) handleClose();\n            },\n          },\n          {\n            default: () => options.content,\n            footer: () =>\n              h(\n                VButton,\n                {\n                  onClick: handleClose,\n                },\n                {\n                  default: () =>\n                    h(\"div\", i18n.global.t(\"core.common.buttons.close\")),\n                }\n              ),\n          }\n        );\n    },\n  });\n\n  app.mount(container);\n\n  return {\n    close: () => {\n      app.unmount();\n      container.remove();\n    },\n  };\n}\n"
  },
  {
    "path": "ui/src/utils/role.ts",
    "content": "import type { Role } from \"@halo-dev/api-client\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\n\nexport function resolveDeepDependencies(\n  role: Role,\n  roleTemplates: Role[]\n): string[] {\n  if (!role) {\n    return [];\n  }\n\n  const result: string[] = [];\n\n  const dependencies: string[] = JSON.parse(\n    role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] || \"[]\"\n  );\n\n  dependencies.forEach((depName) => {\n    result.push(depName);\n    const dep = roleTemplates.find((item) => item.metadata.name === depName);\n\n    if (!dep) {\n      return;\n    }\n\n    resolveDeepDependencies(dep, roleTemplates).forEach((nextDep) =>\n      result.push(nextDep)\n    );\n  });\n\n  return result;\n}\n"
  },
  {
    "path": "ui/src/views/exceptions/Forbidden.vue",
    "content": "<script setup lang=\"ts\">\nimport Exception from \"./components/Exception.vue\";\n</script>\n<template>\n  <Exception code=\"403\" :message=\"$t('core.exception.forbidden.message')\" />\n</template>\n"
  },
  {
    "path": "ui/src/views/exceptions/NotFound.vue",
    "content": "<script setup lang=\"ts\">\nimport Exception from \"./components/Exception.vue\";\n</script>\n<template>\n  <Exception code=\"404\" :message=\"$t('core.exception.not_found.message')\" />\n</template>\n"
  },
  {
    "path": "ui/src/views/exceptions/__tests__/NotFound.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\ndescribe(\"NotFound\", () => {\n  it(\"should render\", () => {\n    expect(true).toBe(true);\n  });\n});\n"
  },
  {
    "path": "ui/src/views/exceptions/components/Exception.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton, VSpace } from \"@halo-dev/components\";\nimport { useRouter } from \"vue-router\";\nwithDefaults(\n  defineProps<{\n    code: number | string;\n    title?: string;\n    message?: string;\n  }>(),\n  {\n    title: undefined,\n    message: undefined,\n  }\n);\n\nconst router = useRouter();\n</script>\n\n<template>\n  <div class=\"grid h-screen place-content-center bg-white px-4\">\n    <div class=\"text-center\">\n      <h1 class=\"text-8xl font-black text-gray-200\">\n        {{ code }}\n      </h1>\n\n      <p class=\"text-2xl font-bold tracking-tight text-gray-900 sm:text-4xl\">\n        {{ title }}\n      </p>\n\n      <p class=\"mt-4 text-gray-500\">{{ message }}</p>\n\n      <div class=\"mt-4\">\n        <VSpace>\n          <VButton @click=\"router.back()\">\n            {{ $t(\"core.common.buttons.back\") }}\n          </VButton>\n          <VButton type=\"secondary\" :route=\"{ path: '/' }\">\n            {{ $t(\"core.exception.actions.home\") }}\n          </VButton>\n        </VSpace>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/src/vite/library-external.ts",
    "content": "import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { HtmlTagDescriptor, Plugin } from \"vite\";\nimport { viteExternalsPlugin as ViteExternals } from \"vite-plugin-externals\";\nimport {\n  viteStaticCopy as ViteStaticCopy,\n  type Target,\n} from \"vite-plugin-static-copy\";\n\n/**\n * It copies the external libraries to the `assets` folder, and injects the script tags into the HTML\n *\n * @param {string} mode\n * @returns An array of plugins\n */\nexport const setupLibraryExternal = (mode: string) => {\n  // Vitest mode doesn't need to setup library external.\n  if (mode === \"test\") {\n    return [];\n  }\n\n  const isProduction = mode === \"production\";\n\n  const staticTargets: Target[] = [\n    {\n      src: `./node_modules/vue/dist/vue.global${\n        isProduction ? \".prod\" : \"\"\n      }.js`,\n      dest: \"vue\",\n      rename: `vue.[hash].js`,\n    },\n    {\n      src: `./node_modules/vue-router/dist/vue-router.global${\n        isProduction ? \".prod\" : \"\"\n      }.js`,\n      dest: \"vue-router\",\n      rename: `vue-router.[hash].js`,\n    },\n    {\n      src: `./node_modules/pinia/dist/pinia.iife.prod.js`,\n      dest: \"pinia\",\n      rename: `pinia.[hash].js`,\n    },\n    {\n      src: \"./node_modules/axios/dist/axios.min.js\",\n      dest: \"axios\",\n      rename: `axios.[hash].js`,\n    },\n    {\n      src: `./node_modules/vue-demi/lib/index.iife.js`,\n      dest: \"vue-demi\",\n      rename: `vue-demi.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@vueuse/shared/index.iife.min.js\",\n      dest: \"vueuse\",\n      rename: `vueuse.shared.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@vueuse/core/index.iife.min.js\",\n      dest: \"vueuse\",\n      rename: `vueuse.core.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@vueuse/components/index.iife.min.js\",\n      dest: \"vueuse\",\n      rename: `vueuse.components.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@vueuse/router/index.iife.min.js\",\n      dest: \"vueuse\",\n      rename: `vueuse.router.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@halo-dev/components/dist/index.iife.js\",\n      dest: \"components\",\n      rename: `components.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@halo-dev/api-client/dist/index.iife.js\",\n      dest: \"api-client\",\n      rename: `api-client.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@halo-dev/ui-shared/dist/index.iife.js\",\n      dest: \"ui-shared\",\n      rename: `ui-shared.[hash].js`,\n    },\n    // TODO: Remove this in the future, only for compatibility.\n    {\n      src: \"./node_modules/@halo-dev/console-shared/index.js\",\n      dest: \"console-shared\",\n      rename: `console-shared.[hash].js`,\n    },\n    {\n      src: \"./node_modules/@halo-dev/richtext-editor/dist/index.iife.js\",\n      dest: \"editor\",\n      rename: `editor.[hash].js`,\n    },\n  ].map((target) => {\n    return {\n      ...target,\n      dest: `ui-assets/${target.dest}`,\n      rename: `${target.rename.replace(\n        \"[hash]\",\n        computeLibraryHash(target.src)\n      )}`,\n    };\n  });\n\n  const injectTags = staticTargets\n    .map((target) => {\n      return {\n        injectTo: \"head\",\n        tag: \"script\",\n        attrs: {\n          src: `/${target.dest}/${target.rename}`,\n          type: \"text/javascript\",\n          \"vite-ignore\": true,\n          ...(!isProduction ? { crossorigin: \"\" } : {}),\n        },\n      };\n    })\n    .filter(Boolean) as HtmlTagDescriptor[];\n\n  return [\n    ViteExternals({\n      vue: \"Vue\",\n      \"vue-router\": \"VueRouter\",\n      pinia: \"Pinia\",\n      axios: \"axios\",\n      \"@halo-dev/ui-shared\": \"HaloUiShared\",\n      \"@halo-dev/components\": \"HaloComponents\",\n      \"@vueuse/core\": \"VueUse\",\n      \"@vueuse/components\": \"VueUse\",\n      \"@vueuse/router\": \"VueUse\",\n      \"vue-demi\": \"VueDemi\",\n      \"@halo-dev/richtext-editor\": \"RichTextEditor\",\n      \"@halo-dev/api-client\": \"HaloApiClient\",\n    }),\n    ViteStaticCopy({\n      targets: staticTargets,\n    }),\n    createInjectExternalTagsPlugin(injectTags),\n  ];\n};\n\nfunction createInjectExternalTagsPlugin(tags: HtmlTagDescriptor[]): Plugin {\n  return {\n    name: \"halo:inject-external-library-tags\",\n    enforce: \"pre\",\n    transformIndexHtml: {\n      order: \"pre\",\n      handler() {\n        return tags;\n      },\n    },\n  };\n}\n\nfunction computeLibraryHash(file: string) {\n  const content = fs.readFileSync(path.resolve(process.cwd(), file), \"utf8\");\n  return crypto.createHash(\"md5\").update(content).digest(\"hex\").substring(0, 8);\n}\n"
  },
  {
    "path": "ui/src/vite/plugin-dev.ts",
    "content": "import type { Plugin } from \"vite\";\n\nexport function devPlugin({ port }: { port: number }): Plugin {\n  const DEV_SERVER_ORIGIN = `http://localhost:${port}`;\n\n  return {\n    name: \"vite-dev-absolute-urls\",\n    apply: \"serve\",\n    transformIndexHtml: {\n      order: \"post\" as const,\n      handler: (html: string) =>\n        html.replace(/ (src|href)=\"(\\/.+?)\"/g, ` $1=\"${DEV_SERVER_ORIGIN}$2\"`),\n    },\n  };\n}\n"
  },
  {
    "path": "ui/tailwind.config.ts",
    "content": "import formkit from \"@formkit/themes/tailwindcss\";\nimport aspectRatio from \"@tailwindcss/aspect-ratio\";\nimport containerQueries from \"@tailwindcss/container-queries\";\nimport forms from \"@tailwindcss/forms\";\nimport { Config } from \"tailwindcss\";\nimport themer from \"tailwindcss-themer\";\n\nexport default {\n  content: [\"./index.html\", \"./{src,uc-src,console-src}/**/*.{vue,js,ts,tsx}\"],\n  theme: {\n    extend: {\n      animation: {\n        breath: \"breath 1s ease-in-out infinite\",\n      },\n      keyframes: {\n        breath: {\n          \"0%\": { transform: \"scale(1)\", opacity: \"0.8\" },\n          \"50%\": { transform: \"scale(1.02)\", opacity: \"1\" },\n          \"100%\": { transform: \"scale(1)\", opacity: \"0.8\" },\n        },\n      },\n    },\n  },\n  plugins: [\n    aspectRatio,\n    formkit,\n    containerQueries,\n    forms,\n    themer({\n      defaultTheme: {\n        extend: {\n          colors: {\n            primary: \"#4CCBA0\",\n            secondary: \"#0E1731\",\n            danger: \"#D71D1D\",\n          },\n          borderRadius: {\n            base: \"4px\",\n          },\n        },\n      },\n    }),\n  ],\n} satisfies Config;\n"
  },
  {
    "path": "ui/tsconfig.app.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"uc-src/**/*\", \"console-src/**/*\"],\n  \"exclude\": [\n    \"src/**/__tests__/*\",\n    \"uc-src/**/__tests__/*\",\n    \"console-src/**/__tests__/*\",\n    \"packages/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"composite\": true,\n    \"noImplicitAny\": false,\n    \"noEmit\": false,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"@uc/*\": [\"./uc-src/*\"],\n      \"@console/*\": [\"./console-src/*\"],\n      \"@halo-dev/api-client\": [\"./packages/api-client/dist/index\"],\n      \"@halo-dev/components\": [\"./packages/components/dist/index\"],\n      \"@halo-dev/richtext-editor\": [\"./packages/editor/dist/index\"],\n      \"@halo-dev/ui-shared\": [\"./packages/shared/dist/index\"]\n    },\n    \"ignoreDeprecations\": \"5.0\",\n    \"types\": [\"unplugin-icons/types/vue\"]\n  }\n}\n"
  },
  {
    "path": "ui/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.vitest.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "ui/tsconfig.node.json",
    "content": "{\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"include\": [\n    \"vite.config.ts\",\n    \"vite.uc.config.ts\",\n    \"vitest.config.*\",\n    \"eslint.config.ts\",\n    \"tailwind.config.ts\",\n    \"src/vite/*.ts\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "ui/tsconfig.vitest.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"exclude\": [],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"lib\": [],\n    \"types\": [\"node\", \"jsdom\"]\n  }\n}\n"
  },
  {
    "path": "ui/uc-src/App.vue",
    "content": "<script lang=\"ts\" setup>\nimport BaseApp from \"@/components/base-app/BaseApp.vue\";\n</script>\n\n<template>\n  <BaseApp />\n</template>\n"
  },
  {
    "path": "ui/uc-src/layouts/BasicLayout.vue",
    "content": "<script lang=\"ts\" setup>\nimport { coreMenuGroups } from \"@uc/router/constant\";\nimport {\n  useOverlayScrollbars,\n  type UseOverlayScrollbarsParams,\n} from \"overlayscrollbars-vue\";\nimport { defineStore } from \"pinia\";\nimport { onMounted, reactive, ref } from \"vue\";\nimport { RouterView, useRoute } from \"vue-router\";\nimport IconLogo from \"~icons/core/logo?width=5rem&height=2rem\";\nimport MenuLoading from \"@/components/menu/MenuLoading.vue\";\nimport { RoutesMenu } from \"@/components/menu/RoutesMenu\";\nimport { useRouteMenuGenerator } from \"@/composables/use-route-menu-generator\";\nimport MobileMenu from \"@/layouts/MobileMenu.vue\";\nimport UserProfileBanner from \"@/layouts/UserProfileBanner.vue\";\n\nconst route = useRoute();\n\nconst { data, isLoading } = useRouteMenuGenerator(coreMenuGroups);\n\n// aside scroll\nconst navbarScroller = ref();\n\nconst useNavbarScrollStore = defineStore(\"navbar\", {\n  state: () => ({\n    y: 0,\n  }),\n});\n\nconst navbarScrollStore = useNavbarScrollStore();\n\nconst reactiveParams = reactive<UseOverlayScrollbarsParams>({\n  options: {\n    scrollbars: {\n      autoHide: \"scroll\",\n      autoHideDelay: 600,\n    },\n  },\n  events: {\n    scroll: (_, onScrollArgs) => {\n      const target = onScrollArgs.target as HTMLElement;\n      navbarScrollStore.y = target.scrollTop;\n    },\n    updated: (instance) => {\n      const { viewport } = instance.elements();\n      if (!viewport) return;\n      viewport.scrollTo({ top: navbarScrollStore.y });\n    },\n  },\n});\nconst [initialize] = useOverlayScrollbars(reactiveParams);\nonMounted(() => {\n  if (navbarScroller.value) {\n    initialize({ target: navbarScroller.value });\n  }\n});\n</script>\n\n<template>\n  <div class=\"layout\">\n    <aside class=\"sidebar\">\n      <div class=\"sidebar__logo-container\">\n        <a\n          href=\"/\"\n          target=\"_blank\"\n          :title=\"$t('core.sidebar.operations.visit_homepage.title')\"\n        >\n          <IconLogo class=\"sidebar__logo\" />\n        </a>\n      </div>\n      <div ref=\"navbarScroller\" class=\"sidebar__content\">\n        <MenuLoading v-if=\"isLoading\" />\n        <RoutesMenu :menus=\"data?.menus || []\" />\n      </div>\n      <div class=\"sidebar__profile\">\n        <UserProfileBanner platform=\"uc\" />\n      </div>\n    </aside>\n\n    <main class=\"main-content\">\n      <slot v-if=\"$slots.default\" />\n      <RouterView v-else />\n      <footer v-if=\"!route.meta.hideFooter\" class=\"main-content__footer\">\n        <span class=\"main-content__footer-text\">Powered by </span>\n        <a\n          href=\"https://www.halo.run\"\n          target=\"_blank\"\n          class=\"main-content__footer-link\"\n        >\n          Halo\n        </a>\n      </footer>\n    </main>\n    <MobileMenu\n      :menus=\"data?.menus || []\"\n      :minimenus=\"data?.minimenus || []\"\n      platform=\"uc\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\">\n.layout {\n  display: flex;\n  min-height: 100vh;\n}\n\n.sidebar {\n  position: fixed;\n  width: theme(\"width.64\");\n  height: 100%;\n  background-color: theme(\"colors.white\");\n  box-shadow: theme(\"boxShadow.DEFAULT\");\n  overflow-y: auto;\n  display: none;\n  flex-direction: column;\n\n  @media (min-width: theme(\"screens.md\")) {\n    display: flex;\n  }\n\n  &__logo-container {\n    display: flex;\n    justify-content: center;\n    padding-top: 1.25rem;\n    padding-bottom: 1.25rem;\n  }\n\n  &__logo {\n    cursor: pointer;\n    user-select: none;\n    transition: all;\n\n    &:hover {\n      filter: brightness(1.25);\n    }\n  }\n\n  &__content {\n    flex: 1;\n    overflow-y: hidden;\n  }\n\n  &__profile {\n    flex: none;\n  }\n}\n\n.main-content {\n  width: 100%;\n  padding-bottom: 3rem;\n  margin-bottom: env(safe-area-inset-bottom);\n  display: flex;\n  flex: auto;\n  flex-direction: column;\n\n  @media (min-width: theme(\"screens.md\")) {\n    width: calc(100% - 16rem);\n    margin-left: theme(\"width.64\");\n    padding-bottom: 0;\n  }\n\n  &__footer {\n    margin-top: auto;\n    padding: 1rem;\n    text-align: center;\n    font-size: 0.875rem;\n  }\n\n  &__footer-text {\n    color: theme(\"colors.gray.600\");\n  }\n\n  &__footer-link {\n    &:hover {\n      color: theme(\"colors.gray.600\");\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/uc-src/main.ts",
    "content": "import { stores } from \"@halo-dev/ui-shared\";\nimport modules from \"@uc/modules\";\nimport router from \"@uc/router\";\nimport { createPinia } from \"pinia\";\nimport \"@/setup/setupStyles\";\nimport { createApp } from \"vue\";\nimport { setLanguage, setupI18n } from \"@/locales\";\nimport { setupApiClient } from \"@/setup/setupApiClient\";\nimport { setupComponents } from \"@/setup/setupComponents\";\nimport { setupCoreModules, setupPluginModules } from \"@/setup/setupModules\";\nimport \"core-js/es/object/has-own\";\nimport { setupUserPermissions } from \"@/setup/setupUserPermissions\";\nimport { setupVueQuery } from \"@/setup/setupVueQuery\";\nimport App from \"./App.vue\";\n\nconst app = createApp(App);\n\nsetupComponents(app);\nsetupI18n(app);\nsetupVueQuery(app);\nsetupApiClient();\n\napp.use(createPinia());\n\nawait initApp();\n\nasync function initApp() {\n  try {\n    setupCoreModules({ app, router, platform: \"uc\", modules });\n\n    const currentUserStore = stores.currentUser();\n    await currentUserStore.fetchCurrentUser();\n\n    const globalInfoStore = stores.globalInfo();\n    await globalInfoStore.fetchGlobalInfo();\n\n    await setLanguage();\n\n    if (currentUserStore.isAnonymous) {\n      return;\n    }\n\n    await setupUserPermissions(app);\n\n    try {\n      await setupPluginModules({ app, router, platform: \"uc\" });\n    } catch (e) {\n      console.error(\"Failed to load plugins\", e);\n    }\n  } catch (error) {\n    console.error(\"Failed to init app\", error);\n  } finally {\n    app.use(router);\n    app.mount(\"#app\");\n  }\n}\n"
  },
  {
    "path": "ui/uc-src/modules/contents/attachments/components/AttachmentDetailModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { type Attachment } from \"@halo-dev/api-client\";\nimport {\n  VButton,\n  VDescription,\n  VDescriptionItem,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport prettyBytes from \"pretty-bytes\";\nimport { useTemplateRef } from \"vue\";\nimport AttachmentImagePreview from \"@/components/attachment/AttachmentImagePreview.vue\";\nimport AttachmentPermalinkList from \"@/components/attachment/AttachmentPermalinkList.vue\";\nimport { isImage } from \"@/utils/image\";\n\nwithDefaults(\n  defineProps<{\n    attachment: Attachment;\n    mountToBody?: boolean;\n  }>(),\n  {\n    mountToBody: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"\n      $t('core.uc_attachment.detail_modal.title', {\n        display_name: attachment?.spec.displayName || '',\n      })\n    \"\n    :width=\"1200\"\n    :mount-to-body=\"mountToBody\"\n    :layer-closable=\"true\"\n    height=\"calc(100vh - 20px)\"\n    :body-class=\"['!p-0']\"\n    @close=\"emit('close')\"\n  >\n    <template #actions>\n      <slot name=\"actions\"></slot>\n    </template>\n    <div>\n      <div class=\"overflow-hidden bg-white\">\n        <VDescription>\n          <VDescriptionItem\n            :label=\"$t('core.uc_attachment.detail_modal.fields.preview')\"\n          >\n            <AttachmentImagePreview\n              v-if=\"isImage(attachment?.spec.mediaType)\"\n              :attachment=\"attachment\"\n            />\n            <div v-else-if=\"attachment?.spec.mediaType?.startsWith('video/')\">\n              <video\n                :src=\"attachment.status?.permalink\"\n                controls\n                class=\"max-w-full rounded sm:max-w-[50%]\"\n              >\n                {{\n                  $t(\n                    \"core.uc_attachment.detail_modal.preview.video_not_support\"\n                  )\n                }}\n              </video>\n            </div>\n            <div v-else-if=\"attachment?.spec.mediaType?.startsWith('audio/')\">\n              <audio :src=\"attachment.status?.permalink\" controls>\n                {{\n                  $t(\n                    \"core.uc_attachment.detail_modal.preview.audio_not_support\"\n                  )\n                }}\n              </audio>\n            </div>\n            <span v-else>\n              {{ $t(\"core.uc_attachment.detail_modal.preview.not_support\") }}\n            </span>\n          </VDescriptionItem>\n          <VDescriptionItem\n            :label=\"$t('core.uc_attachment.detail_modal.fields.display_name')\"\n          >\n            <span>\n              {{ attachment?.spec.displayName }}\n            </span>\n          </VDescriptionItem>\n          <VDescriptionItem\n            :label=\"$t('core.uc_attachment.detail_modal.fields.media_type')\"\n            :content=\"attachment?.spec.mediaType\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.uc_attachment.detail_modal.fields.size')\"\n            :content=\"prettyBytes(attachment?.spec.size || 0)\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.uc_attachment.detail_modal.fields.owner')\"\n            :content=\"attachment?.spec.ownerName\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.uc_attachment.detail_modal.fields.creation_time')\"\n            :content=\"utils.date.format(attachment?.metadata.creationTimestamp)\"\n          />\n          <VDescriptionItem\n            :label=\"$t('core.uc_attachment.detail_modal.fields.permalink')\"\n          >\n            <AttachmentPermalinkList :attachment=\"attachment\" />\n          </VDescriptionItem>\n        </VDescription>\n      </div>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <VButton type=\"default\" @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close_and_shortcut\") }}\n        </VButton>\n        <slot name=\"footer\" />\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/attachments/components/AttachmentSelectorModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton, VModal, VSpace, VTabbar } from \"@halo-dev/components\";\nimport type {\n  AttachmentLike,\n  AttachmentSelectProvider,\n} from \"@halo-dev/ui-shared\";\nimport {\n  computed,\n  markRaw,\n  onMounted,\n  ref,\n  shallowRef,\n  useTemplateRef,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport CoreSelectorProvider from \"./selector-providers/CoreSelectorProvider.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    accepts?: string[];\n    min?: number;\n    max?: number;\n  }>(),\n  {\n    accepts: () => [\"*/*\"],\n    min: undefined,\n    max: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"select\", attachments: AttachmentLike[]): void;\n}>();\n\nconst modal = useTemplateRef<InstanceType<typeof VModal> | null>(\"modal\");\nconst selected = ref<AttachmentLike[]>([] as AttachmentLike[]);\n\nconst attachmentSelectProviders = shallowRef<AttachmentSelectProvider[]>([\n  {\n    id: \"core\",\n    label: t(\"core.uc_attachment.select_modal.providers.default.label\"),\n    component: markRaw(CoreSelectorProvider),\n  },\n]);\n\n// resolve plugin extension points\nconst { pluginModules } = usePluginModuleStore();\n\nonMounted(async () => {\n  for (const pluginModule of pluginModules) {\n    try {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"attachment:selector:create\"];\n\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const providers = await callbackFunction();\n      attachmentSelectProviders.value = [\n        ...attachmentSelectProviders.value,\n        ...providers,\n      ].flat();\n    } catch (error) {\n      console.error(`Error processing plugin module:`, pluginModule, error);\n    }\n  }\n});\n\nconst activeId = ref(attachmentSelectProviders.value[0].id);\n\nconst onChangeProvider = (providerId: string) => {\n  const provider = attachmentSelectProviders.value.find(\n    (provider) => provider.id === providerId\n  );\n\n  if (!provider) {\n    return;\n  }\n\n  activeId.value = providerId;\n};\n\nconst handleConfirm = () => {\n  emit(\"select\", Array.from(selected.value));\n  modal.value?.close();\n};\n\nconst confirmDisabled = computed(() => {\n  if (!selected.value.length) {\n    return true;\n  }\n\n  if (props.min === undefined) {\n    return false;\n  }\n\n  return selected.value.length < props.min;\n});\n\nconst confirmCountMessage = computed(() => {\n  if (!props.min && !props.max) {\n    return selected.value.length;\n  }\n  return `${selected.value.length} / ${props.max || props.min}`;\n});\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"1240\"\n    :mount-to-body=\"true\"\n    :layer-closable=\"true\"\n    :title=\"$t('core.uc_attachment.select_modal.title')\"\n    height=\"calc(100vh - 20px)\"\n    @close=\"emit('close')\"\n  >\n    <VTabbar\n      v-model:active-id=\"activeId\"\n      :items=\"\n        attachmentSelectProviders.map((provider) => ({\n          id: provider.id,\n          label: provider.label,\n        }))\n      \"\n      class=\"w-full\"\n      type=\"outline\"\n    ></VTabbar>\n\n    <div class=\"mt-2\">\n      <template\n        v-for=\"provider in attachmentSelectProviders\"\n        :key=\"provider.id\"\n      >\n        <Suspense>\n          <component\n            :is=\"provider.component\"\n            v-if=\"activeId === provider.id\"\n            v-model:selected=\"selected\"\n            :accepts=\"accepts\"\n            :min=\"min\"\n            :max=\"max\"\n            @change-provider=\"onChangeProvider\"\n          ></component>\n          <template #fallback>\n            {{ $t(\"core.common.status.loading\") }}\n          </template>\n        </Suspense>\n      </template>\n    </div>\n    <template #footer>\n      <VSpace>\n        <VButton\n          type=\"secondary\"\n          :disabled=\"confirmDisabled\"\n          @click=\"handleConfirm\"\n        >\n          {{ $t(\"core.common.buttons.confirm\") }}\n          <span v-if=\"selected.length || props.min || props.max\">\n            {{\n              $t(\"core.uc_attachment.select_modal.operations.select.result\", {\n                count: confirmCountMessage,\n              })\n            }}\n          </span>\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/attachments/components/selector-providers/AttachmentUploadModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { VButton, VModal } from \"@halo-dev/components\";\nimport { ref } from \"vue\";\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :width=\"920\"\n    height=\"calc(100vh - 20px)\"\n    :title=\"$t('core.uc_attachment.upload_modal.title')\"\n    mount-to-body\n    @close=\"emit('close')\"\n  >\n    <div class=\"w-full p-4\">\n      <UppyUpload\n        endpoint=\"/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload\"\n        width=\"100%\"\n        :done-button-handler=\"() => modal?.close()\"\n      />\n    </div>\n\n    <template #footer>\n      <VButton @click=\"modal?.close()\">\n        {{ $t(\"core.common.buttons.close\") }}\n      </VButton>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/attachments/components/selector-providers/CoreSelectorProvider.vue",
    "content": "<script setup lang=\"ts\">\nimport { ucApiClient, type Attachment } from \"@halo-dev/api-client\";\nimport {\n  IconArrowLeft,\n  IconArrowRight,\n  IconCheckboxCircle,\n  IconCheckboxFill,\n  IconClose,\n  IconEye,\n  IconGrid,\n  IconList,\n  IconRefreshLine,\n  IconUpload,\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport type { AttachmentLike } from \"@halo-dev/ui-shared\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport type { SuccessResponse } from \"@uppy/core\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { throttle } from \"es-toolkit/compat\";\nimport { computed, nextTick, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport AttachmentGridListItem from \"@/components/attachment/AttachmentGridListItem.vue\";\nimport { matchMediaTypes } from \"@/utils/media-type\";\nimport AttachmentDetailModal from \"../AttachmentDetailModal.vue\";\nimport AttachmentSelectorListItem from \"./components/AttachmentSelectorListItem.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    selected?: AttachmentLike[];\n    accepts?: string[];\n    min?: number;\n    max?: number;\n  }>(),\n  {\n    selected: () => [],\n    accepts: () => [\"*/*\"],\n    min: undefined,\n    max: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"update:selected\", attachments: AttachmentLike[]): void;\n  (event: \"change-provider\", providerId: string): void;\n}>();\n\nconst page = ref(1);\nconst size = ref(60);\nconst keyword = ref(\"\");\nconst selectedSort = ref();\n\nconst hasFilters = computed(() => {\n  return selectedSort.value;\n});\n\nfunction handleClearFilters() {\n  selectedSort.value = undefined;\n}\n\nconst {\n  data,\n  isFetching,\n  isLoading,\n  refetch: handleFetchAttachments,\n} = useQuery({\n  queryKey: [\n    \"uc:attachments:my\",\n    props.accepts,\n    page,\n    size,\n    keyword,\n    selectedSort,\n  ],\n  queryFn: async () => {\n    const { data } = await ucApiClient.storage.attachment.listMyAttachments({\n      accepts: props.accepts,\n      page: page.value,\n      size: size.value,\n      keyword: keyword.value,\n      sort: [selectedSort.value],\n    });\n    return data;\n  },\n});\n\nconst throttledFetchAttachments = throttle(handleFetchAttachments, 1000, {\n  leading: false,\n  trailing: true,\n});\n\n// Upload\nconst uploadVisible = ref(false);\n\n// Select\nconst selectedAttachment = ref<Attachment>();\nconst selectedAttachmentNames = ref<Set<string>>(new Set<string>());\n\nconst selectedAttachments = computed(() => {\n  return data.value?.items.filter((attachment) =>\n    selectedAttachmentNames.value.has(attachment.metadata.name)\n  );\n});\n\nwatch(\n  () => selectedAttachments.value,\n  (newValue) => {\n    emit(\"update:selected\", newValue || []);\n  },\n  {\n    deep: true,\n  }\n);\n\nconst isChecked = (attachment: Attachment) => {\n  return selectedAttachmentNames.value.has(attachment.metadata.name);\n};\n\nconst isDisabled = (attachment: Attachment) => {\n  const isMatchMediaType = matchMediaTypes(\n    attachment.spec.mediaType || \"*/*\",\n    props.accepts\n  );\n\n  if (\n    props.max !== undefined &&\n    props.max <= selectedAttachmentNames.value.size &&\n    !isChecked(attachment)\n  ) {\n    return true;\n  }\n\n  return !isMatchMediaType;\n};\n\nconst handleSelect = async (attachment: Attachment | undefined) => {\n  if (!attachment) return;\n  if (selectedAttachmentNames.value.has(attachment.metadata.name)) {\n    selectedAttachmentNames.value.delete(attachment.metadata.name);\n    return;\n  }\n  selectedAttachmentNames.value.add(attachment.metadata.name);\n};\n\n// View type\nconst viewTypes = [\n  {\n    name: \"list\",\n    tooltip: t(\"core.uc_attachment.filters.view_type.items.list\"),\n    icon: IconList,\n  },\n  {\n    name: \"grid\",\n    tooltip: t(\"core.uc_attachment.filters.view_type.items.grid\"),\n    icon: IconGrid,\n  },\n];\n\nconst viewType = useLocalStorage(\"attachment-selector-view-type\", \"grid\");\n\n// Detail modal\nfunction handleOpenDetail(attachment: Attachment) {\n  selectedAttachment.value = attachment;\n}\n\nfunction onDetailModalClose() {\n  selectedAttachment.value = undefined;\n}\n\nconst handleSelectPrevious = async () => {\n  if (!data.value) return;\n\n  const index = data.value.items.findIndex(\n    (attachment) =>\n      attachment.metadata.name === selectedAttachment.value?.metadata.name\n  );\n\n  if (index === undefined) return;\n\n  if (index > 0) {\n    selectedAttachment.value = data.value.items[index - 1];\n    return;\n  }\n\n  if (index === 0 && data.value.hasPrevious) {\n    page.value--;\n    await nextTick();\n    await handleFetchAttachments();\n    selectedAttachment.value = data.value.items[data.value.items.length - 1];\n  }\n};\n\nconst handleSelectNext = async () => {\n  if (!data.value) return;\n\n  const index = data.value.items.findIndex(\n    (attachment) =>\n      attachment.metadata.name === selectedAttachment.value?.metadata.name\n  );\n\n  if (index === undefined) return;\n\n  if (index < data.value.items.length - 1) {\n    selectedAttachment.value = data.value.items[index + 1];\n    return;\n  }\n\n  if (index === data.value.items.length - 1 && data.value.hasNext) {\n    page.value++;\n    await nextTick();\n    await handleFetchAttachments();\n    selectedAttachment.value = data.value.items[0];\n  }\n};\n\nfunction onUploadDone() {\n  handleFetchAttachments();\n  uploadVisible.value = false;\n}\n\nfunction onUploaded(response: SuccessResponse) {\n  if (response.body) {\n    handleSelect(response.body as Attachment);\n    page.value = 1;\n    throttledFetchAttachments();\n  }\n}\n</script>\n<template>\n  <div class=\"mb-3 block w-full rounded bg-gray-50 px-3 py-2\">\n    <div class=\"relative flex flex-col items-start sm:flex-row sm:items-center\">\n      <div class=\"flex w-full flex-1 items-center sm:w-auto\">\n        <SearchInput v-model=\"keyword\" />\n      </div>\n      <div class=\"mt-4 flex sm:mt-0\">\n        <VSpace spacing=\"lg\">\n          <FilterCleanButton v-if=\"hasFilters\" @click=\"handleClearFilters\" />\n\n          <FilterDropdown\n            v-model=\"selectedSort\"\n            :label=\"$t('core.common.filters.labels.sort')\"\n            :items=\"[\n              {\n                label: t('core.common.filters.item_labels.default'),\n              },\n              {\n                label: t(\n                  'core.uc_attachment.filters.sort.items.create_time_desc'\n                ),\n                value: 'metadata.creationTimestamp,desc',\n              },\n              {\n                label: t(\n                  'core.uc_attachment.filters.sort.items.create_time_asc'\n                ),\n                value: 'metadata.creationTimestamp,asc',\n              },\n              {\n                label: t(\n                  'core.uc_attachment.filters.sort.items.display_name_desc'\n                ),\n                value: 'spec.displayName,desc',\n              },\n              {\n                label: t(\n                  'core.uc_attachment.filters.sort.items.display_name_asc'\n                ),\n                value: 'spec.displayName,asc',\n              },\n              {\n                label: t('core.uc_attachment.filters.sort.items.size_desc'),\n                value: 'spec.size,desc',\n              },\n              {\n                label: t('core.uc_attachment.filters.sort.items.size_asc'),\n                value: 'spec.size,asc',\n              },\n            ]\"\n          />\n\n          <div class=\"flex flex-row gap-2\">\n            <div\n              v-for=\"item in viewTypes\"\n              :key=\"item.name\"\n              v-tooltip=\"`${item.tooltip}`\"\n              :class=\"{\n                'bg-gray-200 font-bold text-black': viewType === item.name,\n              }\"\n              class=\"cursor-pointer rounded p-1 hover:bg-gray-200\"\n              @click=\"viewType = item.name\"\n            >\n              <component :is=\"item.icon\" class=\"h-4 w-4\" />\n            </div>\n          </div>\n\n          <div class=\"flex flex-row gap-2\">\n            <div\n              class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n              @click=\"handleFetchAttachments()\"\n            >\n              <IconRefreshLine\n                v-tooltip=\"$t('core.common.buttons.refresh')\"\n                :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n              />\n            </div>\n          </div>\n        </VSpace>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"mb-5 space-y-3\">\n    <VButton @click=\"uploadVisible = !uploadVisible\">\n      <template #icon>\n        <IconUpload v-if=\"!uploadVisible\" />\n        <IconClose v-else />\n      </template>\n      {{\n        uploadVisible\n          ? $t(\"core.common.buttons.cancel_upload\")\n          : $t(\"core.common.buttons.upload\")\n      }}\n    </VButton>\n    <Transition v-if=\"uploadVisible\" appear name=\"fade\">\n      <UppyUpload\n        endpoint=\"/apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload\"\n        width=\"100%\"\n        :done-button-handler=\"onUploadDone\"\n        @uploaded=\"onUploaded\"\n      />\n    </Transition>\n  </div>\n\n  <VLoading v-if=\"isLoading\" />\n\n  <VEmpty\n    v-else-if=\"!data?.total\"\n    :message=\"$t('core.uc_attachment.empty.message')\"\n    :title=\"$t('core.uc_attachment.empty.title')\"\n  >\n    <template #actions>\n      <VSpace>\n        <VButton @click=\"handleFetchAttachments\">\n          {{ $t(\"core.common.buttons.refresh\") }}\n        </VButton>\n        <VButton type=\"secondary\" @click=\"uploadVisible = true\">\n          <template #icon>\n            <IconUpload />\n          </template>\n          {{ $t(\"core.uc_attachment.empty.actions.upload\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VEmpty>\n\n  <div v-else>\n    <Transition v-if=\"viewType === 'grid'\" appear name=\"fade\">\n      <div\n        class=\"mt-2 grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10\"\n        role=\"list\"\n      >\n        <AttachmentGridListItem\n          v-for=\"attachment in data.items\"\n          :key=\"attachment.metadata.name\"\n          :attachment=\"attachment\"\n          :is-selected=\"isChecked(attachment)\"\n          :is-disabled=\"isDisabled(attachment)\"\n          @select=\"handleSelect(attachment)\"\n          @click=\"handleSelect(attachment)\"\n        >\n          <template #actions>\n            <IconEye\n              class=\"mr-1 mt-1 hidden h-6 w-6 cursor-pointer text-white transition-all hover:text-primary group-hover:block\"\n              @click.stop=\"handleOpenDetail(attachment)\"\n            />\n          </template>\n        </AttachmentGridListItem>\n      </div>\n    </Transition>\n    <Transition v-if=\"viewType === 'list'\" appear name=\"fade\">\n      <div class=\"overflow-hidden rounded-base border\">\n        <VEntityContainer>\n          <AttachmentSelectorListItem\n            v-for=\"attachment in data.items\"\n            :key=\"attachment.metadata.name\"\n            :attachment=\"attachment\"\n            :is-selected=\"isChecked(attachment)\"\n            @select=\"handleSelect\"\n            @open-detail=\"handleOpenDetail\"\n          >\n            <template #checkbox>\n              <input\n                :checked=\"isChecked(attachment)\"\n                :disabled=\"isDisabled(attachment)\"\n                type=\"checkbox\"\n                @click=\"handleSelect(attachment)\"\n              />\n            </template>\n          </AttachmentSelectorListItem>\n        </VEntityContainer>\n      </div>\n    </Transition>\n  </div>\n\n  <div class=\"mt-4\">\n    <VPagination\n      v-model:page=\"page\"\n      v-model:size=\"size\"\n      :page-label=\"$t('core.components.pagination.page_label')\"\n      :size-label=\"$t('core.components.pagination.size_label')\"\n      :total-label=\"\n        $t('core.components.pagination.total_label', {\n          total: data?.total || 0,\n        })\n      \"\n      :total=\"data?.total || 0\"\n      :size-options=\"[60, 120, 200]\"\n    />\n  </div>\n\n  <AttachmentDetailModal\n    v-if=\"selectedAttachment\"\n    :mount-to-body=\"true\"\n    :attachment=\"selectedAttachment\"\n    @close=\"onDetailModalClose\"\n  >\n    <template #actions>\n      <span\n        v-if=\"isChecked(selectedAttachment)\"\n        @click=\"handleSelect(selectedAttachment)\"\n      >\n        <IconCheckboxFill />\n      </span>\n      <span v-else @click=\"handleSelect(selectedAttachment)\">\n        <IconCheckboxCircle />\n      </span>\n\n      <span @click=\"handleSelectPrevious\">\n        <IconArrowLeft />\n      </span>\n      <span @click=\"handleSelectNext\">\n        <IconArrowRight />\n      </span>\n    </template>\n  </AttachmentDetailModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/attachments/components/selector-providers/components/AttachmentSelectorListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Attachment } from \"@halo-dev/api-client\";\nimport {\n  VEntity,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport prettyBytes from \"pretty-bytes\";\nimport { toRefs } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    attachment: Attachment;\n    isSelected?: boolean;\n  }>(),\n  { isSelected: false }\n);\n\nconst { attachment } = toRefs(props);\n\nconst emit = defineEmits<{\n  (event: \"select\", attachment?: Attachment): void;\n  (event: \"open-detail\", attachment: Attachment): void;\n}>();\n</script>\n\n<template>\n  <VEntity :is-selected=\"isSelected\">\n    <template #checkbox>\n      <slot name=\"checkbox\" />\n    </template>\n    <template #start>\n      <VEntityField>\n        <template #description>\n          <div class=\"h-10 w-10 rounded border bg-white p-1 hover:shadow-sm\">\n            <AttachmentFileTypeIcon\n              :display-ext=\"false\"\n              :file-name=\"attachment.spec.displayName\"\n              :width=\"8\"\n              :height=\"8\"\n            />\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"attachment.spec.displayName\"\n        @click=\"emit('open-detail', attachment)\"\n      >\n        <template #description>\n          <VSpace>\n            <span class=\"text-xs text-gray-500\">\n              {{ attachment.spec.mediaType }}\n            </span>\n            <span class=\"text-xs text-gray-500\">\n              {{ prettyBytes(attachment.spec.size || 0) }}\n            </span>\n          </VSpace>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField :description=\"attachment.spec.ownerName\" />\n      <VEntityField v-if=\"attachment.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <span class=\"truncate text-xs tabular-nums text-gray-500\">\n            {{ utils.date.format(attachment.metadata.creationTimestamp) }}\n          </span>\n        </template>\n      </VEntityField>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/attachments/module.ts",
    "content": "import { definePlugin, utils } from \"@halo-dev/ui-shared\";\nimport { defineAsyncComponent } from \"vue\";\n\nexport default definePlugin({\n  components: {\n    AttachmentSelectorModal: defineAsyncComponent({\n      loader: () => {\n        if (utils.permission.has([\"system:attachments:manage\"])) {\n          return import(\"@console/modules/contents/attachments/components/AttachmentSelectorModal.vue\");\n        }\n        return import(\"@uc/modules/contents/attachments/components/AttachmentSelectorModal.vue\");\n      },\n    }),\n  },\n});\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/PostEditor.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useSaveKeybinding } from \"@console/composables/use-save-keybinding\";\nimport useSlugify from \"@console/composables/use-slugify\";\nimport type { Content, Post, Snapshot } from \"@halo-dev/api-client\";\nimport { publicApiClient, ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconBookRead,\n  IconSave,\n  IconSendPlaneFill,\n  IconSettings,\n  Toast,\n  VButton,\n  VPageHeader,\n} from \"@halo-dev/components\";\nimport type { EditorProvider } from \"@halo-dev/ui-shared\";\nimport { FormType, utils } from \"@halo-dev/ui-shared\";\nimport { useMutation } from \"@tanstack/vue-query\";\nimport { usePostUpdateMutate } from \"@uc/modules/contents/posts/composables/use-post-update-mutate\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { AxiosError, type AxiosRequestConfig } from \"axios\";\nimport { isEqual } from \"es-toolkit\";\nimport ShortUniqueId from \"short-unique-id\";\nimport type { ComputedRef } from \"vue\";\nimport {\n  computed,\n  nextTick,\n  onMounted,\n  provide,\n  ref,\n  shallowRef,\n  toRef,\n  watch,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport { useRouter } from \"vue-router\";\nimport EditorProviderSelector from \"@/components/dropdown-selector/EditorProviderSelector.vue\";\nimport { useAutoSaveContent } from \"@/composables/use-auto-save-content\";\nimport { useContentCache } from \"@/composables/use-content-cache\";\nimport { useEditorExtensionPoints } from \"@/composables/use-editor-extension-points\";\nimport { useSessionKeepAlive } from \"@/composables/use-session-keep-alive\";\nimport { contentAnnotations } from \"@/constants/annotations\";\nimport PostCreationModal from \"./components/PostCreationModal.vue\";\nimport PostSettingEditModal from \"./components/PostSettingEditModal.vue\";\n\nconst uid = new ShortUniqueId();\n\nconst router = useRouter();\nconst { t } = useI18n();\n\nconst formState = ref<Post>({\n  apiVersion: \"content.halo.run/v1alpha1\",\n  kind: \"Post\",\n  metadata: {\n    annotations: {},\n    name: utils.id.uuid(),\n  },\n  spec: {\n    allowComment: true,\n    baseSnapshot: \"\",\n    categories: [],\n    cover: \"\",\n    deleted: false,\n    excerpt: {\n      autoGenerate: true,\n      raw: \"\",\n    },\n    headSnapshot: \"\",\n    htmlMetas: [],\n    owner: \"\",\n    pinned: false,\n    priority: 0,\n    publish: false,\n    publishTime: \"\",\n    releaseSnapshot: \"\",\n    slug: \"\",\n    tags: [],\n    template: \"\",\n    title: \"\",\n    visible: \"PUBLIC\",\n  },\n});\n\nconst content = ref<Content>({\n  content: \"\",\n  raw: \"\",\n  rawType: \"\",\n});\nconst snapshot = ref<Snapshot>();\n\nconst needsUpdatePost = ref(false);\nwatch(\n  [() => formState.value.spec.title, () => formState.value.spec.cover],\n  (value, oldValue) => {\n    needsUpdatePost.value = !isEqual(value, oldValue);\n  }\n);\n\nconst isUpdateMode = computed(\n  () => !!formState.value.metadata.creationTimestamp\n);\n\n// provide some data to editor\nprovide<ComputedRef<string | undefined>>(\n  \"owner\",\n  computed(() => formState.value.spec.owner)\n);\nprovide<ComputedRef<string | undefined>>(\n  \"publishTime\",\n  computed(() => formState.value.spec.publishTime)\n);\nprovide<ComputedRef<string | undefined>>(\n  \"permalink\",\n  computed(() => formState.value.status?.permalink)\n);\n\n// Editor providers\nconst { editorProviders, fetchEditorProviders } = useEditorExtensionPoints();\nconst currentEditorProvider = shallowRef<EditorProvider>();\nconst storedEditorProviderName = useLocalStorage(\"editor-provider-name\", \"\");\n\nconst handleChangeEditorProvider = async (provider: EditorProvider) => {\n  currentEditorProvider.value = provider;\n\n  const { name, rawType } = provider;\n\n  storedEditorProviderName.value = name;\n\n  content.value.rawType = rawType;\n\n  formState.value.metadata.annotations = {\n    ...formState.value.metadata.annotations,\n    [contentAnnotations.PREFERRED_EDITOR]: name,\n  };\n};\n\n// Fetch post data when the route contains the name parameter\nconst name = useRouteQuery<string | undefined>(\"name\");\n\nonMounted(async () => {\n  await fetchEditorProviders();\n\n  if (name.value) {\n    await getLatestPost();\n    await handleFetchContent();\n    handleResetCache();\n    return;\n  }\n\n  // New post, set default editor\n  const provider =\n    editorProviders.value.find(\n      (provider) => provider.name === storedEditorProviderName.value\n    ) || editorProviders.value[0];\n\n  if (provider) {\n    currentEditorProvider.value = provider;\n    content.value.rawType = provider.rawType;\n    formState.value.metadata.annotations = {\n      [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n    };\n  }\n  handleResetCache();\n});\n\nconst snapshotVersion = computed(() => snapshot.value?.metadata.version || 0);\n\n// Post content cache\nconst {\n  currentCache,\n  handleSetContentCache,\n  handleResetCache,\n  handleClearCache,\n} = useContentCache(\n  \"post-content-cache\",\n  name,\n  toRef(content.value, \"raw\"),\n  snapshotVersion\n);\n\nuseAutoSaveContent(currentCache, toRef(content.value, \"raw\"), async () => {\n  // Do not save when the setting modal or the creation modal is open\n  if (postSettingEditModal.value || postPublishModal.value) {\n    return;\n  }\n  if (isUpdateMode.value) {\n    handleSave({ mute: true });\n  } else {\n    handleCreate();\n  }\n});\n\n// Slug generation\nconst { handleGenerateSlug } = useSlugify(\n  computed(() => formState.value.spec.title),\n  computed({\n    get() {\n      return formState.value.spec.slug;\n    },\n    set(value) {\n      formState.value.spec.slug = value;\n    },\n  }),\n  computed(() => !isUpdateMode.value),\n  FormType.POST\n);\n\nasync function getLatestPost() {\n  if (!name.value) {\n    return;\n  }\n  const { data: latestPost } = await ucApiClient.content.post.getMyPost({\n    name: name.value,\n  });\n\n  formState.value = latestPost;\n}\n\n/**\n * Fetch content from the head snapshot.\n */\nasync function handleFetchContent() {\n  const { headSnapshot } = formState.value.spec || {};\n\n  if (!headSnapshot || !name.value) {\n    return;\n  }\n\n  const { data } = await ucApiClient.content.post.getMyPostDraft({\n    name: name.value,\n    patched: true,\n  });\n\n  const {\n    [contentAnnotations.PATCHED_CONTENT]: patchedContent,\n    [contentAnnotations.PATCHED_RAW]: patchedRaw,\n  } = data.metadata.annotations || {};\n\n  const { rawType } = data.spec || {};\n\n  content.value = {\n    content: patchedContent,\n    raw: patchedRaw,\n    rawType,\n  };\n\n  snapshot.value = data;\n\n  if (currentEditorProvider.value) {\n    return;\n  }\n\n  await handleSetEditorProviderFromRemote();\n}\n\nasync function handleSetEditorProviderFromRemote() {\n  const { [contentAnnotations.PREFERRED_EDITOR]: preferredEditorName } =\n    formState.value.metadata.annotations || {};\n\n  const preferredEditor = editorProviders.value.find(\n    (provider) => provider.name === preferredEditorName\n  );\n\n  const provider =\n    preferredEditor ||\n    editorProviders.value.find(\n      (provider) =>\n        provider.rawType.toLowerCase() === content.value.rawType?.toLowerCase()\n    );\n\n  if (provider) {\n    currentEditorProvider.value = provider;\n\n    formState.value.metadata.annotations = {\n      ...formState.value.metadata.annotations,\n      [contentAnnotations.PREFERRED_EDITOR]: provider.name,\n    };\n  } else {\n    Dialog.warning({\n      title: t(\"core.common.dialog.titles.warning\"),\n      description: t(\"core.common.dialog.descriptions.editor_not_found\", {\n        raw_type: content.value.rawType,\n      }),\n      confirmText: t(\"core.common.buttons.confirm\"),\n      showCancel: false,\n      onConfirm: () => {\n        router.back();\n      },\n    });\n  }\n\n  await nextTick();\n}\n\n// Create post\nfunction handleSaveClick() {\n  if (isUpdateMode.value) {\n    handleSave({ mute: false });\n  } else {\n    handleCreate();\n  }\n}\n\nasync function handleCreate() {\n  formState.value.metadata.annotations = {\n    ...formState.value.metadata.annotations,\n    [contentAnnotations.CONTENT_JSON]: JSON.stringify(content.value),\n  };\n  // Set default title and slug\n  if (!formState.value.spec.title) {\n    formState.value.spec.title = t(\"core.post_editor.untitled\");\n  }\n\n  if (!formState.value.spec.slug) {\n    handleGenerateSlug(true);\n  }\n\n  // fixme: check if slug is unique\n  // Finally, we need to check if the slug is unique in the database\n  const { data: postsWithSameSlug } =\n    await publicApiClient.content.post.queryPosts({\n      fieldSelector: [`spec.slug=${formState.value.spec.slug}`],\n    });\n\n  if (postsWithSameSlug.total) {\n    formState.value.spec.slug = `${formState.value.spec.slug}-${uid.randomUUID(8)}`;\n  }\n\n  const { data: createdPost } = await ucApiClient.content.post.createMyPost({\n    post: formState.value,\n  });\n\n  await onCreatePostSuccess(createdPost);\n}\n\nasync function onCreatePostSuccess(data: Post) {\n  formState.value = data;\n  // Update route query params\n  name.value = data.metadata.name;\n  await handleFetchContent();\n  handleClearCache();\n}\n\n// Save post\n\nconst { mutateAsync: postUpdateMutate } = usePostUpdateMutate();\n\nconst { mutateAsync: handleSave, isLoading: isSaving } = useMutation({\n  mutationKey: [\"uc:save-post-content\"],\n  variables: {\n    mute: false,\n  },\n  mutationFn: async () => {\n    // Update title\n    if (needsUpdatePost.value) {\n      const { data: updatedPost } = await postUpdateMutate({\n        postToUpdate: formState.value,\n      });\n\n      formState.value = updatedPost;\n      needsUpdatePost.value = false;\n    }\n\n    // Snapshot always exists in update mode\n    if (!snapshot.value) {\n      return;\n    }\n\n    const { annotations } = snapshot.value.metadata || {};\n\n    snapshot.value.metadata.annotations = {\n      ...annotations,\n      [contentAnnotations.CONTENT_JSON]: JSON.stringify(content.value),\n    };\n\n    if (!isUpdateMode.value || !name.value) {\n      return;\n    }\n\n    const { data } = await ucApiClient.content.post.updateMyPostDraft({\n      name: name.value,\n      snapshot: snapshot.value,\n    });\n\n    snapshot.value = data;\n\n    return data;\n  },\n  onSuccess(_, variables) {\n    if (!variables.mute) Toast.success(t(\"core.common.toast.save_success\"));\n    handleFetchContent();\n    handleClearCache(name.value);\n  },\n  onError() {\n    Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n  },\n});\n\nuseSaveKeybinding(handleSaveClick);\n\n// Publish post\n\nconst postPublishModal = ref(false);\n\nfunction handlePublishClick() {\n  if (isUpdateMode.value) {\n    handlePublish();\n  } else {\n    // Set editor title to post\n    postPublishModal.value = true;\n  }\n}\n\nfunction onPublishPostSuccess() {\n  handleClearCache();\n  router.push({ name: \"Posts\" });\n}\n\nconst { mutateAsync: handlePublish, isLoading: isPublishing } = useMutation({\n  mutationKey: [\"uc:publish-post\"],\n  mutationFn: async () => {\n    await handleSave({ mute: true });\n\n    return await ucApiClient.content.post.publishMyPost(\n      {\n        name: formState.value.metadata.name,\n      },\n      {\n        mute: true,\n      }\n    );\n  },\n  retry: 3,\n  onSuccess() {\n    Toast.success(t(\"core.common.toast.publish_success\"), {\n      duration: 2000,\n    });\n    handleClearCache(formState.value.metadata.name);\n    router.push({ name: \"Posts\" });\n  },\n  onError(error: Error) {\n    if (error instanceof AxiosError) {\n      const { detail, title } = error.response?.data || {};\n      Toast.error(detail || title);\n    } else {\n      Toast.error(t(\"core.common.toast.publish_failed_and_retry\"));\n    }\n  },\n});\n\n// Post setting\nconst postSettingEditModal = ref(false);\n\nasync function handleOpenPostSettingEditModal() {\n  await handleSave({ mute: true });\n  await getLatestPost();\n  postSettingEditModal.value = true;\n}\n\nfunction onUpdatePostSuccess(data: Post) {\n  formState.value = data;\n  handleFetchContent();\n  handleClearCache(data.metadata.name);\n}\n\n// Upload image\nasync function handleUploadImage(file: File, options?: AxiosRequestConfig) {\n  if (!utils.permission.has([\"uc:attachments:manage\"])) {\n    return;\n  }\n\n  const { data } = await ucApiClient.storage.attachment.uploadAttachmentForUc(\n    {\n      file,\n    },\n    options\n  );\n  return data;\n}\n\n// Keep session alive\nuseSessionKeepAlive();\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.post.title')\">\n    <template #icon>\n      <IconBookRead />\n    </template>\n    <template #actions>\n      <EditorProviderSelector\n        v-if=\"editorProviders.length > 1\"\n        :provider=\"currentEditorProvider\"\n        :allow-forced-select=\"!isUpdateMode\"\n        @select=\"handleChangeEditorProvider\"\n      />\n      <VButton\n        size=\"sm\"\n        type=\"default\"\n        :loading=\"isSaving && !isPublishing\"\n        @click=\"handleSaveClick\"\n      >\n        <template #icon>\n          <IconSave />\n        </template>\n        {{ $t(\"core.common.buttons.save\") }}\n      </VButton>\n      <VButton\n        v-if=\"isUpdateMode\"\n        size=\"sm\"\n        type=\"default\"\n        @click=\"handleOpenPostSettingEditModal\"\n      >\n        <template #icon>\n          <IconSettings />\n        </template>\n        {{ $t(\"core.common.buttons.setting\") }}\n      </VButton>\n      <HasPermission :permissions=\"['uc:posts:publish']\">\n        <VButton\n          :loading=\"isPublishing\"\n          type=\"secondary\"\n          @click=\"handlePublishClick\"\n        >\n          <template #icon>\n            <IconSendPlaneFill />\n          </template>\n          {{ $t(\"core.common.buttons.publish\") }}\n        </VButton>\n      </HasPermission>\n    </template>\n  </VPageHeader>\n  <div class=\"editor border-t\" style=\"height: calc(100vh - 3.5rem)\">\n    <component\n      :is=\"currentEditorProvider.component\"\n      v-if=\"currentEditorProvider\"\n      v-model:raw=\"content.raw\"\n      v-model:content=\"content.content\"\n      v-model:title=\"formState.spec.title\"\n      v-model:cover=\"formState.spec.cover\"\n      :upload-image=\"handleUploadImage\"\n      class=\"h-full\"\n      @update=\"handleSetContentCache\"\n    />\n  </div>\n\n  <PostCreationModal\n    v-if=\"postPublishModal\"\n    :title=\"$t('core.uc_post.publish_modal.title')\"\n    :post=\"formState\"\n    :content=\"content\"\n    publish\n    @close=\"postPublishModal = false\"\n    @success=\"onPublishPostSuccess\"\n  />\n\n  <PostSettingEditModal\n    v-if=\"postSettingEditModal\"\n    :post=\"formState\"\n    @close=\"postSettingEditModal = false\"\n    @success=\"onUpdatePostSuccess\"\n  />\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/PostList.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  IconBookRead,\n  IconRefreshLine,\n  VButton,\n  VCard,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VPageHeader,\n  VPagination,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { computed, watch } from \"vue\";\nimport { postLabels } from \"@/constants/labels\";\nimport PostListItem from \"./components/PostListItem.vue\";\n\nconst page = useRouteQuery<number>(\"page\", 1, {\n  transform: Number,\n});\nconst size = useRouteQuery<number>(\"size\", 20, {\n  transform: Number,\n});\nconst keyword = useRouteQuery<string>(\"keyword\", \"\");\n\nconst selectedPublishPhase = useRouteQuery<\n  \"DRAFT\" | \"PENDING_APPROVAL\" | \"PUBLISHED\" | \"FAILED\" | undefined\n>(\"status\");\n\nfunction handleClearFilters() {\n  selectedPublishPhase.value = undefined;\n}\n\nconst hasFilters = computed(() => {\n  return selectedPublishPhase.value !== undefined;\n});\n\nwatch(\n  () => [selectedPublishPhase.value, keyword.value],\n  () => {\n    page.value = 1;\n  }\n);\n\nconst {\n  data: posts,\n  isLoading,\n  isFetching,\n  refetch,\n} = useQuery({\n  queryKey: [\"my-posts\", page, size, keyword, selectedPublishPhase],\n  queryFn: async () => {\n    const labelSelector: string[] = [\"content.halo.run/deleted=false\"];\n    const { data } = await ucApiClient.content.post.listMyPosts({\n      labelSelector,\n      page: page.value,\n      size: size.value,\n      keyword: keyword.value,\n      publishPhase: selectedPublishPhase.value,\n    });\n    return data;\n  },\n  onSuccess(data) {\n    page.value = data.page;\n    size.value = data.size;\n  },\n  refetchInterval: (data) => {\n    const hasDeletingPosts = data?.items.some((post) => post.post.spec.deleted);\n\n    if (hasDeletingPosts) {\n      return 1000;\n    }\n\n    const hasPublishingPost = data?.items.some((post) => {\n      const { spec, metadata } = post.post;\n      return (\n        metadata.labels?.[postLabels.PUBLISHED] !== spec.publish + \"\" &&\n        metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== \"true\"\n      );\n    });\n\n    if (hasPublishingPost) {\n      return 1000;\n    }\n\n    const hasCancelingPublishPost = data?.items.some((post) => {\n      const { spec, metadata } = post.post;\n      return (\n        !spec.publish &&\n        (metadata.labels?.[postLabels.PUBLISHED] === \"true\" ||\n          metadata.labels?.[postLabels.SCHEDULING_PUBLISH] === \"true\")\n      );\n    });\n\n    return hasCancelingPublishPost ? 1000 : false;\n  },\n});\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.uc_post.title')\">\n    <template #icon>\n      <IconBookRead />\n    </template>\n    <template #actions>\n      <VButton :route=\"{ name: 'PostEditor' }\" type=\"secondary\">\n        <template #icon>\n          <IconAddCircle />\n        </template>\n        {{ $t(\"core.common.buttons.new\") }}\n      </VButton>\n    </template>\n  </VPageHeader>\n\n  <div class=\"m-0 md:m-4\">\n    <VCard :body-class=\"['!p-0']\">\n      <template #header>\n        <div class=\"block w-full bg-gray-50 px-4 py-3\">\n          <div\n            class=\"relative flex flex-col flex-wrap items-start justify-between gap-4 sm:flex-row sm:items-center\"\n          >\n            <SearchInput v-model=\"keyword\" />\n\n            <VSpace spacing=\"lg\" class=\"flex-wrap\">\n              <FilterCleanButton\n                v-if=\"hasFilters\"\n                @click=\"handleClearFilters\"\n              />\n              <FilterDropdown\n                v-model=\"selectedPublishPhase\"\n                :label=\"$t('core.common.filters.labels.status')\"\n                :items=\"[\n                  {\n                    label: $t('core.common.filters.item_labels.all'),\n                    value: undefined,\n                  },\n                  {\n                    label: $t('core.post.filters.status.items.published'),\n                    value: 'PUBLISHED',\n                  },\n                  {\n                    label: $t('core.post.filters.status.items.draft'),\n                    value: 'DRAFT',\n                  },\n                ]\"\n              />\n              <div\n                class=\"group cursor-pointer rounded p-1 hover:bg-gray-200\"\n                @click=\"refetch()\"\n              >\n                <IconRefreshLine\n                  v-tooltip=\"$t('core.common.buttons.refresh')\"\n                  :class=\"{ 'animate-spin text-gray-900': isFetching }\"\n                  class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                />\n              </div>\n            </VSpace>\n          </div>\n        </div>\n      </template>\n      <VLoading v-if=\"isLoading\" />\n      <Transition v-else-if=\"!posts?.items.length\" appear name=\"fade\">\n        <VEmpty\n          :message=\"$t('core.post.empty.message')\"\n          :title=\"$t('core.post.empty.title')\"\n        >\n          <template #actions>\n            <VSpace>\n              <VButton @click=\"refetch\">\n                {{ $t(\"core.common.buttons.refresh\") }}\n              </VButton>\n              <VButton\n                v-permission=\"['system:posts:manage']\"\n                :route=\"{ name: 'PostEditor' }\"\n                type=\"secondary\"\n              >\n                <template #icon>\n                  <IconAddCircle />\n                </template>\n                {{ $t(\"core.common.buttons.new\") }}\n              </VButton>\n            </VSpace>\n          </template>\n        </VEmpty>\n      </Transition>\n      <Transition v-else appear name=\"fade\">\n        <VEntityContainer>\n          <PostListItem\n            v-for=\"post in posts.items\"\n            :key=\"post.post.metadata.name\"\n            :post=\"post\"\n          />\n        </VEntityContainer>\n      </Transition>\n\n      <template #footer>\n        <VPagination\n          v-model:page=\"page\"\n          v-model:size=\"size\"\n          :page-label=\"$t('core.components.pagination.page_label')\"\n          :size-label=\"$t('core.components.pagination.size_label')\"\n          :total-label=\"\n            $t('core.components.pagination.total_label', {\n              total: posts?.total || 0,\n            })\n          \"\n          :total=\"posts?.total || 0\"\n          :size-options=\"[20, 30, 50, 100]\"\n        />\n      </template>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/components/PostCreationModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Content, Post } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useMutation } from \"@tanstack/vue-query\";\nimport { nextTick, ref, useTemplateRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport { contentAnnotations } from \"@/constants/annotations\";\nimport { usePostPublishMutate } from \"../composables/use-post-publish-mutate\";\nimport type { PostFormState } from \"../types\";\nimport PostSettingForm from \"./PostSettingForm.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    title: string;\n    content: Content;\n    publish?: boolean;\n    post: Post;\n  }>(),\n  {\n    publish: false,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"success\", post: Post): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutateAsync: postPublishMutate } = usePostPublishMutate();\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"uc:create-post\"],\n  mutationFn: async ({\n    data,\n    annotations,\n  }: {\n    data: PostFormState;\n    annotations: { [key: string]: string };\n  }) => {\n    const post: Post = {\n      apiVersion: \"content.halo.run/v1alpha1\",\n      kind: \"Post\",\n      metadata: {\n        annotations: {\n          ...annotations,\n          [contentAnnotations.CONTENT_JSON]: JSON.stringify(props.content),\n        },\n        name: utils.id.uuid(),\n      },\n      spec: {\n        allowComment: data.allowComment,\n        categories: data.categories,\n        cover: data.cover,\n        deleted: false,\n        excerpt: {\n          autoGenerate: data.excerptAutoGenerate,\n          raw: data.excerptRaw,\n        },\n        htmlMetas: [],\n        pinned: data.pinned,\n        priority: 0,\n        publish: props.publish,\n        publishTime: data.publishTime,\n        slug: data.slug,\n        tags: data.tags,\n        title: data.title,\n        visible: data.visible,\n      },\n    };\n\n    const { data: createdPost } = await ucApiClient.content.post.createMyPost({\n      post,\n    });\n\n    if (props.publish) {\n      await postPublishMutate({ name: post.metadata.name });\n    }\n\n    return createdPost;\n  },\n  onSuccess(data) {\n    if (props.publish) {\n      Toast.success(t(\"core.common.toast.publish_success\"));\n    } else {\n      Toast.success(t(\"core.common.toast.save_success\"));\n    }\n\n    emit(\"success\", data);\n    modal.value?.close();\n  },\n  onError() {\n    if (props.publish) {\n      Toast.error(t(\"core.common.toast.publish_failed_and_retry\"));\n    } else {\n      Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n    }\n  },\n});\n\nconst annotationsFormRef =\n  useTemplateRef<InstanceType<typeof AnnotationsForm>>(\"annotationsFormRef\");\n\nasync function onSubmit(data: PostFormState) {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  mutate({\n    data,\n    annotations: {\n      ...annotations,\n      ...customAnnotations,\n    },\n  });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"title\"\n    :width=\"700\"\n    centered\n    @close=\"emit('close')\"\n  >\n    <PostSettingForm\n      :form-state=\"{\n        title: props.post.spec.title,\n        slug: props.post.spec.slug,\n        allowComment: props.post.spec.allowComment,\n        visible: props.post.spec.visible,\n        pinned: props.post.spec.pinned,\n        excerptAutoGenerate: props.post.spec.excerpt.autoGenerate,\n      }\"\n      @submit=\"onSubmit\"\n    />\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.post.settings.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"post.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :value=\"post.metadata.annotations || {}\"\n          kind=\"Post\"\n          :form-data=\"post\"\n          group=\"content.halo.run\"\n        />\n      </div>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          @click=\"$formkit.submit('post-setting-form')\"\n        >\n          {{\n            props.publish\n              ? $t(\"core.common.buttons.publish\")\n              : $t(\"core.common.buttons.save\")\n          }}\n        </VButton>\n        <VButton type=\"default\" @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/components/PostListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport PostTag from \"@console/modules/contents/posts/tags/components/PostTag.vue\";\nimport type { ListedPost } from \"@halo-dev/api-client\";\nimport { GetThumbnailByUriSizeEnum, ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconExternalLinkLine,\n  IconEye,\n  IconEyeOff,\n  IconTimerLine,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VSpace,\n  VStatusDot,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport StatusDotField from \"@/components/entity-fields/StatusDotField.vue\";\nimport PostContributorList from \"@/components/user/PostContributorList.vue\";\nimport { postLabels } from \"@/constants/labels\";\nimport { usePostPublishMutate } from \"../composables/use-post-publish-mutate\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst props = withDefaults(\n  defineProps<{\n    post: ListedPost;\n  }>(),\n  {}\n);\n\nconst externalUrl = computed(() => {\n  const { status, metadata } = props.post.post;\n  if (metadata.labels?.[postLabels.PUBLISHED] === \"true\") {\n    return status?.permalink;\n  }\n  return `/preview/posts/${metadata.name}`;\n});\n\nconst publishStatus = computed(() => {\n  const { labels } = props.post.post.metadata;\n  return labels?.[postLabels.PUBLISHED] === \"true\"\n    ? t(\"core.post.filters.status.items.published\")\n    : t(\"core.post.filters.status.items.draft\");\n});\n\nconst isPublished = computed(() => {\n  const {\n    [postLabels.PUBLISHED]: published,\n    [postLabels.SCHEDULING_PUBLISH]: schedulingPublish,\n  } = props.post.post.metadata.labels || {};\n  return published !== \"true\" && schedulingPublish !== \"true\";\n});\n\nconst isPublishing = computed(() => {\n  const { spec, metadata } = props.post.post;\n  return (\n    spec.publish &&\n    metadata.labels?.[postLabels.PUBLISHED] !== \"true\" &&\n    metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== \"true\"\n  );\n});\n\nconst { mutateAsync: postPublishMutate } = usePostPublishMutate();\n\nasync function handlePublish() {\n  try {\n    await postPublishMutate({ name: props.post.post.metadata.name });\n\n    Toast.success(t(\"core.common.toast.publish_success\"));\n    queryClient.invalidateQueries({ queryKey: [\"my-posts\"] });\n  } catch (_) {\n    Toast.error(t(\"core.common.toast.publish_failed_and_retry\"));\n  }\n}\n\nfunction handleUnpublish() {\n  Dialog.warning({\n    title: t(\"core.uc_post.operations.cancel_publish.title\"),\n    description: t(\"core.uc_post.operations.cancel_publish.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      await ucApiClient.content.post.unpublishMyPost({\n        name: props.post.post.metadata.name,\n      });\n\n      Toast.success(t(\"core.common.toast.cancel_publish_success\"));\n      queryClient.invalidateQueries({ queryKey: [\"my-posts\"] });\n    },\n  });\n}\n\nfunction handleDelete() {\n  Dialog.warning({\n    title: t(\"core.uc_post.operations.delete.title\"),\n    description: t(\"core.uc_post.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      await ucApiClient.content.post.recycleMyPost({\n        name: props.post.post.metadata.name,\n      });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n\n      queryClient.invalidateQueries({ queryKey: [\"my-posts\"] });\n    },\n  });\n}\n</script>\n\n<template>\n  <VEntity>\n    <template #start>\n      <VEntityField v-if=\"post.post.spec.cover\">\n        <template #description>\n          <div class=\"aspect-h-2 aspect-w-3 w-20 overflow-hidden rounded-md\">\n            <img\n              class=\"h-full w-full object-cover\"\n              :src=\"\n                utils.attachment.getThumbnailUrl(\n                  post.post.spec.cover,\n                  GetThumbnailByUriSizeEnum.S\n                )\n              \"\n            />\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"post.post.spec.title\"\n        :route=\"{\n          name: 'PostEditor',\n          query: { name: post.post.metadata.name },\n        }\"\n        max-width=\"30rem\"\n      >\n        <template #extra>\n          <VSpace class=\"mt-1 sm:mt-0\">\n            <RouterLink\n              v-if=\"post.post.status?.inProgress\"\n              v-tooltip=\"$t('core.common.tooltips.unpublished_content_tip')\"\n              class=\"flex items-center\"\n              :to=\"{\n                name: 'PostEditor',\n                query: { name: post.post.metadata.name },\n              }\"\n            >\n              <VStatusDot state=\"success\" animate />\n            </RouterLink>\n            <a\n              target=\"_blank\"\n              :href=\"externalUrl\"\n              class=\"text-gray-600 opacity-0 transition-all hover:text-gray-900 group-hover:opacity-100\"\n            >\n              <IconExternalLinkLine class=\"h-3.5 w-3.5\" />\n            </a>\n          </VSpace>\n        </template>\n        <template #description>\n          <div class=\"flex flex-col gap-1.5\">\n            <VSpace class=\"flex-wrap !gap-y-1\">\n              <p\n                v-if=\"post.categories.length\"\n                class=\"inline-flex flex-wrap gap-1 text-xs text-gray-500\"\n              >\n                {{ $t(\"core.post.list.fields.categories\") }}\n                <a\n                  v-for=\"(category, categoryIndex) in post.categories\"\n                  :key=\"categoryIndex\"\n                  :href=\"category.status?.permalink\"\n                  :title=\"category.status?.permalink\"\n                  target=\"_blank\"\n                  class=\"cursor-pointer hover:text-gray-900\"\n                >\n                  {{ category.spec.displayName }}\n                </a>\n              </p>\n              <span class=\"text-xs text-gray-500\">\n                {{\n                  $t(\"core.post.list.fields.visits\", {\n                    visits: post.stats.visit,\n                  })\n                }}\n              </span>\n              <span class=\"text-xs text-gray-500\">\n                {{\n                  $t(\"core.post.list.fields.comments\", {\n                    comments: post.stats.totalComment || 0,\n                  })\n                }}\n              </span>\n              <span v-if=\"post.post.spec.pinned\" class=\"text-xs text-gray-500\">\n                {{ $t(\"core.post.list.fields.pinned\") }}\n              </span>\n            </VSpace>\n            <VSpace v-if=\"post.tags.length\" class=\"flex-wrap\">\n              <PostTag\n                v-for=\"(tag, tagIndex) in post.tags\"\n                :key=\"tagIndex\"\n                :tag=\"tag\"\n                :route=\"false\"\n              ></PostTag>\n            </VSpace>\n          </div>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField>\n        <template #description>\n          <PostContributorList\n            :owner=\"post.owner\"\n            :contributors=\"post.contributors\"\n            :allow-view-user-detail=\"false\"\n          />\n        </template>\n      </VEntityField>\n      <VEntityField :description=\"publishStatus\">\n        <template v-if=\"isPublishing\" #description>\n          <VStatusDot :text=\"$t('core.common.tooltips.publishing')\" animate />\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <IconEye\n            v-if=\"post.post.spec.visible === 'PUBLIC'\"\n            v-tooltip=\"$t('core.post.filters.visible.items.public')\"\n            class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n          />\n          <IconEyeOff\n            v-if=\"post.post.spec.visible === 'PRIVATE'\"\n            v-tooltip=\"$t('core.post.filters.visible.items.private')\"\n            class=\"cursor-pointer text-sm transition-all hover:text-blue-600\"\n          />\n        </template>\n      </VEntityField>\n      <StatusDotField\n        v-if=\"props.post.post.spec.deleted\"\n        :tooltip=\"$t('core.common.status.deleting')\"\n        state=\"warning\"\n        animate\n      />\n      <VEntityField v-if=\"post.post.spec.publishTime\">\n        <template #description>\n          <div class=\"inline-flex items-center space-x-2\">\n            <span\n              v-tooltip=\"utils.date.format(post.post.spec.publishTime)\"\n              class=\"entity-field-description\"\n            >\n              {{ utils.date.timeAgo(post.post.spec.publishTime) }}\n            </span>\n            <IconTimerLine\n              v-if=\"\n                post.post.metadata.labels?.[postLabels.SCHEDULING_PUBLISH] ===\n                'true'\n              \"\n              v-tooltip=\"$t('core.post.list.fields.schedule_publish.tooltip')\"\n              class=\"text-sm\"\n            />\n          </div>\n        </template>\n      </VEntityField>\n    </template>\n    <template #dropdownItems>\n      <HasPermission v-if=\"isPublished\" :permissions=\"['uc:posts:publish']\">\n        <VDropdownItem @click=\"handlePublish\">\n          {{ $t(\"core.common.buttons.publish\") }}\n        </VDropdownItem>\n      </HasPermission>\n      <VDropdownItem\n        @click=\"\n          $router.push({\n            name: 'PostEditor',\n            query: { name: post.post.metadata.name },\n          })\n        \"\n      >\n        {{ $t(\"core.common.buttons.edit\") }}\n      </VDropdownItem>\n      <HasPermission v-if=\"!isPublished\" :permissions=\"['uc:posts:publish']\">\n        <VDropdownItem type=\"danger\" @click=\"handleUnpublish\">\n          {{ $t(\"core.common.buttons.cancel_publish\") }}\n        </VDropdownItem>\n      </HasPermission>\n      <HasPermission :permissions=\"['uc:posts:recycle']\">\n        <VDropdownDivider />\n        <VDropdownItem type=\"danger\" @click=\"handleDelete\">\n          {{ $t(\"core.common.buttons.delete\") }}\n        </VDropdownItem>\n      </HasPermission>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/components/PostSettingEditModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Post } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { usePostUpdateMutate } from \"@uc/modules/contents/posts/composables/use-post-update-mutate\";\nimport { nextTick, ref, useTemplateRef } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type AnnotationsForm from \"@/components/form/AnnotationsForm.vue\";\nimport type { PostFormState } from \"../types\";\nimport PostSettingForm from \"./PostSettingForm.vue\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    post: Post;\n  }>(),\n  {}\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n  (event: \"success\", post: Post): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutateAsync, isLoading } = usePostUpdateMutate();\n\nconst annotationsFormRef =\n  useTemplateRef<InstanceType<typeof AnnotationsForm>>(\"annotationsFormRef\");\n\nasync function onSubmit(data: PostFormState) {\n  annotationsFormRef.value?.handleSubmit();\n  await nextTick();\n\n  const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =\n    annotationsFormRef.value || {};\n\n  if (customFormInvalid || specFormInvalid) {\n    return;\n  }\n\n  const postToUpdate: Post = {\n    ...props.post,\n    metadata: {\n      ...props.post.metadata,\n      annotations: {\n        ...(props.post.metadata.annotations || {}),\n        ...annotations,\n        ...customAnnotations,\n      },\n    },\n    spec: {\n      ...props.post.spec,\n      allowComment: data.allowComment,\n      categories: data.categories,\n      cover: data.cover,\n      excerpt: {\n        autoGenerate: data.excerptAutoGenerate,\n        raw: data.excerptRaw,\n      },\n      pinned: data.pinned,\n      publishTime: data.publishTime,\n      slug: data.slug,\n      tags: data.tags,\n      title: data.title,\n      visible: data.visible,\n    },\n  };\n\n  const { data: newPost } = await mutateAsync({ postToUpdate });\n\n  Toast.success(t(\"core.common.toast.save_success\"));\n  emit(\"success\", newPost);\n  modal.value?.close();\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"$t('core.uc_post.setting_modal.title')\"\n    :width=\"700\"\n    centered\n    @close=\"emit('close')\"\n  >\n    <PostSettingForm\n      :form-state=\"{\n        title: post.spec.title,\n        slug: post.spec.slug,\n        cover: post.spec.cover,\n        categories: post.spec.categories,\n        tags: post.spec.tags,\n        allowComment: post.spec.allowComment,\n        visible: post.spec.visible,\n        pinned: post.spec.pinned,\n        publishTime: post.spec.publishTime\n          ? utils.date.toDatetimeLocal(post.spec.publishTime)\n          : undefined,\n        excerptAutoGenerate: post.spec.excerpt.autoGenerate,\n        excerptRaw: post.spec.excerpt.raw,\n      }\"\n      :name=\"post.metadata.name\"\n      update-mode\n      @submit=\"onSubmit\"\n    />\n\n    <div class=\"py-5\">\n      <div class=\"border-t border-gray-200\"></div>\n    </div>\n\n    <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n      <div class=\"md:col-span-1\">\n        <div class=\"sticky top-0\">\n          <span class=\"text-base font-medium text-gray-900\">\n            {{ $t(\"core.post.settings.groups.annotations\") }}\n          </span>\n        </div>\n      </div>\n      <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n        <AnnotationsForm\n          :key=\"post.metadata.name\"\n          ref=\"annotationsFormRef\"\n          :value=\"post.metadata.annotations || {}\"\n          kind=\"Post\"\n          :form-data=\"post\"\n          group=\"content.halo.run\"\n        />\n      </div>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          @click=\"$formkit.submit('post-setting-form')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton type=\"default\" @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/components/PostSettingForm.vue",
    "content": "<script lang=\"ts\" setup>\nimport useSlugify from \"@console/composables/use-slugify\";\nimport type { FormKitNode } from \"@formkit/core\";\nimport { publicApiClient } from \"@halo-dev/api-client\";\nimport { IconRefreshLine } from \"@halo-dev/components\";\nimport { FormType, utils } from \"@halo-dev/ui-shared\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport type { PostFormState } from \"../types\";\n\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    name?: string;\n    formState?: PostFormState;\n    updateMode?: boolean;\n  }>(),\n  {\n    name: undefined,\n    formState: undefined,\n    updateMode: false,\n  }\n);\n\nconst internalFormState = ref<PostFormState>(\n  props.formState || {\n    title: \"\",\n    slug: \"\",\n    categories: [],\n    tags: [],\n    excerptAutoGenerate: true,\n    excerptRaw: \"\",\n    allowComment: true,\n    pinned: false,\n    visible: \"PUBLIC\",\n    publishTime: undefined,\n    cover: undefined,\n  }\n);\n\nconst emit = defineEmits<{\n  (event: \"submit\", data: PostFormState): void;\n}>();\n\nfunction onSubmit(data: PostFormState) {\n  emit(\"submit\", {\n    ...data,\n    publishTime: data.publishTime\n      ? utils.date.toISOString(data.publishTime)\n      : undefined,\n  });\n}\n\n// slug\nconst { handleGenerateSlug } = useSlugify(\n  computed(() => internalFormState.value?.title || \"\"),\n  computed({\n    get() {\n      return internalFormState.value?.slug || \"\";\n    },\n    set(value) {\n      internalFormState.value.slug = value;\n    },\n  }),\n  computed(() => !props.updateMode),\n  FormType.POST\n);\n\n// fixme: check if slug is unique\n// Finally, we need to check if the slug is unique in the database\nasync function slugUniqueValidation(node: FormKitNode) {\n  const value = node.value;\n  if (!value) {\n    return true;\n  }\n\n  const fieldSelector = [`spec.slug=${value}`];\n\n  if (props.name) {\n    fieldSelector.push(`metadata.name!=${props.name}`);\n  }\n\n  const { data: postsWithSameSlug } =\n    await publicApiClient.content.post.queryPosts({\n      fieldSelector,\n      page: 1,\n      size: 1,\n    });\n\n  return !postsWithSameSlug.total;\n}\n\nconst isScheduledPublish = computed(() => {\n  const { publishTime } = internalFormState.value;\n  return publishTime && new Date(publishTime) > new Date();\n});\n\nconst publishTimeHelp = computed(() => {\n  return isScheduledPublish.value\n    ? t(\"core.post.settings.fields.publish_time.help.schedule_publish\", {\n        datetime: utils.date.format(internalFormState.value.publishTime),\n      })\n    : \"\";\n});\n</script>\n\n<template>\n  <!-- @vue-ignore -->\n  <FormKit\n    id=\"post-setting-form\"\n    v-model=\"internalFormState\"\n    type=\"form\"\n    name=\"post-setting-form\"\n    :config=\"{ validationVisibility: 'submit' }\"\n    @submit=\"onSubmit\"\n  >\n    <div>\n      <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n        <div class=\"md:col-span-1\">\n          <div class=\"sticky top-0\">\n            <span class=\"text-base font-medium text-gray-900\">\n              {{ $t(\"core.post.settings.groups.general\") }}\n            </span>\n          </div>\n        </div>\n        <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n          <FormKit\n            :label=\"$t('core.post.settings.fields.title.label')\"\n            type=\"text\"\n            name=\"title\"\n            validation=\"required|length:0,1024\"\n          ></FormKit>\n          <FormKit\n            :label=\"$t('core.post.settings.fields.slug.label')\"\n            name=\"slug\"\n            type=\"text\"\n            validation=\"required|length:0,1024|slugUniqueValidation\"\n            :validation-rules=\"{ slugUniqueValidation }\"\n            :validation-messages=\"{\n              slugUniqueValidation: $t(\n                'core.common.form.validation.slug_unique'\n              ),\n            }\"\n            :help=\"$t('core.post.settings.fields.slug.help')\"\n          >\n            <template #suffix>\n              <div\n                v-tooltip=\"$t('core.post.settings.fields.slug.refresh_message')\"\n                class=\"group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100\"\n                @click=\"handleGenerateSlug(true)\"\n              >\n                <IconRefreshLine\n                  class=\"h-4 w-4 text-gray-500 group-hover:text-gray-700\"\n                />\n              </div>\n            </template>\n          </FormKit>\n          <FormKit\n            :label=\"$t('core.post.settings.fields.categories.label')\"\n            name=\"categories\"\n            type=\"categorySelect\"\n            :multiple=\"true\"\n          />\n          <FormKit\n            :label=\"$t('core.post.settings.fields.tags.label')\"\n            name=\"tags\"\n            type=\"tagSelect\"\n            :multiple=\"true\"\n          />\n          <FormKit\n            :value=\"true\"\n            name=\"excerptAutoGenerate\"\n            :label=\"$t('core.post.settings.fields.auto_generate_excerpt.label')\"\n            type=\"checkbox\"\n          >\n          </FormKit>\n          <FormKit\n            v-if=\"!internalFormState.excerptAutoGenerate\"\n            :label=\"$t('core.post.settings.fields.raw_excerpt.label')\"\n            name=\"excerptRaw\"\n            type=\"textarea\"\n            auto-height\n            :max-auto-height=\"200\"\n            validation=\"length:0,1024\"\n          ></FormKit>\n          <HasPermission\n            :permissions=\"['system:attachments:view', 'uc:attachments:manage']\"\n          >\n            <FormKit\n              name=\"cover\"\n              :label=\"$t('core.post.settings.fields.cover.label')\"\n              type=\"attachment\"\n              width=\"50%\"\n              aspect-ratio=\"16/9\"\n              :accepts=\"['image/*']\"\n              validation=\"length:0,1024\"\n            ></FormKit>\n          </HasPermission>\n        </div>\n      </div>\n\n      <div class=\"py-5\">\n        <div class=\"border-t border-gray-200\"></div>\n      </div>\n\n      <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n        <div class=\"md:col-span-1\">\n          <div class=\"sticky top-0\">\n            <span class=\"text-base font-medium text-gray-900\">\n              {{ $t(\"core.post.settings.groups.advanced\") }}\n            </span>\n          </div>\n        </div>\n        <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n          <FormKit\n            name=\"allowComment\"\n            :label=\"$t('core.post.settings.fields.allow_comment.label')\"\n            type=\"checkbox\"\n          ></FormKit>\n          <FormKit\n            :label=\"$t('core.post.settings.fields.pinned.label')\"\n            name=\"pinned\"\n            type=\"checkbox\"\n          ></FormKit>\n          <FormKit\n            :options=\"[\n              { label: $t('core.common.select.public'), value: 'PUBLIC' },\n              {\n                label: $t('core.common.select.private'),\n                value: 'PRIVATE',\n              },\n            ]\"\n            :label=\"$t('core.post.settings.fields.visible.label')\"\n            name=\"visible\"\n            type=\"select\"\n          ></FormKit>\n          <FormKit\n            name=\"publishTime\"\n            :label=\"$t('core.post.settings.fields.publish_time.label')\"\n            type=\"datetime-local\"\n            min=\"0000-01-01T00:00\"\n            max=\"9999-12-31T23:59\"\n            :help=\"publishTimeHelp\"\n          ></FormKit>\n        </div>\n      </div>\n    </div>\n  </FormKit>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/composables/use-post-publish-mutate.ts",
    "content": "import { ucApiClient } from \"@halo-dev/api-client\";\nimport { useMutation } from \"@tanstack/vue-query\";\n\nexport function usePostPublishMutate() {\n  return useMutation({\n    mutationKey: [\"uc:publish-post\"],\n    mutationFn: async ({ name }: { name: string }) => {\n      return await ucApiClient.content.post.publishMyPost(\n        {\n          name: name,\n        },\n        {\n          mute: true,\n        }\n      );\n    },\n    retry: 3,\n  });\n}\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/composables/use-post-update-mutate.ts",
    "content": "import type { Post } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Toast } from \"@halo-dev/components\";\nimport { useMutation } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nexport function usePostUpdateMutate() {\n  const { t } = useI18n();\n\n  return useMutation({\n    mutationKey: [\"uc:update-post\"],\n    mutationFn: async ({ postToUpdate }: { postToUpdate: Post }) => {\n      const { data: latestPost } = await ucApiClient.content.post.getMyPost({\n        name: postToUpdate.metadata.name,\n      });\n\n      return await ucApiClient.content.post.updateMyPost(\n        {\n          name: postToUpdate.metadata.name,\n          post: {\n            ...latestPost,\n            spec: {\n              ...latestPost.spec,\n              ...postToUpdate.spec,\n            },\n            metadata: {\n              ...latestPost.metadata,\n              annotations: postToUpdate.metadata.annotations,\n            },\n          },\n        },\n        {\n          mute: true,\n        }\n      );\n    },\n    retry: 3,\n    onError: () => {\n      Toast.error(t(\"core.common.toast.save_failed_and_retry\"));\n    },\n  });\n}\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/module.ts",
    "content": "import { IconBookRead } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport BasicLayout from \"@uc/layouts/BasicLayout.vue\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  ucRoutes: [\n    {\n      path: \"/posts\",\n      name: \"PostsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.uc_post.title\",\n        searchable: true,\n        permissions: [\"uc:posts:manage\"],\n        menu: {\n          name: \"core.uc_sidebar.menu.items.posts\",\n          group: \"content\",\n          icon: markRaw(IconBookRead),\n          priority: 0,\n          mobile: true,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Posts\",\n          component: () => import(\"./PostList.vue\"),\n        },\n        {\n          path: \"editor\",\n          name: \"PostEditor\",\n          component: () => import(\"./PostEditor.vue\"),\n          meta: {\n            title: \"core.post_editor.title\",\n            searchable: true,\n            hideFooter: true,\n            permissions: [\"uc:posts:manage\"],\n          },\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/uc-src/modules/contents/posts/types/index.ts",
    "content": "import type { PostSpecVisibleEnum } from \"@halo-dev/api-client\";\n\nexport interface PostFormState {\n  title: string;\n  slug: string;\n  categories?: string[];\n  tags?: string[];\n  excerptAutoGenerate: boolean;\n  excerptRaw?: string;\n  allowComment: boolean;\n  pinned: boolean;\n  visible: PostSpecVisibleEnum;\n  publishTime?: string;\n  cover?: string;\n}\n"
  },
  {
    "path": "ui/uc-src/modules/index.ts",
    "content": "import type { PluginModule } from \"@halo-dev/ui-shared\";\n\nconst modules = import.meta.glob(\"./**/module.ts\", {\n  eager: true,\n  import: \"default\",\n}) as Record<string, PluginModule>;\n\nexport default modules;\n"
  },
  {
    "path": "ui/uc-src/modules/notifications/Notifications.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  paginate,\n  ucApiClient,\n  type Notification,\n  type NotificationV1alpha1UcApiListUserNotificationsRequest,\n} from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  IconCheckboxCircle,\n  IconDeleteBin,\n  IconNotificationBadgeLine,\n  Toast,\n  VButton,\n  VCard,\n  VEmpty,\n  VLoading,\n  VPageHeader,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { chunk } from \"es-toolkit\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport NotificationContent from \"./components/NotificationContent.vue\";\nimport NotificationListItem from \"./components/NotificationListItem.vue\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\nconst { currentUser } = stores.currentUser();\n\nconst activeTab = useRouteQuery(\"tab\", \"unread\");\n\nconst {\n  data: notifications,\n  isLoading,\n  refetch,\n  isFetching,\n} = useQuery({\n  queryKey: [\"user-notifications\", activeTab],\n  queryFn: async () => {\n    return await paginate<\n      NotificationV1alpha1UcApiListUserNotificationsRequest,\n      Notification\n    >(\n      (params) =>\n        ucApiClient.notification.notification.listUserNotifications(params),\n      {\n        username: currentUser?.user.metadata.name as string,\n        fieldSelector: [`spec.unread=${activeTab.value === \"unread\"}`],\n        size: 1000,\n      }\n    );\n  },\n  cacheTime: 0,\n  refetchInterval(data) {\n    const hasDeletingNotifications = data?.some(\n      (item) => item.metadata.deletionTimestamp !== undefined\n    );\n\n    return hasDeletingNotifications ? 1000 : false;\n  },\n});\n\nconst selectedNotificationName = useRouteQuery<string | undefined>(\"name\");\n\nconst selectedNotification = computed(() => {\n  return notifications.value?.find(\n    (item) => item.metadata.name === selectedNotificationName.value\n  );\n});\n\nfunction handleDeleteNotifications() {\n  Dialog.warning({\n    title: t(\"core.uc_notification.operations.delete_all.title\"),\n    description: t(\"core.uc_notification.operations.delete_all.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    confirmType: \"danger\",\n    onConfirm: async () => {\n      if (!notifications.value || notifications.value.length === 0) {\n        return;\n      }\n\n      if (!currentUser) {\n        throw new Error(\"Current user is not found\");\n      }\n\n      const notificationChunks = chunk(notifications.value, 5);\n\n      for (const chunk of notificationChunks) {\n        await Promise.all(\n          chunk.map((notification) =>\n            ucApiClient.notification.notification.deleteSpecifiedNotification({\n              username: currentUser.user.metadata.name,\n              name: notification.metadata.name,\n            })\n          )\n        );\n      }\n\n      await queryClient.invalidateQueries({ queryKey: [\"user-notifications\"] });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n}\n\nfunction handleMarkAllAsRead() {\n  Dialog.warning({\n    title: t(\"core.uc_notification.operations.mark_all_as_read.title\"),\n    description: t(\n      \"core.uc_notification.operations.mark_all_as_read.description\"\n    ),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    onConfirm: async () => {\n      if (!notifications.value || notifications.value.length === 0) {\n        return;\n      }\n\n      if (!currentUser) {\n        throw new Error(\"Current user is not found\");\n      }\n\n      const names = notifications.value?.map(\n        (notification) => notification.metadata.name\n      );\n\n      await ucApiClient.notification.notification.markNotificationsAsRead({\n        username: currentUser.user.metadata.name,\n        markSpecifiedRequest: {\n          names,\n        },\n      });\n\n      await queryClient.invalidateQueries({ queryKey: [\"user-notifications\"] });\n    },\n  });\n}\n</script>\n\n<template>\n  <VPageHeader :title=\"$t('core.uc_notification.title')\">\n    <template #icon>\n      <IconNotificationBadgeLine />\n    </template>\n  </VPageHeader>\n  <div class=\"m-0 md:m-4\">\n    <VCard\n      style=\"height: calc(100vh - 5.5rem)\"\n      :body-class=\"['h-full', '!p-0']\"\n    >\n      <div class=\"grid h-full grid-cols-12 divide-y sm:divide-x sm:divide-y-0\">\n        <div\n          class=\"relative col-span-12 flex h-full flex-col overflow-hidden sm:col-span-6 lg:col-span-5 xl:col-span-3\"\n        >\n          <div class=\"sticky top-0 z-10 flex-none\">\n            <VTabbar\n              v-model:active-id=\"activeTab\"\n              class=\"!rounded-none\"\n              :items=\"[\n                { id: 'unread', label: $t('core.uc_notification.tabs.unread') },\n                { id: 'read', label: $t('core.uc_notification.tabs.read') },\n              ]\"\n              type=\"outline\"\n              @change=\"selectedNotificationName = undefined\"\n            ></VTabbar>\n\n            <div\n              class=\"absolute right-4 top-1/2 flex -translate-y-1/2 items-center gap-2\"\n            >\n              <button\n                v-if=\"activeTab === 'unread'\"\n                class=\"flex h-7 w-7 cursor-pointer items-center justify-center rounded-full hover:bg-gray-200 disabled:pointer-events-none disabled:opacity-70\"\n                :disabled=\"!notifications?.length\"\n                @click=\"handleMarkAllAsRead\"\n              >\n                <IconCheckboxCircle\n                  class=\"h-4 w-4 text-gray-600 group-hover:text-gray-900\"\n                />\n              </button>\n              <button\n                class=\"group flex h-7 w-7 cursor-pointer items-center justify-center rounded-full hover:bg-gray-200 disabled:pointer-events-none disabled:opacity-70\"\n                :disabled=\"!notifications?.length\"\n                @click=\"handleDeleteNotifications\"\n              >\n                <IconDeleteBin\n                  class=\"h-4 w-4 text-gray-600 group-hover:text-red-600\"\n                />\n              </button>\n            </div>\n          </div>\n          <OverlayScrollbarsComponent\n            element=\"div\"\n            :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n            class=\"h-full w-full flex-1\"\n            defer\n          >\n            <VLoading v-if=\"isLoading\" />\n            <Transition v-else-if=\"!notifications?.length\" appear name=\"fade\">\n              <VEmpty\n                :title=\"`${\n                  activeTab === 'unread'\n                    ? $t('core.uc_notification.empty.titles.unread')\n                    : $t('core.uc_notification.empty.titles.read')\n                }`\"\n              >\n                <template #actions>\n                  <VButton :loading=\"isFetching && !isLoading\" @click=\"refetch\">\n                    {{ $t(\"core.common.buttons.refresh\") }}\n                  </VButton>\n                </template>\n              </VEmpty>\n            </Transition>\n            <Transition v-else appear name=\"fade\">\n              <ul\n                class=\"box-border h-full w-full divide-y divide-gray-100\"\n                role=\"list\"\n              >\n                <li\n                  v-for=\"notification in notifications\"\n                  :key=\"notification.metadata.name\"\n                  @click=\"selectedNotificationName = notification.metadata.name\"\n                >\n                  <NotificationListItem\n                    :notification=\"notification\"\n                    :is-selected=\"\n                      selectedNotificationName === notification.metadata.name\n                    \"\n                  />\n                </li>\n              </ul>\n            </Transition>\n          </OverlayScrollbarsComponent>\n        </div>\n        <div\n          class=\"col-span-12 overflow-auto sm:col-span-6 lg:col-span-7 xl:col-span-9\"\n        >\n          <NotificationContent :notification=\"selectedNotification\" />\n        </div>\n      </div>\n    </VCard>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/notifications/components/NotificationContent.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Notification } from \"@halo-dev/api-client\";\nimport { OverlayScrollbarsComponent } from \"overlayscrollbars-vue\";\nimport sanitize from \"sanitize-html\";\nimport { computed } from \"vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    notification?: Notification;\n  }>(),\n  { notification: undefined }\n);\n\nconst content = computed(() => {\n  return sanitize(props.notification?.spec?.htmlContent || \"\");\n});\n</script>\n\n<template>\n  <OverlayScrollbarsComponent\n    element=\"div\"\n    :options=\"{ scrollbars: { autoHide: 'scroll' } }\"\n    class=\"h-full w-full\"\n    defer\n  >\n    <div class=\"markdown-body h-full w-full p-2 text-sm\" v-html=\"content\"></div>\n  </OverlayScrollbarsComponent>\n</template>\n\n<style scoped lang=\"scss\">\n.markdown-body :deep(ul) {\n  list-style: disc !important;\n}\n\n.markdown-body :deep(ol) {\n  list-style: decimal !important;\n}\n</style>\n"
  },
  {
    "path": "ui/uc-src/modules/notifications/components/NotificationListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { Notification } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VStatusDot } from \"@halo-dev/components\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport sanitize from \"sanitize-html\";\nimport { computed, ref, watch } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst props = withDefaults(\n  defineProps<{\n    notification: Notification;\n    isSelected: boolean;\n  }>(),\n  {}\n);\n\nconst { currentUser } = stores.currentUser();\n\nconst isRead = ref();\n\nconst { mutate: handleMarkAsRead } = useMutation({\n  mutationKey: [\"notification-mark-as-read\"],\n  mutationFn: async ({ refetch }: { refetch: boolean }) => {\n    const { data } =\n      await ucApiClient.notification.notification.markNotificationAsRead({\n        name: props.notification.metadata.name,\n        username: currentUser?.user.metadata.name as string,\n      });\n\n    if (refetch) {\n      await queryClient.invalidateQueries({ queryKey: [\"user-notifications\"] });\n    }\n\n    return data;\n  },\n  onSuccess() {\n    isRead.value = true;\n  },\n});\n\nfunction handleDelete() {\n  Dialog.warning({\n    title: t(\"core.uc_notification.operations.delete.title\"),\n    description: t(\"core.uc_notification.operations.delete.description\"),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    confirmType: \"danger\",\n    async onConfirm() {\n      await ucApiClient.notification.notification.deleteSpecifiedNotification({\n        name: props.notification.metadata.name,\n        username: currentUser?.user.metadata.name as string,\n      });\n\n      await queryClient.invalidateQueries({ queryKey: [\"user-notifications\"] });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n    },\n  });\n}\n\nwatch(\n  () => props.isSelected,\n  (value) => {\n    if (value && props.notification.spec?.unread) {\n      handleMarkAsRead({ refetch: false });\n    }\n  },\n  {\n    immediate: true,\n  }\n);\n\nconst content = computed(() => {\n  // Clean html tags\n  return sanitize(props.notification.spec?.htmlContent || \"\", {\n    allowedTags: [],\n    allowedAttributes: {},\n  });\n});\n</script>\n<template>\n  <div\n    class=\"group relative flex cursor-pointer flex-col gap-2 p-4\"\n    :class=\"{ 'bg-gray-50': isSelected }\"\n  >\n    <div\n      v-if=\"isSelected\"\n      class=\"absolute inset-y-0 left-0 w-0.5 bg-primary\"\n    ></div>\n    <div class=\"flex items-center justify-between\">\n      <div\n        class=\"truncate text-sm\"\n        :class=\"{ 'font-semibold': notification.spec?.unread && !isRead }\"\n      >\n        {{ notification.spec?.title }}\n      </div>\n      <VStatusDot\n        v-if=\"notification.metadata.deletionTimestamp\"\n        v-tooltip=\"$t('core.common.status.deleting')\"\n        state=\"warning\"\n        animate\n      />\n    </div>\n    <div\n      v-if=\"notification.spec?.htmlContent\"\n      class=\"line-clamp-1 text-xs text-gray-600\"\n    >\n      {{ content }}\n    </div>\n    <div class=\"flex h-6 items-end justify-between\">\n      <div class=\"text-xs text-gray-600\">\n        {{ utils.date.timeAgo(notification.metadata.creationTimestamp) }}\n      </div>\n      <div class=\"hidden space-x-2 group-hover:block\">\n        <span\n          v-if=\"notification.spec?.unread && !isRead\"\n          class=\"text-sm text-gray-600 hover:text-gray-900\"\n          @click.stop=\"handleMarkAsRead({ refetch: true })\"\n        >\n          {{ $t(\"core.uc_notification.operations.mark_as_read.button\") }}\n        </span>\n        <span\n          class=\"text-sm text-red-600 hover:text-red-700\"\n          @click.stop=\"handleDelete\"\n        >\n          {{ $t(\"core.common.buttons.delete\") }}\n        </span>\n      </div>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/notifications/module.ts",
    "content": "import { IconNotificationBadgeLine } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport BasicLayout from \"@uc/layouts/BasicLayout.vue\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  ucRoutes: [\n    {\n      path: \"/notifications\",\n      name: \"NotificationsRoot\",\n      component: BasicLayout,\n      meta: {\n        title: \"core.uc_notification.title\",\n        searchable: true,\n        hideFooter: true,\n        menu: {\n          name: \"core.uc_sidebar.menu.items.notification\",\n          group: \"dashboard\",\n          icon: markRaw(IconNotificationBadgeLine),\n          priority: 1,\n          mobile: true,\n        },\n      },\n      children: [\n        {\n          path: \"\",\n          name: \"Notifications\",\n          component: () => import(\"./Notifications.vue\"),\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/uc-src/modules/profile/Profile.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  VButton,\n  VDropdown,\n  VDropdownItem,\n  VLoading,\n  VTabbar,\n} from \"@halo-dev/components\";\nimport { stores, type UserProfileTab } from \"@halo-dev/ui-shared\";\nimport { useRouteQuery } from \"@vueuse/router\";\nimport { storeToRefs } from \"pinia\";\nimport {\n  computed,\n  defineAsyncComponent,\n  onMounted,\n  ref,\n  shallowRef,\n} from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport UserAvatar from \"@/components/user-avatar/UserAvatar.vue\";\nimport { usePluginModuleStore } from \"@/stores/plugin\";\nimport PasswordChangeModal from \"./components/PasswordChangeModal.vue\";\nimport ProfileEditingModal from \"./components/ProfileEditingModal.vue\";\n\nconst { t } = useI18n();\n\nconst editingModal = ref(false);\nconst passwordChangeModal = ref(false);\n\nconst { currentUser } = storeToRefs(stores.currentUser());\nconst { fetchCurrentUser } = stores.currentUser();\nfetchCurrentUser();\n\nconst tabs = shallowRef<UserProfileTab[]>([\n  {\n    id: \"detail\",\n    label: t(\"core.uc_profile.tabs.detail\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/Detail.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 10,\n  },\n  {\n    id: \"notification-preferences\",\n    label: t(\"core.uc_profile.tabs.notification-preferences\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/NotificationPreferences.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 20,\n  },\n  {\n    id: \"pat\",\n    label: t(\"core.uc_profile.tabs.pat\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/PersonalAccessTokens.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 30,\n  },\n  {\n    id: \"authentication\",\n    label: t(\"core.uc_profile.tabs.authentication\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/Authentication.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 40,\n  },\n  {\n    id: \"devices\",\n    label: t(\"core.uc_profile.tabs.devices\"),\n    component: defineAsyncComponent({\n      loader: () => import(\"./tabs/Devices.vue\"),\n      loadingComponent: VLoading,\n    }),\n    priority: 50,\n  },\n]);\n\n// Collect uc:profile:tabs:create extension points\nconst { pluginModules } = usePluginModuleStore();\n\nonMounted(async () => {\n  for (const pluginModule of pluginModules) {\n    try {\n      const callbackFunction =\n        pluginModule?.extensionPoints?.[\"uc:user:profile:tabs:create\"];\n      if (typeof callbackFunction !== \"function\") {\n        continue;\n      }\n\n      const providers = await callbackFunction();\n\n      tabs.value = [...tabs.value, ...providers].sort(\n        (a, b) => a.priority - b.priority\n      );\n    } catch (error) {\n      console.error(`Error processing plugin module:`, pluginModule, error);\n    }\n  }\n});\n\nconst tabbarItems = computed(() => {\n  return tabs.value.map((tab) => ({\n    id: tab.id,\n    label: tab.label,\n  }));\n});\n\nconst activeTab = useRouteQuery<string>(\"tab\", tabs.value[0].id, {\n  mode: \"push\",\n});\n</script>\n<template>\n  <ProfileEditingModal v-if=\"editingModal\" @close=\"editingModal = false\" />\n\n  <PasswordChangeModal\n    v-if=\"passwordChangeModal\"\n    @close=\"passwordChangeModal = false\"\n  />\n\n  <header class=\"bg-white\">\n    <div class=\"p-4\">\n      <div class=\"flex items-center justify-between\">\n        <div class=\"flex flex-row items-center gap-5\">\n          <div class=\"group relative h-20 w-20\">\n            <UserAvatar\n              :name=\"currentUser?.user.metadata.name\"\n              is-current-user\n            />\n          </div>\n          <div class=\"block\">\n            <h1 class=\"truncate text-lg font-bold text-gray-900\">\n              {{ currentUser?.user.spec.displayName }}\n            </h1>\n            <span class=\"text-sm text-gray-600\">\n              @{{ currentUser?.user.metadata.name }}\n            </span>\n          </div>\n        </div>\n        <div>\n          <VDropdown>\n            <VButton type=\"default\">\n              {{ $t(\"core.common.buttons.edit\") }}\n            </VButton>\n            <template #popper>\n              <VDropdownItem @click=\"editingModal = true\">\n                {{ $t(\"core.uc_profile.actions.update_profile.title\") }}\n              </VDropdownItem>\n              <VDropdownItem @click=\"passwordChangeModal = true\">\n                {{ $t(\"core.uc_profile.actions.change_password.title\") }}\n              </VDropdownItem>\n            </template>\n          </VDropdown>\n        </div>\n      </div>\n    </div>\n  </header>\n  <section class=\"bg-white p-4\">\n    <VTabbar\n      v-model:active-id=\"activeTab\"\n      :items=\"tabbarItems\"\n      class=\"w-full\"\n      type=\"outline\"\n    ></VTabbar>\n    <div class=\"mt-2\">\n      <template v-for=\"tab in tabs\" :key=\"tab.id\">\n        <component :is=\"tab.component\" v-if=\"activeTab === tab.id\" />\n      </template>\n    </div>\n  </section>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/components/EmailVerifyModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { VerifyCodeRequest } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { useMutation } from \"@tanstack/vue-query\";\nimport { useIntervalFn } from \"@vueuse/shared\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst { t } = useI18n();\n\nconst { currentUser, fetchCurrentUser } = stores.currentUser();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\n// count down\nconst timer = ref(0);\nconst { pause, resume, isActive } = useIntervalFn(\n  () => {\n    if (timer.value <= 0) {\n      pause();\n    } else {\n      timer.value--;\n    }\n  },\n  1000,\n  {\n    immediate: false,\n  }\n);\n\nconst email = ref(currentUser?.user?.spec.email);\n\nconst { mutate: sendVerifyCode, isLoading: isSending } = useMutation({\n  mutationKey: [\"send-verify-code\"],\n  mutationFn: async () => {\n    if (!email.value) {\n      Toast.error(\n        t(\n          \"core.uc_profile.email_verify_modal.operations.send_code.toast_email_empty\"\n        )\n      );\n      throw new Error(\"email is empty\");\n    }\n    return await consoleApiClient.user.sendEmailVerificationCode({\n      emailVerifyRequest: {\n        email: email.value,\n      },\n    });\n  },\n  onSuccess() {\n    Toast.success(\n      t(\"core.uc_profile.email_verify_modal.operations.send_code.toast_success\")\n    );\n    timer.value = 60;\n    resume();\n  },\n});\n\nconst sendVerifyCodeButtonText = computed(() => {\n  if (isSending.value) {\n    return t(\n      \"core.uc_profile.email_verify_modal.operations.send_code.buttons.sending\"\n    );\n  }\n  return isActive.value\n    ? t(\n        \"core.uc_profile.email_verify_modal.operations.send_code.buttons.countdown\",\n        { timer: timer.value }\n      )\n    : t(\"core.uc_profile.email_verify_modal.operations.send_code.buttons.send\");\n});\n\nconst { mutate: verifyEmail, isLoading: isVerifying } = useMutation({\n  mutationKey: [\"verify-email\"],\n  mutationFn: async ({ password, code }: VerifyCodeRequest) => {\n    return await consoleApiClient.user.verifyEmail({\n      verifyCodeRequest: {\n        password,\n        code,\n      },\n    });\n  },\n  onSuccess() {\n    Toast.success(\n      t(\"core.uc_profile.email_verify_modal.operations.verify.toast_success\")\n    );\n    fetchCurrentUser();\n    modal.value?.close();\n  },\n});\n\nfunction handleVerify({ password, code }: VerifyCodeRequest) {\n  verifyEmail({ password, code });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"\n      currentUser?.user.spec.emailVerified\n        ? $t('core.uc_profile.email_verify_modal.titles.modify')\n        : $t('core.uc_profile.email_verify_modal.titles.verify')\n    \"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      id=\"email-verify-form\"\n      type=\"form\"\n      name=\"email-verify-form\"\n      @submit=\"handleVerify\"\n    >\n      <FormKit\n        v-model=\"email\"\n        type=\"email\"\n        :label=\"\n          currentUser?.user.spec.emailVerified\n            ? $t('core.uc_profile.email_verify_modal.fields.new_email.label')\n            : $t('core.uc_profile.email_verify_modal.fields.email.label')\n        \"\n        name=\"email\"\n        validation=\"required|email\"\n      ></FormKit>\n      <FormKit\n        type=\"number\"\n        name=\"code\"\n        :label=\"$t('core.uc_profile.email_verify_modal.fields.code.label')\"\n        validation=\"required\"\n      >\n        <template #suffix>\n          <VButton\n            :loading=\"isSending\"\n            :disabled=\"isActive\"\n            class=\"rounded-none border-y-0 border-l border-r-0 tabular-nums\"\n            @click=\"sendVerifyCode\"\n          >\n            {{ sendVerifyCodeButtonText }}\n          </VButton>\n        </template>\n      </FormKit>\n      <FormKit\n        :label=\"$t('core.uc_profile.email_verify_modal.fields.password.label')\"\n        :help=\"$t('core.uc_profile.email_verify_modal.fields.password.help')\"\n        name=\"password\"\n        type=\"password\"\n        validation=\"required:trim\"\n      ></FormKit>\n    </FormKit>\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isVerifying\"\n          type=\"secondary\"\n          @click=\"$formkit.submit('email-verify-form')\"\n        >\n          {{ $t(\"core.common.buttons.verify\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/components/PasswordChangeModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { onMounted, ref } from \"vue\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { PASSWORD_REGEX } from \"@/constants/regex\";\nimport { setFocus } from \"@/formkit/utils/focus\";\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\ninterface PasswordChangeFormState {\n  oldPassword: string;\n  password: string;\n  password_confirm?: string;\n}\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst formState = ref<PasswordChangeFormState>({\n  oldPassword: \"\",\n  password: \"\",\n  password_confirm: \"\",\n});\nconst isSubmitting = ref(false);\n\nonMounted(() => {\n  setFocus(\"passwordInput\");\n});\n\nconst handleChangePassword = async () => {\n  try {\n    isSubmitting.value = true;\n\n    const changeOwnPasswordRequest = cloneDeep(formState.value);\n    delete changeOwnPasswordRequest.password_confirm;\n\n    await consoleApiClient.user.changeOwnPassword({\n      changeOwnPasswordRequest,\n    });\n\n    window.location.reload();\n  } catch (e) {\n    console.error(e);\n  } finally {\n    isSubmitting.value = false;\n  }\n};\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"500\"\n    :title=\"$t('core.uc_profile.change_password_modal.title')\"\n    @close=\"emit('close')\"\n  >\n    <!-- @vue-ignore -->\n    <FormKit\n      id=\"password-form\"\n      v-model=\"formState\"\n      name=\"password-form\"\n      :actions=\"false\"\n      type=\"form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      @submit=\"handleChangePassword\"\n    >\n      <FormKit\n        id=\"passwordInput\"\n        :label=\"\n          $t('core.uc_profile.change_password_modal.fields.old_password.label')\n        \"\n        name=\"oldPassword\"\n        type=\"password\"\n        validation=\"required:trim\"\n      ></FormKit>\n      <FormKit\n        :label=\"\n          $t('core.uc_profile.change_password_modal.fields.new_password.label')\n        \"\n        name=\"password\"\n        type=\"password\"\n        :validation=\"[\n          ['required'],\n          ['length', 5, 257],\n          ['matches', PASSWORD_REGEX],\n        ]\"\n        :validation-messages=\"{\n          matches: $t('core.formkit.validation.password'),\n        }\"\n      ></FormKit>\n      <FormKit\n        :label=\"\n          $t(\n            'core.uc_profile.change_password_modal.fields.confirm_password.label'\n          )\n        \"\n        name=\"password_confirm\"\n        type=\"password\"\n        validation=\"confirm|required\"\n      ></FormKit>\n    </FormKit>\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('password-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/components/PersonalAccessTokenCreationModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PatSpec, PersonalAccessToken } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Dialog, Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { useClipboard } from \"@vueuse/core\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { useRoleTemplateSelection } from \"@/composables/use-role\";\nimport { patAnnotations, rbacAnnotations } from \"@/constants/annotations\";\nimport { roleLabels } from \"@/constants/labels\";\nimport { useRoleStore } from \"@/stores/role\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst formState = ref<\n  Omit<PersonalAccessToken, \"spec\"> & {\n    spec: PatSpec;\n  }\n>({\n  kind: \"PersonalAccessToken\",\n  apiVersion: \"security.halo.run/v1alpha1\",\n  metadata: {\n    generateName: \"pat-\",\n    name: \"\",\n  },\n  spec: {\n    description: \"\",\n    expiresAt: \"\",\n    name: \"\",\n    roles: [],\n    tokenId: \"\",\n    username: \"\",\n  },\n});\n\nconst { permissions } = useRoleStore();\n\nconst availableRoleTemplates = computed(() => {\n  return permissions.permissions.filter((role) => {\n    return (\n      role.metadata.labels?.[roleLabels.TEMPLATE] === \"true\" &&\n      role.metadata.labels?.[roleLabels.HIDDEN] !== \"true\"\n    );\n  });\n});\n\nconst { roleTemplateGroups, handleRoleTemplateSelect, selectedRoleTemplates } =\n  useRoleTemplateSelection(availableRoleTemplates);\n\nconst { copy } = useClipboard({\n  legacy: true,\n});\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"pat-creation\"],\n  mutationFn: async () => {\n    if (formState.value.spec?.expiresAt) {\n      formState.value.spec.expiresAt = utils.date.toISOString(\n        formState.value.spec.expiresAt\n      );\n    }\n    formState.value.spec = {\n      ...formState.value.spec,\n      roles: Array.from(selectedRoleTemplates.value),\n    };\n    const { data } = await ucApiClient.security.personalAccessToken.generatePat(\n      {\n        personalAccessToken: formState.value,\n      }\n    );\n    return data;\n  },\n  onSuccess(data) {\n    queryClient.invalidateQueries({ queryKey: [\"personal-access-tokens\"] });\n    emit(\"close\");\n\n    const token = data.metadata.annotations?.[patAnnotations.ACCESS_TOKEN];\n\n    setTimeout(() => {\n      Dialog.info({\n        title: t(\"core.uc_profile.pat.operations.copy.title\"),\n        description: token,\n        confirmType: \"secondary\",\n        confirmText: t(\"core.common.buttons.copy\"),\n        showCancel: false,\n        onConfirm: () => {\n          copy(token || \"\");\n          Toast.success(t(\"core.common.toast.copy_success\"));\n        },\n      });\n    });\n  },\n});\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"700\"\n    :title=\"$t('core.uc_profile.pat.creation_modal.title')\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n        <div class=\"md:col-span-1\">\n          <div class=\"sticky top-0\">\n            <span class=\"text-base font-medium text-gray-900\">\n              {{ $t(\"core.uc_profile.pat.creation_modal.groups.general\") }}\n            </span>\n          </div>\n        </div>\n        <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n          <!-- @vue-ignore -->\n          <FormKit\n            id=\"pat-creation-form\"\n            v-model=\"formState.spec\"\n            type=\"form\"\n            name=\"pat-creation-form\"\n            @submit=\"mutate()\"\n          >\n            <FormKit\n              validation=\"required\"\n              type=\"text\"\n              name=\"name\"\n              :label=\"\n                $t('core.uc_profile.pat.creation_modal.fields.name.label')\n              \"\n            ></FormKit>\n            <FormKit\n              type=\"datetime-local\"\n              name=\"expiresAt\"\n              :label=\"\n                $t('core.uc_profile.pat.creation_modal.fields.expiresAt.label')\n              \"\n              :help=\"\n                $t('core.uc_profile.pat.creation_modal.fields.expiresAt.help')\n              \"\n            ></FormKit>\n            <FormKit\n              type=\"textarea\"\n              name=\"description\"\n              :label=\"\n                $t(\n                  'core.uc_profile.pat.creation_modal.fields.description.label'\n                )\n              \"\n            ></FormKit>\n          </FormKit>\n        </div>\n      </div>\n      <div v-if=\"roleTemplateGroups.length\" class=\"py-5\">\n        <div class=\"border-t border-gray-200\"></div>\n      </div>\n      <div\n        v-if=\"roleTemplateGroups.length\"\n        class=\"md:grid md:grid-cols-4 md:gap-6\"\n      >\n        <div class=\"md:col-span-1\">\n          <div class=\"sticky top-0\">\n            <span class=\"text-base font-medium text-gray-900\">\n              {{ $t(\"core.uc_profile.pat.creation_modal.groups.permissions\") }}\n            </span>\n          </div>\n        </div>\n        <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n          <dl class=\"divide-y divide-gray-100\">\n            <div\n              v-for=\"(group, groupIndex) in roleTemplateGroups\"\n              :key=\"groupIndex\"\n              class=\"flex flex-col gap-3 bg-white py-5 first:pt-0\"\n            >\n              <dt class=\"text-sm font-medium text-gray-900\">\n                <div>\n                  {{ $t(`core.rbac.${group.module}`, group.module as string) }}\n                </div>\n              </dt>\n              <dd class=\"text-sm text-gray-900\">\n                <ul class=\"space-y-2\">\n                  <li v-for=\"(roleTemplate, index) in group.roles\" :key=\"index\">\n                    <label\n                      class=\"inline-flex w-full cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary\"\n                    >\n                      <input\n                        v-model=\"selectedRoleTemplates\"\n                        :value=\"roleTemplate.metadata.name\"\n                        type=\"checkbox\"\n                        @change=\"handleRoleTemplateSelect\"\n                      />\n                      <div class=\"flex flex-1 flex-col gap-y-3\">\n                        <span class=\"font-medium text-gray-900\">\n                          {{\n                            $t(\n                              `core.rbac.${\n                                roleTemplate.metadata.annotations?.[\n                                  rbacAnnotations.DISPLAY_NAME\n                                ]\n                              }`,\n                              roleTemplate.metadata.annotations?.[\n                                rbacAnnotations.DISPLAY_NAME\n                              ] as string\n                            )\n                          }}\n                        </span>\n                        <span\n                          v-if=\"\n                            roleTemplate.metadata.annotations?.[\n                              rbacAnnotations.DEPENDENCIES\n                            ]\n                          \"\n                          class=\"text-xs text-gray-400\"\n                        >\n                          {{\n                            $t(\"core.role.common.text.dependent_on\", {\n                              roles: JSON.parse(\n                                roleTemplate.metadata.annotations?.[\n                                  rbacAnnotations.DEPENDENCIES\n                                ]\n                              )\n                                .map((item: string) =>\n                                  $t(`core.rbac.${item}`, item as string)\n                                )\n                                .join(\"，\"),\n                            })\n                          }}\n                        </span>\n                      </div>\n                    </label>\n                  </li>\n                </ul>\n              </dd>\n            </div>\n          </dl>\n        </div>\n      </div>\n    </div>\n\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('pat-creation-form')\"\n        />\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/components/PersonalAccessTokenListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PersonalAccessToken } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VStatusDot,\n  type StatusDotState,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst props = withDefaults(\n  defineProps<{\n    token: PersonalAccessToken;\n  }>(),\n  {}\n);\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nfunction handleDelete() {\n  Dialog.warning({\n    title: t(\"core.uc_profile.pat.operations.delete.title\"),\n    description: t(\"core.uc_profile.pat.operations.delete.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      await ucApiClient.security.personalAccessToken.deletePat({\n        name: props.token.metadata.name,\n      });\n\n      Toast.success(t(\"core.common.toast.delete_success\"));\n      queryClient.invalidateQueries({ queryKey: [\"personal-access-tokens\"] });\n    },\n  });\n}\n\nfunction handleRevoke() {\n  Dialog.warning({\n    title: t(\"core.uc_profile.pat.operations.revoke.title\"),\n    description: t(\"core.uc_profile.pat.operations.revoke.description\"),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      await ucApiClient.security.personalAccessToken.revokePat({\n        name: props.token.metadata.name,\n      });\n\n      Toast.success(t(\"core.uc_profile.pat.operations.revoke.toast_success\"));\n      queryClient.invalidateQueries({ queryKey: [\"personal-access-tokens\"] });\n    },\n  });\n}\n\nasync function handleRestore() {\n  await ucApiClient.security.personalAccessToken.restorePat({\n    name: props.token.metadata.name,\n  });\n\n  Toast.success(t(\"core.uc_profile.pat.operations.restore.toast_success\"));\n  queryClient.invalidateQueries({ queryKey: [\"personal-access-tokens\"] });\n}\n\nconst statusText = computed(() => {\n  const { expiresAt } = props.token.spec || {};\n  if (expiresAt && new Date(expiresAt) < new Date()) {\n    return t(\"core.uc_profile.pat.list.fields.status.expired\");\n  }\n  return t(\n    props.token.spec?.revoked\n      ? \"core.uc_profile.pat.list.fields.status.revoked\"\n      : \"core.uc_profile.pat.list.fields.status.normal\"\n  );\n});\n\nconst statusTheme = computed<StatusDotState>(() => {\n  const { expiresAt } = props.token.spec || {};\n  if (expiresAt && new Date(expiresAt) < new Date()) {\n    return \"warning\";\n  }\n  return props.token.spec?.revoked ? \"default\" : \"success\";\n});\n</script>\n\n<template>\n  <VEntity>\n    <template #start>\n      <VEntityField\n        :title=\"token.spec?.name || token.metadata.name\"\n        :description=\"token.spec?.description\"\n      ></VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"token.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField v-if=\"!token.spec?.revoked\">\n        <template #description>\n          <div class=\"truncate text-xs tabular-nums text-gray-500\">\n            <span\n              v-if=\"token.spec?.expiresAt\"\n              v-tooltip=\"utils.date.format(token.spec.expiresAt)\"\n            >\n              {{\n                $t(\"core.uc_profile.pat.list.fields.expiresAt.dynamic\", {\n                  expiresAt: utils.date.timeAgo(token.spec?.expiresAt),\n                })\n              }}\n            </span>\n            <span v-else>\n              {{ $t(\"core.uc_profile.pat.list.fields.expiresAt.forever\") }}\n            </span>\n          </div>\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <VStatusDot :text=\"statusText\" :state=\"statusTheme\" />\n        </template>\n      </VEntityField>\n      <VEntityField\n        :description=\"utils.date.format(token.metadata.creationTimestamp)\"\n      ></VEntityField>\n    </template>\n    <template #dropdownItems>\n      <VDropdownItem\n        v-if=\"!token.spec?.revoked\"\n        type=\"danger\"\n        @click=\"handleRevoke\"\n      >\n        {{ $t(\"core.uc_profile.pat.operations.revoke.button\") }}\n      </VDropdownItem>\n      <VDropdownItem v-else @click=\"handleRestore\">\n        {{ $t(\"core.uc_profile.pat.operations.restore.button\") }}\n      </VDropdownItem>\n      <VDropdownDivider />\n      <VDropdownItem type=\"danger\" @click=\"handleDelete\">\n        {{ $t(\"core.common.buttons.delete\") }}\n      </VDropdownItem>\n    </template>\n  </VEntity>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/components/ProfileEditingModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { User } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { onMounted, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport SubmitButton from \"@/components/button/SubmitButton.vue\";\nimport { setFocus } from \"@/formkit/utils/focus\";\nimport EmailVerifyModal from \"./EmailVerifyModal.vue\";\n\nconst { t } = useI18n();\nconst currentUserStore = stores.currentUser();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\nconst formState = ref<User>(\n  cloneDeep(currentUserStore.currentUser?.user) || {\n    spec: {\n      displayName: \"\",\n      email: \"\",\n      password: \"\",\n      bio: \"\",\n      disabled: false,\n      loginHistoryLimit: 0,\n    },\n    apiVersion: \"v1alpha1\",\n    kind: \"User\",\n    metadata: {\n      name: \"\",\n    },\n  }\n);\nconst isSubmitting = ref(false);\n\nonMounted(() => {\n  setFocus(\"displayNameInput\");\n});\n\nconst handleUpdateUser = async () => {\n  try {\n    isSubmitting.value = true;\n\n    if (formState.value.spec.displayName) {\n      formState.value.spec.displayName =\n        formState.value.spec.displayName.trim();\n    }\n\n    await consoleApiClient.user.updateCurrentUser({\n      user: formState.value,\n    });\n\n    modal.value?.close();\n\n    Toast.success(t(\"core.common.toast.save_success\"));\n  } catch (e) {\n    console.error(\"Failed to update profile\", e);\n  } finally {\n    isSubmitting.value = false;\n    currentUserStore.fetchCurrentUser();\n  }\n};\n\n// verify email\nconst emailVerifyModal = ref(false);\n\nasync function onEmailVerifyModalClose() {\n  emailVerifyModal.value = false;\n  await currentUserStore.fetchCurrentUser();\n  if (currentUserStore.currentUser)\n    formState.value = cloneDeep(currentUserStore.currentUser.user);\n}\n</script>\n<template>\n  <VModal\n    ref=\"modal\"\n    :title=\"$t('core.uc_profile.editing_modal.title')\"\n    :width=\"700\"\n    @close=\"emit('close')\"\n  >\n    <FormKit\n      id=\"user-form\"\n      name=\"user-form\"\n      :config=\"{ validationVisibility: 'submit' }\"\n      type=\"form\"\n      @submit=\"handleUpdateUser\"\n    >\n      <div>\n        <div class=\"md:grid md:grid-cols-4 md:gap-6\">\n          <div class=\"md:col-span-1\">\n            <div class=\"sticky top-0\">\n              <span class=\"text-base font-medium text-gray-900\">\n                {{ $t(\"core.uc_profile.editing_modal.groups.general\") }}\n              </span>\n            </div>\n          </div>\n          <div class=\"mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0\">\n            <FormKit\n              id=\"userNameInput\"\n              v-model=\"formState.metadata.name\"\n              :disabled=\"true\"\n              :label=\"$t('core.uc_profile.editing_modal.fields.username.label')\"\n              type=\"text\"\n              name=\"name\"\n            ></FormKit>\n            <FormKit\n              id=\"displayNameInput\"\n              v-model=\"formState.spec.displayName\"\n              :label=\"\n                $t('core.uc_profile.editing_modal.fields.display_name.label')\n              \"\n              type=\"text\"\n              name=\"displayName\"\n              validation=\"required|length:0,50\"\n            ></FormKit>\n            <FormKit\n              v-model=\"formState.spec.email\"\n              :label=\"$t('core.uc_profile.editing_modal.fields.email.label')\"\n              type=\"email\"\n              name=\"email\"\n              readonly\n              validation=\"required|email|length:0,100\"\n            >\n              <template #suffix>\n                <VButton\n                  class=\"rounded-none border-y-0 border-l border-r-0\"\n                  @click=\"emailVerifyModal = true\"\n                >\n                  {{ $t(\"core.common.buttons.modify\") }}\n                </VButton>\n              </template>\n            </FormKit>\n            <FormKit\n              v-model=\"formState.spec.bio\"\n              :label=\"$t('core.uc_profile.editing_modal.fields.bio.label')\"\n              type=\"textarea\"\n              auto-height\n              :max-auto-height=\"200\"\n              name=\"bio\"\n              validation=\"length:0,2048\"\n            ></FormKit>\n          </div>\n        </div>\n      </div>\n    </FormKit>\n\n    <template #footer>\n      <VSpace>\n        <SubmitButton\n          :loading=\"isSubmitting\"\n          type=\"secondary\"\n          :text=\"$t('core.common.buttons.submit')\"\n          @submit=\"$formkit.submit('user-form')\"\n        >\n        </SubmitButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.cancel_and_shortcut\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n\n  <EmailVerifyModal v-if=\"emailVerifyModal\" @close=\"onEmailVerifyModalClose\" />\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/module.ts",
    "content": "import { IconAccountCircleLine } from \"@halo-dev/components\";\nimport { definePlugin } from \"@halo-dev/ui-shared\";\nimport BasicLayout from \"@uc/layouts/BasicLayout.vue\";\nimport { markRaw } from \"vue\";\n\nexport default definePlugin({\n  ucRoutes: [\n    {\n      path: \"/\",\n      component: BasicLayout,\n      name: \"Root\",\n      redirect: \"/profile\",\n      children: [\n        {\n          path: \"profile\",\n          name: \"Profile\",\n          component: () => import(\"./Profile.vue\"),\n          meta: {\n            title: \"core.uc_profile.title\",\n            searchable: true,\n            menu: {\n              name: \"core.uc_sidebar.menu.items.profile\",\n              group: \"dashboard\",\n              icon: markRaw(IconAccountCircleLine),\n              priority: 0,\n              mobile: true,\n            },\n          },\n        },\n      ],\n    },\n  ],\n});\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/Authentication.vue",
    "content": "<script lang=\"ts\" setup>\nimport AuthProviders from \"./components/AuthProviders.vue\";\nimport TwoFactor from \"./components/TwoFactor.vue\";\n</script>\n\n<template>\n  <div class=\"flex flex-col gap-4\">\n    <AuthProviders />\n    <TwoFactor />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/Detail.vue",
    "content": "<script lang=\"ts\" setup>\nimport {\n  IconShieldUser,\n  VAlert,\n  VButton,\n  VDescription,\n  VDescriptionItem,\n  VSpace,\n  VTag,\n} from \"@halo-dev/components\";\nimport { stores, utils } from \"@halo-dev/ui-shared\";\nimport { storeToRefs } from \"pinia\";\nimport { ref } from \"vue\";\nimport RiVerifiedBadgeLine from \"~icons/ri/verified-badge-line\";\nimport { rbacAnnotations } from \"@/constants/annotations\";\nimport EmailVerifyModal from \"../components/EmailVerifyModal.vue\";\n\nconst { currentUser } = storeToRefs(stores.currentUser());\n\n// verify email\nconst emailVerifyModal = ref(false);\n</script>\n<template>\n  <div class=\"border-t border-gray-100\">\n    <VDescription>\n      <VDescriptionItem\n        :label=\"$t('core.uc_profile.detail.fields.display_name')\"\n        :content=\"currentUser?.user.spec.displayName\"\n        class=\"!px-2\"\n      />\n      <VDescriptionItem\n        :label=\"$t('core.uc_profile.detail.fields.username')\"\n        :content=\"currentUser?.user.metadata.name\"\n        class=\"!px-2\"\n      />\n      <VDescriptionItem\n        :label=\"$t('core.uc_profile.detail.fields.email')\"\n        class=\"!px-2\"\n      >\n        <div v-if=\"currentUser\" class=\"w-full xl:w-1/2\">\n          <VAlert\n            v-if=\"!currentUser.user.spec.email\"\n            :title=\"$t('core.uc_profile.detail.email_not_set.title')\"\n            :description=\"\n              $t('core.uc_profile.detail.email_not_set.description')\n            \"\n            type=\"warning\"\n            :closable=\"false\"\n          >\n            <template #actions>\n              <VButton size=\"sm\" @click=\"emailVerifyModal = true\">\n                {{ $t(\"core.common.buttons.setting\") }}\n              </VButton>\n            </template>\n          </VAlert>\n\n          <div v-else>\n            <div class=\"flex items-center space-x-2\">\n              <span>{{ currentUser.user.spec.email }}</span>\n              <RiVerifiedBadgeLine\n                v-if=\"currentUser.user.spec.emailVerified\"\n                v-tooltip=\"$t('core.uc_profile.detail.email_verified.tooltip')\"\n                class=\"text-xs text-blue-600\"\n              />\n            </div>\n            <div v-if=\"!currentUser.user.spec.emailVerified\" class=\"mt-3\">\n              <VAlert\n                :title=\"$t('core.uc_profile.detail.email_not_verified.title')\"\n                :description=\"\n                  $t('core.uc_profile.detail.email_not_verified.description')\n                \"\n                type=\"warning\"\n                :closable=\"false\"\n              >\n                <template #actions>\n                  <VButton size=\"sm\" @click=\"emailVerifyModal = true\">\n                    {{ $t(\"core.common.buttons.verify\") }}\n                  </VButton>\n                </template>\n              </VAlert>\n            </div>\n          </div>\n        </div>\n      </VDescriptionItem>\n      <VDescriptionItem\n        :label=\"$t('core.uc_profile.detail.fields.roles')\"\n        class=\"!px-2\"\n      >\n        <VSpace>\n          <VTag v-for=\"role in currentUser?.roles\" :key=\"role.metadata.name\">\n            <template #leftIcon>\n              <IconShieldUser />\n            </template>\n            {{\n              role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||\n              role.metadata.name\n            }}\n          </VTag>\n        </VSpace>\n      </VDescriptionItem>\n      <VDescriptionItem\n        :label=\"$t('core.uc_profile.detail.fields.bio')\"\n        :content=\"currentUser?.user.spec?.bio || $t('core.common.text.none')\"\n        class=\"!px-2\"\n      />\n      <VDescriptionItem\n        :label=\"$t('core.uc_profile.detail.fields.creation_time')\"\n        :content=\"\n          utils.date.format(currentUser?.user.metadata?.creationTimestamp)\n        \"\n        class=\"!px-2\"\n      />\n    </VDescription>\n\n    <EmailVerifyModal\n      v-if=\"emailVerifyModal\"\n      @close=\"emailVerifyModal = false\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/Devices.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  Toast,\n  VButton,\n  VEntityContainer,\n  VLoading,\n} from \"@halo-dev/components\";\nimport { useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { computed } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport DeviceListItem from \"./components/DeviceListItem.vue\";\n\nconst { t } = useI18n();\nconst queryClient = useQueryClient();\n\nconst { data, isLoading } = useQuery({\n  queryKey: [\"uc:devices\"],\n  queryFn: async () => {\n    const { data } = await ucApiClient.security.device.listDevices();\n    return data;\n  },\n  refetchInterval(data) {\n    const hasDeletingData = data?.some(\n      (device) => !!device.device.metadata.deletionTimestamp\n    );\n\n    return hasDeletingData ? 1000 : false;\n  },\n});\n\nconst otherDevices = computed(() => {\n  if (!data.value) {\n    return [];\n  }\n\n  return data.value.filter((device) => !device.currentDevice);\n});\n\nfunction handleRevokeOtherDevices() {\n  Dialog.warning({\n    title: t(\"core.uc_profile.device.operations.revoke_others.title\"),\n    description: t(\n      \"core.uc_profile.device.operations.revoke_others.description\"\n    ),\n    confirmType: \"danger\",\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    async onConfirm() {\n      for (const device of otherDevices.value) {\n        await ucApiClient.security.device.revokeDevice({\n          deviceId: device.device.metadata.name,\n        });\n      }\n\n      queryClient.invalidateQueries({ queryKey: [\"uc:devices\"] });\n\n      Toast.success(\n        t(\"core.uc_profile.device.operations.revoke_others.toast_success\")\n      );\n    },\n  });\n}\n</script>\n\n<template>\n  <VLoading v-if=\"isLoading\" />\n\n  <TransitionGroup v-else appear name=\"fade\">\n    <div class=\"overflow-hidden rounded-base border\">\n      <VEntityContainer>\n        <DeviceListItem\n          v-for=\"device in data\"\n          :key=\"device?.device.spec.sessionId\"\n          :device=\"device\"\n        />\n      </VEntityContainer>\n    </div>\n    <div v-if=\"otherDevices.length\" class=\"mt-5\">\n      <VButton @click=\"handleRevokeOtherDevices\">\n        {{ $t(\"core.uc_profile.device.operations.revoke_others.title\") }}\n      </VButton>\n    </div>\n  </TransitionGroup>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/NotificationPreferences.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ReasonTypeNotifierRequest } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { VLoading, VSwitch } from \"@halo-dev/components\";\nimport { stores } from \"@halo-dev/ui-shared\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { cloneDeep } from \"es-toolkit\";\nimport { storeToRefs } from \"pinia\";\nimport { computed } from \"vue\";\n\nconst { currentUser } = storeToRefs(stores.currentUser());\n\nconst queryClient = useQueryClient();\n\nconst { data, isLoading } = useQuery({\n  queryKey: [\"notification-preferences\"],\n  queryFn: async () => {\n    if (!currentUser.value) {\n      return null;\n    }\n\n    const { data } =\n      await ucApiClient.notification.notification.listUserNotificationPreferences(\n        {\n          username: currentUser.value?.user.metadata.name,\n        }\n      );\n\n    return data;\n  },\n  enabled: computed(() => !!currentUser.value),\n});\n\nconst {\n  mutate,\n  isLoading: mutating,\n  variables,\n} = useMutation({\n  mutationKey: [\"update-notification-preferences\"],\n  mutationFn: async ({\n    state,\n    reasonTypeIndex,\n    notifierIndex,\n  }: {\n    state: boolean;\n    reasonTypeIndex: number;\n    notifierIndex: number;\n  }) => {\n    const preferences = cloneDeep(data.value);\n\n    if (!currentUser.value || !preferences) {\n      return;\n    }\n\n    if (!preferences.stateMatrix) {\n      preferences.stateMatrix = [];\n    }\n\n    preferences.stateMatrix[reasonTypeIndex][notifierIndex] = state;\n\n    const reasonTypeNotifiers = data.value?.reasonTypes\n      ?.map((reasonType, currentReasonTypeIndex) => {\n        return {\n          reasonType: reasonType.name,\n          notifiers: data.value?.notifiers\n            ?.map((notifier, currentNotifierIndex) => {\n              if (\n                preferences.stateMatrix?.[currentReasonTypeIndex][\n                  currentNotifierIndex\n                ]\n              ) {\n                return notifier.name;\n              }\n            })\n            .filter(Boolean),\n        };\n      })\n      .filter(Boolean) as Array<ReasonTypeNotifierRequest>;\n\n    return await ucApiClient.notification.notification.saveUserNotificationPreferences(\n      {\n        username: currentUser.value.user.metadata.name,\n        reasonTypeNotifierCollectionRequest: {\n          reasonTypeNotifiers,\n        },\n      }\n    );\n  },\n  onSuccess() {\n    queryClient.invalidateQueries({ queryKey: [\"notification-preferences\"] });\n  },\n});\n</script>\n\n<template>\n  <VLoading v-if=\"isLoading\" />\n\n  <Transition v-else appear name=\"fade\">\n    <div class=\"box-border h-full w-full overflow-auto rounded-base border\">\n      <table class=\"min-w-full divide-y divide-gray-100\">\n        <thead class=\"bg-gray-50\">\n          <tr>\n            <th\n              class=\"px-4 py-3 text-left text-sm font-semibold text-gray-900 sm:w-96\"\n              scope=\"col\"\n            >\n              {{ $t(\"core.uc_profile.notification-preferences.fields.type\") }}\n            </th>\n            <th\n              v-for=\"notifier in data?.notifiers\"\n              :key=\"notifier.name\"\n              scope=\"col\"\n              class=\"px-4 py-3 text-left text-sm font-semibold text-gray-900\"\n            >\n              {{ notifier.displayName }}\n            </th>\n          </tr>\n        </thead>\n        <tbody class=\"divide-y divide-gray-100 bg-white\">\n          <template\n            v-for=\"(reasonType, index) in data?.reasonTypes\"\n            :key=\"reasonType.name\"\n          >\n            <HasPermission :permissions=\"reasonType.uiPermissions || []\">\n              <tr>\n                <td\n                  class=\"whitespace-nowrap px-4 py-3 text-sm font-medium text-gray-900\"\n                >\n                  {{ reasonType.displayName }}\n                </td>\n                <td\n                  v-for=\"(notifier, notifierIndex) in data?.notifiers\"\n                  :key=\"notifier.name\"\n                  class=\"whitespace-nowrap px-4 py-3 text-sm text-gray-500\"\n                >\n                  <VSwitch\n                    :model-value=\"data?.stateMatrix?.[index][notifierIndex]\"\n                    :loading=\"\n                      mutating &&\n                      variables?.reasonTypeIndex === index &&\n                      variables?.notifierIndex === notifierIndex\n                    \"\n                    @change=\"\n                      mutate({\n                        state: !data?.stateMatrix?.[index][notifierIndex],\n                        reasonTypeIndex: index,\n                        notifierIndex: notifierIndex,\n                      })\n                    \"\n                  />\n                </td>\n              </tr>\n            </HasPermission>\n          </template>\n        </tbody>\n      </table>\n    </div>\n  </Transition>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/PersonalAccessTokens.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { PersonalAccessToken } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  IconAddCircle,\n  VButton,\n  VEmpty,\n  VEntityContainer,\n  VLoading,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport PersonalAccessTokenCreationModal from \"../components/PersonalAccessTokenCreationModal.vue\";\nimport PersonalAccessTokenListItem from \"../components/PersonalAccessTokenListItem.vue\";\n\nconst {\n  data: pats,\n  isLoading,\n  refetch,\n} = useQuery<PersonalAccessToken[]>({\n  queryKey: [\"personal-access-tokens\"],\n  queryFn: async () => {\n    const { data } =\n      await ucApiClient.security.personalAccessToken.obtainPats();\n    return data;\n  },\n  refetchInterval(data) {\n    const deletingTokens = data?.filter(\n      (token) => !!token.metadata.deletionTimestamp\n    );\n    return deletingTokens?.length ? 1000 : false;\n  },\n});\n\nconst creationModal = ref(false);\n</script>\n<template>\n  <div v-if=\"pats?.length\" class=\"my-5 flex justify-end\">\n    <VButton type=\"secondary\" @click=\"creationModal = true\">\n      <template #icon>\n        <IconAddCircle />\n      </template>\n      {{ $t(\"core.common.buttons.new\") }}\n    </VButton>\n  </div>\n\n  <VLoading v-if=\"isLoading\" />\n\n  <Transition v-else-if=\"!pats?.length\" appear name=\"fade\">\n    <VEmpty\n      :message=\"$t('core.uc_profile.pat.list.empty.message')\"\n      :title=\"$t('core.uc_profile.pat.list.empty.title')\"\n    >\n      <template #actions>\n        <VSpace>\n          <VButton @click=\"refetch\">\n            {{ $t(\"core.common.buttons.refresh\") }}\n          </VButton>\n          <VButton type=\"secondary\" @click=\"creationModal = true\">\n            <template #icon>\n              <IconAddCircle />\n            </template>\n            {{ $t(\"core.common.buttons.new\") }}\n          </VButton>\n        </VSpace>\n      </template>\n    </VEmpty>\n  </Transition>\n\n  <Transition v-else appear name=\"fade\">\n    <div class=\"overflow-hidden rounded-base border\">\n      <VEntityContainer>\n        <PersonalAccessTokenListItem\n          v-for=\"token in pats\"\n          :key=\"token.metadata.name\"\n          :token=\"token\"\n        />\n      </VEntityContainer>\n    </div>\n  </Transition>\n\n  <PersonalAccessTokenCreationModal\n    v-if=\"creationModal\"\n    @close=\"creationModal = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/AuthProviders.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { ListedAuthProvider } from \"@halo-dev/api-client\";\nimport { consoleApiClient } from \"@halo-dev/api-client\";\nimport {\n  Dialog,\n  VButton,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport axios from \"axios\";\nimport { useI18n } from \"vue-i18n\";\n\nconst { t } = useI18n();\n\nconst { data: authProviders } = useQuery<ListedAuthProvider[]>({\n  queryKey: [\"user-auth-providers\"],\n  queryFn: async () => {\n    const { data } =\n      await consoleApiClient.auth.authProvider.listAuthProviders();\n    return data.filter(\n      (authProvider) => authProvider.enabled && authProvider.supportsBinding\n    );\n  },\n});\n\nconst handleUnbindAuth = (authProvider: ListedAuthProvider) => {\n  Dialog.warning({\n    title: t(\"core.uc_profile.auth_providers.operations.unbind.title\", {\n      display_name: authProvider.displayName,\n    }),\n    confirmText: t(\"core.common.buttons.confirm\"),\n    cancelText: t(\"core.common.buttons.cancel\"),\n    confirmType: \"danger\",\n    onConfirm: async () => {\n      await axios.put(`${authProvider.unbindingUrl}`, {\n        withCredentials: true,\n      });\n\n      window.location.reload();\n    },\n  });\n};\n\nconst handleBindAuth = (authProvider: ListedAuthProvider) => {\n  if (!authProvider.bindingUrl) {\n    return;\n  }\n  window.location.href = `${\n    authProvider.bindingUrl\n  }?redirect_uri=${encodeURIComponent(window.location.href)}`;\n};\n</script>\n\n<template>\n  <Transition v-if=\"authProviders?.length\" appear name=\"fade\">\n    <div class=\"overflow-hidden rounded-base border\">\n      <div class=\"flex items-center justify-between bg-gray-50 px-4 py-3\">\n        <span class=\"text-sm font-semibold text-gray-900\">\n          {{ $t(\"core.uc_profile.auth_providers.title\") }}\n        </span>\n      </div>\n      <VEntityContainer>\n        <VEntity v-for=\"authProvider in authProviders\" :key=\"authProvider.name\">\n          <template #start>\n            <VEntityField v-if=\"authProvider.logo\">\n              <template #description>\n                <div class=\"size-8 overflow-hidden rounded-lg\">\n                  <img\n                    class=\"size-full object-cover\"\n                    :src=\"authProvider.logo\"\n                    :alt=\"authProvider.displayName\"\n                  />\n                </div>\n              </template>\n            </VEntityField>\n            <VEntityField\n              :title=\"authProvider.displayName\"\n              :description=\"authProvider.description\"\n            />\n          </template>\n          <template #end>\n            <VEntityField>\n              <template #description>\n                <VButton\n                  v-if=\"authProvider.isBound\"\n                  size=\"sm\"\n                  @click=\"handleUnbindAuth(authProvider)\"\n                >\n                  {{\n                    $t(\n                      \"core.uc_profile.auth_providers.operations.unbind.button\"\n                    )\n                  }}\n                </VButton>\n                <VButton\n                  v-else\n                  size=\"sm\"\n                  type=\"secondary\"\n                  @click=\"handleBindAuth(authProvider)\"\n                >\n                  {{\n                    $t(\"core.uc_profile.auth_providers.operations.bind.button\")\n                  }}\n                </VButton>\n              </template>\n            </VEntityField>\n          </template>\n        </VEntity>\n      </VEntityContainer>\n    </div>\n  </Transition>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/DeviceDetailModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { UserDevice } from \"@halo-dev/api-client\";\nimport {\n  VButton,\n  VDescription,\n  VDescriptionItem,\n  VModal,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { ref } from \"vue\";\nimport { useUserAgent } from \"../composables/use-user-agent\";\nimport { useUserDevice } from \"../composables/use-user-device\";\n\nconst props = withDefaults(\n  defineProps<{\n    device: UserDevice;\n  }>(),\n  {}\n);\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { os, browser } = useUserAgent(props.device.device.spec.userAgent);\n\nconst { handleRevoke } = useUserDevice(props.device);\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :body-class=\"['!p-0']\"\n    :width=\"650\"\n    :title=\"$t('core.uc_profile.device.detail_modal.title')\"\n    @close=\"emit('close')\"\n  >\n    <VDescription>\n      <VDescriptionItem\n        class=\"!px-4\"\n        :label=\"$t('core.uc_profile.device.detail_modal.fields.os')\"\n        :content=\"os\"\n      />\n      <VDescriptionItem\n        class=\"!px-4\"\n        :label=\"$t('core.uc_profile.device.detail_modal.fields.browser')\"\n        :content=\"browser\"\n      />\n      <VDescriptionItem\n        class=\"!px-4\"\n        label=\"IP\"\n        :content=\"device.device.spec.ipAddress\"\n      />\n      <VDescriptionItem\n        class=\"!px-4\"\n        :label=\"\n          $t('core.uc_profile.device.detail_modal.fields.creation_timestamp')\n        \"\n        :content=\"utils.date.format(device.device.metadata.creationTimestamp)\"\n      />\n      <VDescriptionItem\n        class=\"!px-4\"\n        :label=\"\n          $t('core.uc_profile.device.detail_modal.fields.last_accessed_times')\n        \"\n        :content=\"utils.date.format(device.device.spec.lastAccessedTime)\"\n      />\n      <VDescriptionItem\n        class=\"!px-4\"\n        :label=\"\n          $t(\n            'core.uc_profile.device.detail_modal.fields.last_authenticated_time'\n          )\n        \"\n        :content=\"utils.date.format(device.device.spec.lastAuthenticatedTime)\"\n      />\n    </VDescription>\n    <template #footer>\n      <VSpace>\n        <VButton type=\"danger\" @click=\"handleRevoke\">\n          {{ $t(\"core.common.buttons.revoke\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/DeviceListItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { UserDevice } from \"@halo-dev/api-client\";\nimport {\n  VDropdownDivider,\n  VDropdownItem,\n  VEntity,\n  VEntityField,\n  VStatusDot,\n  VTag,\n} from \"@halo-dev/components\";\nimport { utils } from \"@halo-dev/ui-shared\";\nimport { computed, markRaw, ref, type Component, type Raw } from \"vue\";\nimport RiComputerLine from \"~icons/ri/computer-line\";\nimport RiSmartphoneLine from \"~icons/ri/smartphone-line\";\nimport RiTabletLine from \"~icons/ri/tablet-line\";\nimport { useUserAgent } from \"../composables/use-user-agent\";\nimport { useUserDevice } from \"../composables/use-user-device\";\nimport DeviceDetailModal from \"./DeviceDetailModal.vue\";\n\nconst props = withDefaults(\n  defineProps<{\n    device: UserDevice;\n  }>(),\n  {}\n);\n\nconst { ua, os, browser } = useUserAgent(props.device.device.spec.userAgent);\n\nconst deviceIcons: Record<string, Raw<Component>> = {\n  console: markRaw(RiComputerLine),\n  mobile: markRaw(RiSmartphoneLine),\n  tablet: markRaw(RiTabletLine),\n};\n\nconst getDeviceIcon = computed(() => {\n  const deviceType = ua.value.getDevice().type;\n  const defaultIcon = deviceIcons[\"console\"];\n\n  if (!deviceType) {\n    return defaultIcon;\n  }\n\n  return deviceIcons[deviceType] || defaultIcon;\n});\n\nconst { handleRevoke } = useUserDevice(props.device);\n\nconst detailModal = ref(false);\n</script>\n\n<template>\n  <VEntity>\n    <template #start>\n      <VEntityField>\n        <template #description>\n          <VStatusDot :state=\"device.active ? 'success' : 'default'\" />\n        </template>\n      </VEntityField>\n      <VEntityField>\n        <template #description>\n          <component :is=\"getDeviceIcon\" class=\"text-lg\" />\n        </template>\n      </VEntityField>\n      <VEntityField\n        :title=\"os\"\n        :description=\"browser\"\n        class=\"cursor-pointer\"\n        @click=\"detailModal = true\"\n      >\n        <template v-if=\"device.currentDevice\" #extra>\n          <VTag>\n            {{ $t(\"core.uc_profile.device.list.fields.current\") }}\n          </VTag>\n        </template>\n      </VEntityField>\n    </template>\n    <template #end>\n      <VEntityField v-if=\"device.device.metadata.deletionTimestamp\">\n        <template #description>\n          <VStatusDot\n            v-tooltip=\"$t('core.common.status.deleting')\"\n            state=\"warning\"\n            animate\n          />\n        </template>\n      </VEntityField>\n      <VEntityField :description=\"device.device.spec.ipAddress\"></VEntityField>\n      <VEntityField v-if=\"device.device.spec.lastAccessedTime\">\n        <template #description>\n          <span\n            v-tooltip=\"utils.date.format(device.device.spec.lastAccessedTime)\"\n            class=\"truncate text-xs tabular-nums text-gray-500\"\n          >\n            {{\n              $t(\"core.uc_profile.device.list.fields.last_accessed_time\", {\n                time: utils.date.timeAgo(device.device.spec.lastAccessedTime),\n              })\n            }}\n          </span>\n        </template>\n      </VEntityField>\n    </template>\n    <template #dropdownItems>\n      <VDropdownItem @click=\"detailModal = true\">\n        {{ $t(\"core.common.buttons.detail\") }}\n      </VDropdownItem>\n      <VDropdownDivider />\n      <VDropdownItem type=\"danger\" @click=\"handleRevoke\">\n        {{ $t(\"core.common.buttons.revoke\") }}\n      </VDropdownItem>\n    </template>\n  </VEntity>\n\n  <DeviceDetailModal\n    v-if=\"detailModal\"\n    :device=\"device\"\n    @close=\"detailModal = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/PasswordValidationForm.vue",
    "content": "<script lang=\"ts\" setup>\nconst emit = defineEmits<{\n  (event: \"submit\", password: string): void;\n}>();\n\nfunction onSubmit({ password }: { password: string }) {\n  emit(\"submit\", password);\n}\n</script>\n\n<template>\n  <FormKit\n    id=\"password-validation-form\"\n    type=\"form\"\n    name=\"password-validation-form\"\n    @submit=\"onSubmit\"\n  >\n    <FormKit\n      type=\"password\"\n      :label=\"\n        $t('core.uc_profile.2fa.password_validation_form.fields.password.label')\n      \"\n      validation=\"required\"\n      name=\"password\"\n      :help=\"\n        $t('core.uc_profile.2fa.password_validation_form.fields.password.help')\n      \"\n      autocomplete=\"current-password\"\n    ></FormKit>\n  </FormKit>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/TotpConfigureModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport type { TotpRequest } from \"@halo-dev/api-client\";\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VAlert, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/vue-query\";\nimport { useQRCode } from \"@vueuse/integrations/useQRCode\";\nimport { computed, ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { data } = useQuery({\n  queryKey: [\"totp-auth-link\"],\n  queryFn: async () => {\n    const { data } = await ucApiClient.security.twoFactor.getTotpAuthLink();\n    return data;\n  },\n});\n\nconst qrcode = useQRCode(computed(() => data.value?.authLink || \"\"));\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"configure-totp\"],\n  mutationFn: async ({ totpRequest }: { totpRequest: TotpRequest }) => {\n    await ucApiClient.security.twoFactor.configurerTotp({\n      totpRequest: totpRequest,\n    });\n  },\n  onSuccess() {\n    Toast.success(t(\"core.common.toast.save_success\"));\n    queryClient.invalidateQueries({ queryKey: [\"two-factor-settings\"] });\n    modal.value?.close();\n  },\n});\n\nfunction onSubmit(data: TotpRequest) {\n  mutate({ totpRequest: data });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"500\"\n    :centered=\"false\"\n    :title=\"$t('core.uc_profile.2fa.methods.totp.operations.configure.title')\"\n    @close=\"emit('close')\"\n  >\n    <div>\n      <VAlert\n        :title=\"$t('core.common.text.tip')\"\n        :description=\"\n          $t('core.uc_profile.2fa.methods.totp.operations.configure.tips')\n        \"\n        type=\"warning\"\n        :closable=\"false\"\n      ></VAlert>\n      <div class=\"my-4 space-y-3 border-b border-gray-100 pb-4 text-gray-900\">\n        <div class=\"text-sm font-semibold\">\n          {{\n            $t(\n              \"core.uc_profile.2fa.methods.totp.operations.configure.fields.qrcode.label\"\n            )\n          }}\n        </div>\n        <img :src=\"qrcode\" class=\"rounded-base border border-gray-100\" />\n        <details>\n          <summary class=\"cursor-pointer select-none text-sm text-gray-800\">\n            {{\n              $t(\n                \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.label\"\n              )\n            }}\n          </summary>\n          <div class=\"mt-3 rounded-base border border-gray-100 p-2\">\n            <span class=\"text-sm text-gray-600\">\n              {{\n                $t(\n                  \"core.uc_profile.2fa.methods.totp.operations.configure.fields.manual.help\"\n                )\n              }}\n            </span>\n            <div class=\"mt-2\">\n              <code\n                class=\"select-all rounded bg-gray-200 p-1 text-xs text-gray-900\"\n              >\n                {{ data?.rawSecret }}\n              </code>\n            </div>\n          </div>\n        </details>\n      </div>\n      <FormKit id=\"totp-form\" type=\"form\" name=\"totp-form\" @submit=\"onSubmit\">\n        <FormKit\n          type=\"number\"\n          name=\"code\"\n          :label=\"\n            $t(\n              'core.uc_profile.2fa.methods.totp.operations.configure.fields.code.label'\n            )\n          \"\n          validation=\"required\"\n          :help=\"\n            $t(\n              'core.uc_profile.2fa.methods.totp.operations.configure.fields.code.help'\n            )\n          \"\n        ></FormKit>\n        <FormKit\n          type=\"password\"\n          :label=\"\n            $t(\n              'core.uc_profile.2fa.methods.totp.operations.configure.fields.password.label'\n            )\n          \"\n          validation=\"required\"\n          name=\"password\"\n          :help=\"\n            $t(\n              'core.uc_profile.2fa.methods.totp.operations.configure.fields.password.help'\n            )\n          \"\n          autocomplete=\"current-password\"\n        ></FormKit>\n        <FormKit\n          :model-value=\"data?.rawSecret\"\n          type=\"hidden\"\n          name=\"secret\"\n        ></FormKit>\n      </FormKit>\n    </div>\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          @click=\"$formkit.submit('totp-form')\"\n        >\n          {{ $t(\"core.common.buttons.save\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/TotpDeletionModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport PasswordValidationForm from \"./PasswordValidationForm.vue\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"totp-deletion\"],\n  mutationFn: async ({ password }: { password: string }) => {\n    return await ucApiClient.security.twoFactor.deleteTotp({\n      passwordRequest: {\n        password: password,\n      },\n    });\n  },\n  onSuccess() {\n    Toast.success(t(\"core.common.toast.disable_success\"));\n    queryClient.invalidateQueries({ queryKey: [\"two-factor-settings\"] });\n    modal.value?.close();\n  },\n});\n\nfunction onSubmit(password: string) {\n  mutate({ password });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"500\"\n    :centered=\"false\"\n    :title=\"$t('core.uc_profile.2fa.operations.disable_totp.title')\"\n    @close=\"emit('close')\"\n  >\n    <PasswordValidationForm @submit=\"onSubmit\" />\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"danger\"\n          @click=\"$formkit.submit('password-validation-form')\"\n        >\n          {{ $t(\"core.common.buttons.disable\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/TwoFactor.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport {\n  VButton,\n  VEntity,\n  VEntityContainer,\n  VEntityField,\n  VSpace,\n} from \"@halo-dev/components\";\nimport { useQuery } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport RiShieldKeyholeLine from \"~icons/ri/shield-keyhole-line\";\nimport StatusDotField from \"@/components/entity-fields/StatusDotField.vue\";\nimport TotpConfigureModal from \"./TotpConfigureModal.vue\";\nimport TotpDeletionModal from \"./TotpDeletionModal.vue\";\nimport TwoFactorDisableModal from \"./TwoFactorDisableModal.vue\";\nimport TwoFactorEnableModal from \"./TwoFactorEnableModal.vue\";\n\nconst { data: settings, isLoading } = useQuery({\n  queryKey: [\"two-factor-settings\"],\n  queryFn: async () => {\n    const { data } =\n      await ucApiClient.security.twoFactor.getTwoFactorAuthenticationSettings();\n    return data;\n  },\n});\n\nconst twoFactorEnableModalVisible = ref(false);\nconst twoFactorDisableModalVisible = ref(false);\n\nfunction onEnabledChange(payload: Event) {\n  const target = payload.target as HTMLInputElement;\n  // Do not change the checked state of the checkbox\n  target.checked = !target.checked;\n  if (settings.value?.enabled) {\n    twoFactorDisableModalVisible.value = true;\n  } else {\n    twoFactorEnableModalVisible.value = true;\n  }\n}\n\nconst totpConfigureModalVisible = ref(false);\nconst totpDeletionModalVisible = ref(false);\n</script>\n\n<template>\n  <Transition v-if=\"!isLoading\" appear name=\"fade\">\n    <div class=\"overflow-hidden rounded-base border\">\n      <div class=\"flex items-center justify-between bg-gray-50 px-4 py-3\">\n        <span class=\"text-sm font-semibold text-gray-900\">\n          {{ $t(\"core.uc_profile.2fa.methods.title\") }}\n        </span>\n        <label class=\"flex cursor-pointer items-center space-x-2\">\n          <input\n            type=\"checkbox\"\n            :checked=\"settings?.enabled\"\n            @change=\"onEnabledChange\"\n          />\n          <span class=\"text-sm font-medium text-gray-700\">\n            {{ $t(\"core.uc_profile.2fa.operations.enable.button\") }}\n          </span>\n        </label>\n      </div>\n      <VEntityContainer>\n        <VEntity>\n          <template #start>\n            <VEntityField>\n              <template #description>\n                <RiShieldKeyholeLine />\n              </template>\n            </VEntityField>\n            <VEntityField\n              :title=\"$t('core.uc_profile.2fa.methods.totp.title')\"\n              :description=\"$t('core.uc_profile.2fa.methods.totp.description')\"\n            />\n          </template>\n          <template #end>\n            <StatusDotField\n              :state=\"settings?.totpConfigured ? 'success' : 'default'\"\n              :text=\"\n                settings?.totpConfigured\n                  ? $t(\n                      'core.uc_profile.2fa.methods.totp.fields.status.configured'\n                    )\n                  : $t(\n                      'core.uc_profile.2fa.methods.totp.fields.status.not_configured'\n                    )\n              \"\n            ></StatusDotField>\n            <VEntityField>\n              <template #description>\n                <VSpace>\n                  <VButton size=\"sm\" @click=\"totpConfigureModalVisible = true\">\n                    {{\n                      settings?.totpConfigured\n                        ? $t(\n                            \"core.uc_profile.2fa.methods.totp.operations.reconfigure.button\"\n                          )\n                        : $t(\n                            \"core.uc_profile.2fa.methods.totp.operations.configure.button\"\n                          )\n                    }}\n                  </VButton>\n                  <VButton\n                    v-if=\"settings?.totpConfigured\"\n                    size=\"sm\"\n                    type=\"danger\"\n                    @click=\"totpDeletionModalVisible = true\"\n                  >\n                    {{ $t(\"core.common.buttons.disable\") }}\n                  </VButton>\n                </VSpace>\n              </template>\n            </VEntityField>\n          </template>\n        </VEntity>\n      </VEntityContainer>\n    </div>\n  </Transition>\n  <TotpConfigureModal\n    v-if=\"totpConfigureModalVisible\"\n    @close=\"totpConfigureModalVisible = false\"\n  />\n\n  <TotpDeletionModal\n    v-if=\"totpDeletionModalVisible\"\n    @close=\"totpDeletionModalVisible = false\"\n  />\n\n  <TwoFactorEnableModal\n    v-if=\"twoFactorEnableModalVisible\"\n    @close=\"twoFactorEnableModalVisible = false\"\n  />\n\n  <TwoFactorDisableModal\n    v-if=\"twoFactorDisableModalVisible\"\n    @close=\"twoFactorDisableModalVisible = false\"\n  />\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/TwoFactorDisableModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport PasswordValidationForm from \"./PasswordValidationForm.vue\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"disable-two-factor\"],\n  mutationFn: async ({ password }: { password: string }) => {\n    return await ucApiClient.security.twoFactor.disableTwoFactor({\n      passwordRequest: {\n        password: password,\n      },\n    });\n  },\n  onSuccess() {\n    Toast.success(t(\"core.common.toast.disable_success\"));\n    queryClient.invalidateQueries({ queryKey: [\"two-factor-settings\"] });\n    modal.value?.close();\n  },\n});\n\nfunction onSubmit(password: string) {\n  mutate({ password });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"500\"\n    :centered=\"false\"\n    :title=\"$t('core.uc_profile.2fa.operations.disable.title')\"\n    @close=\"emit('close')\"\n  >\n    <PasswordValidationForm @submit=\"onSubmit\" />\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"danger\"\n          @click=\"$formkit.submit('password-validation-form')\"\n        >\n          {{ $t(\"core.common.buttons.disable\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/components/TwoFactorEnableModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ucApiClient } from \"@halo-dev/api-client\";\nimport { Toast, VButton, VModal, VSpace } from \"@halo-dev/components\";\nimport { useMutation, useQueryClient } from \"@tanstack/vue-query\";\nimport { ref } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\nimport PasswordValidationForm from \"./PasswordValidationForm.vue\";\n\nconst queryClient = useQueryClient();\nconst { t } = useI18n();\n\nconst emit = defineEmits<{\n  (event: \"close\"): void;\n}>();\n\nconst modal = ref<InstanceType<typeof VModal> | null>(null);\n\nconst { mutate, isLoading } = useMutation({\n  mutationKey: [\"enable-two-factor\"],\n  mutationFn: async ({ password }: { password: string }) => {\n    return await ucApiClient.security.twoFactor.enableTwoFactor({\n      passwordRequest: {\n        password: password,\n      },\n    });\n  },\n  onSuccess() {\n    Toast.success(t(\"core.common.toast.enable_success\"));\n    queryClient.invalidateQueries({ queryKey: [\"two-factor-settings\"] });\n    modal.value?.close();\n  },\n});\n\nfunction onSubmit(password: string) {\n  mutate({ password });\n}\n</script>\n\n<template>\n  <VModal\n    ref=\"modal\"\n    :width=\"500\"\n    :centered=\"false\"\n    :title=\"$t('core.uc_profile.2fa.operations.enable.title')\"\n    @close=\"emit('close')\"\n  >\n    <PasswordValidationForm @submit=\"onSubmit\" />\n    <template #footer>\n      <VSpace>\n        <VButton\n          :loading=\"isLoading\"\n          type=\"secondary\"\n          @click=\"$formkit.submit('password-validation-form')\"\n        >\n          {{ $t(\"core.common.buttons.enable\") }}\n        </VButton>\n        <VButton @click=\"modal?.close()\">\n          {{ $t(\"core.common.buttons.close\") }}\n        </VButton>\n      </VSpace>\n    </template>\n  </VModal>\n</template>\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/composables/use-user-agent.ts",
    "content": "import { UAParser } from \"ua-parser-js\";\nimport { computed } from \"vue\";\n\nexport function useUserAgent(userAgent?: string) {\n  const ua = computed(() => new UAParser(userAgent));\n\n  const os = computed(() =>\n    [ua.value.getOS().name, ua.value.getOS().version].filter(Boolean).join(\" \")\n  );\n\n  const browser = computed(() =>\n    [ua.value.getBrowser().name, ua.value.getBrowser().version]\n      .filter(Boolean)\n      .join(\" \")\n  );\n\n  return {\n    ua,\n    os,\n    browser,\n  };\n}\n"
  },
  {
    "path": "ui/uc-src/modules/profile/tabs/composables/use-user-device.ts",
    "content": "import { ucApiClient, type UserDevice } from \"@halo-dev/api-client\";\nimport { Dialog, Toast } from \"@halo-dev/components\";\nimport { useQueryClient } from \"@tanstack/vue-query\";\nimport { useI18n } from \"vue-i18n\";\n\nexport function useUserDevice(device: UserDevice) {\n  const { t } = useI18n();\n  const queryClient = useQueryClient();\n\n  function handleRevoke() {\n    Dialog.warning({\n      title: t(\"core.uc_profile.device.operations.revoke.title\"),\n      description: t(\"core.uc_profile.device.operations.revoke.description\"),\n      confirmType: \"danger\",\n      confirmText: t(\"core.common.buttons.confirm\"),\n      cancelText: t(\"core.common.buttons.cancel\"),\n      async onConfirm() {\n        await ucApiClient.security.device.revokeDevice({\n          deviceId: device.device.metadata.name,\n        });\n\n        Toast.success(t(\"core.common.toast.delete_success\"));\n\n        queryClient.invalidateQueries({ queryKey: [\"uc:devices\"] });\n      },\n    });\n  }\n  return { handleRevoke };\n}\n"
  },
  {
    "path": "ui/uc-src/router/constant.ts",
    "content": "import type { MenuGroupType } from \"@halo-dev/ui-shared\";\n\nexport const coreMenuGroups: MenuGroupType[] = [\n  {\n    id: \"dashboard\",\n    name: undefined,\n    priority: 0,\n  },\n  {\n    id: \"content\",\n    name: \"core.sidebar.menu.groups.content\",\n    priority: 1,\n  },\n  {\n    id: \"interface\",\n    name: \"core.sidebar.menu.groups.interface\",\n    priority: 2,\n  },\n  {\n    id: \"system\",\n    name: \"core.sidebar.menu.groups.system\",\n    priority: 3,\n  },\n  {\n    id: \"tool\",\n    name: \"core.sidebar.menu.groups.tool\",\n    priority: 4,\n  },\n];\n"
  },
  {
    "path": "ui/uc-src/router/guards/auth-check.ts",
    "content": "import { stores } from \"@halo-dev/ui-shared\";\nimport type { Router } from \"vue-router\";\n\nexport function setupAuthCheckGuard(router: Router) {\n  router.beforeEach((_to, _from, next) => {\n    const currentUserStore = stores.currentUser();\n\n    if (currentUserStore.isAnonymous) {\n      window.location.href = `/login?redirect_uri=${encodeURIComponent(\n        window.location.href\n      )}`;\n      return;\n    }\n\n    next();\n  });\n}\n"
  },
  {
    "path": "ui/uc-src/router/guards/permission.ts",
    "content": "import { utils } from \"@halo-dev/ui-shared\";\nimport type { RouteLocationNormalized, Router } from \"vue-router\";\n\nexport function setupPermissionGuard(router: Router) {\n  router.beforeEach(async (to, _from, next) => {\n    if (\n      await checkRoutePermissions(\n        to,\n        utils.permission.getUserPermissions() || []\n      )\n    ) {\n      next();\n    } else {\n      next({ name: \"Forbidden\" });\n    }\n  });\n}\n\nasync function checkRoutePermissions(\n  to: RouteLocationNormalized,\n  uiPermissions: string[]\n): Promise<boolean> {\n  const { meta } = to;\n\n  if (!meta?.permissions) {\n    return true;\n  }\n\n  if (typeof meta.permissions === \"function\") {\n    try {\n      return await meta.permissions(uiPermissions);\n    } catch (e) {\n      console.error(\n        `Error checking permissions for route ${String(to.name)}:`,\n        e\n      );\n      return false;\n    }\n  }\n\n  return utils.permission.has(meta.permissions as string[]);\n}\n"
  },
  {
    "path": "ui/uc-src/router/index.ts",
    "content": "import routesConfig from \"@uc/router/routes.config\";\nimport {\n  createRouter,\n  createWebHistory,\n  type RouteLocationNormalized,\n  type RouteLocationNormalizedLoaded,\n} from \"vue-router\";\nimport { setupStopImplicitSubmission } from \"@/formkit/plugins/stop-implicit-submission\";\nimport { setupProcessBarGuard } from \"@/router/process-bar\";\nimport { setupAuthCheckGuard } from \"./guards/auth-check\";\nimport { setupPermissionGuard } from \"./guards/permission\";\n\nconst router = createRouter({\n  history: createWebHistory(\"/uc/\"),\n  routes: routesConfig,\n  scrollBehavior: (\n    to: RouteLocationNormalized,\n    from: RouteLocationNormalizedLoaded\n  ) => {\n    if (to.name !== from.name) {\n      return { left: 0, top: 0 };\n    }\n  },\n});\n\nsetupAuthCheckGuard(router);\nsetupPermissionGuard(router);\nsetupProcessBarGuard(router);\nsetupStopImplicitSubmission(router);\n\nexport default router;\n"
  },
  {
    "path": "ui/uc-src/router/routes.config.ts",
    "content": "import BasicLayout from \"@uc/layouts/BasicLayout.vue\";\nimport type { RouteRecordRaw } from \"vue-router\";\n\nexport const routes: Array<RouteRecordRaw> = [\n  {\n    path: \"/:pathMatch(.*)*\",\n    component: BasicLayout,\n    children: [\n      {\n        path: \"\",\n        name: \"NotFound\",\n        component: () => import(\"@/views/exceptions/NotFound.vue\"),\n      },\n    ],\n  },\n  {\n    path: \"/403\",\n    component: BasicLayout,\n    children: [\n      {\n        path: \"\",\n        name: \"Forbidden\",\n        component: () => import(\"@/views/exceptions/Forbidden.vue\"),\n      },\n    ],\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "ui/uc.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta content=\"IE=edge\" http-equiv=\"X-UA-Compatible\" />\n    <meta content=\"webkit\" name=\"renderer\" />\n    <meta\n      content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\"\n      name=\"viewport\"\n    />\n    <meta content=\"noindex,nofollow\" name=\"robots\" />\n    <title></title>\n    <link href=\"/ui-assets/favicon.ico\" rel=\"shortcut icon\" type=\"image/x-icon\" />\n    <link href=\"/ui-assets/favicon.ico\" rel=\"icon\" type=\"image/x-icon\" />\n    <meta name=\"msapplication-TileColor\" content=\"#da532c\" />\n    <meta name=\"theme-color\" content=\"#ffffff\" />\n    <style>\n      body {\n        height: 100%;\n        background-color: #f5f5f5;\n      }\n\n      #loader {\n        position: absolute;\n        top: 0;\n        right: 0;\n        bottom: 0;\n        left: 0;\n        margin: auto;\n        border: solid 3px #e5e5e5;\n        border-top-color: #333;\n        border-radius: 50%;\n        width: 30px;\n        height: 30px;\n        animation: spin 0.6s linear infinite;\n      }\n\n      @keyframes spin {\n        to {\n          transform: rotate(360deg);\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <noscript>\n      <strong>\n        We're sorry but halo console client doesn't work properly without JavaScript enabled. Please enable it to\n        continue.\n      </strong>\n    </noscript>\n    <div id=\"app\">\n      <div id=\"loader\"></div>\n    </div>\n    <script src=\"/uc-src/main.ts\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "ui/vite.config.ts",
    "content": "import fs from \"node:fs\";\nimport path, { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport VueI18n from \"@intlify/unplugin-vue-i18n/vite\";\nimport Vue from \"@vitejs/plugin-vue\";\nimport VueJsx from \"@vitejs/plugin-vue-jsx\";\nimport Gzip from \"rollup-plugin-gzip\";\nimport Icons from \"unplugin-icons/vite\";\nimport { defineConfig } from \"vite\";\nimport { configDefaults } from \"vitest/config\";\nimport { setupLibraryExternal } from \"./src/vite/library-external\";\nimport { devPlugin } from \"./src/vite/plugin-dev\";\n\nconst DEV_SERVER_PORT = 3000;\n\nexport default defineConfig(({ mode }) => {\n  const isProduction = mode === \"production\";\n  const isTest = mode === \"test\";\n\n  return {\n    experimental: {\n      bundledDev: !isTest,\n    },\n    plugins: [\n      Vue(),\n      VueJsx(),\n      Gzip(),\n      Icons({\n        compiler: \"vue3\",\n        customCollections: {\n          core: {\n            logo: () => fs.readFileSync(\"./src/assets/logo.svg\", \"utf-8\"),\n          },\n        },\n      }),\n      VueI18n({\n        include: [path.resolve(__dirname, \"./src/locales/*.json\")],\n      }),\n      setupLibraryExternal(mode),\n      devPlugin({ port: DEV_SERVER_PORT }),\n    ],\n    resolve: {\n      alias: {\n        \"@\": path.resolve(__dirname, \"src\"),\n        \"@console\": path.resolve(__dirname, \"console-src\"),\n        \"@uc\": path.resolve(__dirname, \"uc-src\"),\n      },\n    },\n    server: {\n      port: DEV_SERVER_PORT,\n      fs: {\n        strict: isProduction,\n      },\n    },\n    build: {\n      outDir: path.resolve(__dirname, \"build/dist/ui\"),\n      emptyOutDir: true,\n      assetsDir: \"ui-assets\",\n      rolldownOptions: {\n        input: {\n          console: resolve(import.meta.dirname, \"console.html\"),\n          uc: resolve(import.meta.dirname, \"uc.html\"),\n        },\n        output: {\n          codeSplitting: {\n            groups: [\n              {\n                name: \"vendor\",\n                test: /node_modules/,\n                entriesAware: true,\n                entriesAwareMergeThreshold: 28000, // bytes\n              },\n            ],\n          },\n        },\n      },\n    },\n    test: {\n      environment: \"jsdom\",\n      include: [\"**/*.spec.ts\"],\n      root: fileURLToPath(new URL(\"./\", import.meta.url)),\n      exclude: [...configDefaults.exclude, \"./packages/**/*.ts\"],\n      reporters: \"html\",\n      outputFile: \"build/test-result/index.html\",\n    },\n  };\n});\n"
  }
]